Home Posts Post Search Tag Search

Advanced Functional Elixir - 04 - Act On It
Published on: 2026-03-19 Tags: Blog, Side Project, Testing, Advanced Functional Programming, Act on It, Monoids, Identity

Act On It

Try building a couple of new monoids on your own:
• Product: Combines numbers through multiplication.
• Min: Selects the smallest value using an Ord instance.

Follow the same pattern you saw in Sum and Max: implement empty/1, append/2, wrap/2, and unwrap/1. The goal is to get comfortable applying the Monoid structure in different contexts.

Product

For this we will create a new module for Product and go from there.

defmodule FunPark.Monoid.ActProduct do
  defstruct value: 1
end

defimpl FunPark.Monoid, for: FunPark.Monoid.ActProduct do
  alias FunPark.Monoid.ActProduct, as: Product

  def empty(_), do: %Product{}

  def append(%Product{value: value_a}, %Product{value: value_b}),
    do: %Product{value: value_a * value_b}

  def wrap(%Product{}, value) when is_number(value), do: %Product{value: value}

  def unwrap(%Product{value: value}), do: value
end

Let’s test it out

iex(69)> prod_a = %FunPark.Monoid.ActProduct{value: 1}
%FunPark.Monoid.ActProduct{value: 1}
iex(70)> prod_b = %FunPark.Monoid.ActProduct{value: 2}
%FunPark.Monoid.ActProduct{value: 2}
iex(71)> prod_a = FunPark.Monoid.wrap(%FunPark.Monoid.ActProduct{}, 1)
%FunPark.Monoid.ActProduct{value: 1}
iex(72)> prod_b = FunPark.Monoid.wrap(%FunPark.Monoid.ActProduct{}, 2)
%FunPark.Monoid.ActProduct{value: 2}
iex(73)> value = FunPark.Monoid.append(prod_a, prod_b)
%FunPark.Monoid.ActProduct{value: 2}
iex(74)> value = FunPark.Monoid.append(value, prod_b)
%FunPark.Monoid.ActProduct{value: 4}

Okay so now lets add in a wrapper within the utils module so that we can auto-wrap and then send it back unwrapped. This was already there.

  def m_append(monoid, a, b) when is_struct(monoid) do
    append(wrap(monoid, a), wrap(monoid, b)) |> unwrap()
  end


  def m_concat(monoid, values) when is_struct(monoid) and is_list(values) do
    fold_l(values, empty(monoid), fn value, acc ->
      append(acc, wrap(monoid, value))
    end)
    |> unwrap()
  end

Now let’s test it out.

iex(76)> FunPark.Monoid.Utils.m_append(%FunPark.Monoid.ActProduct{}, 2, 4)
8

Now let’s add that to the Math module

  def product(a, b) do
    m_append(%Monoid.ActProduct{}, a, b)
  end

  def product(list) when is_list(list) do
    m_concat(%Monoid.ActProduct{}, list)
  end

Now let’s test it out.

iex(78)> FunPark.Math.product(2, 4)
8
iex(79)> FunPark.Math.product(2, 8)
16
iex(80)> FunPark.Math.product([2, 4, 8])
64

Min

Okay so we need to use some of the same logic for this next one.

defmodule FunPark.Monoid.Min do
  defstruct value: nil, id: nil, ord: FunPark.Ord
end


defimpl FunPark.Monoid, for: FunPark.Monoid.Min do
  alias FunPark.Monoid.Min
  alias FunPark.Ord.Utils

  def empty(%Min{id: id, ord: ord}), do: %Min{value: id, id: id, ord: ord}

  def append(%Min{value: a, ord: ord} = min1, %Min{value: b}) do
    %Min{min1 | value: Utils.min(a, b, ord)}
  end

  def wrap(%Min{ord: ord}, value) do
    %Min{value: value, ord: Utils.to_ord_map(ord)}
  end

  def unwrap(%Min{value: value}), do: value
end

Let’s test it out.

iex(82)> min_a = FunPark.Monoid.wrap(%FunPark.Monoid.Min{}, 2)
%FunPark.Monoid.Min{
  value: 2,
  id: nil,
  ord: %{
    lt?: &FunPark.Ord.lt?/2,
    gt?: &FunPark.Ord.gt?/2,
    ge?: &FunPark.Ord.ge?/2,
    le?: &FunPark.Ord.le?/2
  }
}
iex(83)> min_b = FunPark.Monoid.wrap(%FunPark.Monoid.Min{}, 4)
%FunPark.Monoid.Min{
  value: 4,
  id: nil,
  ord: %{
    lt?: &FunPark.Ord.lt?/2,
    gt?: &FunPark.Ord.gt?/2,
    ge?: &FunPark.Ord.ge?/2,
    le?: &FunPark.Ord.le?/2
  }
}
iex(84)> FunPark.Monoid.append(min_a, min_b)
%FunPark.Monoid.Min{
  value: 2,
  id: nil,
  ord: %{
    lt?: &FunPark.Ord.lt?/2,
    gt?: &FunPark.Ord.gt?/2,
    ge?: &FunPark.Ord.ge?/2,
    le?: &FunPark.Ord.le?/2
  }
}

Okay so now let’s add in a wrapper so we can just test values without a wrapper within the Ord.Utils, again this was already there.

  def m_append(monoid, a, b) when is_struct(monoid) do
    append(wrap(monoid, a), wrap(monoid, b)) |> unwrap()
  end


  def m_concat(monoid, values) when is_struct(monoid) and is_list(values) do
    fold_l(values, empty(monoid), fn value, acc ->
      append(acc, wrap(monoid, value))
    end)
    |> unwrap()
  end

Let’s test it out.

iex(84)> FunPark.Monoid.append(min_a, min_b)
%FunPark.Monoid.Min{
  value: 2,
  id: nil,
  ord: %{
    lt?: &FunPark.Ord.lt?/2,
    gt?: &FunPark.Ord.gt?/2,
    ge?: &FunPark.Ord.ge?/2,
    le?: &FunPark.Ord.le?/2
  }
}
iex(85)> FunPark.Monoid.Utils.m_append(%FunPark.Monoid.Min{}, 2, 3)
2
iex(86)> FunPark.Monoid.Utils.m_concat(%FunPark.Monoid.Min{}, [3, 4, 5])
3

Now we can add it to the Math module.

  def min(a, b) do
    m_append(%Monoid.Min{id: Float.max_finite()}, a, b)
  end

  def min(list) when is_list(list) do
    m_concat(%Monoid.Min{id: Float.max_finite()}, list)
  end

Now let’s test it out.

iex(87)> FunPark.Math.min(4, 5)
4
iex(88)> FunPark.Math.min(4, 8)
4
iex(89)> FunPark.Math.min(6,3)
3