The IO container forms a functor. An article for object-oriented programmers.

This article is an instalment in an article series about functors. Previous articles have covered Maybe, Lazy, and other functors. This article provides another example.

Functor #

In a recent article, I gave an example of what IO might look like in C#. The IO<T> container already has sufficient API to make it a functor. All it needs is a Select method:

public IO<TResult> Select<TResult>(Func<TTResult> selector)
    return SelectMany(x => new IO<TResult>(() => selector(x)));

This is an instance method on IO<T>, but you can also write it as an extension method, if that's more to your liking.

When you call selector(x), the return value is an object of the type TResult. The SelectMany method, however, wants you to return an object of the type IO<TResult>, so you use the IO constructor to wrap that return value.

Haskell #

The C# IO<T> container is an illustration of how Haskell's IO type works. It should come as no surprise to Haskellers that IO is a functor. In fact, it's a monad, and all monads are also functors.

The C# IO<T> API is based around a constructor and the SelectMany method. The constructor wraps a plain T value in IO<T>, so that corresponds to Haskell's return method. The SelectMany method corresponds to Haskell's monadic bind operator >>=. When you have lawful return and >>= implementations, you can have a Monad instance. When you have a Monad instance, you not only can have Functor and Applicative instances, you must have them.

Conclusion #

IO forms a functor, among other abstractions. In C#, this manifests as a proper implementation of a Select method.

Next: Monomorphic functors.


The constructor wraps a plain T value in IO<T>

Did you mean to say that the constructor wraps a Lazy<T> value in IO<T>?

2020-06-22 14:05 UTC

Tyson, thank you for writing. Well, yes, that's technically what happens... I'm deliberately being imprecise with the language because I'm trying to draw a parallel to Haskell. In Haskell, return takes a value and wraps it in IO (the type is effectively a -> IO a). In Haskell, however, computation is lazy by default. This means that the value you wrap in IO is already lazy. This turns out to be important, as I'll explain in a future article, so in C# we have to first make sure that the value is lazy.

The concept, however, involves taking a 'bare' value and wrapping it in a container, and that's the reason I chose my words as I did.

2020-06-22 14:45 UTC

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.


Monday, 22 June 2020 06:23:00 UTC


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