Tech

Migrating signal usage from Elm 0.16 to 0.17.

Moving signal transformations away from signals.

In Elm 0.16, in order to manipulate and transform inputs from signals, we had four main functions at hand:

  • map, for transformations
  • filter, for filtering
  • merge, for joining
  • foldp, for stateful transformations over time

The following examples will show how this has changed, and how to do it in the new world. For the most uses, we can easily replace it with the new syntax.

map

map would allow you to take a signal of one kind, and turn it to a signal of another kind. This looks like this:

0.16

type alias Model =
    { currentTime : Time
    }

type Action
    = SetCurrentTime Time
    | NoOp

currentTime : Signal Time
currentTime = Time.every Time.second

setCurrentTime : Signal
setCurrentTime = Signal.map SetCurrentTime currentTime

update : Action -> Model -> (Model, Effects Action)
update action model =
    case action of
        NoOp ->
            ( model, Effects.none)

        SetCurrentTime time ->
            ( { model | currentTime = time }
            , Effects.none
            )

init : Model
init =
    { currentTime = 0
    }

view : Signal.Address Action -> Model -> Html
view address model =
    div
        []
        [ text <| toString <| model.currentTime]

app : StartApp Model Action
app =
    StartApp.start
        { init = (init, Effects.none)
        , view = view
        , update = update
        , inputs = [ setCurrentTime ]
        }

main : Signal Html
main = app.html

0.17

In 0.17, this now looks like this:

type alias Model =
    { currentTime : Time
    }

type Msg
    = SetCurrentTime Time
    | NoOp

-- Every now takes a "tagger" function.
-- Each time a second goes by, it will wrap the time with SetCurrentTime
-- and then send this message to the update loop
setCurrentTime : Sub Msg
setCurrentTime = Time.every Time.second SetCurrentTime

update : Msg -> Model -> (Model, Cmd Msg)
update action model =
    case action of
        NoOp ->
            ( model, Cmd.none)

        SetCurrentTime time ->
            ( { model | currentTime = time }
            , Cmd.none
            )

init : Model
init =
    { currentTime = 0
    }

view : Model -> Html Action
view model =
    div
        []
        [ text <| toString <| model.currentTime]

-- Here, we use the model to figure out what we are currently subscribed to
-- We will use this more in the future
handleSubs : Model -> Sub Action
handleSubs model =
    setCurrentTime

main : Program Never
main =
    Html.program
        { init = (init, Cmd.none)
        , view = view
        , update = update
        , subscriptions = handleSubs
        }

Filter

Filters allow you to only listen to values when they pass some evaluation. They're useful for controlling when things get sent to your update loop. For example, if we don't want to update the current time, based on whether or not something is even, then we might do something like this:

0.16

-- only produce a changed value if the time is even
currentTime : Signal Time
currentTime =
    Time.every Time.second
        |> Signal.filter (\x -> x % 2 == 0)

setCurrentTime : Signal Action
setCurrentTime = Signal.map SetCurrentTime currentTime

0.17

In 0.17, we can that too - but it looks like this instead.

type Msg
    = SetCurrentTime Time
    | NoOp

setCurrentTime : Sub Msg
setCurrentTime =
    Time.every Time.second
        |> (\time ->
            if time % 2 == 0 then
                SetCurrentTime time
            else
                NoOp
            )

0.16

What about if we wanted to get the current time, only if a certain boolean was enabled?

currentTime : Signal Time
currentTime =
    Time.every Time.second
        |> Signal.filter (\x -> x % 2 == 0)


setCurrentTime : Signal Action
setCurrentTime =
    Signal.map2 (\time model -> if model.isListening then SetCurrentTime Time else NoOp) currentTime app.model)

0.17

In 0.17, this logic is handled in our handleSubs function. This is the reason why we pass in the model to the subscriptions function - it's important to be able to change what we are subscribed to.


handleSubs : Model -> Sub Action
handleSubs model =
    if model.isListening then
        Time.every Time.second SetCurrentTime
    else
        Sub.none

The cool thing about subscriptions in 0.17 is that if we aren't currently subscribed to a subscription, that given subscription will stop being listened to entirely. This was not the case in 0.16

Merge

Merge allows you to take two signals of the same type, and create a new signal that will produce values when either of the signals merged changes. In the case of a collision, the left signal would win.

0.16

In 0.16, it would look something like this:

setMouseMove : Signal Action
setMouseMove = Signal.map SetMouseMove Mouse.moves

allSignals : Signal Action
allSignals =
    Signal.merge setCurrentTime setMouseMove

0.17

In 0.17, this is pretty much the same. Instead of merge, we use batch. This is similiar to mergeMany in 0.16, or the concept of Effects.batch.


handleSubs : Model -> Sub Action
handleSubs model =
    Sub.batch
        [ Time.every Time.second SetCurrentTime
        , Mouse.moves SetMouseMove
        ]

Foldp

Foldp is complicated. If you were using foldp before, then you probably want to be using just an update loop now. If you had a legitimate use cases for using foldp separately from your main model or update function, then let us know on #elm-dev Slack channel or the elm-dev mailing list.


Noah Hall
@eeue56
Engineer at NoRedInk