Introduction to FRP in Elm

Say that we wanted to write some JavaScript (pseudo)code to keep track of whether the user is currently pressing the mouse button. We might start by defining a mutable variable, and then installing two "callback" functions that get invoked by the JavaScript run-time system when the mousedown and mouseup events are triggered:

// the "state"
var isDown = false;

// the "view" of the state
function printIsDown() { mainElement.innerHTML = isDown.toString(); }

// event handlers to update the state
function handleMouseDown() { isDown = true;  printIsDown(); }
function handleMouseUp()   { isDown = false; printIsDown(); }

// the "controller" that maps events to handlers
element.addEventListener("mousedown", handleMouseDown);
element.addEventListener("mouseup",   handleMouseUp);

(Note: The point of this example is to make plain the structure of managing the state, so we will avoid the natural urge to refactor.)

This is quite a roundabout way of implementing what can be described simply as "a boolean that is true only when the mouse button is being pressed." Furthermore, in a hypothetical typed dialect of JavaScript, the types of these functions would be rather uninformative:

printIsDown : () -> void
handleMouseDown : () -> void
handleMouseUp : () -> void
Element.addEventListener : (string, () -> void) -> void

Matters quickly become more complicated when managing state that depends on multiple events and multiple intermediate computations.

Functional reactive programming (FRP) is a paradigm that allows time-varying values (such as isDown) to be implemented directly using the building blocks of functional programming, rather than resorting to using mutation and callbacks as above.

What is a Signal?

"A signal is a value that changes over time." 1 In Elm, Mouse.isDown is a primitive signal provided by the language that has type Signal Bool. We get to treat isDown as if it always refers to the "current" value, without worrying about the low-level details of how it gets updated.

Better still, we can abstract over and compose signals using the building blocks of functional programing. For example, we can use

Signal.map : (a -> b) -> Signal a -> Signal b

to "lift" the pure function not, which negates Bools, to operate on the current value of isDown as follows

isUp = Signal.map (\curValIsDown -> not curValIsDown) Mouse.isDown

or, more clearly:

isUp = Signal.map not Mouse.isDown

The result is a signal of boolean values (of type Signal Bool) that updates whenever the Mouse.isDown signal updates. How cool is that?

As the name suggests, the behavior and type of Signal.map is analogous to mapping functions that operate on other kinds of data — List.map, String.map, Maybe.map, Dict.map, etc.

Rendering to HTML

Writing to an HTML window is the primary effect that Elm programs can affect. The main definition in an Elm module specifies what to render, and this something takes the form of an Element value, a type defined in the Graphics.Element library. (As we will see later, main can also be defined using other means.)

The elm-repl comes in handy when testing out programs that do not render anything to HTML (notice that we did not have to define main in those cases). But now you will want to try out these examples in the browser — either by compiling Elm source files with elm-make (see HW0 for a refresher); using the live editor online; or building the Elm website locally so that you can run your own live editor without an Internet connection.

A first example:

import Text exposing (fromString)
import Graphics.Element exposing (Element, leftAligned)

main : Element
main =
  leftAligned (fromString "Hello, world!")

The Text library provides fromString to convert a String to a Text value and several functions for styling and manipulating such values. The leftAligned function, among several others, converts the Text value into an Element, which is the type of value we need to provide main in order to render. Notice also that import statements are used to load names of types (e.g. Element) as well as values (e.g. fromString) into scope.

In fact, because we will often want to change what is rendered over time, a main definition is actually a signal of Elements (i.e. of type Signal Element). When main is defined to be an expression e of type Element rather than Signal Element, it is interpreted as signal Signal.constant e, where the function

Signal.constant : a -> Signal a

turns a pure value into a Signal, one that is constant. So we could have written the more explicit:

main : Signal Element
main =
  Signal.constant (leftAligned (fromString "Hello, world!"))

Now that we've introduced ourselves to the world, let's write something slightly more interesting to the window, say, whether or not the mouse button is currently being pressed. For this, we make use of the toString function in Basics:

import Text exposing (fromString)
import Graphics.Element exposing (Element, leftAligned)
import Mouse
import Signal

isUp = Signal.map not Mouse.isDown

main : Signal Element
main =
  Signal.map (\b -> leftAligned (fromString (toString b))) isUp

Try this out and click around. Then try the following variation:

main : Signal Element
main =
  Signal.map Graphics.Element.show isUp

Can you figure out what the type of show is? After verifying your answer by reading the documentation, also take a peek at the implementation in the library; click "Browse Source" and navigate to src/Graphics/Element.elm. Poking around in the libraries can be a great way to help learn a new language.

A Word About Imports

Notice that most libraries, even pretty common ones, require explicit imports. I think this will be especially handy when learning the language, because it will require carefully understanding the structure and purposes of the libraries.

Also notice that, in the example above, I've chosen to mix and match qualified and unqualified imports. As you develop experience writing Elm code, you will find a style of imports that works for you. This will depend on factors such as how many functions you plan to use from a given module — I don't know about you, but I'm planning to go crazy with the Mouse library and I don't want to list all of its definitions explictly — as well as how unique imported names are — leftAligned is much less likely than, say, map to be defined by other imported modules, so using leftAligned without qualification seems like a good choice to me.

A Word About Function Composition

Depending on your prior experience and tastes, you may prefer to scrap the anonymous lambda in the main definition above in favor of one that emphasizes function composition, such as

(\b -> b |> toString |> fromString |> leftAligned)

or

(toString >> fromString >> leftAligned)

or

(\b -> leftAligned <| fromString <| toString <| b)

or

(leftAligned << fromString << toString)

or

(\b -> (leftAligned >> fromString) <| toString <| b)

or

(\b -> b |> toString |> (leftAligned << fromString))

All of these definitions are equivalent, so choose a style that you like best and that fits well within the code around it. (But you better not choose versions like the last couple, because "pipelining" in both directions won't help anyone, including yourself, understand your code.)

Recall that Basics defines these infix operators.

Folding From the Past

Let's now consider a slightly more interesting example, to count and display the number of times the user has clicked a mouse button. There is no such signal built in to Elm, so we will define our own in terms of the signals that are provided.

One option is to build on the mouse click event, which is separate but closely related to the mouse-down event we have seen. For this, Elm provides the primitive

Mouse.clicks : Signal ()

which produces the dummy "unit" value (written ()) of the dummy "unit" type (also written ()) for every mouse button click. Releasing a button does not trigger an update to this signal.

Basic Model-View-Controller Architecture

In order to build a Signal of Elements to render, it is often convenient to factor the work intro three parts.

1: Model

The model keeps track of all the information that is needed to produce the desired result. We may be sloppy and sometimes (actually, often) refer to the model as the state, but let us not be tricked into thinking there is anything mutable at the source level.

In order to display the number of clicks, all we need to maintain is an integer counter:

type alias State = Int

initState : State
initState = 0

The use of the alias keyword defines State to be completely synonymous with Int; it is simply a shorthand and does not define a new type. Type aliases can be used to abbreviate long types as well as to give an indication about the intended use of the type.

2: View

The view defines how to render the model to the screen. In this case, it's rather simple:

 view : State -> Element
 view s = leftAligned (fromString (toString s))

Or, more succinctly:

 view = leftAligned << fromString << toString

3: Controller

The controller defines the connection between a signal and a function that transforms and renders the state when the signal updates. For this, we use the function

Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b

to apply a folding function to all values (of type a) ever produced by the signal, starting with some initial state (of type b), and turning it into a signal of state values (of type b). This is, in fact, the same fold pattern as provided for other datatypes:

List.foldl : (a -> b -> b) -> b -> List a -> b
List.foldr : (a -> b -> b) -> b -> List a -> b

For Signal, this operation is called "folding from the past" rather than "from the left". Folding "from the right" would mean folding "from the future"...

To finish our task, we define a folding function update and then put everything together in main as follows:

update : a -> State -> State
update _ i = i + 1

main : Signal Element
main =
  Signal.map view (Signal.foldp update initState Mouse.clicks)

Download this program as IntroFRP.elm and try it out.

Something to consider: could we have implemented this functionality by using the Mouse.isDown signal rather than Mouse.clicks?

Finally, say we wanted to track the passage of time rather than the number of clicks. By factoring our main definition into the generic form

main = Signal.map VIEW (Signal.foldp UPDATE INIT_STATE BASE_SIGNAL)

we can implement this with a minor tweak of the previous definition:

main =
  Signal.map view (Signal.foldp update initState (Time.every Time.second))

Check out the Time library.

Wait a minute...

We are factoring our Elm code into models, views, and controllers just like in the JavaScript code we started with. So, what have we really gained?

Well, in JavaScript, we would write logic in event handlers to determine which parts of the state need to be updated for different events. In Elm, we define new signals directly as functions of existing ones, and we leave it to the Elm compiler and run-time to figure out how and when to recompute signal values that are derived from more primitive ones. So, we have gained a lot!

This begs the question: how will the compiler figure this all out?

Compiling to JavaScript

The Elm compiler is left with the responsibility to generate target JavaScript that manages mutable state and event handlers, similar to the pseudocode we started with. A way to understand the structure of an Elm program is to think of a signal graph, where nodes are units of computation and edges indicate the flow of values. The incoming edges to a signal graph are the "primitive" JavaScript signals, or events, such as mouse-down, mouse-up, mouse-click, etc. The rest of the graph constitutes a pure function defined in terms of these primitive signals.

As a result, a naive approach would be to recompute the entire program based on changes to any signal. This would, of course, be inefficient and is also unnecessary, because many parts of the signal graph are likely to be independent of changes to many primitive signals.

Instead, the compiler tracks dependencies in the signal graph and uses a concurrent message-passing system to more efficiently recompute only those parts of a program that depend on changes to primitive signals.

We will not go into any of the details of the compilation process, but you can find more information about it in the Reading links posted below. At a basic level, however, our intuitions for how the process might work should resemble our intuitions about how optimizing compilers for functional languages (even without FRP) work: a source language may be purely functional with immutable data structures being copied all over the place, but we know that, below the hood, the compiler is working to identify opportunities to reuse and cache previously computed results. In fact, we will see much more of this principle in the coming weeks as we study how to realize efficient data structures in purely functional languages.

2D Graphics

The Graphics.Element library defines an API for laying out text, images, and other Elements. In addition, the Graphics.Collage library provides tools for defining freeform graphics, comprising shapes, lines, colors, etc. Start exploring by going through the 2D Graphics examples on the Elm Examples page. Homework 1 will provide a reason to really dig in.


Reading

Required

Additional

For a more comprehensive background on FRP and introduction to Elm, you may want to skim parts of Evan Czaplicki's

You will find that some terminology and language features have since changed (e.g. Signal.lift is now called Signal.map). You will also find detailed explanation of the compilation process that we will not cover in this class.



[1] From an earlier version of: http://elm-lang.org/learn/What-is-FRP.elm