category

Functors, Monads and Applicatives with real encapsulation


License
MIT

Documentation

Category

Functors, Monads and Applicatives with real encapsulation

Translation Table

Type Class Function Haskell Elixir
Functor fmap <$> <~
Functor flip fmap <&> ~>
Monad bind >>= >>>
Applicative ap <*> <<~

Usage

As example of Functor, Monad and Applicative type classes usage we will consider classic sum type Maybe which implements all of them. Maybe is generic type which accepts one other type as parameter. It's very similar to List type in this meaning of parametricity: @type t :: [String.t] is List of strings, @type t :: Maybe.t(String.t) is Maybe String, @type t :: [a] is List of a, @type t :: Maybe.t(a) is Maybe a. Essence of Maybe is representation of optional values, so this data type have 2 constructors. Here is example of Maybe Integer type:

iex> use Category
:ok
iex> j = Maybe.just(1)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> n = Maybe.nothing()
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> Maybe.is?(j)
true
iex> Maybe.is?(n)
true

There is significant difference between Maybe type @type t :: Maybe.t(integer) and more common in Elixir @type t :: nil | integer. As you can see, values Maybe.just(1) and Maybe.nothing() are values of the same type, which means that they can be composed efficiently. But values 1 and nil are values of different types (Integer and Atom), which means that they can't be composed efficiently. Let's consider examples of this efficient or inefficient compositions one by one.

Functor

Let's say we have value x of type Maybe.t(integer). And we have a function which accepts Integer and returns Integer, for example increment = &(&1 + 1). How do we apply increment to x? Without Maybe type code will look like this:

case x do
  nil -> x
  _ -> increment.(x)
end

I can't say it is beautiful or convenient. But if we will use Maybe type which implements Functor behaviour, we can use fmap function (or its infix versions <~ or ~>) which will apply given function to internal state of Maybe in case of Maybe.just(a) or do nothing in case of Maybe.nothing. Final code will look much better:

Functor.fmap(increment, x)

or

increment <~ x

or

x ~> increment

Let's show how it works in iex:

iex> use Category
:ok
iex> j = Maybe.just(1)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> n = Maybe.nothing()
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> increment = &(&1 + 1)
#Function<6.128620087/1 in :erl_eval.expr/5>
iex> increment <~ j |> Maybe.get!
2
iex> increment <~ n |> Maybe.is_nothing?
true

Btw, as I mentioned before, List data type also implements Functor type class, and in reality every Elixir or Erlang developer already knows what is Functor and how it works, because map = fmap. So if you understand List data type and generic map function then you already understand Functors:

iex> Enum.map([1, 2, 3], &(&1 + 1))
[2, 3, 4]

Applicative

Applicative type class feels like some kind of generalization of Functor. Example is pretty similar to previous one. But in this case we have two values x and y of type Maybe.t(integer). And we have a function which accepts two integers and returns integer, for example sum = &Kernel.+/2. How do we apply sum to x and y? Without Maybe code will look like:

case is_nil(x) or is_nil(y) do
  true -> nil
  false -> sum.(x, y)
end

As you can see it looks ugly even with arity 2. Imagine how it will look with arity 10? But we can use Maybe. It implements Applicative behaviour which means that we can use ap (or its infix version <<~). We know how fmap applies function to Maybe value, ap is pretty similar - in our example it applies Maybe function to Maybe value. It's not so interesting itself (because there are not so many practical cases when we have Maybe function, right?), but it is very useful in combinations with fmap. Our final code will look like

sum <~ x <<~ y

Let's show how it works in iex:

iex> use Category
:ok
iex> x = Maybe.just(1)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> y = Maybe.just(2)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> n = Maybe.nothing()
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> sum = &Kernel.+/2
&:erlang.+/2
iex> sum <~ x <<~ y |> Maybe.get!
3
iex> sum <~ x <<~ n |> Maybe.is_nothing?
true
iex> sum <~ n <<~ y |> Maybe.is_nothing?
true

In example above, sum function has arity 2, but of course we can compute functions with bigger arities using applicative notation, for example:

iex> use Category
:ok
iex> f = fn x, y, z -> x * x + y * y + z * z end
#Function<18.128620087/3 in :erl_eval.expr/5>
iex> x = Maybe.just(1)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> y = Maybe.just(2)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> z = Maybe.just(3)
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> n = Maybe.nothing()
#Function<2.96330962/2 in Category.Data.Maybe.eval/2>
iex> f <~ x <<~ y <<~ z |> Maybe.get!
14
iex> f <~ x <<~ n <<~ z |> Maybe.is_nothing?
true

Btw, List data type also implements Applicative type class! In Elixir there is no direct equivalent like it was for fmap, but Applicative ap for List data type more or less corresponds to comprehensions like this:

iex> for f <- [&(&1 + 1), &(&1 * 2)], x <- [1, 2, 3], do:  f.(x)
[2, 3, 4, 2, 4, 6]

Monad

Let's say we have a program which accepts Maybe Integer as input, but this program is able to work only with natural numbers. To make code safe, we have to use function like this:

int2mnat = fn
  x when is_integer(x) and x >= 0 -> Maybe.just(x)
  _ -> Maybe.nothing()
end

It completely make sense because there are no natural numbers corresponding to negative integers, so we have to use Maybe data type here. Function int2mnat accepts Integer and returns Maybe Natural as we want. But look, input of our program is Maybe Integer, not Integer, how do we deal with that? Without Monad type class, code will look like:

case Maybe.is_just?(x) do
  true -> x |> Maybe.get! |> int2mnat.()
  false -> x
end

But as we know, Maybe implements Monad type class, so we can use bind function (or >>> infix equivalent) to make code better:

x >>> int2mnat

Let's show how it works in iex:

iex> use Category
:ok
iex> int2mnat = fn
...>   x when is_integer(x) and x >= 0 -> Maybe.just(x)
...>   _ -> Maybe.nothing()
...> end
#Function<6.128620087/1 in :erl_eval.expr/5>
iex> j0 = Maybe.just(1)
#Function<2.85804733/2 in Category.Data.Maybe.eval/2>
iex> j1 = Maybe.just(-1)
#Function<2.85804733/2 in Category.Data.Maybe.eval/2>
iex> n = Maybe.nothing()
#Function<2.85804733/2 in Category.Data.Maybe.eval/2>
iex> j0 >>> int2mnat |> Maybe.get!
1
iex> j1 >>> int2mnat |> Maybe.is_nothing?
true
iex> n >>> int2mnat |> Maybe.is_nothing?
true

And as you already guessed, List data type also implements Monad type class! For List data type in Elixir, bind = flat_map:

iex> f = &(&1 > 1 && [&1, &1] || [])
#Function<6.128620087/1 in :erl_eval.expr/5>
iex> Enum.flat_map([1, 2, 3], f)
[2, 2, 3, 3]

So if you know how flat_map works, you already understand monads!

Installation

The package can be installed by adding category to your list of dependencies in mix.exs:

def deps do
  [
    {:category, "~> 0.1.0"}
  ]
end

Made with ❤️ by Ilja Tkachuk aka timCF