The Reader functor by Mark Seemann
Normal functions form functors. An article for object-oriented programmers.
This article is an instalment in an article series about functors. In a previous article you saw, for example, how to implement the Maybe functor in C#. In this article, you'll see another functor example: Reader.
The Reader functor is similar to the Identity functor in the sense that it seems practically useless. If that's the case, then why care about it?
As I wrote about the Identity functor:
"The inutility of Identity doesn't mean that it doesn't exist. The Identity functor exists, whether it's useful or not. You can ignore it, but it still exists. In C# or F# I've never had any use for it (although I've described it before), while it turns out to be occasionally useful in Haskell, where it's built-in. The value of Identity is language-dependent."
The same holds for Reader. It exists. Furthermore, it teaches us something important about ordinary functions.
Reader interface #
Imagine the following interface:
public interface IReader<R, A> { A Run(R environment); }
An IReader
object can produce a value of the type A
when given a value of the type R
. The input is typically called the environment
. A Reader reads the environment and produces a value. A possible (although not particularly useful) implementation might be:
public class GuidToStringReader : IReader<Guid, string> { private readonly string format; public GuidToStringReader(string format) { this.format = format; } public string Run(Guid environment) { return environment.ToString(format); } }
This may be a silly example, but it illustrates that a a simple class can implement a constructed version of the interface: IReader<Guid, string>
. It also demonstrates that a class can take further arguments via its constructor.
While the IReader
interface only takes a single input argument, we know that an argument list is isomorphic to a parameter object or tuple. Thus, IReader
is equivalent to every possible function type - up to isomorphism, assuming that unit is also a value.
While the practical utility of the Reader functor may not be immediately apparent, it's hard to argue that it isn't ubiquitous. Every method is (with a bit of hand-waving) a Reader.
Functor #
You can turn the IReader
interface into a functor by adding an appropriate Select
method:
public static IReader<R, B> Select<A, B, R>(this IReader<R, A> reader, Func<A, B> selector) { return new FuncReader<R, B>(r => selector(reader.Run(r))); } private sealed class FuncReader<R, A> : IReader<R, A> { private readonly Func<R, A> func; public FuncReader(Func<R, A> func) { this.func = func; } public A Run(R environment) { return func(environment); } }
The implementation of Select
requires a private class to capture the projected function. FuncReader
is, however, an implementation detail.
When you Run
a Reader, the output is a value of the type A
, and since selector
is a function that takes an A
value as input, you can use the output of Run
as input to selector
. Thus, the return type of the lambda expression r => selector(reader.Run(r))
is B
. Therefore, Select
returns an IReader<R, B>
.
Here's an example of using the Select
method to project an IReader<Guid, string>
to IReader<Guid, int>
:
[Fact] public void WrappedFunctorExample() { IReader<Guid, string> r = new GuidToStringReader("N"); IReader<Guid, int> projected = r.Select(s => s.Count(c => c.IsDigit())); var input = new Guid("{CAB5397D-3CF9-40BB-8CBD-B3243B7FDC23}"); Assert.Equal(16, projected.Run(input)); }
The expected result is 16
because the input
Guid
contains 16 digits (the numbers from 0 to 9). Count them if you don't believe me.
As usual, you can also use query syntax:
[Fact] public void QuerySyntaxFunctorExample() { var projected = from s in new GuidToStringReader("N") select TimeSpan.FromMinutes(s.Length); var input = new Guid("{FE2AB9C6-DDB1-466C-8AAA-C70E02F964B9}"); Assert.Equal(32, projected.Run(input).TotalMinutes); }
The actual computation shown here makes little sense, since the result will always be 32
, but it illustrates that arbitrary projections are possible.
Raw functions #
The IReader<R, A>
interface isn't really necessary. It was just meant as an introduction to make things a bit easier for object-oriented programmers. You can write a similar Select
extension method for any Func<R, A>
:
public static Func<R, B> Select<A, B, R>(this Func<R, A> func, Func<A, B> selector) { return r => selector(func(r)); }
Compare this implementation to the one above. It's essentially the same lambda expression, but now Select
returns the raw function instead of wrapping it in a class.
In the following, I'll use raw functions instead of the IReader
interface.
First functor law #
The Select
method obeys the first functor law. As usual, it's proper computer-science work to actually prove this, but you can write some tests to demonstrate the first functor law for the IReader<R, A>
interface. In this article, you'll see parametrised tests written with xUnit.net. First, the first functor law:
[Theory] [InlineData("")] [InlineData("foo")] [InlineData("bar")] [InlineData("corge")] [InlineData("antidisestablishmentarianism")] public void FirstFunctorLaw(string input) { T id<T>(T x) => x; Func<string, int> f = s => s.Length; Func<string, int> actual = f.Select(id); Assert.Equal(f(input), actual(input)); }
The 'original' Reader f
(for function) takes a string
as input and returns its length. The id
function (which isn't built-in in C#) is implemented as a local function. It returns whichever input it's given.
Since id
returns any input without modifying it, it'll also return any number produced by f
without modification.
To evaluate whether f
is equal to f.Select(id)
, the assertion calls both functions with the same input. If the functions have equal behaviour, they ought to return the same output.
The above test cases all pass.
Second functor law #
Like the above example, you can also write a parametrised test that demonstrates that a function (Reader) obeys the second functor law:
[Theory] [InlineData("")] [InlineData("foo")] [InlineData("bar")] [InlineData("corge")] [InlineData("antidisestablishmentarianism")] public void SecondFunctorLaw(string input) { Func<string, int> h = s => s.Length; Func<int, bool> g = i => i % 2 == 0; Func<bool, char> f = b => b ? 't' : 'f'; Assert.Equal( h.Select(g).Select(f)(input), h.Select(i => f(g(i)))(input)); }
You can't easily compare two different functions for equality, so, like above, this test defines equality as the functions producing the same result when you invoke them.
Again, while the test doesn't prove anything, it demonstrates that for the five test cases, it doesn't matter if you project the 'original' Reader h
in one or two steps.
Haskell #
In Haskell, normal functions a -> b
are already Functor
instances, which means that you can easily replicate the functions from the SecondFunctorLaw
test:
> h = length > g i = i `mod` 2 == 0 > f b = if b then 't' else 'f' > (fmap f $ fmap g $ h) "ploeh" 'f'
Here f
, g
, and h
are equivalent to their above C# namesakes, while the last line composes the functions stepwise and calls the composition with the input string "ploeh"
. In Haskell you generally read code from right to left, so this composition corresponds to h.Select(g).Select(f)
.
Conclusion #
Functions give rise to functors, usually known collectively as the Reader functor. Even in Haskell where this fact is ingrained into the fabric of the language, I rarely make use of it. It just is. In C#, it's likely to be even less useful for practical programming purposes.
That a function a -> b
forms a functor, however, is an important insight into just what a function actually is. It describes an essential property of functions. In itself this may still seem underwhelming, but mixed with some other properties (that I'll describe in a future article) it can produce some profound insights. So stay tuned.
Next: The IO functor.