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.
There are three main functions for working with the wrapped state value:
-
State.get: aquire the state value.
State.map2 (+) State.get State.get |> State.run 42 -- == (84, 42)
-
State.put: set the state to a certain value.
State.put 5 |> State.run 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) |> State.map (\_ -> "finished") |> State.run 42 -- == ("finished", 43)
Note that
State.modify
(because it combines get and put) discards the value currently stored in the 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 State.run
does just that.
run : s -> State s a -> (a, s)
run initialState (State f) =
f initialState
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 =
let
mark : Int -> State (Array Bool) ()
mark n =
State.modify (Array.set n False)
multiplesToMark length n =
range (n * n) length n
in
State.map Array.length State.get
|> andThen (\length ->
State.mapState mark (multiplesToMark length n)
|> andThen (\_ ->
State.map (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 =
let
mark : Int -> State (Array Bool) ()
mark n =
State.modify (Array.set n False)
multiplesToMark length n =
range (n * n) length n
getArrayLength =
State.map Array.length State.get
markMultiples length =
-- mapState shares the Array Int between invocations of mark.
State.mapState mark (multiplesToMark length n)
setNextIndex _ =
State.map (toNextIndex n) State.get
in
getArrayLength
|> 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
State.map f State.get
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.
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 =
let
initialState =
Dict.fromList [ ( 0, 1 ), ( 1, 1 ) ]
in
State.finalValue initialState (fibHelper n)
fibHelper : Int -> State (Dict Int Int) Int
fibHelper n =
let
-- 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)
|> State.map (\_ -> 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
in
State.get
|> 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 =
let
initialState =
Dict.fromList [ ( 0, 1 ), ( 1, 1 ) ]
in
State.finalValue initialState << fibsHelper
fibsHelper : List Int -> State (Dict Int Int) (List Int)
fibsHelper =
State.traverse fibHelper
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 =
let
myGenerator = Random.int 0 10
(a, newSeed1) = Random.step myGenerator seed
(b, newSeed2) = Random.step myGenerator newSeed1
(c, newSeed3) = Random.step myGenerator newSeed2
in
(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 =
let
myGenerator = Random.int 0 10
in
Random.map3 (,,) myGenerator myGenerator myGenerator
|> (flip Random.step) seed
|> Tuple.first
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 =
let
(newValue, newState) = f value state
in
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 =
let
(newValue, newState) = f state
in
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 =
let
helper : s -> (s, b)
helper initialState =
let (newValue, newState) = f initialState
-- unwrap the newly created state
(State h) = g newValue
in
h newState
in
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.