Implementing map
for a specific functor is often easy if you reason about the type signature. Try writing it and the rest should fall into place.
Let’s do that for IO
, Nond
and Maybe
. You will see that it’s an easy exercise.
Tutorials often start with the maybe functor because it’s, objectively, the simplest one. I will do the same.
Given a function:
f :: a -> b
it should return
map(f) :: Maybe<A> -> Maybe<B>
Try yourself to complete the implementation:
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) => ...
It must return a function Maybe<A> -> Maybe<B>
. Therefore:
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) =>
(Maybe<A> maybeA) => ...
What to do with maybeA
? Well, we could easily pattern match on it and take 2 different paths based on whether it contains a value or not:
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) =>
(Maybe<A> maybeA) => maybeA switch
{
Just<A> a => ...
Nothing<A> => ...
};
If there is no value, it makes sense to propagate the absence of a value, returning a Nothing<B>
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) =>
(Maybe<A> maybeA) => maybeA switch
{
Just<A> a => ...
Nothing<A> => new Nothing<B>()
};
If there is a value, we can apply f
to it to get a B
value. Since the function is supposed to return a Maybe<B>
, we elevate the B
value as a Just<B>
:
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) =>
(Maybe<A> maybeA) => maybeA switch
{
Just<A> a => new Just<B>(f(a.Value)),
Nothing<A> => new Nothing<B>()
};
We could have used the implementation of Run
:
Func<Maybe<A>, Maybe<B>> Map<A, B>(this Func<A, B> f) =>
maybeA =>
maybeA.Run<Maybe<B>>(
just: a => new Just<B>(f(a)),
nothing: () => new Nothing<B>());
And that’s it.
It works as follows:
// given a value that may or may not contain a string
Maybe<string> maybeAString = new Just<string>("foo");
// and a function to calculate the length of a string
Func<string, int> length = s => s.Length;
// Map elevates length to work on Maybe values
Func<Maybe<string>,Maybe<int>> lengthF = length.Map();
// So we can calculate the length of a Maybe<string>.
// if the value does not exist, we will get a Nothing<int>
var maybeLength = lengthF(maybeAString);
Assert.IsType<Just<int>>(maybeLength);
Assert.Equal(3, ((Just<int>) maybeLength).Value);
The Maybe Monad is one of those that lends itself very well to being interpreted with the metaphor of the box. You can imagine it as a box either empty (in the case of Nothing<A>
) or containing a value (in the case of Just<A>
).
You can make this apparent defining an extension method on it:
Maybe<B> Map<A, B>(this Maybe<A> maybeA, Func<A, B> f) =>
f.Map()(maybeA);
which lets you use Maybe
as follows:
Maybe<string> maybeAString = new Just<string>("foo");
Func<string, int> length = s => s.Length;
var maybeLength = maybeAString.Map(length);
Assert.IsType<Just<int>>(maybeLength);
Assert.Equal(3, ((Just<int>) maybeLength).Value);
I guess you can see how:
maybeAString.Map(length)
resembles the LINQ’s expression:
maybeAString.Select(length)
In fact, LINQ’s Select
is the implementation of Map
for the functor IEnumerable
.
For Nond
, let’s start from a test:
var nondeterministicString = new Nond<string>(new[]
{ "foo", "bar", "barbaz" });
Func<string, int> length = s => s.Length;
Func<Nond<string>, Nond<int>> lengthM = length.Map();
var results = lengthM(nondeterministicString).Run();
Assert.Equal(new []{3, 3, 6}, results);
Try to implement Map
. You should get something like:
Func<Nond<A>, Nond<B>> Map<A, B>(this Func<A, B> f) => nondA =>
{
var values = nondA.Run();
IEnumerable<B> enumerable = values.Select(f);
return new Nond<B>(enumerable);
};
which is simply equivalent to:
Func<Nond<A>, Nond<B>> Map<A, B>(this Func<A, B> f) => nondA =>
new Nond<B>(nondA.Items.Select(f));
Actually, it is easy to convince oneself that the Nond
class is redundant, and that IEnumerable
can be directly used as the nondeterministic Monad and Function, thanks to the native LINQ support.
LINQ’s Select
is the most glaring example of the boring box metaphor. It’s very easy to see IEnumerable
as a box containing an arbitrary number of values, and Select
as a way to apply a function to them from outside the container.
And finally, let’s distill Map
for IO
. The approach is the same: start from the signature, and try to follow how it leads you.
Given a function:
f :: A -> B
the IO Functor’s map implementation will elevate it to:
f.Map() :: IO<A> -> IO<B>
The implementation is not that hard at all:
Func<IO<A>, IO<B>> Map<A, B>(Func<A, B> f) =>
ioa =>
{
A a = ioa.Run();
var b = f(a);
return new IO<B>(() => b);
};
which inlined is:
Func<IO<A>, IO<B>> Map<A, B>(Func<A, B> f) =>
ioa => new IO<B>(() => f(ioa.Run()));
It’s easy to implement it as an Extension Method. Here’s a test using it:
var io = new IO<string>(() =>
{
File.WriteAllText("output.txt", "I'm a side effect");
return "foo";
});
Func<string, int> length = s => s.Length;
// let's elevate length
var lengthM = length.Map();
var l = lengthM(io);
var result = l.Run();
Assert.Equal(3, result);
Assert.Equal("I'm a side effect", File.ReadAllText("output.txt"));
I’m not particularly fond of the box metaphor, but if you really want to see IO trough that lens, sqeeze your eyes and imagine it as a box containing the value it will eventually generate when run (together with side-effects).
It helps to to define an extension method that takes the IO Monad as the first parameter:
static IO<B> Map<A, B>(this IO<A> ioa, Func<A, B> f) =>
f.Map()(ioa);
Then, you use it as follows:
IO<int> l = io.Map(length);
Read io.Map(length)
as:
length : string -> int
io
, which contains / will eventually produce a string
This is equivalent of using LINQ as the following:
IO<int> l = io.Select(length);
Wow, you did it!
Those 9 chapters were quite a mouthful, right? Now that you’ve dined
on monads, reward yourself with a slice of meringue pie! You’ve really
earned it!
Cheers!
There are some topics I didn’t get around to covering.
Either
, Reader
and Writer
.You might like the series State Monad For The Rest Of Us.
Interested in FP? Be the first to be notified when new introductory
articles on the topic are published.