Elm Effect Managers — The Missing Manual

Starting in version 0.17, Elm abolished Signals and replaced them with Subscriptions and Commands.

A command is a request for the Elm runtime to perform an operation that has side effects. An app can return a command from its update function; Elm will then schedule the appropriate operation in the background. Apart from the other side effects that operation can have, it can also generate messages and send them back to your app.

Elm’s core libraries expose functions for creating commands to do a number of simple things: generate a random number, send an HTTP request, check whether an app is currently visible, and so on.

The Elm datatype for commands is Cmd msg; a value of type Cmd msg represents a command that can generate messages of type msg for your app.

A subscription is a description of the sources of outside-world information your app would like to be kept in the loop about. An app can return a subscription from its subscriptions function, which is called every time the model changes. For as long as you subscribe to a subscription, your app will receive any messages generated by it.

Elm’s core libraries also expose functions for creating subscriptions: you can create a subscription to incoming messages from a websocket, mouse events in the browser, clock ticks, and more.

The Elm datatype for subscriptions is Sub msg; a value of type Sub msg represents a subscription that can generate messages of type msg for your app.

Behind the scenes, commands and subscriptions are provided by effect managers. Effect managers are special modules that can define new types of commands and subscriptions. Like a normal module, effect manager modules can expose pure functions for consumption by other modules. But there are a few key differences — and it is these differences that give effect managers their power. Let’s take a look…

Command and subscription types

As described above, commands are requests for an effectful operation to be performed; subscriptions are descriptions of information sources an app would like to hear from. Because “a request to generate a random number” carries different information from “a request to send an HTTP GET request,” Elm provides a way for effect manager authors to define their own internal representations for the commands and subscriptions they will expose.

First, the library author defines a normal Elm type to represent her command or subscription. In existing libraries, it appears this type is usually named MyCmd or MySub  —  but this is not a requirement. The type does need to have a type parameter; this is the type of message that your command or subscription will generate.

As an example, let’s consider an effect manager that exposes a command to generate a globally unique ID (GUID). (This is a good candidate for an effect manager, because whereas pure functions return the same values when called with the same inputs, we want each generated ID to be different from the last.) Since we only have one command, and it appears to be un-customizable (“generate a GUID”, not “generate a GUID of length 23 with the prefix ‘pony’”), it might seem at first glance that we could represent the command with this very simple type:

-- Version 1: Incorrect
type MyCmd msg = GenerateGUID

Unfortunately, this won’t work. The truth is, despite appearances to the contrary, there is not just one possible “generate GUID” command. Our command can be customized, whether we like it or not.

This is because commands generate messages, and different apps will need our GUID-generating command to produce different types of message. So our internal representation of the Generate command must at least contain information about what kind of message we need to produce, and how to make that kind of message out of the primitive GUID we’ll generate.

Let’s try again. We’ll assume that our effect manager’s core capability is to generate GUIDs of type Int. Then a Cmd msg to generate a GUID must contain a function of type Int -> msg that will allow us to transform the GUID we generate into a message:

-- This will work
type MyCmd msg = GenerateGUID (Int -> msg)

This way, `GenerateGUID identity` is a command to generate an Int, `GenerateGUID toString` is a command to generate a String, and so on.

Once we’ve defined a type, we can tell Elm that we want to use this type to represent commands. We do this at the top of the module, in our effect module declaration:

effect module Guid where { command = MyCmd } exposing ()

If we were working on a subscription, we would write { subscription = MySub } instead. If our module exposed both commands and subscriptions, we could write { command = MyCmd, subscription = MySub }.

Once we declare our module to use a certain type to represent commands, Elm gives us access to a magical function called command, of type MyCmd msg -> Cmd msg. This allows us to take a MyCmd like GenerateGUID f and turn it into an Elm command to give back the user! (The function is called `subscription` for subscriptions, and works the same way: `MySub msg -> Sub msg`.)

Let them eat Cmds

Speaking of which, let’s use that magic function to expose a command, Guid.generate, that will allow end users to request a GUID.

effect module Guid where { command = MyCmd } exposing (generate)
generate : Cmd Int
generate = command (GenerateGUID identity)

In this snippet, we’ve added the new value generate to our list of exposed identifiers. generate is a Cmd Int: it represents a request to generate a GUID and send the generated Int as a message directly to the app. To create the Cmd Intto return to the user, we first build up a MyCmd Int using the Generate constructor we defined earlier, then apply the magical command function to it to transform it into a real Elm command. Why pass in identity as the argument to GenerateGUID? Since this is a Cmd Int, we don’t need to transform the generated GUID to another type to make it into a message: our messages are already Ints. The “transformation” from Int to message is simply identity.

Of course, most users won’t want a Cmd Int. They’ll want a Cmd Msg for whatever type Msg their app uses. Although they could call Cmd.map to transform Guid.generate into the type of command they need, there’s no reason to require them to do this. Let’s change generate to allow for customization from the get-go:

generate : (Int -> msg) -> Cmd msg
generate msgFromInt = command (GenerateGUID msgFromInt)

This way, an app that had a message type like `type Msg = … | NewGUID Int | …` could write `Guid.generate NewGUID` and get back a ready-to-use Cmd Msg.

Enabling Cmd.map (or Sub.map)

Cmd.map is a Platform function that turns commands that generate one type of message into commands that generate a different kind of message. Implementing Cmd.map for a certain type of command requires knowledge of that command type’s implementation — so it’s up to us. Elm expects effect managers that expose commands to implement the cmdMap : (a -> b) -> MyCmd a -> MyCmd b function (subMap for subscription-exposing modules). When a user calls Cmd.map, Elm will actually call the effect manager’s implementation of cmdMap.

In our simple module, cmdMap is implemented using function composition:

cmdMap : (a -> b) -> MyCmd a -> MyCmd b
cmdMap f (GenerateGUID intToA) = GenerateGUID (intToA >> f)

Here, intToA >> f is the composition of intToA : Int -> a with f : a -> b to produce a new function of type Int -> b.

The effect manager lifecycle

Now that we’ve fully defined our command’s representation, it’s time to make it do something. To do this, it will help to understand the lifecycle of an effect manager.

When an app imports an effect manager, Elm will begin by initializing it. Effect managers must provide a value init of type Task Never state. To initialize your effect manager, Elm performs the task and stores the result — a value of type state (which can be any type you’d like) — as your effect manager’s initial state. For example, if you provide

init : Task Never Int
init = Task.succeed 0

as your init, then Elm will gather that your effect manager uses values of type Int to represent its internal state, and will set the initial state to the value 0. You can think of the state as being the “model” for your effect manager. It can store information that you’d like to persist across different invocations of commands and subscriptions throughout the lifetime of an app.

After initializing, nothing happens until the client app returns a command or subscription from your effect manager. At this point (or really, sometime after this point, when Elm gets around to it), Elm calls onEffects, another function the effect manager must implement. onEffects takes a few arguments. The first is a Router, an opaque object used to generate tasks that send messages to the main app or to the effect manager itself. Next up are two lists, of type (List (MyCmd msg)) and (List (MySub msg)), representing the commands and messages Elm wants you to deal with. If you are implementing only commands or only subscriptions, your onEffects function will only be passed one list, not two. Finally you are passed the current state of your effect manager, which Elm has been keeping track of for you. You are supposed to return a task that describes a chain of operations for Elm to do behind the scenes. The success type of the task should be the type of your state: if it succeeds, Elm will update your effect manager’s state to equal the task’s success value.

Let’s write a simple onEffects for our GUID module. For now, we’ll just have the first GUID we generate be 0, the second be 1, the third be 2, and so on. (This is obviously not how we would do it in a real module!)

onEffects : Platform.Router msg Never -> List (MyCmd msg) -> Int -> Task Never Int
onEffects router cmdLst nextId =
case cmdLst of
[] ->
Task.succeed nextId
        (Generate f) :: cmds ->
Platform.sendToApp router (f nextId)
`Task.andThen` \_ -> onEffects router cmds (nextId + 1)

First, note the arguments. The Router is parametrized by the types `msg` (the client app’s message type) and `Never` (because we don’t send any “self-messages” in this module). We receive a list of commands but not of subscriptions, because this module only exposes a new command type. We receive an Int representing the current state. Why is our current state an Int? Because in our init, we used Task.succeed 0 as our initializing task. (Semantically, we are using the state to remember what the next number to generate is; it starts out at 0, then once we generate 0, it increases to 1, etc.)

Because Elm might give us a list of commands all at once, we use recursion to work through them. The goal is to return a task that (a) describes to Elm everything that must be done, and (b) succeeds, in the end, returning our fully up-to-date state.

If there are no commands to process, we just return `Task.succeed nextId`, which says: don’t do anything, but succeed with the same state we have currently. This is a no-op.

If there are commands to process, we create a chain of two tasks. The first asks Elm to send a message to the app (f nextId, where f is the function, stored inside the Generate command itself, that turns an Int into a message), and the second is the task that handles all the other commands we have to deal with, by calling onEffects recursively. Importantly, the state we pass to this recursive call has been increased by 1.