Skip to content


Folders and files

Last commit message
Last commit date

Latest commit



34 Commits

Repository files navigation

Threading state through computation

This library provides ways to compose functions of the type s -> (a, s).

From time to time, you'll see a pattern like this in your code

(newValue,     newState)     = f state
(newerValue,   newerState)   = g newValue   newState
(newererValue, newererState) = h newerValue newerState

The above pattern is ugly and error-prone (because of typo's, for instance). It can be abstracted by creating a function that composes f and g, that is the output of f is the input to g.

    f :      s -> (a, s) 
    g : a -> s -> (a, s) 

This library implements the described composition and provides a bunch of helper functions for working with State. For a more in-depth explanation of how the implementation works, see the derivation.

This library is advanced and relatively abstract. If anything is unclear, please open an issue.

Working with values within State

There are three main functions for working with the wrapped state value:

  • State.get: aquire the state value.

    State.map2 (+) State.get State.get
        |> 42
        -- == (84, 42)
  • State.put: set the state to a certain value.

    State.put 5
        |> 3
        -- == ((), 5)

    Note that State.put discards the value currently stored in the state.

  • State.modify: apply a function to the state; a combination of get and set

    State.modify (\v -> v + 1)
        |> (\_ -> "finished")
        |> 42
        -- == ("finished", 43)

    Note that State.modify (because it combines get and put) discards the value currently stored in the state.

Extracting results from State

Remember, we really build a large s -> (a, s) function out of smaller components. To run this function, we need an initial state, and get a final state and a final value. The function does just that.

run : s -> State s a -> (a, s)
run initialState (State f) = 
    f initialState

Structuring computation with andThen

The composition operator for functions wrapped in State is called andThen. It is the primary way to structure computations that involve State. When not used with care, this can lead to truly awful code. This is what the Haskellers at Facebook call a code tornado:

cycle : Int -> State (Array Bool) (Maybe Int)
cycle n =
        mark : Int -> State (Array Bool) ()
        mark n =
            State.modify (Array.set n False)

        multiplesToMark length n =
            range (n * n) length n

    in Array.length State.get
            |> andThen (\length ->  
                State.mapState mark (multiplesToMark length n)
                    |> andThen  (\_ -> 
               (toNextIndex n) State.get

This problem can be solved by extracting subcomputations and giving them a descriptive name. Not only is the final composition (in the 'in' clause) very readable and understandable, the individual components are also nicely reusable.

cycle : Int -> State (Array Bool) (Maybe Int)
cycle n =
        mark : Int -> State (Array Bool) ()
        mark n =
            State.modify (Array.set n False)

        multiplesToMark length n =
            range (n * n) length n

        getArrayLength =
   Array.length State.get

        markMultiples length =
            -- mapState shares the Array Int between invocations of mark.
            State.mapState mark (multiplesToMark length n)

        setNextIndex _ =
   (toNextIndex n) State.get
            |> andThen markMultiples 
            |> andThen setNextIndex

When using andThen, try to break up your computation into small, reusable bits and give them a descriptive name. A general Elm principle is that the shortest code is often not the best code. Don't take shortcuts with andThen in production code.


  • Prevent code tornadoes

  • Name subcomputations/functions appropriately

  • A bit more verbose is better than a bit shorter

  • Limit the amount of code that is "in State" to a minimum Try to keep functions pure and use the helper functions in this package to let the work on values "in State".

  • Limit the use of andThen in combination with State.state Instead of this

    State.get |> andThen (\value -> state (f value))

    write f State.get

Use cases

By design, most functional programs don't use state, because it's quite cumbersome to work with. It's a good idea to see whether you can do without this library.

Sometimes though, there are very good reasons. This is mostly the case in traditionally imperative algorithms that use nonlocal mutable variables. There are a few other cases in standard Elm where the pattern in the (motivation)[#movation] pops up, the primary ones being working with random values and updating (child) components. This library is not made for the latter purposes, but its concepts transfer over.

Finally, there is a pattern from Haskell that uses State to hold configuration information (on its own called Read) and to store logging information (on its own called Write). This pattern hasn't really found its way to Elm, and it may not need to, because Elm solves its problems differently. In any case, experiment and see what works for you.

Caching function results

Some computations are resource-intensive and should preferably only be performed once. The classical example of this is the fibonacci sequence.

fib : Int -> Int 
fib n = 
    case n of 
        0 -> 1
        1 -> 1
        _ -> fib (n - 1) + fib (n - 2)

Every evaluation of fib (except the base cases 0 and 1) requires two more evaluations. Those evaluations each too require two more evaluations, making the time complexity of this function exponential. Furthermore, the two trees (fib (n - 1) and fib (n - 2)) overlap, doing the same calculations twice. These problems can we solved by caching already calculated values. This is where State comes in:

fib : Int -> Int
fib n =
        initialState =
            Dict.fromList [ ( 0, 1 ), ( 1, 1 ) ]
        State.finalValue initialState (fibHelper n)

fibHelper : Int -> State (Dict Int Int) Int
fibHelper n =
        -- takes n as an argument to preserve laziness
        calculateStatefullFib : Int -> State (Dict Int Int) Int
        calculateStatefullFib n =
            State.map2 (+) (fibHelper (n - 1)) (fibHelper (n - 2))

        addNewValue : Int -> State (Dict Int Int) Int
        addNewValue solution =
            State.modify (Dict.insert n solution)
                |> (\_ -> solution)

        modifyWhenNeeded : Dict Int Int -> State (Dict Int Int) Int
        modifyWhenNeeded cache =
            case Dict.get n cache of
                Just cachedSolution ->
                    state cachedSolution

                Nothing ->
                    calculateStatefullFib n
                        |> andThen addNewValue
            |> andThen modifyWhenNeeded

Notice how, with the help of choosing descriptive names, this function reads like a step-by-step description of what happens. This is what makes using andThen so nice.

It is also possible to reuse the cache for multiple invocations of fibHelper, for example using State.traverse.

fibs : List Int -> List Int
fibs =
        initialState =
            Dict.fromList [ ( 0, 1 ), ( 1, 1 ) ]
        State.finalValue initialState << fibsHelper

fibsHelper : List Int -> State (Dict Int Int) (List Int)
fibsHelper =
    State.traverse fibHelper

Threading a Random seed

When working with random values, you have to update the seed after every computation. Note how this is very similar to the general pattern in the motivation section.

myRandomValues =
        myGenerator = 0 10

        (a, newSeed1) = Random.step myGenerator seed
        (b, newSeed2) = Random.step myGenerator newSeed1
        (c, newSeed3) = Random.step myGenerator newSeed2
        (a, b, c)

The Random.mapN functions do exactly what compose described above does. I advice to use the random-specific functions when working with random values.

myRandomValues =
        myGenerator = 0 10
        Random.map3 (,,) myGenerator myGenerator myGenerator
            |> (flip Random.step) seed
            |> Tuple.first

Recursively applying update

Don't use this library for this purpose, prefer Fresheyeball/elm-return.

The typical signature for update with TEA is

update : Msg -> Model -> (Model, Cmd Msg)

When we drop the first argument (partial function application) and swap the order of the types in the tuple, we get

update' : Model -> (Cmd Msg, Model) 

This is a function with the same type as the ones wrapped in State. The elm-effects library uses a pattern similar to this library to consecutively apply operations that generate side-effects to a value.

Elm-effects is a bit less powerful than this package (technically, it's just a Writer monad, not a State monad). If you need the more powerful functions then reevaluate, use this package and maybe contribute to elm-effects.

A derivation of how the composition of functions of the form s -> (a, s) works.

In the example from the beginning,

(newValue,     newState)     = f state
(newerValue,   newerState)   = g newValue   newState
(newererValue, newererState) = h newerValue newerState

f and g have the following types.

    f :      s -> (a, s) 
    g : a -> s -> (a, s) 

We want to compose these two functions, which means that the a and s returned by f serve as an input to g. This can be done as follows:

    composed : a -> s -> (s, a) 
    composed value state = 
            (newValue, newState) = f value state
            g newValue newState

Let's generalize this a little further

    compose :       (s -> (a, s))
            -> (a -> s -> (b, s))
            ->      (s -> (b, s))
    compose f g state = 
            (newValue, newState) = f state
            g newValue newState

The essence of this library is composing multiple functions of the type s -> (a, s) into one giant function of the same type. All the intermediate threading (result of f is input to g) is automatically taken care of. For better error messages, the s -> (a, s) function is wrapped in a type.

type State s a = 
    State (s -> (a, s))

Compose can then be written as

compose : State s a -> (a -> State s b) -> State s b
compose (State f) g = 
        helper : s -> (s, b)
        helper initialState = 
            let (newValue, newState) = f initialState

                -- unwrap the newly created state
                (State h) = g newValue
                h newState
        State helper 

Functions of the type (a -> Wrapper b) -> Wrapper a -> Wrapper b are called andThen in Elm (see Maybe.andThen and Random.andThen), but sometimes also referred to as bind or (>>=).


  • RangeError: Recursion limit exceeded: Older versions of this package would often throw this error at runtime. Most of these cases are now fixed, but if you run into this error, please report it.


Threading state through computations in elm







No packages published