Asynchronous computations form monads. An article for object-oriented programmers.

This article is an instalment in an article series about monads. A previous article described how asynchronous computations form functors. In this article, you'll see that asynchronous computations also form monads. You'll learn about closely related monads: .NET Tasks and F# asynchronous workflows.

Before we start, I'm going to repeat the warning from the article about asynchronous functors. .NET Tasks aren't referentially transparent, whereas F# asynchronous computations are. You could argue, then, that .NET Tasks aren't proper monads, but you mostly observe the difference when you perform impure operations. As a general observation, when impure operations are allowed, the conclusions of this overall article series are precarious. We can't radically change how the .NET languages work, so we'll have to soldier on, pretending that impure operations are delegated to other parts of our system. Under this undue assumption, we can pretend that Task<T> forms a monad. Also, while there are differences, it sometimes helps to think of Task<T> as a sort of poor man's IO monad.

Monadic bind #

A monad must define either a bind or join function. In C#, monadic bind is called SelectMany. You can define one as an extension method on the Task<T> class:

public async static Task<TResult> SelectMany<TTResult>(
    this Task<T> source,
    Func<T, Task<TResult>> selector)
{
    T x = await source;
    return await selector(x);
}

With SelectMany, you can compose various tasks and flatten as you go:

Task<intx = AsyncValue(  42);
Task<inty = AsyncValue(1337);
Task<intz = x.SelectMany(async i => i + await y);

If you're wondering how this is useful, since C# already has async and await keywords for that purpose, I can understand you. Had you not had that language feature, monadic bind would have been helpful, but now it feels a little odd. (I haven't been deep into the bowels of how that language feature works, but from what little I've seen, monads play a central role - just as they do in the LINQ language feature.)

In F# you can define a bind function that works on Async<'a> values:

// ('a -> Async<'b>) -> Async<'a> -> Async<'b>
let bind f x = async {
    let! x' = x
    return! f x' }

For both the C# and the F# examples, the exercise seems a little redundant, since they're both based on language features. The C# SelectMany implementation uses the async and await keywords, and the F# bind function uses the built-in async computation expression. For that reason, I'll also skip the section on syntactic sugar that I've included in the previous articles in this article series. Syntactic sugar is already built into the languages.

Flatten #

In the introduction you learned that if you have a Flatten or Join function, you can implement SelectMany, and the other way around. Since we've already defined SelectMany for Task<T>, we can use that to implement Flatten. In this article I use the name Flatten rather than Join. This is an arbitrary choice that doesn't impact behaviour. Perhaps you find it confusing that I'm inconsistent, but I do it in order to demonstrate that the behaviour is the same even if the name is different.

The concept of a monad is universal, but the names used to describe its components differ from language to language. What C# calls SelectMany, Scala calls flatMap, and what Haskell calls join, other languages may call Flatten.

You can always implement Flatten by using SelectMany with the identity function.

public static Task<T> Flatten<T>(this Task<Task<T>> source)
{
    return source.SelectMany(x => x);
}

The F# version uses the same implementation - it's just a bit terser:

// Async<Async<'a>> -> Async<'a>
let flatten x = bind id x

In F#, id is a built-in function.

Return #

Apart from monadic bind, a monad must also define a way to put a normal value into the monad. Conceptually, I call this function return (because that's the name that Haskell uses). You don't, however, have to define a static method called Return. What's of importance is that the capability exists. For Task<T> this function already exists: It's called FromResult.

In F#, it's not built-in, but easy to implement:

// 'a -> Async<'a>
let fromValue x = async { return x }

I called it fromValue inspired by the C# method name (and also because return is a reserved keyword in F#).

Left identity #

We need to identify the return function in order to examine the monad laws. Now that this is done, let's see what the laws look like for the asynchronous monads, starting with the left identity law.

[Property(QuietOnSuccess = true)]
public void TaskHasLeftIdentity(Func<intstringh_int a)
{
    Func<int, Task<int>> @return = Task.FromResult;
    Task<stringh(int x) => Task.FromResult(h_(x));
 
    Assert.Equal(@return(a).SelectMany(h).Result, h(a).Result);
}

Like in the previous article the test uses FsCheck 2.11.0 and xUnit.net 2.4.0. FScheck can generate arbitrary functions in addition to arbitrary values, but it unfortunately, it can't generate asynchronous computations. Instead, I've asked FsCheck to generate a function that I then convert to an asynchronous computation.

The code I'm using for this article is quite old, and neither FsCheck 2.11.0 nor xUnit.net 2.4.0 can handle asynchronous unit tests (a capability that later versions do have). Thus, the assertion has to force the computations to run by accessing the Result property. Not modern best practice, but it gets the point across, I hope.

In F# I wrote monad law examples using Kleisli composition. I first defined a function called fish:

// ('a -> Async<'b>) -> ('b -> Async<'c>) -> 'a -> Async<'c>
let fish f g x = async {
    let! x' = f x
    return! g x' }

Keep in mind that fish is also a verb, so that's okay for a function name. The function then implements the fish operator:

let (>=>) = Async.fish

This enables us to give an example of the left identity law using Kleisli composition:

[<Property(QuietOnSuccess = true)>]
let ``Async fish has left identity`` (h' : int -> string) a =
    let h x = async { return h' x }
 
    let  left = Async.fromValue >=> h
    let right = h
 
    Async.RunSynchronously (left a) =! Async.RunSynchronously (right a)

The =! operator is an Unquote operator that I usually read as must equal. It's a test assertion that'll throw an exception if the left and right sides aren't equal.

Right identity #

In a similar manner, we can showcase the right identity law as a test - first in C#:

[Property(QuietOnSuccess = true)]
public void TaskHasRightIdentity(int a)
{
    Func<int, Task<int>> @return = Task.FromResult;
    Task<intm = Task.FromResult(a);
    Assert.Equal(m.SelectMany(@return).Result, m.Result);
}

Here's a Kleisli-composition-based F# property that demonstrates the right identity law for asynchronous workflows:

[<Property(QuietOnSuccess = true)>]
let ``Async fish has right identity`` (f' : int -> string) a =
    let f x = async { return f' x }
 
    let  left = f
    let right = f >=> Async.fromValue
 
    Async.RunSynchronously (left a) =! Async.RunSynchronously (right a)

As always, even a property-based test constitutes no proof that the law holds. I show it only to illustrate what the laws look like in 'real' code.

Associativity #

The last monad law is the associativity law that describes how (at least) three functions compose.

[Property(QuietOnSuccess = true)]
public void TaskIsAssociative(
    Func<DateTime, intf_,
    Func<intstringg_,
    Func<stringbyteh_,
    DateTime a)
{
    Task<intf(DateTime x) => Task.FromResult(f_(x));
    Task<stringg(int x) => Task.FromResult(g_(x));
    Task<byteh(string x) => Task.FromResult(h_(x));
 
    Task<intm = f(a);
 
    Assert.Equal(m.SelectMany(g).SelectMany(h).Result, m.SelectMany(x => g(x).SelectMany(h)).Result);
}

This property once more relies on FsCheck's ability to generate arbitrary pure functions, which it then converts to asynchronous computations. The same does the Kleisli-composition-based F# property:

[<Property(QuietOnSuccess = true)>]
let ``Async fish is associative`` (f' : int -> string) (g' : string -> byte) (h' : byte -> bool) a =
    let f x = async { return f' x }
    let g x = async { return g' x }
    let h x = async { return h' x }
 
    let  left = (f >=>  g) >=> h
    let right =  f >=> (g  >=> h)
 
    Async.RunSynchronously (left a) =! Async.RunSynchronously (right a)

It's easier to see the associativity that the law is named after when using Kleisli composition, but as the article about the monad laws explained, the two variations are equivalent.

Conclusion #

Whether you do asynchronous programming with Task<T> or Async<'a>, asynchronous computations form monads. This enables you to piecemeal compose asynchronous workflows.

Next: The State monad.



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, 06 June 2022 07:33:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 06 June 2022 07:33:00 UTC