The IO functor by Mark Seemann
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<T, TResult> 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.
Comments
Did you mean to say that the constructor wraps a
Lazy<T>
value inIO<T>
?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 inIO
(the type is effectivelya -> IO a
). In Haskell, however, computation is lazy by default. This means that the value you wrap inIO
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.