Home Posts Post Search Tag Search

Advanced Functional Elixir - 06 - Compose in Context with Monads
Published on: 2026-04-09 Tags: Blog, Advanced Functional Programming, FunPark, Identity, Monads

6. Compose in Context with Monads

Monads are inspired by category theory; monads compose computations within a context. Compose means building a concrete thing from reusable components. Context refers to the surrounding environment: database connection, current user, or a config map.


In functional programming compose means combining behavior. It can involve: read-only (Reader), absence (Maybe), failure (Either), or asynchronous computation (Effect).

Build the Monad

There are 2 operations that comprise a Monad: transformation (Map), and chains of context-aware computations (Bind).

Transform With a Functor

Functors allow follow 2 rules:

• Identity: Mapping with the identity function returns a copy of the original
structure. map(fn x -> x end, F(a)) = F(a)
• Composition: Mapping in two steps is the same as mapping once with a
composed function. map(f, map(g, F(a))) = map(fn x -> f.(g.(x)) end, F(a))

We want to add a new function that will add points to a Patrons reward_points.

  def promotion(%__MODULE__{} = patron, points) do
    new_points = Math.sum(get_reward_points(patron), points)

    change(patron, %{reward_points: new_points})
  end

Run It

iex(1)> alice = FunPark.Patron.make("Alice", 14, 125, reward_points: 25)
%FunPark.Patron{
  id: 3138,
  name: "Alice",
  age: 14,
  height: 125,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 25,
  likes: [],
  dislikes: []
}
iex(2)> beth = FunPark.Patron.make("Beth", 15, 140, reward_points: 10)
%FunPark.Patron{
  id: 3202,
  name: "Beth",
  age: 15,
  height: 140,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 10,
  likes: [],
  dislikes: []
}
iex(3)> charles = FunPark.Patron.make("Charles", 13, 130, reward_points: 50)
%FunPark.Patron{
  id: 3266,
  name: "Charles",
  age: 13,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 50,
  likes: [],
  dislikes: []
}
iex(4)> patrons = [alice, beth, charles]
[
  %FunPark.Patron{
    id: 3138,
    name: "Alice",
    age: 14,
    height: 125,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 25,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 3202,
    name: "Beth",
    age: 15,
    height: 140,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 10,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 3266,
    name: "Charles",
    age: 13,
    height: 130,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 50,
    likes: [],
    dislikes: []
  }
]
iex(5)> patrons |> Enum.map(&FunPark.Patron.promotion(&1, 10))
[
  %FunPark.Patron{
    id: 3138,
    name: "Alice",
    age: 14,
    height: 125,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 35,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 3202,
    name: "Beth",
    age: 15,
    height: 140,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 20,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 3266,
    name: "Charles",
    age: 13,
    height: 130,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 60,
    likes: [],
    dislikes: []
  }
]

Sequence Computations

A monad includes behavior for chaining computations within a context. For the remainder, as there is no agreed upon name, we will call it bind.


Bind operators follow three rules:

The bind operation follows three laws:
• Left identity: Wrapping a value and then binding it to a function is the
same as applying the function directly. bind(pure(a), f) = f(a)
• Right identity: Binding a monad to pure has no effect. bind(m, pure) = m
• Associativity: It doesn’t matter how you nest your bindings—the result is
the same. bind(bind(m, f), g) = bind(m, fn x -> bind(f(x), g) end)

Run It

First let’s bind the Kleisli function that takes an input and returns a function.

iex(6)> kleisli_fn = fn x -> if rem(x, 2) == 0, do: [x * x], else: [] end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(7)> list = [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex(8)> list |> Enum.flat_map(kleisli_fn)
[4, 16, 36]

Independent Computations

Applicative is useful when we need to combine two things that are already inside a context. It follows four fundamental rules:

• Identity: Applying a wrapped identity function has no effect. ap(pure(fn x ->
x end), F(a)) = F(a)
• Homomorphism: Lifting a function and a value separately is the same as
applying them directly. ap(pure(f), pure(a)) = pure(f.(a))
• Interchange: A function in context can be applied to a pure value, or the
value can be lifted into a function and applied to the context instead.
ap(F(f), pure(a)) = ap(pure(fn g -> g.(a) end), F(f))
• Composition: Applying functions step by step inside the context behaves
the same as applying them all at once. ap(ap(ap(pure(fn f -> fn g -> fn x -> f.(g.(x)))
end, F(f)), F(g)), F(a)) = ap(F(f), ap(F(g), F(a)))

Before we run it it seems that the next bit of code will do 2 things to a list and then combine the outcomes into 1 longer list.

Run It

iex(9)> ap = fn values, funcs -> for f <- funcs, v <- values, do: f.(v) end
#Function<41.39164016/2 in :erl_eval.expr/6>
iex(10)> add_one = fn x -> x + 1 end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(11)> add_two = fn x -> x + 2 end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(12)> func_list = [add_one, add_two]
[#Function<42.39164016/1 in :erl_eval.expr/6>,
 #Function<42.39164016/1 in :erl_eval.expr/6>]
iex(13)> list = [10, 20, 30]
[10, 20, 30]
iex(14)> list |> ap.(func_list)
[11, 21, 31, 12, 22, 32]

Bind each step depends on the results of the previous, ap happens independently.

The Protocol

defprotocol FunPark.Monad do
  def map(monad_value, func)
  def bind(monad_value, func_returning_monad)
  def ap(monadic_func, monad_value)
end

1. map/2 applies a function to a value in a context, preserving the structure.
2. bind/2 sequences computations, allowing each step to determine the next,
all within the context.
3. ap/2 applies a function to a value, where both are in the same context.

Model Neutrality with Identity

Okay let’s start out with the identity module.

  @enforce_keys [:value]
  defstruct [:value]

  def pure(value), do: %__MODULE__{value: value}
  def extract(%__MODULE__{value: value}), do: value

Pure is the identity and extract/1 is used to access the value.

Run It

iex(15)> alice = FunPark.Patron.make("Alice", 14, 130)
%FunPark.Patron{
  id: 3714,
  name: "Alice",
  age: 14,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(16)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 3714,
    name: "Alice",
    age: 14,
    height: 130,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(17)> FunPark.Identity.extract(alice_monad)
%FunPark.Patron{
  id: 3714,
  name: "Alice",
  age: 14,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(18)> :apple |> FunPark.Identity.pure() |> FunPark.Identity.extract()
:apple

This is just the beginning but we are simply adding the value passed into the struct for Identity and then accessing it.

Equality

defimpl FunPark.Eq, for: FunPark.Identity do
  alias FunPark.Identity
  alias FunPark.Eq

  def eq?(%Identity{value: v1}, %Identity{value: v2}), do: Eq.eq?(v1, v2)

  def not_eq?(%Identity{value: v1}, %Identity{value: v2}),
    do: Eq.not_eq?(v1, v2)
end

This will unwrap and then apply the EQ protocol.

Run It

iex(23)> FunPark.Eq.Utils.eq?(alice, alice)
true
iex(24)> FunPark.Eq.Utils.eq?(alice, beth)
false
iex(25)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4034,
    name: "Alice",
    age: 14,
    height: 130,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(26)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4098,
    name: "Beth",
    age: 16,
    height: 125,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(27)> FunPark.Eq.Utils.eq?(alice_monad, alice_monad)
iex(27)> FunPark.Eq.Utils.eq?(alice_monad, alice_monad)
true
iex(28)> FunPark.Eq.Utils.eq?(alice_monad, beth_monad)
false

You can see that the monad and the pure struct are the same things here.

Ordering

defimpl FunPark.Ord, for: FunPark.Identity do
  alias FunPark.Ord
  alias FunPark.Identity

  def lt?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.lt?(v1, v2)
  def le?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.le?(v1, v2)
  def gt?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.gt?(v1, v2)
  def ge?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.ge?(v1, v2)
end

Again an unwrap and then a comparison

Run It

iex(29)> alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip)
%FunPark.Patron{
  id: 4162,
  name: "Alice",
  age: 14,
  height: 135,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(30)> beth = FunPark.Patron.make("Beth", 16, 125)
%FunPark.Patron{
  id: 4226,
  name: "Beth",
  age: 16,
  height: 125,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(31)> FunPark.Ord.Utils.compare(alice, beth)
:lt
iex(32)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4162,
    name: "Alice",
    age: 14,
    height: 135,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(33)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4226,
    name: "Beth",
    age: 16,
    height: 125,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(34)> FunPark.Ord.Utils.compare(alice_monad, beth_monad)
:lt

Lift Eq and Order

We can now use some custom Eq and Ord implementations.

def lift_eq(custom_eq) do
    custom_eq = Eq.Utils.to_eq_map(custom_eq)

    %{
      eq?: fn
        %__MODULE__{value: a}, %__MODULE__{value: b} -> custom_eq.eq?.(a, b)
      end,
      not_eq?: fn
        %__MODULE__{value: a}, %__MODULE__{value: b} ->
          custom_eq.not_eq?.(a, b)
      end
    }
  end


  def lift_ord(custom_ord) do
    custom_ord = Ord.Utils.to_ord_map(custom_ord)

    %{
      lt?: fn
        %__MODULE__{value: v1}, %__MODULE__{value: v2} ->
          custom_ord.lt?.(v1, v2)
      end,
      le?: fn
        %__MODULE__{value: v1}, %__MODULE__{value: v2} ->
          custom_ord.le?.(v1, v2)
      end,
      gt?: fn
        %__MODULE__{value: v1}, %__MODULE__{value: v2} ->
          custom_ord.gt?.(v1, v2)
      end,
      ge?: fn
        %__MODULE__{value: v1}, %__MODULE__{value: v2} ->
          custom_ord.ge?.(v1, v2)
      end
    }
  end

These will unwrap the Eq and Ord functions as well as the values from the Monad.

Run It

iex(35)> alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip)
%FunPark.Patron{
  id: 4290,
  name: "Alice",
  age: 14,
  height: 135,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(36)> beth = FunPark.Patron.make("Beth", 16, 125)
%FunPark.Patron{
  id: 4354,
  name: "Beth",
  age: 16,
  height: 125,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(37)> priority_ord = FunPark.Patron.ord_by_priority()
%{
  ge?: #Function<3.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
  gt?: #Function<2.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
  le?: #Function<1.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
  lt?: #Function<0.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>
}
iex(38)> FunPark.Ord.Utils.compare(alice, beth, priority_ord)
:gt
iex(39)> lifted_priority_ord = FunPark.Identity.lift_ord(priority_ord)
%{
  ge?: #Function<6.74003289/2 in FunPark.Identity.lift_ord/1>,
  gt?: #Function<5.74003289/2 in FunPark.Identity.lift_ord/1>,
  le?: #Function<4.74003289/2 in FunPark.Identity.lift_ord/1>,
  lt?: #Function<3.74003289/2 in FunPark.Identity.lift_ord/1>
}
iex(40)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4290,
    name: "Alice",
    age: 14,
    height: 135,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(41)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
  value: %FunPark.Patron{
    id: 4354,
    name: "Beth",
    age: 16,
    height: 125,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(42)> FunPark.Ord.Utils.compare(alice_monad, beth_monad, lifted_priority_ord)
:gt

Monadic Logic

defimpl FunPark.Monad, for: FunPark.Identity do
  alias FunPark.Identity

  def map(%Identity{value: value}, func) do
    Identity.pure(func.(value))
  end

  def bind(%Identity{value: value}, func) do
    func.(value)
  end

  def ap(%Identity{value: func}, %Identity{value: value}) do
    Identity.pure(func.(value))
  end
end

• map/2 unwraps the value from the Identity, applies the function, and rewraps
the result.
• bind/2 unwraps the value from the Identity and applies it to the provided
function, which must return a new Identity.
• ap/2 unwraps both the function and the value, applies the function to the
value, and rewraps the result.

Now let’s take this and doing some work to add some wait times together.

  def add_wait_time(
        %__MODULE__{wait_time: wait_time} = ride,
        minutes
      )
      when is_number(minutes) and minutes > 0 do
    change(ride, %{wait_time: Math.sum(wait_time, minutes)})
  end

This will add the wait time to the current wait time ensuring that the new time is positive.

Run It

iex(43)> tea_cup = FunPark.Ride.make("Tea Cup", wait_time: 10)
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 10,
  online: true,
  tags: []
}
iex(44)> FunPark.Ride.add_wait_time(tea_cup, 20)
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 30,
  online: true,
  tags: []
}
iex(45)> tea_cup
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 10,
  online: true,
  tags: []
}
iex(46)> |> FunPark.Ride.add_wait_time(20)
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 30,
  online: true,
  tags: []
}
iex(47)> |> FunPark.Ride.add_wait_time(10)
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 40,
  online: true,
  tags: []
}
iex(48)> |> FunPark.Ride.add_wait_time(5)
%FunPark.Ride{
  id: 4418,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 45,
  online: true,
  tags: []
}
iex(50)> tea_cup_m = FunPark.Identity.pure(tea_cup)
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 10,
    online: true,
    tags: []
  }
}
iex(51)> add_wait = FunPark.Utils.curry_r(&FunPark.Ride.add_wait_time/2)
#Function<3.64560675/1 in FunPark.Utils.curry_r/3>
iex(52)> FunPark.Monad.map(tea_cup_m, add_wait.(20))
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 30,
    online: true,
    tags: []
  }
}
iex(53)> tea_cup_m
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 10,
    online: true,
    tags: []
  }
}
iex(54)> |> FunPark.Monad.map(add_wait.(20))
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 30,
    online: true,
    tags: []
  }
}
iex(55)> |> FunPark.Monad.map(add_wait.(10))
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 40,
    online: true,
    tags: []
  }
}
iex(56)> |> FunPark.Monad.map(add_wait.(5))
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 45,
    online: true,
    tags: []
  }
}

We can see that you can now add wait times and then even pipe them into each other. We also can do the same thing with the Identity of the Monad, using the map. Let’s continue.


The bind/2 function differs in one key way: it lets each step choose the structure of the result. In monads with multiple structures—like Maybe or Either—bind can switch between them. But Identity has only one structure, so there’s nothing to switch to.

iex(57)> sensor_1 = &FunPark.Identity.pure(add_wait.(10).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(58)> sensor_2 = &FunPark.Identity.pure(add_wait.(5).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(59)> sensor_3 = &FunPark.Identity.pure(add_wait.(20).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(60)> tea_cup_m
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 10,
    online: true,
    tags: []
  }
}
iex(61)> |> FunPark.Monad.bind(sensor_1)
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 20,
    online: true,
    tags: []
  }
}
iex(62)> |> FunPark.Monad.bind(sensor_2)
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 25,
    online: true,
    tags: []
  }
}
iex(63)> |> FunPark.Monad.bind(sensor_3)
%FunPark.Identity{
  value: %FunPark.Ride{
    id: 4418,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 45,
    online: true,
    tags: []
  }
}

We are doing everything so far without a real need to use this form factor. But the point of all of this is to give us a way to always use the same data structure to do work on a context. The sensors will come from out side the Ride context and we might not be able to control everything that is sent, but we can at least be sure we can pipe the information from one Monad to an other.

What You’ve Learned

Explained above but this is just the beginning.