Describing State in FP Scala
When learning FP, it’s quite common to assume that modelling state mutation is hard. A simple value update in an imperative style can be hard to reason in pure FP code:
In small scope, imperative mutations look appealing, but what if we want to compose them. How would one even approach such a task? In that case, we would leverage our knowledge in functional composition by describing mutation as a function type State[S, A] = S => (S, A)
. With this simple representation, we can express our program from above like this:
As you can see from the syntax above, it does not look pretty. To improve code readability, we might want to represent it as a for
expression.
Let’s useCats
library to help us build a Monad
definition (if the ?
Looks weird you can learn more about it here. Another way to express this is ({type L[A] = State[S, A]})#L
:
Now the imperative code above can be expressed as:
Building Pseudo-Random composable Generator
A more interesting example is trying to build a composable generator that mimics the features of ScalaCheck. We can define our Generator State as type Gen[A] = State[Seed, A]
, where Seed is:
And some primitive values builders can be expressed as:
When primitives are out of the way we can start building values that have size boundaries:
We can easily compose generators to get more complex generators:
We can now use our generator like this: genString(Seed(20L))
Adding configurations
That’s all good and great, but how do we add some configurations. Let’s image we want to prefix all our string with a certain prefix
. To do that we need to adjust our State
to support configuration passing. We should change it to:
Let’s redefine our Gen
type as well as define how our configuration looks like:
There are some minor places where the code needs to change:
And finally our genString
:
We can now use our generator like this: genString(Config(prependToStr = "test-"), Seed(20L))
Adding Logging
The final bit that would make this generator super awesome is some logging mechanism that would let us debug the generating process. As before we need to look into types to express this concern. The vital thing to note is that our logging accumulates. So it must be a Monoid, that means that it describes zero and associative binary operation, that adds logs together:
As previously we need to adjust our Gen
definition:
And our genChar
and genString
definition:
If we call it as before, our output looks a bit different:
Conclusion
Building a stateful application requires an understanding of primitive constructs as state (S => (S, A))
for simulating mutation,writer (A => (W, A))
for aggregation of messages, reader (A => B)
for adding additional dependencies and configurations to your computation. With these concepts at hand, we can model arbitrary composable programs that express even the most complicated requirements.
References
Hope you liked the post, here are some links to help you understand these concepts even better:
- Github repository addressing even more unusual cases of this generator by building
ScalaCheck
Spec2
test suite — https://github.com/Algiras/genos - Cats
State
documentation https://typelevel.org/cats/datatypes/state.html — a borrowed theSeed
definition from here - If you need something like this, please use https://github.com/NICTA/rng