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.
"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 Bool
s, 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.
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 Element
s (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.
Notice that most libraries, even pretty common ones, require explicit import
s. 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 import
s 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.
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.
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.
In order to build a Signal
of Element
s to render, it is often convenient to factor the work intro three parts.
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.
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
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.
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?
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.
The Graphics.Element
library defines an API for laying out text, images, and other Element
s. 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.
Signal
, Graphics.Element
, Graphics.Collage
.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