$30 off During Our Annual Pro Sale. View Details »

Simplify your UI management with (algebraic data) types

Simplify your UI management with (algebraic data) types

With higher and higher user expectations and a rapid technological evolution, the web platform complexity has exploded.
If we use JavaScript, it might be difficult to manage richer client-side features and larger codebase. With ReScript (ReasonML) concepts, we'll see with a few practical examples how algebraic data-types can help us represent more efficiently and simply the different states in our interfaces, while avoiding bugs.

Code demo: https://github.com/bloodyowl/2020-10-types-talk

Matthias Le Brun

October 15, 2020
Tweet

More Decks by Matthias Le Brun

Other Decks in Technology

Transcript

  1. Simplify your UI management with
    (algebraic data) types

    View Slide

  2. Matthias Le Brun


    Lead front-end developer @ BeOp


    Co-founder & podcast host @ Putain de Code !

    View Slide

  3. Conversational units for publishers & advertisers matched
    against the page content using a semantic engine

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. 3 things you need to deal in


    almost any UI application:


    1. Optionality


    2. Success


    3. Requests

    View Slide

  10. Optionality

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. Ring ring

    View Slide

  15. We want to push a notification to
    motivate the user if there's been some
    low or no activity day in the last month

    View Slide

  16. Ok

    View Slide

  17. let activitySummaryByDay: array
    undefined
    /
    /
    no activity recorded for the day


    type dayActivity = {


    calories: int,


    workoutDuration: int,


    standUpHours: int,


    }

    /
    /
    some activity

    View Slide

  18. last30daysActivity.find(item
    =
    >


    item
    =
    =
    undefined
    |
    |
    item.calories < 50


    )

    View Slide

  19. last30daysActivity.find(item
    =
    >


    item
    =
    =
    undefined
    |
    |
    item.calories < 50


    )

    /
    /
    undefined

    View Slide

  20. undefined

    View Slide

  21. undefined
    Had every day activity above 50cal?


    Is it a day without any data?

    View Slide

  22. Send


    noti
    f
    i
    cation
    Do


    nothing

    View Slide

  23. let find:


    (


    array<'item>,


    'item
    =
    >
    bool


    )
    =
    >


    'item | undefined

    View Slide

  24. let find:


    (


    array<'item | undefined>,


    'item | undefined
    =
    >
    bool


    )
    =
    >


    'item | undefined | undefined

    View Slide

  25. let find:


    (


    array<'item | undefined>,


    'item | undefined
    =
    >
    bool


    )
    =
    >


    'item | undefined | undefined

    View Slide

  26. let index = last30daysActivity.findIndex(item
    =
    >


    item
    =
    =
    undefined
    |
    |
    item.calories < 50


    );


    if(index
    =
    =
    -1) {

    /
    /
    not found


    } else {


    let item = last30daysActivity[index];


    if(item
    =
    =
    undefined) {

    /
    /
    found day without recorded activity


    } else {

    /
    /
    found day with low activity


    }


    }
    The "correct" way exposes implementation details

    View Slide

  27. option<'item>

    View Slide

  28. type option<'item> =


    | Some('item)


    | None
    Option is a container, holding a value or nothing

    View Slide

  29. let getBy:


    (array<'item>, 'item
    =
    >
    bool)
    =
    >


    option<'item>

    View Slide

  30. last30daysActivity
    -
    >
    Array.getBy(item
    =
    >


    switch item {


    | None
    =
    >
    true


    | Some({calories}) when calories < 50
    =
    >
    true


    | _
    =
    >
    false


    })

    View Slide

  31. let getBy:


    (array>
    >
    , 'item
    =
    >
    bool)
    =
    >


    option>
    >

    View Slide

  32. None
    /
    /
    No match


    Some(None)
    /
    /
    Found an empty day


    Some(Some(x))
    /
    /
    Found a filled day

    View Slide

  33. None
    /
    /
    No match


    Some(None)
    /
    /
    Found an empty day


    Some(Some(x))
    /
    /
    Found a filled day
    Do your f**king 10 000 steps

    View Slide

  34. let map:


    (option<'a>, 'a
    =
    >
    'b)
    =
    >
    option<'b>;


    let flatMap:


    (option<'a>, 'a
    =
    >
    option<'b>)
    =
    >


    option<'b>;
    Manipulation helpers

    View Slide

  35. let bannerRatio =


    user.banner
    /
    /
    option

    -
    >
    Option.flatMap(banner
    =
    >
    banner.image)
    /
    /
    option

    -
    >
    Option.map(({width, height})
    =
    >
    height /. width)
    /
    /
    option

    -
    >
    Option.getWithDefault(16.0 / 9.0)
    /
    /
    float
    Manipulation helpers

    View Slide

  36. Success

    View Slide

  37. /
    /
    BE CAREFUL
    !
    !
    !

    /
    /
    Throws an error


    let f: 'param
    =
    >
    'success;

    View Slide

  38. try {


    let x = f()


    handleSuccess(x)


    } catch(err) {


    handleFailure(err)


    }
    More codepaths, no idea where it's going
    to be handled or if it is

    View Slide

  39. promise


    .then(


    handleSuccess,


    handleFailure


    )
    More codepaths, no idea where it's going
    to be handled or if it is

    View Slide

  40. result<'ok, 'error>
    Result is a container, holding a value or an error

    View Slide

  41. type result<'ok, 'error> =


    | Ok('ok)


    | Error('error)

    View Slide

  42. let f: 'param
    =
    >


    result<'success, 'error>;

    View Slide

  43. switch f() {


    | Ok(x)
    =
    >
    handleSuccess(x)


    | Error(err)
    =
    >
    handleFailure(err)


    }
    You need to handle the possibility of error if you
    want to extract the value

    View Slide

  44. Future.t<'value>
    Simple promises, just a potential value

    View Slide

  45. Future.t>
    >
    Composable

    View Slide

  46. future

    -
    >
    Future.get(handleResult)
    One path

    View Slide

  47. setValue(_
    =
    >
    result)

    /
    /
    then


    switch value {


    | Ok(value)
    =
    >
    /
    >


    | Error(error)
    =
    >
    /
    >


    }
    You can pass around and store the result


    for later extraction

    View Slide

  48. let map:


    (result<'a, 'b>, 'a
    =
    >
    'c)
    =
    >


    result<'c, 'b>;


    let flatMap:


    (result<'a, 'b>, 'a
    =
    >
    result<'c, 'd>)
    =
    >


    result<'c, 'd>;
    Manipulation helpers

    View Slide

  49. let value =


    parseUserInput()
    /
    /
    result

    -
    >
    Result.map(x
    =
    >
    x * 100)
    /
    /
    result

    -
    >
    Result.map(Int.toString)
    /
    /
    result
    Manipulation helpers

    View Slide

  50. Requests

    View Slide

  51. View Slide

  52. View Slide

  53. type state = {


    isLoading: bool,


    error: error | null,


    data: data | null


    }

    View Slide

  54. type state = {


    isLoading: true | false,


    error: error | null,


    data: data | null


    }

    View Slide

  55. type state = {


    isLoading: true | false,


    error: error | null,


    data: data | null


    }
    2
    2
    2

    View Slide

  56. isLoading error data
    FALSE NULL NULL
    TRUE NULL NULL
    FALSE ERROR NULL
    TRUE TRUE NULL
    FALSE NULL DATA
    TRUE FALSE DATA
    FALSE TRUE DATA
    TRUE TRUE DATA
    Not asked yet
    Loading
    Done with error
    Done with success

    View Slide

  57. Sum vs Product possible states
    A + B + C


    = Not a lot
    A * B * C


    = A lot

    View Slide

  58. type requestStatus<'value> =


    | NotAsked


    | Loading


    | Done<'value>

    View Slide

  59. requestStatus<


    result<'ok, 'error>


    >
    Composable!

    View Slide

  60. switch (resource) {


    | NotAsked
    =
    >
    ""


    | Loading
    =
    >
    "Loading
    .
    .
    .
    "


    | Done(Ok(value))
    =
    >
    "Loaded: "
    +
    +
    value


    | Done(Error(_))
    =
    >
    "An error occurred"


    }

    View Slide

  61. Exhaustive pattern
    matching check

    View Slide

  62. 25 ┆
    <
    /
    button>


    26 ┆
    /
    >


    27 ┆ {switch user {


    28 ┆ | NotAsked | Loading
    =
    >
    React.null


    . ┆
    .
    .
    .


    37 ┆ | Done(Ok(None))
    =
    >
    "No result was received"
    -
    >
    React
    38 ┆ }}


    39 ┆
    <
    /
    >


    40 ┆ }



    You forgot to handle a possible case here, for example:


    Done (Error _)


    View Slide

  63. Code comparison time

    View Slide

  64. Conclusion

    View Slide

  65. Types aren't necessarily boring
    annotations that make your life harder
    They can be a tool to help your
    compiler guide you and to make
    some bugs impossible by design

    View Slide

  66. Closely match natural language:
    easier reasoning
    This field HAS SOME user or NONE


    This field HAS a user or IS NULL
    This function RETURNS a OK value or an ERROR


    This function RETURNS a value or THROWS an ERROR

    View Slide

  67. Accidental complexity ↘


    Actual business complexity ↗

    View Slide

  68. Accidental complexity ↘


    Actual business complexity ↗

    View Slide

  69. Thank you! Questions?
    Matthias Le Brun


    Lead front-end developer @ BeOp


    Co-founder & podcast host @ Putain de Code !

    View Slide