← BACK

Frontend Functional Programming with Purescript and Elm

Written by Andre Levi

Using the time I have for personal projects, I decided to spend a week or so furthering my understanding of functional programming (FP). I’ve spent some time using Racket on a superficial level, but I have yet to develop a strong understanding and appreciation of what FP offers. My goal was to make my favorite “hello world” variant — a simple particle system in the canvas. I explored both Purescript and Elm, eventually settling on Elm because of its simpler IO model. However, I had to make some minor changes to the Elm source to get the performance I wanted.

The finished product

When I was looking at languages, I specifically wanted a language that compiles to JS, with the very optimistic hope of discovering a viable alternative to plain JS at work. I also wanted a Haskell-like language in order to get a feel for what working with a richer-than-Java type system would be like. Elm and Purescript were the two candidates — Elm seemed more mature, but I started with Purescript because of its strict evaluation and similarity to Haskell.

After a couple days of working through Purescript’s introductory book, I realized that I may have been a bit overly ambitious.  It turns out these monad thingies are quite difficult to understand! I was fortunate enough to have our lead developer David provide some whiteboard-mentoring on the subject, but I quickly realized that I wouldn’t be able to grok Eff monads, extensible effect systems, and row polymorphism within the project’s timeframe.  Finally, after a frustrating experience with Purescript’s Bower-driven dependency system, I decided to give Elm another look.

Elm’s creator, Evan Czaplicki, favored a signal-based FRP system over monadic IO. In general, it seemed more approachable to those uninitiated to FP. I had experience using signals with ReactiveCocoa on an iOS project, so a quick recap from David was enough to get going. And with Elm’s simple and friendly syntax, I was able to quickly put together a simple particle system.

Even with the simplified learning curve, performance was a challenge. This is because Elm’s abstraction for graphics, Collage, will draw shapes and primitive graphics directly to the canvas, but the exposed functions dealing with images will create a new <img> element at every render call instead. At least, this is the case in Elm 2.1.0. So by default, using images with Collage has the overhead of creating new <img> elements on every tick, setting their src (which seems to be a heavyweight process), setting up listeners, and eventual garbage collection costs.

image

Interestingly enough, the source code of Graphics.Collage contains a sprite function, but the module does not expose it. Furthermore, the native JS code for Graphics.Collage has a corresponding drawImage() function that will draw images directly to the canvas, but will also create an <img> element on every call. So it was a matter of editing Elm to add image caching to drawImage() and expose the sprite function in order to achieve smooth performance (on desktop, at least).

image

I suspect that some of my code may be imperative-flavored, and I assume that it will become blindingly obvious once I get more experience with functional programming. Ultimately, tinkering with Purescript and Elm was a great way to cut my teeth on FP and richer type systems. From here on I’ll be reading “Learn you a Haskell For Great Good” in my free time, in order to develop a stronger FP foundation under less time-sensitive conditions. And once I get my chops up, I’ll be giving Purescript another try.

Source Code

import List exposing (map, head, concatMap, concat, append)
import Color exposing (..)
import Graphics.Collage exposing (..)
import Graphics.Element exposing (..)
import Mouse
import Window
import Random exposing (generate, list, float)
import Debug
import AnimationFrame

-- HELPERS

zip4 : List a -> List b -> List c -> List d -> List (a,b,c,d)
zip4 ws xs ys zs =
  case (ws, xs, ys, zs) of
    ( w :: ws', x :: xs', y :: ys', z :: zs' ) ->
        (w,x,y,z) :: zip4 ws' xs' ys' zs'
    (_, _, _, _) ->
        []

-- CONSTANTS

particleCount = 15
xBounds = 200
yBounds = 200

spritesheet = "imgs/sprites/particle_o_2x.png"
frameCount = 11
maxFrame = frameCount - 1

-- MODEL

type alias Particle =
  { x: Float
  , y: Float
  , speed: Float -- responsbile for velocity and frame change frequency
  , frame: Float
  }

xs = generate (list particleCount (float -xBounds xBounds)) <| Random.initialSeed 0
ys = generate (list particleCount (float -yBounds yBounds)) <| (snd xs)
speeds = generate (list particleCount (float 0 2)) <| (snd ys)
framePointers = generate (list particleCount (float 0 maxFrame)) <| (snd speeds)
zippedProperties = zip4 (fst xs) (fst ys) (fst speeds) (fst framePointers)

particles : List Particle
particles = map (\(x, y, speed, frame) -> { x = x, y = y, speed = speed, frame = frame}) zippedProperties

-- UPDATE

update : (Int, Int) -> List Particle -> List Particle
update mousePosition particles =
  map (\p -> step p mousePosition) particles

step : Particle -> (Int, Int) -> Particle
step p (mouseX, mouseY) =
  let
    velocityX = p.speed + ((toFloat mouseX) / 300)
    velocityY = p.speed + ((toFloat mouseY) / 300)
    newX = p.x + velocityX
    newY = p.y + velocityY
    newFrame = if p.frame + p.speed > maxFrame then 0 else p.frame + (p.speed / 40)
  in
     { p |
        x <- if newX > xBounds then -xBounds else newX,
        y <- if newY > yBounds then -yBounds else newY,
        frame <-newFrame
     }

-- VIEW

showParticle : Particle -> Form
showParticle p =
  sprite 40 40 ((floor p.frame) * 40, 0) spritesheet
    |> move (p.x, p.y)

view : (Int, Int) -> List Particle -> Element
view (w, h) particles =
    collage w h <| map showParticle particles

-- SIGNALS

main : Signal Element
main =
  Signal.map2 view Window.dimensions (Signal.foldp update particles input)

input : Signal (Int, Int)
input =
  Signal.sampleOn AnimationFrame.frame Mouse.position