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


Justin Woo

March 02, 2017

More Decks by Justin Woo

Other Decks in Programming


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

  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!
  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!
  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)
  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
  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
  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
  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
  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
  10. Moving the cursor moveCursor :: Direction -> State -> State

    moveCursor direction [email protected]{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.
  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
  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.
  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.
  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.
  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.
  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)
  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
  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
  19. Result

  20. 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
  21. Fin