Signals
Reactive programs require us to write programs that respond to a stream of "events". Additionally, making these programs composable allows us to use all our functional programming techniques to manipulate makes these programs - Functional Reactive Programming (FRP).
The first concept that is needs is how do we represent a stream of events? Just like List a
represent an extensible structure containing elements of type a, we introduce Signal a
which contains an extensible structure which contains the type a.
In some sense there is a relationship/duality between extensible structures (Lists) in space (memory) and Streams of events (Signals) that are extensible structures in time.
A Signal is a special type that represents a set of values changing over time type Signal a
.The coordinates representing the location of the mouse (Mouse.position) and the keys pressed on the keyboard are examples of Signals.
Our Reactive programs contain "networks" of such signals that we process using some special functions that can process Signals. Here are the some key functions (defined in the Signal Module):
- Map
map : (a -> b) -> Signal a -> Signal b
- Merge
merge : Signal a -> Signal a -> Signal a
- Manage state:
foldp : (a -> s -> s) -> s -> Signal a -> Signal s
- Filter
filter : (a -> Bool) -> a -> Signal a -> Signal a
Understanding how to use all these functions and getting familiar with some standard Signals like the mouse position (Mouse.position : Signal (Int, Int)
) and keyboard presses (Keyboard.presses : Signal KeyCode
) should take us a long way towards a clear understanding of FRP.
Mouse Signals
import Graphics.Element exposing (..)
import Mouse
main : Signal Element
main =
Signal.map show Mouse.position
Try this out by pasting this code in the online editor/runner.
Let us see if we can understand this code. Mouse.position is a Signal that contains a tuple of Integers which are the mouse coordinates, we want to show
each of these tuples.
Just like we can use the function map for lists:
map : (a -> b) -> List a -> List b
, the Signal module defines a map function that maps a function over a Signal: map : (a -> b) -> Signal a -> Signal b
.
So that is all we need to do map show over the Mouse position Signal using Signal.map! (Amazing how close the code in the program is to this description in the text.)
Now let us look at how to write a simple function that operates on two values and map it over two Signals.
import Mouse
import Signal exposing (map2)
import Graphics.Element exposing (Element, show)
combine x y = show (x, y)
main : Signal Element
main =
map2 combine Mouse.x Mouse.y
This should give you a clear picture of how we can take normal functions like combine
that we can define and lift/map them over Signals.
Keyboard Signals
Now let us look at some programs that manipulate Keyboard events.
Here we will display the Signals generated form the 4 arrow keys.
import Keyboard
import Signal
import Graphics.Element exposing (show)
main = Signal.map show Keyboard.arrows
Another simple program that manages the keyboard signals from the standards keys.
import Keyboard
import Signal
import Graphics.Element exposing (show)
import Char exposing (..)
import String exposing (..)
main = Signal.map show (Signal.map fromCode Keyboard.presses)
Easy enough, and I think that we can see a pattern emerging of how we write programs to process Signals.
Managing state
Now that we are familiar with managing Signals, we can look at how do we manage state. So far we just dealt with events as they arise and routed them through our programs. So we did not have and acton that depended on the previous state of a Signal.
If for example, we want to create a program to count the mouse clicks that have happened so far, then we need to keep a running count of the clicks that have happened. Then increment this count every time a new click happens.
To functional programers they have already encountered a similar problem with lists. Foe example, How do you sum the elements for a list? You have to take an initial value for the sum and then increment this value as we process each element in the list. This is done using a function fold. We covered an example of this in the post on Elm basics.
There is a function similar to fold that works for signals called foldp (for fold over the past?) Looking at the signature:
foldp : (a -> s -> s) -> s -> Signal a -> Signal s
we see that foldp
takes a function and a initial state
and updates this initial state every time an event happens.
This idea becomes one of the main pattern in functional reactive pograms for managing state.
Okay here is a simple program that illustrates this.
module MouseSignals1 where
import Mouse
import Signal exposing (map, foldp)
import Graphics.Element exposing (.. )
clickCounter : Int
clickCounter = 0
-- increment uses only the counter but we need two arguments as we are going to fold over the counter and the signal.
increment: a-> Int -> Int
increment something counter = counter+1
main : Signal Element
main = map show (foldp increment clickCounter (Mouse.clicks))
This code is a bit verbose because I explicitly created a counter and the increment
function. Shown below is the code that more experienced functional programmers will write.
Note that in this example increment uses only the counter value and just increments it. We are not look at the state of the Signal, we just need to know that it happened in this example. We do need two arguments as we are going to fold over the counter and the signal.
In other examples we will use the values stored in the signal.
module MouseSignals1 where
import Mouse
import Signal exposing (map, foldp)
import Graphics.Element exposing (show)
main = map show (foldp (\_ n -> n+1) 0 (Mouse.clicks))
Like most good functional programs this one is mesmerizing in its conciseness and simplicity!
Exercises
- Try and make a counter that counts the number of seconds since the program is launched. Hint: you can use the function
fps : number -> Signal Time
which is one of the tickers defined in the module Time. See the documentation here..
import Signal exposing (..)
import Time exposing (..)
import Graphics.Element exposing (..)
main = map show counter
increment = Time.fps 1
counter = Signal.foldp (\_ n -> n + 1) 0 increment
- Write a program that counts the number of key presses.
import Keyboard
import Signal
import Graphics.Element exposing (show)
main = Signal.map show (Signal.foldp (\_ n -> n + 1) 0 Keyboard.presses)
- Write a program that builds up a string of all the keys that are pressed.
import Keyboard
import Signal
import Graphics.Element exposing (show)
import Char exposing (..)
import String exposing (..)
main = Signal.map show (Signal.foldp (\keyStr s -> s++ (fromChar keyStr)) "" (Signal.map fromCode Keyboard.presses))
- Write a simple clock program that displays the current time. The code below is not very pretty, add a background and style the display to make a pretty clock.
import Signal exposing (map)
import Graphics.Element exposing (show)
import Time
import Date exposing (year, hour, minute, second, fromTime)
main =
Signal.map currentTime (Time.every Time.second)
--construct the time from the time in milliseconds since epoch
currentTime t =
let date' = fromTime t
hour' = toString (Date.hour date')
minute' = toString (Date.minute date')
second' = toString (Date.second date')
year' = toString (year date')
now = "The current time is: " ++ hour' ++ ":" ++ minute' ++ ":" ++ second'
in
show now
- Can you construct a program that takes the mouse position,runs it through two different functions offSetPosition and relativePosition and then displays both results. (This exercise was suggested to me by Vinay, and it really illustrates how difficult it is to work with Signals until you have mastered the Signal idioms.)
import Graphics.Element exposing (..)
import Mouse
import Signal exposing (map, map2)
combine : a -> b -> Element
combine a b = (flow right) [show a, show b]
relMousePosition mp = (toFloat(fst mp)/2, toFloat(snd mp)/2)
offsetMousePosition mp = (fst mp +100, snd mp +100)
main = map2 combine (map relMousePosition Mouse.position) (map offsetMousePosition Mouse.position)
Or you can use the combine function in the package Signal.Extra which has a few nice additional functions.
- Can you display a list of Signals? Use 4 Mouse Signals as an example.
import Graphics.Element exposing (show, down, flow, leftAligned)
import Mouse
import Signal.Extra exposing (mapMany)
main = mapMany (show) [Mouse.x, Mouse.y, Mouse.x, Mouse.y]
If you have not already done this before please take a look at this great article explaining Signals on elm-lang.
Mailboxes
The next topic to look at is using Mailboxes and Tasks.
Here is a simple example of using a Mailbox to handle button clicks on a web page.
import Html
import Html.Events as Events
import Random
view : Signal.Address String -> String -> Html.Html
view address message =
Html.div
[]
[ Html.button
[ Events.onClick address "Welcome To Elm" ]
[ Html.text "Click Me" ]
,Html.div [] [ Html.text message ]
]
mb : Signal.Mailbox String
mb =
Signal.mailbox ""
main : Signal Html.Html
main =
Signal.map (view mb.address) mb.signal
Exercise
- Can you extend the previous message to two buttons that display different messages in the same text field?
import Html
import Html.Events as Events
import String
view : Signal.Address String -> String -> Html.Html
view address message =
Html.div
[]
[ Html.button
[ Events.onClick address "Welcome To Elm" ]
[ Html.text "Click Me" ]
,Html.button
[ Events.onClick address (String.reverse "Elm Welcomes you") ]
[ Html.text "Click Me 2" ]
,Html.div [] [ Html.text message ]
]
mb : Signal.Mailbox String
mb =
Signal.mailbox ""
main : Signal Html.Html
main =
Signal.map (view mb.address) mb.signal
- Can you update two different fields when two buttons are clicked?
import Html
import Html.Events as Events
import String
-- define the model for the view
type alias Model = {s1:String, s2:String}
--define the view
view : Signal.Address Model -> Model -> Html.Html
view address model =
Html.div
[]
[ Html.button
[ Events.onClick address {model|s1="Hi from Button 1", s2 ="-"} ]
[ Html.text "Click Me" ]
,Html.button
[ Events.onClick address {model | s1 = "-", s2 = "Button2 says Hello"} ]
[ Html.text "Click Me 2" ]
,Html.div [] [ Html.text (model.s1) ]
,Html.div [] [ Html.text (model.s2) ]
]
mb : Signal.Mailbox Model
mb =
Signal.mailbox {s1="", s2=""}
-- This is the controller that wires up/controls the model and the View
main : Signal Html.Html
main =
Signal.map (view mb.address) mb.signal
Notice how we are very close to the class Model/View controller pattern here. We will revisit this in more detail when discuss web applications and Web Architecture.
Tasks - Asynchronous Operations
The next ingredient that we need is running processes Asynchronously. This is done using Tasks in Elm.
Retrieving data from a web page
import Http
import Graphics.Element exposing (..)
import Html exposing (Html)
import Task exposing (Task, andThen)
main : Signal Element
main =
Signal.map show readme.signal
-- set up mailbox
-- the signal is piped directly to main
-- the address lets us update the signal
readme : Signal.Mailbox String
readme =
Signal.mailbox ""
-- send some markdown to our readme mailbox
report : String -> Task x ()
report markdown =
Signal.send readme.address markdown
-- get the readme *and then* send the result to our mailbox
port fetchReadme : Task Http.Error ()
port fetchReadme =
Http.getString readmeUrl `andThen` report
-- the URL of the README.md that we desire
readmeUrl : String
readmeUrl =
"https://raw.githubusercontent.com/elm-lang/core/master/README.md"
Effects
One final topic and we are done with processing Signals.