Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing an Etch-a-Sketch with Purescript

Writing an Etch-a-Sketch with Purescript

Talk I gave for TampereJS about how to write an Etch-a-Sketch demo using Purescript-Pux

https://github.com/justinwoo/purescript-etch-sketch

Justin Woo

March 02, 2017
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. Writing an Etch-a-Sketch with
    Purescript
    Justin Woo
    2 Mar

    View full-size slide

  2. What is Purescript?
    Functional programming language inspired by Haskell
    Sounds scary, but has plenty of usability improvements
    Features simple and useful Foreign Function Interface
    Meaning hand-written JS can be integrated first-class
    Statically typed with a powerful type system
    And much more!

    View full-size slide

  3. Why Purescript?
    Powerful type system
    Compiler can tell you the mechanical correctness of your program
    Use the type system to encode more information
    Powerful abstractions make for simpler problem solving
    Mature JS backend
    Use with browsers, Node, or anything that runs JS!

    View full-size slide

  4. Pre-FAQs
    What are all of those dollar signs?
    $ is used to avoid writing parentheses until the end of the line
    Because balancing multiple parens visually is hard
    What about all those other weird symbols/operators?
    Like normal math operators, they’re just easier to read and delimit operands
    E.g. add 1 1 vs 1 + 1, map increment [1,2,3] vs increment <$> [1,2,3]
    In JS, I use array.map(project), not _.map(array, project)

    View full-size slide

  5. What is Purescript-Pux?
    Purescript interface to React
    Similar to Elm architecture
    Action → State → State
    Write effectful code in Purescript instead of passing off to magic runtime or
    partially-typed ports
    Also comes with type-safe CSS and routing
    Can use or be used in existing React code

    View full-size slide

  6. What is an Etch-a-Sketch?
    Some kind of board that can be drawn on using a cursor
    2-dimensional
    Some collection of points
    A cursor
    Cursor moves UDLR
    Collection of points built up of cursor’s previous positions

    View full-size slide

  7. Modeling our problem
    State of our application:
    type State =
    { cursor :: Coords
    , points :: Set Coords
    , width :: Int
    , height :: Int
    , increment :: Int
    }
    What are coords?
    data Coords = Coords Int Int
    derive instance eqCoords :: Eq Coords
    derive instance ordCoords :: Ord Coords
    Derivable instances Eq and Ord
    Instances used so that Coords can be used in a
    Set

    View full-size slide

  8. Quick intro to typeclasses
    Typeclass: kind of like Java interfaces
    class Eq a where
    eq :: a -> a -> Boolean
    Example instance:
    instance eqCoords :: Eq Coords where
    eq (Coords ax ay) (Coords bx by) =
    ax == bx && ay == by
    Constraints are set on the set items:
    data Set a
    instance eqSet :: Eq a => Eq (Set a) where
    eq (Set m1) (Set m2) = m1 == m2
    insert :: forall a. Ord a =>
    a -> Set a -> Set a

    View full-size slide

  9. Writing our logic
    The app has a few allowed actions. Mainly, the
    movement of the cursor
    data Action
    = MoveCursor Direction
    | ClearScreen
    | NoOp
    The cursor can move in one of four directions
    data Direction
    = Up | Down | Left | Right
    The update function uses an action and the old
    state to produce a new state
    update :: Action -> State -> State
    update (MoveCursor direction) state =
    moveCursor direction state
    update ClearScreen state =
    state { points = mempty }
    update NoOp state =
    state

    View full-size slide

  10. Moving the cursor
    moveCursor :: Direction -> State -> State
    moveCursor direction state@{cursor: (Coords x y)} =
    if isInvalidPoint state cursor'
    then state
    else state {cursor = cursor', points = points'}
    where
    points' = insert state.cursor state.points
    cursor' = case direction of
    Up -> Coords x (y - 1)
    Down -> Coords x (y + 1)
    Left -> Coords (x - 1) y
    Right -> Coords (x + 1) y
    Attempt to move the cursor by calculating
    the new cursor position (remember SVG
    grids originate at the top left).
    If the new cursor position is invalid, return
    the state as-is.
    Otherwise, replace the old cursor and
    update the points by inserting the old cursor
    into our set of points.

    View full-size slide

  11. What’s an invalid point?
    One that’s outside of our canvas!
    isInvalidPoint :: State -> Coords -> Boolean
    isInvalidPoint {increment, width, height} (Coords x y)
    | x < 0 = true
    | y < 0 = true
    | x > width / increment - 1 = true
    | y > height / increment - 1 = true
    | otherwise = false

    View full-size slide

  12. Writing our view
    pointView :: Int -> String -> Coords -> Html Action
    pointView increment color (Coords x y) =
    rect
    [ key $ color <> show x <> "x" <> show y <> "y"
    , width $ show increment
    , height $ show increment
    , HA.fill $ color
    , HA.x $ show (x * increment)
    , HA.y $ show (y * increment)
    ]
    []
    First, we need a point view (aka “pixel”).
    Must have the increment size, and then
    be positioned accordingly.

    View full-size slide

  13. Writing our view (cont.)
    view :: State -> Html Action
    view state =
    let
    pointView' = pointView state.increment
    points = pointView' "black" <$>
    fromFoldable state.points
    cursor = pointView' "grey" state.cursor
    in
    [...]
    fromFoldable: Foldable → Array, <$>: infix map
    For our view, we first need to prepare the point
    views of our cursor and our points.
    We partially apply arguments here as needed.
    To create an array of elements that we’ll need
    for our rendering, we use fromFoldable to
    create an array out of our foldable set, and then
    map over our point view function.

    View full-size slide

  14. Writing our view (cont. 2)
    div []
    [ div []
    [ button
    [ onClick (const ClearScreen) ]
    [ text "Clear" ] ]
    , div []
    [ svg
    [ style do border solid (px $ toNumber 1) black
    , width $ show state.width
    , height $ show state.height ]
    $ snoc points cursor ] ]
    Then we construct our app
    view.
    The Pux Html DSL works that
    we have a node, an array of
    node properties, and then an
    array of child nodes.
    We use our point views from
    before and construct our points
    accordingly here.

    View full-size slide

  15. Wiring in our inputs
    getKeyDirections :: forall e. Eff (dom :: DOM | e) (Signal
    Action)
    getKeyDirections = do
    ups <- map (actions $ MoveCursor Up) <$> keyPressed 38
    downs <- map (actions $ MoveCursor Down) <$> keyPressed 40
    lefts <- map (actions $ MoveCursor Left) <$> keyPressed 37
    rights <- map (actions $ MoveCursor Right) <$> keyPressed 39
    pure $ ups <> downs <> lefts <> rights
    where
    actions x = if _ then x else NoOp
    Next, we have this
    seemingly scary block for
    how we can run an effectful
    function of DOM effects
    and get back a signal of
    actions.
    We’ll go into detail to see
    that it’s actually quite
    simple.

    View full-size slide

  16. Effectful code in more depth
    keyPressed :: forall e. Int → Eff (dom :: DOM | e) (Signal Boolean)
    actions :: Action → Boolean → Action
    getKeyDirections = do
    map: (a -> b) -> f a -> f b
    (Boolean -> Action) -> Signal Boolean -> Signal Action
    ups <- map (actions $ MoveCursor Up) <$> keyPressed 38
    Eff _ (Signal Boolean)
    <$>: (a -> b) -> f a -> f b
    (Signal Boolean -> Signal Action) -> Eff _ (Signal Boolean) -> Eff _ (Signal Action)

    View full-size slide

  17. Effectful code in more depth (cont.)
    pure :: a -> f a
    (<>) / append :: a -> a -> a -- associative! (x <> y) <> z = x <> (y <> z)
    getKeyDirections :: forall e. Eff (dom :: DOM | e) (Signal Action)
    getKeyDirections = do
    ups :: Signal Action
    [...]
    pure $ ups <> downs <> lefts <> rights
    Signal Action -> Signal Action -> Signal Action
    Signal Action -> Eff _ (Signal Action)
    ** Note that you will almost never solve this by hand as this is the compiler’s job

    View full-size slide

  18. Putting it all together
    Now we just need to use our main function of Eff
    _ Unit to.
    We first run our getKeyDirections function
    through our effect to get the signal of actions.
    We then initialize the Pux application.
    Finally, we render the result to the DOM.
    main :: forall e. Eff _ Unit
    main = do
    keyDirections <- getKeyDirections
    app <- start
    { initialState
    , update: fromSimple update
    , view
    , inputs: [ keyDirections ] }
    renderToDOM "#app" app.html

    View full-size slide

  19. Conclusions
    Purescript is fun
    Powerful types help us model the problem AND get solutions easily
    Writing code with controlled effects is easy
    Complicated parts can be simplified down mathematically
    We didn’t need to know what “Monads” were to do this
    Repo available here: https://github.com/justinwoo/purescript-etch-sketch

    View full-size slide