- on Mon 09 June 2014
Sunday evening our CTO emails us:
Reply to if you want to learn Elm, FP and FRP. This week you come: 5 times, every day, 1pm-2pm. You can bring your food/brown bag...Friday we will be present a html5 snake.
Fast forward one week and Boom! A fully functional snake! (Refresh page to restart).
Learning functional programming (FP) and functional reactive programming (FRP) had been on my long-term roadmap anyways. So I figured -- why wait?
Output of the week
My week of functional reactive snake broke down into 2 days adjusting to Elm syntax and the functional style, 2 days of staring confused as my colleagues discussed signal graphs, one evening reading others' code and the Elm documentation, and finally one solid day of writing my own code and refactoring. In the end, I felt like I was starting to get the hang of it, and I was proud that I produced the game above.
Why should I care about FP? What's the benefit for me?
Honestly the sheer number of people I hear talking about FP is enough to spark my curiosity and give it a try. A friend summarized the CRAFT conference saying "Yeah one of the overarching themes was definitely the shift from OOP to FP."
But why should I care about FRP if my focus is on Objective-C and iOS development?
My main motivation is to be able to use the Reactive Cocoa library. At Prezi we are using it so much now that our experienced Objective-C developers seem to be writing almost exclusively in Reactive Cocoa. Most of our teams are adopting reactive programming principles nowadays saying that it leads to cleaner, more declarative code. I was a little surprised to find out that even our backend API team is using reactive programming, thereby quashing my assumption that such a paradigm is only useful in client code.
Justification aside, it's always fun to learn new technologies right?
I've been learning Objective-C/Cocoa pretty religiously for the last 4 months as a new member in the iOS team at Prezi. Before Objective-C, I was pretty familiar with Python and C, and now I feel quite comfortable with the object-oriented paradigm. My only FP experience would be my familiarity with Python's map, filter, reduce and lambda functions (if that counts).
Whitespace. Not a fan of braces? Me neither. I mean come on, who can resist a function definition which has neither parentheses nor braces:
flipDirection : Direction -> Direction flipDirection direction = case direction of Up -> Down Down -> Up Left -> Right Right -> Left
UNIX-like pipelines. For those who like composing or chaining functions, Elm offers the
|> operator as an alternative to grouping arguments with parentheses.
drawCell: Color -> Position -> Form drawCell color cell = rect 10 10 |> filled color |> move (toFloat cell.x*10, toFloat cell.y*10) |> move (-92,0)
Natural separation of concerns. As I polished up my snake game, the code I had written naturally fell into one of 3 categories: model-logic, rendering, and signals. I thought I had gotten lucky, but then I heard Elm creator Evan Czaplicki mention in a talk that this usually happens. The
main function brings these three branches together.
The functional syntax at first sight has a few oddities.
Arrows. To declare the type of a function you say something like
foo:int -> bool -> float. This was really annoying me until I realized that the majority of the time you can read it like a C function declaration
float foo(int, bool). This isn't a perfect analogy because foo doesn't require 2 arguments. It's perfectly acceptable to pass foo just an int, and it will return a function which takes a bool and returns a float. In other words, the return type of a function is either the right most type (like float in this example) or a function with a signature equal to some suffix of the original signature.
Anonymous functions. Many languages support lambda functions, but when you write in FP you are somehow forced to use them more often. And what surprised me is how few characters are necessary to define one. For example, in Python I have to write
lambda x, y : x + y whereas in Elm I write
\x y -> x + y.
No for loops, no mutable objects. It's tough to change your way of thinking from "I'll just create a helper array which I populate using a for loop" to "I'll map my array through this function and then this function." Even doing a simple thing like checking for containment can look a little funny (that is, if you're a noob like me and not aware of convenience methods like any):
containsCell : Position -> [Position] -> Bool containsCell c cells = length (filter (\cell -> sameCell c cell) cells) >= 1
Wrapping my brain around signals and the transformations you can apply to them was probably the most difficult part of the week. Here's what I managed to understand:
Signals are signals. What does a real-world signal do? It emits values over time. Like a heartbeat or a polygraph, a signal in FRP is just emitting values of a specified type over time.
Signals track events. Elm's standard library includes the Time, Http, Keyboard, and Mouse libraries where you are likely to find most of the signals you'll ever need. To create your own signal or transform a given signal you can make use of the the -- wait for it -- Signal library.
Once you go signal you never go back. I'm not talking about how addictive FRP is, rather the
lift function in Elm. Because signals themselves persist (while their emitted values may change) it doesn't make sense to write a function which takes a signal and returns a nonsignal (which value of the signal would it use?). Instead you have functions which operate on nonsignal types and then you have functions which transform signals (take 1 or more signals and return a signal).
lift function glues the nonsignal and signal parts of your program together. Its type is
lift : (a -> b) -> Signal a -> Signal b
Here a and b refer to an arbitrary type. So the
lift function can transform your function
f':Signal Int -> Signal Bool which you can then apply to a signal like
Mouse.x for example.
This is where the natural separation of concerns comes from. You write your normal functions, then eventually you apply lift functions to integrate your model with signals, but once you're writing functions on the level of signals, there's no going back.
All roads lead to main. Your program's logic can be represented by a signal graph where all roads eventually lead to the master
main function which has type
Signal Element. Here's an example of main and lift in action (maybe this is specific to Elm and not FRP):
main : Signal Element main = lift render gameStateSignal gameStateSignal : Signal GameState gameStateSignal = foldp (nextState board) (GameOn startSnake startApple) wrappedSignal render:GameState -> Element render state = case state of GameOn snake apple -> drawCells snake.cells apple GameOver score -> asText ("GameOver. Snake Length: " ++ (show score))
Signals can remember. To maintain some sort of state in the stateless world of FP, you have to use the
foldp function. Whenever a signal emits a value, it takes the previous value and the current value to do something interesting. So
foldp to me means remembering and state-modification.
I really liked Elm and would be glad to work more with it. But I think the more important point is that it's given me a solid introduction into the world of FRP and in turn I'm more confident in using things like the Reactive Cocoa library.