State Monad for the Rest of Us
arialdomartini.github.ioA series of articles starting from the very scratch and getting to the State Monad. It's thought to be novice-friendly: although using F#, it assumes no knowledge of it. If only it aroused someone's curiosity around F#, that would make my day.
It shows how algorithms with mutable state can be implemented with pure functions, with immutable variables only.
The series itself is an excuse to take several detours on other Functional Programming topics: currying, partial application, recursion, Functors, Applicative Functors.
The source code includes F# and C# examples. In the next weeks it will be followed by code examples showing how to apply a state monad in practice.
Not all the ways through it, but a good intro.
On first glance, your example:
Isn't that far off from what is pretty easily done in C#:(decimal | DivideByZeroException) Divide(decimal n, decimal d) => n / d;
(Sharing for folks who aren't familiar with C# Tuple types and extension methods)using System.Runtime.CompilerServices; (decimal? , DivideByZeroException?) Divide(decimal n, decimal d) { try { return (n / d, null); } catch { return (null, new DivideByZeroException()); } } Divide(6, 2).Switch( (result) => Console.WriteLine($"6 /2 is {result}"), (ex) => Console.WriteLine("This is an exception!") ); Divide(6, 0).Switch( (result) => Console.WriteLine($"6 /2 is {result}"), (ex) => Console.WriteLine("This is an exception!") ); public class DivideByZeroException : Exception; public static class TupleExtension { public static void Switch<T, E>(this ValueTuple<T, E> tuple, Action<T> action1, Action<E> action2) { if (tuple.Item1 is null && tuple.Item2 is null) { } else if (tuple.Item1 is null) { action2(tuple.Item2); } else { action1(tuple.Item1); } } }You'll be keeping a lot more info in your head with your approach. For instance, the following illegal states have legal representations in your system:
Hence your unhandled base case in the extension method. Sum types are a more accurate representation and more pleasant to use as a result.(null, null) (result, new DivideByZeroException())(btw, you're looking through the Monads For The Rest Of Us tutorial, but the the State Monad For The Rest Of Us tutorial is the one linked to this thread)
I don't see those as necessarily illegal states, but even so, it's not hard to handle by adding a third `Action` for it (I kept the example short and left the first `if` condition empty).
The second case, if there is an exception, it would be handled as such by simply switching the order of the `if` in the extension method. None of this seems confusing nor onerous in day-to-day use.
What does it mean to receive both value and error in the context of division? What about neither?
The issue is not that they cannot be handled, it's that they can be. You included an entire extra branch and check for something that will never occur because they're legal values for your tuple type. I assume it'll be optimized out of this trivial example, but imagine writing a function to receive your tuple type in a codebase where you didn't create the tuple. ADTs prevent this entire worry because there is no way to represent illegal states.
It's only "invalid" if you define it as such. We can state "if we receive neither, then this is the same as an error state". And "if we receive both, than an exception occurred and should be handled".What does it mean to receive both value and error in the context of division? What about neither?I don't see the issue?
The return type of a function is an assertion. You’re saying that values of the type exist. The return value is an assertion about the arguments. You’re saying that they imply the result. The issue is that your code is incorrect, in a logic sense, if it states that for some given value the mathematical operation of division results in both an error and a number.
The issue is not one of usability or (direct) understandability, it’s an issue of correctness. Something like “well it works fine for me if I just remember to check if I receive both” or “everyone on my team understands that receiving both means an error occurred” just isn’t a valid response, it doesn’t address the problem.
Now you have 3 different states that all represent an error:
One of which does not actually use our custom error type, and one of which contains a value for some reason. Our function is simple division! Are there really 3 different ways division can fail?(null, new DivideByZeroException()) (null, null) (result, new DivideByZeroException())This isn't a showstopper, it's just more stuff you have to keep in your head.
> I don't see the issue?
> if we receive both, than an exception occurred and should be handled
P(decimal ∩ DivideByZeroException) = 0 , therefore we only need 2 states, not 4.
You'd love Go because they also treat Sums as Ad-Hoc Product types.
Very cool, thanks for sharing. I wonder how easy it would be to port this to Rust, just for the heck of it...
One minor thought: where you have `A Tree is either a Leaf or a Node containing 2 branches each containing another Tree structure.`, perhaps you could have `A Tree is either a Leaf or a Node _made of_ 2 branches each containing another Tree structure.`
So that "made of" easily maps to the "of" in F#'s type definition. Just an idea.
Given that the book opens by defining a recursive datatype, it would be an exercise in frustration to port this to Rust...
How so? Can't you just put your Tree in a Box?
enum Tree { Leaf, Node(Box<Tree>, Box<Tree>), } fn number_of_leaves(tree: &Tree) -> u32 { match tree { Tree::Leaf => 1, Tree::Node(left, right) => number_of_leaves(left) + number_of_leaves(right), } } #[cfg(test)] mod tests { use super::*; #[test] fn counts_the_number_of_leaves_in_a_tree() { let tree_with_3_leaves = Tree::Node( Box::new(Tree::Leaf), Box::new(Tree::Node( Box::new(Tree::Leaf), Box::new(Tree::Leaf), )), ); let leaves = number_of_leaves(&tree_with_3_leaves); assert_eq!(leaves, 3); } }Yes, and the frustration is on dealing with all the boxes and unboxing, at the exact correct place, and that won't unify with each other.
There's a sibling comment about porting it to C#. The Rust version will be less frustrating than the C#, but it is the same kind of bad.
On Rust specifically, this happens because Rust is a low level language.
The sibling comment looks way closer to port of Go which is rather unidiomatic C#. I understand where it is coming from, but there is a much terser and type-safe way to express it:
Once we have type unions[0], the syntax could be easily promoted (assuming current proposal state) to e.g.#pragma warning disable CS8509 // The switch expression does not handle all possible values. // Justification: The switch expression is exhaustive, and Roslyn emits optimal default guard. // In AOT, ILC should be able to statically prove by whole program view that only two subclasses of Tree exist and optimize it away. // In practice, it seems null check and recursive nature of the call prevent it for now. It is still compact and fast codegen however. var treeWith3Leaves = new Tree.Node( new Tree.Leaf(), new Tree.Node( new Tree.Leaf(), new Tree.Leaf())); var leaves = NumberOfLeaves(treeWith3Leaves); if (leaves != 3) { throw new Exception("Huh, something went wrong."); } static int NumberOfLeaves(Tree tree) { return tree switch { Tree.Leaf => 1, Tree.Node n => NumberOfLeaves(n.Left) + NumberOfLeaves(n.Right) }; } abstract record Tree { public record Leaf: Tree; public record Node(Tree Left, Tree Right): Tree; }
Or a bit lower-level variant:union Tree { Leaf; Node(Tree Left, Tree Right); }
This will allow to remove exhaustiveness suppression and not pay for (house of) leaves.record Node(Tree Left, Tree Right); union struct Tree { Leaf; Node; }[0]: https://github.com/dotnet/csharplang/blob/18a527bcc1f0bdaf54...
Geez, that's way easier than I remembered. When I originally encountered this while learning Rust I figured recursion was my problem and switched to learning Haskell. (No regrets.) I guess I need to take my findings back to Rust!
Cool idea! Thank you for the suggestion. Fixed!
Happy to contribute even if in a minuscule way :-) FYI you changed the definition when it first appears, but not in the shaded box when you repeat it later in Chapter 1
For someone who's curious about these topics, do you recommend F# as a language to learn and apply them in practice? I've always thought of Haskell as THE functional programming language because of its purity and elegant syntax which looks very much like math. But lately I've seen people talk about F#, Clojure, ... and I wonder if Haskell is mostly an academic language whereas these are mostly practical?
Those two languages are popular because they bring some of the benefit of languages like Haskell into an environment that is compatible with an outdated but widely used OOP language.
Haskell will bring you more of the benefits from FP with expressive types; Idris will bring you even more. But you will lose the integration.
A lot of people mistake Purity as a language feature for meaning a more pure functional language. What makes a language functional is that their basic unit of abstraction is functions, not whether they isolate IO effects by default.
Impure functions lack referential transparency, which is often sacrificed but kind of important to being a function - getting the same result every time you call it with the same input. Alternatively, you can say that every function has an implicit input parameter of the the current program state, but that turns every function on one bit of input to a potentially astronomically complex function of gigabytes of input.
I do agree with what you're saying about the value of referential transparency and the formal definition of a function as a lambda term. In fact, I work professionally as a Haskell developer and highly value this aspect of the language design; equational reasoning is a really powerful tool.
My point is that functional programming as engineering practice and language family is broader then variants of pure lambda calculi and Haskell occupies one particular place in that broad space.
Haskell focuses on both, but prioritizes research. I have had several Haskell jobs and highly recommend it for the personal enjoyment.
Haskell is far from being an academic language. It’s used in industry and has very practical features and libraries.
Same as F#, OCaml, etc.
F# is not particularly practical as far as jobs go, but it benefits from the large C# ecosystem.
I'm primarily a C# dev, but after dabbling in some F#, I concluded that most of the niceties in F# could also be done (to some extent and with some limitations) on C# as they have the same compile targets.
You should probably mention in the introduction what the state monad is to those who don't use F#. At first blush it just seems redundant—what is a monad if not a representation of state?
Some uses you'll find in Haskell:
- representation of "the real world" where you can send data, control things, etc;
- representation of early return error semantics, like you have with exceptions;
- encapsulation of different execution threads, so you only interface with the group;
- encapsulation of a computer architecture, so something compiled for your GPU runs on the GPU, and something compiled for your CPU run on your CPU;
- encapsulation of transactions, so you can have a multi-threaded software changing whatever shared value you want and none of that leaks to the larger software;
- data structure, like libraries that encode hardware description into them.
That list is not in any way comprehensive. It's just stuff that I can remember right now.
You can also use it for representing errors, for representing global read-only variables, for representing global write-only variables, for representing non-determinism, for representing continuations, and other things (since you can write your own).
For example, this is a really minimalistic arithmetic syntax tree interpreter in Haskell. You could write errors using the Either type:
It's cumbersome, and there's a staircase growing. Because Either is a monad, you could rewrite the division op like this:data Term = Constant Int | Divide Term Term eval :: Term -> Either String Int eval (Constant n) = n eval (Divide left right) = case eval left of Left errorString -> Left errorString Right leftResult -> case eval right of Left errorString -> Left errorString Right rightResult -> if rightResult == 0 then Left "Division by zero" else Right (div leftResult rightResult)
This is a good article about the either monad: https://mmhaskell.com/blog/2022/3/3/using-either-as-a-monadeval (Divide left right) = do leftResult <- eval left rightResult <- eval right if rightResult == 0 then Left "Division by zero" else Right (div leftResult rightResult)The state monad is basically a wrapper over a pure function that takes a state and returns a tuple with the result and a new state. In an imperative language, you'd just actually change state, rather than doing that.
An example from this wiki page: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/S...
In an imperative language, for pseudo-random numbers, you could modify global variables every time you want a new random number. Haskell lets you do this in IO functions. But a pure way would be to return the result of the new global variable every time.
It's annoying. You could use the state monad to simplify this:-- randomR takes a range, so randomR (1,6) produces a random number between 1 and 6 rollPair :: StdGen -> ((Int, Int), StdGen) rollPair s0 = let (r1, s1) = randomR (1,6) s0 (r2, s2) = randomR (1,6) s1 in ((r1, r2), s2)rollDieS :: State StdGen Int rollDieS = state (randomR (1,6)) rollPairS :: State StdGen (Int, Int) rollPairS = do r1 <- rollDieS r2 <- rollDieS return (r1, r2)Just realized I should have written one of these for the Constant evaluation:
eval (Constant n) = Right n eval (Constant n) = return n> The state monad is basically a wrapper over a pure function that takes a state and returns a tuple with the result and a new state.
So... it's a monad? I don't understand why this is worth calling State as distinct from non-State when that doesn't seem to add any meaning. Surely there must be something implied by a State monad that is not implied by a non-State monad. That seems relevant to call out in an introduction.
I don't see why you think all monads are just like the State monad. The Maybe and Either monads have nothing to do with state, for example.
> The Maybe and Either monads have nothing to do with state, for example.
See this is what I'm talking about. What does `state` mean to you where monads don't have any relation? It's just an abstract structure—what makes something state or not is the context.
State can be modeled as a monad, but monads aren't a synonym for state. Just like squares are technically rectangles, but "rectangle" isn't a synonym for "square."
How do I think of the State monad?
- Like a Mealy machine: https://en.wikipedia.org/wiki/Mealy_machine
- Like a pure way to compose functions that use a nameless, global mutable variable.
- As a newtype wrapper over the type "s -> (a, s)" where s is the type of the state and a is the type of the computation.
monads in no way are a representation of state tho
How do you figure? They're an abstract concept commonly used to represent values over a sequence of operations. How does that not describe state? Do you mean something very specific with the term "state" that might make it more meaningful?
I think "context" is a bit more all-encompassing.
If I have an Int, then I just have an Int.
If I have an Identity Int, then I really still just have an Int.
If I have a [Int] then I have a list of Ints.
If I have a Maybe Int, then I either have an Int or I have Nothing.
If I have a Reader r Int, then I have a computation taking some input of type r and producing an Int. That computation can't modify the value of r.
If I have a State s Int, I have a computation taking some initial state of type s and producing an Int. The value of s may change during the computation.
All of these are monads, but calling all of them "state" is somewhat reductive.
The Haskell wiki tutorial on monads refers to them vaguely as "strategies" - I mostly agree with that description.
Monads can encode state but they don't have to.
I tend to think of a monad in computer science as something which is constructed from a type and a computation and wraps the return values of that computation in that type. That allows you to bake in "context" into that type which could be state in the conventional sense but it could be something else.
To make this practical, let's make a simple example of a simple somewhat useful monad with no state. Imagine you have all the regular trigonometric functions sine, cosine etc which accept radians[1] and you need versions which work in degrees. You could make a unit conversion monad which converts arguments on the way in and wraps the result in an inverse conversion monad such that if you passed it to arcsine arccosine etc on it it would know and return degrees on the way out.
Neither of those monads would have any state in the usual computer science sense, the main conversion is just multiplying all the inputs by pi/180 before calling the wrapped function and constructing a result monad type so the inverse trig functions do the opposite of that.
[1] ie the correct versions. Fight me physicists and engineers and you freaks who use gradians whoever you are[2].
[2] I think it's surveyors but I could be wrong.
Monads can be used to attach all sorts of things besides state to data. For example, you can associate read-only data, such as configuration, authenticated identies, etc. You can associate I/O methods, which can be real or mocked. You can have a place in which to scribble logs/traces (this feels like state). And yes, you can associate mutable state, of course.
The point of monads, from the point of view of programmer convenience, is that they let you have a bag of contextual stuff along with your values. Thus if you think of a server application that processes requests, you might want to carry all of:
with each request. Thus each function that handles a request can get all of that context, and can be fully deterministic, yet it can function in a non-deterministic world.- configuration applicable to this request (or global configuration) - request metadata (time received, from where, from whom, authenticated how, etc.) - request processing state - logs/traces - I/O methods, so that - you can make all your code deterministic and thus easier to write tests for - so you can mock the world (see previous item)Besides this there's everything about the Maybe ("Optional") and Error/Either monads that just makes it easy to make sure all errors and "nulls" are handled.
And what makes all of this possible (in Haskell) is the use of operators (functions) and syntax that lets one write "statements" that are actually combined into large expressions.
Ok, this is super-interesting to me, as I'm currently dealing with the "configuration and interfaces as global state" pattern in a legacy application. Among other things, this makes it hell to test and refactor. Refactoring it into a functional abstraction like this seems like exactly what we need.you might want to carry all of: - configuration applicable to this request (or global configuration) [...]But ... I'm kind of struggling to figure out what "global configuration as a monad" would look like (very much not a Haskell programmer). How would something like this work in practice?
"Global configuration as a monad" is represented by the Reader monad, which is basically a wrapper over a function that takes a configuration/environment as a parameter and then returns something. It replaces a read-only global configuration.
This is Haskell, but here's a really simple example.
Here's the same thing, but with a record with one boolean in it:-- As an ordinary function: foo :: Int -> Bool -> Int foo n shouldAdd = if shouldAdd then n + 1 else n - 1 -- Exactly the same, but using the Reader monad foo :: Int -> Reader Bool Int foo n = do shouldAdd <- ask return (if shouldAdd then n + 1 else n - 1)
It's basically a way to have implicit read-only parameters. You can call a bunch of functions that take a configuration parameter without having to actually pass the configuration as a parameter explicitly. In an object oriented language, you might use classes for this (if not global variables).-- Config: a record with one boolean in it, -- and you access it with a function called `shouldAdd` data Config = MkConfig { shouldAdd :: Bool } foo :: Int -> Config -> Int foo n cfg = if shouldAdd cfg then n + 1 else n - 1 foo :: Int -> Reader Config Int foo n = do mustAdd <- asks shouldAdd return (if mustAdd then n + 1 else n - 1)In Java or C++ or other OOPish languages, say, you might make all your classes be Configurable (unless they are Configuration), which means their constructors would take a Configuration in some way, possibly with an explicit Configuration argument or with a Configurable argument whose configuration to copy. This way all your objects will know how to find configuration information.
Sure, that makes sense, and is a go-to pattern for my own projects. But to map this onto FP, making classes Configurable feels like OO version of currying. I.e., I'm taking some parameters and baking them into methods that I will call later. But does this have anything to do with monads or some monadic version of state?
It's more like `Configured<T>` -- a Functor, something not too dissimilar to `List<T>`, but a) with just one `T` in it, b) with all your configuration things in the `Configured<T>` instance.
So everywhere you deal with a value that is of some type `Configured<T>` you can then refer to all the configuration methods you'd expect of you `Configuration` types. In Java you'd say something like `this.getConfigBlah()`, and it would just work.
> I'm taking some parameters and baking them into methods that I will call later.
Yes.
> But does this have anything to do with monads or some monadic version of state?
In languages that have monads: yes!
> In Java you'd say something like `this.getConfigBlah()`
No, the whole point is that it’s completely dissimilar to that.
> But does this have anything to do with monads or some monadic version of state?
There is no monadic version of state. The State monad is a way of chaining things together (not unlike shell pipes) which is nice in Haskell because it gives access to some of the language syntactic sugar (the do notation). There is nothing particularly special there otherwise.
The key takeaway here is that you can avoid littering your global scope by explicitly passing what’s shared as functions arguments and then remove side effects if you also pass by value.
Yes, it is this simple.
They are an abstraction of a sequence of operations, not the state used in those operations.
They're both, right? It's not like it specifically ignores the inputs and outputs of those operations. These are specifically entailed in the definition of the monad.
I strongly, strongly suspect there's something about how F# processes monads in the context of a do expression that explains what is the focus here.
Unfortunately I don't have the time to go through the steps now, hence why I specifically recommended modifying the introduction to give some clue about the terms involved.
An "if" statement or "map" function also has inputs and outputs and probably some internal state due to implementation details, but we don't say they represent state either. They represent operations, or a sequence of operations.
> An "if" statement or "map" function also has inputs and outputs and probably some internal state due to implementation details, but we don't say they represent state either.
They absolutely are in the context of a program and not just abstract expression, which never matters. The distinction strikes me as meaningless. At the very least it's worth qualifying with "mutable state" if that's the attribute of state you care about. There's still "constant expressions" as other forms of values and "statically initialized values" as other forms of state.
It's not about monads as an abstract concept. It's specifically about the State monad.
The key idea is that you can have something ressembling states with pure functions if you pass the a representation of the initial state as an argument and have the modified state be part of what the function return. You can then use this modified state as an argument for the next function call. The State monad is just an handy way to do this chaining.
But, yes, the actual state representation is in the way the argument is encoded, that's true.
Edit: I find it hilarious that I’m the one downvoted here while all the comments saying complete non sense about monads - including the ones clearly having no clue of what a monad actually is - are not. Never change HN.
Monad patterns cut down on boilerplate even in languages where you might not strictly “need” them. I’ve been working on a project in Elixir and I found the writer monad to be exactly what I needed. I wrote about it here: https://lambdaland.org/posts/2024-05-01_definitely_not_about...
A week or two ago, I found myself working in the same codebase. I had to do some refactoring and, thanks to the monad I had already established, the refactoring reduced code while expanding functionality. The best refactors happen when the patterns make it easy to do.
Any function matching the type
St -> (a, St)
can viewed as `State St' parameterized over `a'. There are several behaviors that organize such functions, such as Monad which "overloads the semicolon" to work on stateful computations. Another behavior implements a stateful interface: `MonadState St'. get :: St -> (St, St)
put :: St -> (St -> ((), St))
With type classes in Haskell, behaviour is type-directed so in order to reap the benefits we declare a new type. {-# Language DerivingVia #-}
{-# Language GADTSyntax #-}
-- get :: My St
-- put :: St -> My ()
newtype My a where
My :: (St -> (a, St)) -> My a
deriving (Functor, Applicative, Monad, MonadState St, MonadFix)
via State StIs there are correct way around?
vsSt -> (a, St)
I've found both in the wild and it can be very confusing!St -> (St, a)The order doesn’t matter as long as you’re consistent, but Control.Monad.State uses the first, and you’ll have a bad time (in Haskell anyway) if you’re not compatible with that.
It’s the same thing. Order doesn’t matter in the tuple. Just use a bind which is in line with the signature of the type you want to use.
Isn't every monad a state monad?
Not at all.
A monad is just a type constructor and two functions respecting some rules.
It can be widely different from the State monad ranging from useful (Option) to completely weird (the naive list monad) to doing nothing (identity monad).