A pair of standard helper functions for the State monad. An article for object-oriented programmers.

The State monad is completely defined by its two defining functions (SelectMany and Return). While you can get by without them, two additional helper functions (get and put) are so convenient that they're typically included. To be clear, they're not part of the State monad - rather, you can consider them part of what we may term a standard State API.

In short, get is a function that, as the name implies, gets the state while inside the State monad, and put replaces the state with a new value.

Later in this article, I'll show how to implement these two functions, as well as a usage example. Before we get to that, however, I want to show a motivating example. In other words, an example that doesn't use get and put.

The code shown in this article uses the C# State implementation from the State monad article.

Aggregator #

Imagine that you have to implement a simple Aggregator.

"How do we combine the results of individual but related messages so that they can be processed as a whole?"

[...] "Use a stateful filter, an Aggregator, to collect and store individual messages until it receives a complete set of related messages. Then, the Aggregator publishes a single message distilled from the individual messages."

The example that I'll give here is simplified and mostly focuses on how to use the State monad to implement the desired behaviour. The book Enterprise Integration Patterns starts with a simple example where messages arrive with a correlation ID as an integer. The message payload is also a an integer, just to keep things simple. The Aggregator should only publish an aggregated message once it has received three correlated messages.

Using the State monad, you could implement an Aggregator like this:

public sealed class Aggregator :
    IState<IReadOnlyDictionary<int, IReadOnlyCollection<int>>, Maybe<Tuple<intintint>>>
{
    private readonly int correlationId;
    private readonly int value;
 
    public Aggregator(int correlationIdint value)
    {
        this.correlationId = correlationId;
        this.value = value;
    }
 
    public Tuple<Maybe<Tuple<intintint>>, IReadOnlyDictionary<int, IReadOnlyCollection<int>>> Run(
        IReadOnlyDictionary<int, IReadOnlyCollection<int>> state)
    {
        if (state.TryGetValue(correlationId, out var coll))
        {
            if (coll.Count == 2)
            {
                var retVal =
                    Tuple.Create(coll.ElementAt(0), coll.ElementAt(1), value);
                var newState = state.Remove(correlationId);
                return Tuple.Create(retVal.ToMaybe(), newState);
            }
            else
            {
                var newColl = coll.Append(value);
                var newState = state.Replace(correlationId, newColl);
                return Tuple.Create(new Maybe<Tuple<intintint>>(), newState);
            }
        }
        else
        {
            var newColl = new[] { value };
            var newState = state.Add(correlationId, newColl);
            return Tuple.Create(new Maybe<Tuple<intintint>>(), newState);
        }
    }
}

The Aggregator class implements the IState<S, T> interface. The full generic type is something of a mouthful, though.

The state type (S) is IReadOnlyDictionary<int, IReadOnlyCollection<int>> - in other words, a dictionary of collections. Each entry in the dictionary is keyed by a correlation ID. Each value is a collection of messages that belong to that ID. Keep in mind that, in order to keep the example simple, each message is just a number (an int).

The value to produce (T) is Maybe<Tuple<intintint>>. This code example uses this implementation of the Maybe monad. The value produced may or may not be empty, depending on whether the Aggregator has received all three required messages in order to produce an aggregated message. Again, for simplicity, the aggregated message is just a triple (a three-tuple).

The Run method starts by querying the state dictionary for an entry that corresponds to the correlationId. This entry may or may not be present. If the message is the first in a series of three, there will be no entry, but if it's the second or third message, the entry will be present.

In that case, the Run method checks the Count of the collection. If the Count is 2, it means that two other messages with the same correlationId was already received. This means that the Aggregator is now handling the third and final message. Thus, it creates the retVal tuple, removes the entry from the dictionary to create the newState, and returns both.

If the state contains an entry for the correlationId, but the Count isn't 2, the Run method updates the entry by appending the value, updating the state to newState, and returns that together with an empty Maybe value.

Finally, if there is no entry for the correlationId, the Run method creates a new collection containing only the value, adds it to the state dictionary, and returns the newState together with an empty Maybe value.

Message handler #

A message handler could be a background service that receives messages from a durable queue, a REST endpoint, or based on some other technology.

After it receives a message, a message handler would create a new instance of the Aggregator:

var a = new Aggregator(msg.CorrelationId, msg.Value);

Since Aggregator implements the IState<S, T> interface, the object a represents a stateful computation. A message handler might keep the current state in memory, or rehydrate it from some persistent storage system. Keep in mind that the state must be of the type IReadOnlyDictionary<int, IReadOnlyCollection<int>>. Wherever it comes from, assume that this state is a variable called s (for state).

The message handler can now Run the stateful computation by supplying s:

var t = a.Run(s);

The result is a tuple where the first item is a Maybe value, and the second item is the new state.

The message handler can now publish the triple if the Maybe value is populated. In any case, it can update the 'current' state with the new state. That's a nice little impureim sandwich.

Notice how this design is different from a typical object-oriented solution. In object-oriented programming, you'd typically have an object than contains the state and then receives the run-time value as input to a method that might then mutate the state. Data with behaviour, as it's sometimes characterised.

The State-based computation turns such a design on its head. The computation closes over the run-time values, and the state is supplied as an argument to the Run method. This is an example of the shift of perspective often required to think functionally, rather than object-oriented. That's why it takes time learning Functional Programming (FP); it's not about syntax. It's a different way to think.

An object like the above a seems almost frivolous, since it's going to have a short lifetime. Calling code will create it only to call its Run method and then let it go out of scope to be garbage-collected.

Of course, in a language more attuned to FP like Haskell, it's a different story:

let h = handle (corrId msg) (val msg)

Instead of creating an object using a constructor, you only pass the message values to a function called handle. The return value h is a State value that an overall message handler can then later run with a state s:

let (m, ns) = runState h s

The return value is a tuple where m is the Maybe value that may or may not contain the aggregated message; ns is the new state.

Is this better? #

Is this approach to state mutation better than the default kind of state mutation possible with most languages (including C#)? Why make things so complicated?

There's more than one answer. First, in a language like Haskell, state mutation is in general not possible. While you can do state mutation with the IO container in Haskell, this sets you completely free. You don't want to be free, because with freedom comes innumerable ways to shoot yourself in the foot. Constraints liberate.

While the IO monad allows uncontrolled state mutation (together with all sorts of other impure actions), the State monad constrains itself and callers to only one type of apparent mutation. The type of the state being 'mutated' is visible in the type system, and that's the only type of value you can 'mutate' (in Haskell, that is).

The State monad uses the type system to clearly communicate what the type of state is. Given a language like Haskell, or otherwise given sufficient programming discipline, you can tell from an object's type exactly what to expect.

This also goes a long way to explain why monads are such an important concept in Functional Programming. When discussing FP, a common question is: How do you perform side effects? The answer, as may be already implied by this article, is that you use monads. The State monad for local state mutation, and the IO monad for 'global' side effects.

Get #

Clearly you can write an implementation of IState<S, T> like the above Aggregator class. Must we always write a class that implements the interface in order to work within the State monad?

Monads are all about composition. Usually, you can compose even complex behaviour from smaller building blocks. Just consider the list monad, which in C# is epitomised by the IEnumerable<T> interface. You can write quite complex logic using the building blocks of Where, Select, Aggregate, Zip, etcetera.

Likewise, we should expect that to be the case with the State monad, and it is so. The useful extra combinators are get and put.

The get function enables a composition to retrieve the current state. Given the IState<S, T> interface, you can implement it like this:

public static IState<S, S> Get<S>()
{
    return new GetState<S>();
}
 
private class GetState<S> : IState<S, S>
{
    public Tuple<S, S> Run(S state)
    {
        return Tuple.Create(state, state);
    }
}

The Get function represents a stateful computation that copies the state over to the 'value' dimension, so to speak. Notice that the return type is IState<S, S>. Copying the state over to the position of the T generic type means that it becomes accessible to the expressions that run inside of Select and SelectMany.

You'll see an example once I rewrite the above Aggregator to be entirely based on composition, but in order to do that, I also need the put function.

Put #

The put function enables you to write a new state value to the underlying state dimension. The implementation in the current code base looks like this:

public static IState<S, Unit> Put<S>(S s)
{
    return new PutState<S>(s);
}
 
private class PutState<S> : IState<S, Unit>
{
    private readonly S s;
 
    public PutState(S s)
    {
        this.s = s;
    }
 
    public Tuple<Unit, S> Run(S state)
    {
        return Tuple.Create(Unit.Default, s);
    }
}

This implementation uses a Unit value to represent void. As usual, we have the problem in C-based languages that void isn't a value, but fortunately, unit is isomorphic to void.

Notice that the Run method ignores the current state and instead replaces it with the new state s.

Look, no classes! #

The Get and Put functions are enough that we can now rewrite the functionality currently locked up in the Aggregator class. Instead of having to define a new class for that purpose, it's possible to compose our way to the same functionality by writing a function:

public static IState<IReadOnlyDictionary<int, IReadOnlyCollection<int>>, Maybe<Tuple<intintint>>>
    Aggregate(int correlationIdint value)
{
    return
        from state in State.Get<IReadOnlyDictionary<int, IReadOnlyCollection<int>>>()
        let mcoll = state.TryGetValue(correlationId)
        let retVal = from coll in mcoll.Where(c => c.Count == 2)
                     select Tuple.Create(coll.ElementAt(0), coll.ElementAt(1), value)
        let newState = retVal
            .Select(_ => state.Remove(correlationId))
            .GetValueOrFallback(
                state.Replace(
                    correlationId,
                    mcoll
                        .Select(coll => coll.Append(value))
                        .GetValueOrFallback(new[] { value })))
        from _ in State.Put(newState)
        select retVal;
}

Okay, I admit that there's a hint of code golf over this. It's certainly not idiomatic C#. To be clear, I'm not endorsing this style of C#; I'm only showing it to explain the abstraction offered by the State monad. Adopt such code at your own peril.

The first observation to be made about this code example is that it's written entirely in query syntax. There's a good reason for that. Query syntax is syntactic sugar on top of SelectMany, so you could, conceivably, also write the above expression using method call syntax. However, in order to make early values available to later expressions, you'd have to pass a lot of tuples around. For example, the above expression makes repeated use of mcoll, so had you been using method call syntax instead of query syntax, you would have had to pass that value on to subsequent computations as one item in a tuple. Not impossible, but awkward. With query syntax, all values remain in scope so that you can refer to them later.

The expression starts by using Get to get the current state. The state variable is now available in the rest of the expression.

The state is a dictionary, so the next step is to query it for an entry that corresponds to the correlationId. I've used an overload of TryGetValue that returns a Maybe value, which also explains (I hope) the m prefix of mcoll.

Next, the expression filters mcoll and creates a triple if the coll has a Count of two. Notice that the nested query syntax expression (from...select) isn't running in the State monad, but rather in the Maybe monad. The result, retVal, is another Maybe value.

That takes care of the 'return value', but we also need to calculate the new state. This happens in a somewhat roundabout way. The reason that it's not more straightforward is that C# query syntax doesn't allow branching (apart from the ternary ?..: operator) and (this version of the language, at least) has weak pattern-matching abilities.

Instead, it uses retVal and mcoll as indicators of how to update the state. If retVal is populated, it means that the Aggregate computation will return a triple, in which case it must Remove the entry from the state dictionary. On the other hand, if that's not the case, it must update the entry. Again, there are two options. If mcoll was already populated, it should be updated by appending the value. If not, a new entry containing only the value should be added.

Finally, the expression uses Put to save the new state, after which it returns retVal.

While this is far from idiomatic C# code, the point is that you can compose your way to the desired behaviour. You don't have to write a new class. Not that this is necessarily an improvement in C#. I'm mostly stating this to highlight a difference in philosophy.

Of course, this is all much more elegant in Haskell, where the same functionality is as terse as this:

handle :: (Ord k, MonadState (Map k [a]) m) => k -> a -> m (Maybe (a, a, a))
handle correlationId value = do
  m <- get
  let (retVal, newState) =
        case Map.lookup correlationId m of
          Just [x, y] -> (Just (x, y, value), Map.delete correlationId m)
          Just  _ -> (Nothing, Map.adjust (++ [value]) correlationId m)
          Nothing -> (Nothing, Map.insert correlationId [value] m)
  put newState
  return retVal

Notice that this implementation also makes use of get and put.

Modify #

The Get and Put functions are basic functions based on the State monad abstraction. These two functions, again, can be used to define some secondary helper functions, whereof Modify is one:

public static IState<S, Unit> Modify<S>(Func<S, S> modify)
{
    return Get<S>().SelectMany(s => Put(modify(s)));
}

It wasn't required for the above Aggregate function, but here's a basic unit test that demonstrates how it works:

[Fact]
public void ModifyExample()
{
    var x = State.Modify((int i) => i + 1);
    var actual = x.Run(1);
    Assert.Equal(2, actual.Item2);
}

It can be useful if you need to perform an 'atomic' state modification. For a realistic Haskell example, you may want to refer to my article An example of state-based testing in Haskell.

Gets #

Another occasionally useful second-order helper function is Gets:

public static IState<S, T> Gets<ST>(Func<S, T> selector)
{
    return Get<S>().Select(selector);
}

This function can be useful as a combinator if you need just a projection of the current state, instead of the whole state.

Here's another basic unit test as an example:

[Fact]
public void GetsExample()
{
    IState<stringintx = State.Gets((string s) => s.Length);
    Tuple<intstringactual = x.Run("bar");
    Assert.Equal(Tuple.Create(3, "bar"), actual);
}

While the above Aggregator example didn't require Modify or Gets, I wanted to include them here for completeness sake.

F# #

Most of the code shown in this article has been C#, with the occasional Haskell code. You can also implement the State monad, as well as the helper methods, in F#, where it'd feel more natural to dispense with interfaces and instead work directly with functions. To make things a little clearer, you may want to define a type alias:

type State<'a, 's> = ('s -> 'a * 's)

You can now define a State module that works directly with that kind of function:

module State =
    let run state (f : State<_, _>) = f state
 
    let lift x state = x, state
 
    let map f x state =
        let x', newState = run state x
        f x', newState
 
    let bind (f : 'a -> State<'b, 's>) (x : State<'a, 's>) state =
        let x', newState = run state x
        run newState (f x')
 
    let get state = state, state
 
    let put newState _ = (), newState
 
    let modify f = get |> map f |> bind put

This is code I originally wrote for a Code Review answer. You can go there to see all the details, as well as a motivating example.

I see that I never got around to add a gets function... I'll leave that as an exercise.

In C#, I've based the example on an interface (IState<S, T>), but it would also be possible to implement the State monad as extension methods on Func<S, Tuple<T, S>>. Try it! It might be another good exercise.

Conclusion #

The State monad usually comes with a few helper functions: get, put, modify, and gets. They can be useful as combinators you can use to compose a stateful combination from smaller building blocks, just like you can use LINQ to compose complex queries over data.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 04 July 2022 09:15:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 04 July 2022 09:15:00 UTC