Dart Functors, Applicatives, And Monads In Pictures

Oleksandr Leushchenko
9 min readJan 4, 2022

--

This is a translation of the Functors, Applicatives, And Monads In Pictures article from Haskell into Dart. I borrowed some ideas and text paragraphs from translations to JavaScript, Python, Swift, and Kotlin. Thanks, folks!

If you enjoy this post, be sure to say thanks to the author of the original version: Aditya Bhargava, @_egonschiele on Twitter.

Dart is NOT a functional language, but still, there are a few packages (like fpdart and dartz) that might make your life happier and your apps better. The purpose of this translation is to help you with basics so that you may adopt FP packages in your apps more meaningfully.

Here’s a simple value:

And we know how to apply a function to this value:

Simple enough. Let's extend this by saying that any value can be in a context (do not mess with Flutter’s context). For now, you can think of a context as a box that you can put a value in:

Now when you apply a function to this value, you’ll get different results depending on the context. This is the idea that Functors, Applicatives, Monads, Arrows, etc are all based on. The Maybe data type defines two related contexts:

/// The Maybe type encapsulates an optional value.
/// Using Maybe is a good way to deal with errors or
/// exceptional cases without resorting to
/// drastic measures such as error.

abstract class Maybe<T> {}
/// Represents a value of type Maybe that contains a value
/// (represented as Just a).
class Just<T> extends Maybe<T> {
Just(this.value);
final T value;
}
/// Represents an empty Maybe that holds nothing
/// (in which case it has the value of Nothing)
class Nothing<T> extends Maybe<T> {}

Note: here I kept the original names (mostly to match pictures), but in fpdart and dartz packages this class is named Option with Some and None subclasses.

In a second we’ll see how function application is different when something is a Just a versus a Nothing. First, let’s talk about Functors!

Functors

When a value is wrapped in a context, you can’t apply a normal function to it:

This is where fmap (map in fpdart/dartz) comes in. fmap is from the street, fmap is hip to contexts. fmap knows how to apply functions to values that are wrapped in a context. For example, suppose you want to apply (+3) to Just 2. Use fmap:

num plus3(num x) => x + 3;Just(2).fmap(plus3); // Just 5

Bam! fmap shows us how it’s done! But how does fmap know how to apply the function?

Just what is a Functor, really?

Functor is just an abstract class (a typeclass in Haskell). Here’s the definition:

In Dart:

abstract class Functor<T> {
Functor<U> fmap<U>(U Function(T) f);
}

A Functor is any data type that defines how fmap applies to it. Here’s how fmap works:

So we can do this:

Just(2).fmap(plus3); // Just 5

And fmap magically applies this function, because Maybe is a Functor. It specifies how fmap applies to Justs and Nothings:

abstract class Maybe<T> implements Functor<T> {}class Just<T> extends Maybe<T> {

@override
Maybe<U> fmap<U>(U Function(T) f) => Just(f(value));
}
class Nothing<T> extends Maybe<T> {
@override
Maybe<U> fmap<U>(U Function(T) f) => Nothing();
}

Here’s what is happening behind the scenes when we write Just(2).fmap(plus3):

So then you’re like, alright fmap, please apply plus3 to a Nothing?

Nothing<num>().fmap(plus3); // Nothing
Bill O’Reilly being totally ignorant about the functor

Like Morpheus in the Matrix, fmap knows just what to do; you start with Nothing, and you end up with Nothing! fmap is zen. Now it makes sense why the Maybe data type exists. For example, here’s how you work with a database record in a language without Maybe:

final post = Post.findByID(1);
if (post != null) {
return post.title;
} else {
return null;
}

But with Maybe:

final getPostTitle = (Post post) => post.title;
return Post.findByID(42).fmap(getPostTitle);

If findPost returns a post, we will get the title with getPostTitle. If it returns Nothing, we will return Nothing! Pretty neat, huh?

Here’s another example: what happens when you apply a function to a list?

[2, 4, 6].map((x) => x + 3); // [5, 7, 9]

Lists are functors too! Here’s the definition:

Iterable<T> map<T>(T toElement(E e)) => MappedIterable<E, T>(this, toElement);

Okay, okay, one last example: what happens when you apply a function to another function?

fmap((x) => x + 3, (x) => x + 2);

Here’s a function:

Here’s a function applied to another function:

The result is just another function!

typedef IntFunction = int Function(int);IntFunction fmap(IntFunction f, IntFunction g) => (x) => f(g(x));final foo = fmap((x) => x + 3, (x) => x + 2);
foo(10); // 15

So functions are Functors too! When you use fmap on a function, you’re just doing function composition!

Applicatives

Applicatives take it to the next level. With an applicative, our values are wrapped in a context, just like Functors:

But our functions are wrapped in a context too!

Yeah. Let that sink in. Applicatives don’t kid around. Unlike Haskell, Dart doesn’t have yet a built-in way to deal with Applicative. But it is very easy to add one! We can define an apply (ap in fpdart/dartz) function for every type supporting Applicative, which knows how to apply a function wrapped in the context of the type to a value wrapped in the same context:

abstract class Applicative<T> {  Applicative<U> apply<U>(Applicative<U Function(T)> f);}abstract class Maybe<T> implements …, Applicative<T> {}class Just<T> extends Maybe<T> {

@override
Maybe<U> apply<U>(covariant Maybe<U Function(T)> f) => f.fmap((ff) => ff(value)) as Maybe<U>;
}
class Nothing<T> extends Maybe<T> {

@override
Maybe<U> apply<U>(covariant Maybe<U Function(T)> f) => Nothing();
}

i.e:

Just(2).apply(Just((x) => x + 3)); // Just 5

If we have multiple wrapped functions and values, we might expect this to happen:

extension ApplicativeList on List {
Iterable<U> apply<T, U>(List<U Function(T)> list) sync* {
for (final item in list) {
for (var i = 0; i < length; i++) {
yield item(this[i]);
}
}
}
}

With this definition, we can apply a list of functions to a list of values:

[1, 2, 3].apply<int, int>([
(x) => x * 2,
(x) => x + 3,
]); // [2, 4, 6, 4, 5, 6]

Note: the original article now shows how Applicatives are more powerful than Functors in that they allow function application with multiple parameters. Again this is not feasible in Dart, but we can work around it by defining the function we want to handle in a curried way.

Here’s something you can do with Applicatives that you can’t do with Functors. How do you apply a function that takes two arguments to two wrapped values?

final curriedAddition = (num x) => (num y) => x + y;
final a = Just(3).fmap(curriedAddition); // Just<(int) => int>
Just(5).fmap(a); // COMPILATION ERROR
//??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST

Applicatives:

Just(5).apply(a); // Just 8

Applicative pushes Functor aside. “Big boys can use functions with any number of arguments,” it says. “Armed with curry function, map and apply, I can take any function that expects any number of unwrapped values. Then I pass it all wrapped values, and I get a wrapped value out! AHAHAHAHAH!”

final curriedTimes = (int x) => (int y) => x * y;
Just(5).apply(Just(3).fmap(curriedTimes)); // Just 15

Monads

How to learn about Monads:

  1. Get a PhD in computer science.
  2. Throw it away because you don’t need it for this section!

Monads add a new twist.

Functors apply a function to a wrapped value:

Applicatives apply a wrapped function to a wrapped value:

Monads apply a function that returns a wrapped value to a wrapped value. Monads have a function called bind (>>= in Haskell) to do this.

Let’s see an example. Good ol’ Maybe is a monad:

Suppose half is a function that only works on even numbers:

Maybe<num> half(num a) => a % 2 == 0 ? Just(a / 2) : Nothing();

What if we feed it a wrapped value?

We need to use bind (also bind in fpdart/dartz) to shove our wrapped value into the function. Here’s a photo of bind:

Here’s how it works:

Just(3).bind(half); // Nothing
Just(4).bind(half); // Just 2
Nothing<num>().bind(half); // Nothing

What’s happening inside? Monad is another abstract class:

abstract class Monad<T> {
Maybe<U> bind<U>(Maybe<U> Function(T) f);
}

Where “bind” is:

So Maybe is a Monad:

abstract class Maybe<T> implements ..., Monad<T> {}class Just<T> extends Maybe<T> {

@override
Maybe<U> bind<U>(covariant Maybe<U> Function(T) f) => f(value);
}
class Nothing<T> extends Maybe<T> {
@override
Maybe<U> bind<U>(covariant Maybe<U> Function(T) f) => Nothing();
}

Here it is in action with a Just 3!

And if you pass in a Nothing it’s even simpler:

You can also chain these calls:

Just(20).bind(half).bind(half).bind(half); // Nothing

Cool stuff! So now we know that Maybe is a Functor, an Applicative, and a Monad.

Note: the original article now describes Haskell’s IO Monad. Since Dart doesn’t distinguish between pure and impure functions, it doesn’t need an IO monad at all, so this translation skips it.

Conclusion

  1. A functor is a data type that implements the Functor abstract class.
  2. An applicative is a data type that implements the Applicative abstract class.
  3. A monad is a data type that implements the Monad abstract class.
  4. A Maybe implements all three, so it is a functor, an applicative, and a monad.

What is the difference between the three?

  • functors: you apply a function to a wrapped value using fmap
  • applicatives: you apply a wrapped function to a wrapped value using apply
  • monads: you apply a function that returns a wrapped value, to a wrapped value using bind.

So, dear friend (I think we are friends by this point), I think we both agree that monads are easy and a SMART IDEA(tm). Now that you’ve wet your whistle on this guide, why not pull a Mel Gibson and grab the whole bottle. Check out LYAH’s section on Monads. There’s a lot of things I’ve glossed over because Miran does a great job going in-depth with this stuff.

Thanks for reading! As a bonus for those of you who finished this article, I’ve created a DartPad with all samples:

Russia started an unfair and cruel war against my country. If you found this translation interesting or useful, please, donate to Ukraine’s Armed Forces. I also recommend my friends volunteers — the “Yellow Tape”, you can be 100% sure that the money will support our victory. Thanks in advance to everyone who participated.

Glory to Ukraine! 🇺🇦

--

--