SL

Functors, Applicatives, and Monads in Elixir

Big ol' note: By no means am I an expert on any of the below content. I'm currently learning this to the best of my ability and thought to write down my experiences and thoughts with implementing these in Elixir.

We're going to be comparing Elixir protocols with Haskell's type classes using a go-to type for the subject at hand: Maybe. The Maybe type encapsulates an optional value. A value of type Maybe a either contains a value of type a (represented as Just a), or it is empty (represented as Nothing). Here's how Haskell defines the type:

data Maybe a = Just a
             | Nothing

Because of Elixir's lack of strong typing and type constructors, we define the Elixir version of Maybe slightly different:

defmodule Maybe do
  @type t :: %__MODULE__{
    just: term,
    nothing: boolean
  }
  defstruct just: nil,
            nothing: false

  def just(v), do: __MODULE__ |> struct(just: v)
  def nothing, do: __MODULE__ |> struct(nothing: true)
end

The struct is acting as a wrapper for the values and the module contains some helper functions to act as a pseudo-replacement for Haskell's type constructors. In Elixir, the type Maybe a now either contains a value of type term (represented as Maybe.just(term)) or is empty (represented as Maybe.nothing).

Functor

Using functors, we can generalize how Enum.map works for Enumerables on any data type, including Maybe. We can accomplish this by generalizing the action and implementing that action for the desired data types. This generalized action for Functors is known as fmap. Take a look at the definition for the Functor protocol:

defprotocol Functor do
  @spec fmap(t, (term -> term)) :: t
  def fmap(functor, fun)
end

and compare it to Haskell's Functor type class:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

We can start to see similarities between the two purely by looking at the types. Both require a functor and function as arguments. Haskell's f a represents a functor with a type constructor of a, similar to [a] being a list of as. You may notice that the order of the arguments in both differ. This mostly has to do with how Elixir's Protocol dispatch process works as it looks at the first argument's type in order to dispatch the call to the correct implementation.

Now, any type that wishes to be a functor only needs to implement the fmap function, using Elixir's defimpl macro or Haskell's instance keyword:

defimpl Functor, for: Maybe do
  def fmap(%{nothing: true} = f, _), do: f
  def fmap(%{just: a}, fun) do
    fun
    |> apply([a])
    |> Maybe.just
  end
end
instance Functor Maybe  where
    fmap _ Nothing  = Nothing
    fmap f (Just a) = Just (f a)

While the Haskell version is more concise, they both define the same core functionality. Calling fmap on a "nothing" will always return a "nothing", ignoring the function that is passed as well. Calling fmap on a value will unwrap the value by pattern matching the Maybe functor and applying the passed function on the value.

Defining and Using Functors

Let's take a look at what this looks like in use:

f0 = fn x -> x + 2 end

Maybe.just(5)      # Define
|> Functor.fmap(f0) # Use
# %Maybe{just: 7, nothing: false}

Since our functors in Elixir are just simple structs, our "Just 5" value can be defined in the same variety of ways: Maybe.just(5), %Maybe{just: 5}, struct(Maybe, just: 5), etc. The power of the functor comes from the Functor protocol. When we want to work with the functor, we pass it to fmap/2 along with a function, in this case a small anonymous function that adds 2. From here, Elixir's protocol dispatch takes over, inspecting the functor's type to direct the call to the functor's implementation of fmap/2.

The result of fmap/2 is another functor, so if necessary, we can continue to pipe additional calls to fmap/2. At any time during this pipeline, any one of the included functions could potentially return a "nothing" value (Maybe.nothing) without throwing an error.

f1 = fn x-> x + 2 end
f2 = fn _ -> Maybe.nothing end
f3 = fn x -> x * 3 end

Maybe.just(5)
|> Functor.fmap(f1)
|> Functor.fmap(f2)
|> Functor.fmap(f3)
# %Maybe{just: nil, nothing: true}

In a real world situation, this allows us to worry less about catching and accounting for possible points of failure since our implementation of Functor for Maybe already accounts for this case (remember, calling fmap on a "nothing" will always return a "nothing").

Applicative (Functor)

I'm going to leave out the Haskell from here until I can figure how to handle a few things better in Elixir.

Applicatives (or more specifically applicative functors) are a special form of Functor where the value within the functor is a function.

defprotocol Applicative do
  @spec apply(t, Functor.t) :: t
  def apply(fun, f)
end
defimpl Applicative, for: Maybe do
  def apply(%{nothing: true} = f, _), do: f
  def apply(%{just: fun}, f) do
    f |> Control.Functor.fmap(fun)
  end
end

Maybe is now a Functor as well as an Applicative. Again, our Maybe implementation of Applicative destructures the first argument, returning a "nothing" value if one is present. However, now it is expected that the value in our functor is a function of type (term -> term). Here's an example where this would be useful:

f4 = fn file ->
  case File.stat(file) do
    {:ok, s} ->
      Maybe.just(&(&1 |> magic_function(s))
    {:error, _} ->
      Maybe.nothing
  end
end

I know this is a convoluted example with simpler alternatives. Let me know if you have a better example for this.

In this specific case, we've created a closure over the File.Stat variable (s) using it in an eventual call to our magic_function/2 function. We only want the call to magic_function/2 to occur when the file actually exists, so Maybe comes in to save the day.

Because our anonymous function is wrapped in a Maybe, it might be tricky call this function with another Maybe. We could pull out the function manually:

# "something.txt" exists
%Maybe{just: f5} = f4.("something.txt")

But this would need to be duplicated everywhere it was necessary. There also may be cases where this process would be too cumbersome to carry out manually. Lucky for us, the implementations of Applicative.apply/2 will handle this for all functors that have it available.

f4.("something.txt")
|> Applicative.apply(Maybe.just(5))

Monad

While I definitely don't want to go down the "let's describe what a monad is" road, I will say I've read that it is best to think of a monad as an abstract data type of actions. A list monad represents actions on a list. An IO monad represents actions on IO.

defprotocol Monad do
  @spec bind(t, (term -> t)) :: t
  def bind(m, fun)
end

Without going into detail just yet on Monad, did you notice any similarities to Functor and Applicative? Here are their typespecs right next to each other (expanding Applicative's to clear it up):

@spec fmap( t, (term -> term)) :: t             # Functor
@spec apply(t [(term -> term)], Functor.t) :: t # Applicative
@spec bind( t, (term -> t)) :: t                # Monad

They all accept a value and a function to modify that value, returning the type again.

In Functor.fmap/2, we take an unwrapped value, apply the function, and return a wrapped result. In Applicative.apply/2, we do the same as Functor.fmap/2, but the function is wrapped as well as the value. In Monad.bind/2, we do the same as Functor.fmap/2 as well, but the function argument returns the wrapped value opposed to it being wrapped after the fact.

defimpl Monad, for: Maybe do
  def bind(%{nothing: true} = f, _), do: f
  def bind(%{just: v}, fun) do
    fun |> apply([v])
  end
end

Following the types, the implementation for bind/2 looks very similar to that of fmap/2 and apply/2 because they are all similar. The difference come down to how the functions receive the data and how it expects the function to transform that data.

Wrapping Up

In my eyes, it's yet to be determined whether or not Elixir and Erlang really need functors, applicatives, and monads. The data types that follow these protocols can structure things in a way so that they can hide state, side-effects, etc. away from the pure side of the code, and Haskell uses them to it's advantage to become more than just an academic language and to be able to solve real world problems.

But since Elixir is a dynamic language, we don't always have the compiler to tell us what to do. We can add side-effecting code in our pure functions and others would be none the wiser, but typically, Elixir and Erlang developers will hide the same state, side-effects, etc. away into designated actors. There's no need for a State monad to maintain state between function calls because we have abstractions around actors like GenServer and Agent to do this for us, interfacing with those actors by message passing.

I'm going to keep going down this adventurous path for a little while longer, but I don't quite yet see how many cases that I'd use these in lieu of actors.

If you're interested in seeing the code behind this post, take a look at Control, an exploratory look into functors, applicatives, and monads for Elixir.