Home Posts Post Search Tag Search

Advanced Functional Elixir - 08 - Manage Absence with Maybe
Published on: 2026-04-12 Tags: elixir, Blog, FunPark, Monoids, Identity, curry, Monads, Maybe, Nothing, Just

8. Manage Absence with Maybe

Maybe is a quantum: not a value, but the possibility of one. Maybe extends to list, enabling loops over collections or quantized values.

Build the Structures

Maybe context has two structures. Just for the presence and Nothing for the absence.

Just

defmodule FunPark.Monad.Maybe.Just do
  @enforce_keys [:value]
  defstruct [:value]

  def pure(nil), do: raise(ArgumentError, "Cannot wrap nil in a Just")
  def pure(value), do: %__MODULE__{value: value}
end

This is the start of making the structure stay consistent and removing any way that we will continue if we don’t have the right structure.

Nothing

defmodule FunPark.Monad.Maybe.Nothing do
  defstruct []

  def pure, do: %__MODULE__{}
end

Maybe

defmodule FunPark.Monad.Maybe do
  import FunPark.Monad, only: [bind: 2, map: 2]
  import FunPark.Foldable, only: [fold_l: 3]
  alias FunPark.Monad.Maybe.{Just, Nothing}
  # alias FunPark.Monad.Either.{Left, Right}
  alias FunPark.Eq
  alias FunPark.Identity
  alias FunPark.Ord


  def just(value), do: Just.pure(value)
  def nothing, do: Nothing.pure()
  def pure(value), do: just(value)

  def just?(%Just{}), do: true
  def just?(_), do: false

  def nothing?(%Nothing{}), do: true
  def nothing?(_), do: false

  def guard(maybe, true), do: maybe
  def guard(_maybe, false), do: nothing()

  def filter(maybe, predicate) do
    bind(maybe, fn value ->
      if predicate.(value) do
        pure(value)
      else
        nothing()
      end
    end)
  end

We have some helper functions that will allow us to know that we are on the right track. The guard/2 will come in handy later to allow us to keep stacks working. There is the just/1 and nothing/0 that will delegate to the pure function.


just/1 and nothing/1 are refinement predicates, used to check if we are in the _Just or Nothing branch.

Run It

iex(1)> just_a = FunPark.Monad.Maybe.just("A")
%FunPark.Monad.Maybe.Just{value: "A"}
iex(2)> nothing = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(3)> FunPark.Monad.Maybe.just?(just_a)
true
iex(4)> FunPark.Monad.Maybe.nothing?(just_a)
false

So we see that we are getting a true or false for the ? branches and the just will build a value into the Just struct.

Fold Branches

Maybe is quantum of possibility it doesn’t collapse unless observed. just will build the function and nothing will give us a fallback. Folding is the way to get the function to collapse.

defimpl FunPark.Foldable, for: FunPark.Monad.Maybe do
  alias FunPark.Monad.Maybe.{Just, Nothing}
  def fold_l(%Just{value: value}, just_fn, _nothing_fn),
    do: just_fn.(value)
  def fold_l(%Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
  def fold_r(%Just{value: value}, just_fn, _nothing_fn),
    do: just_fn.(value)
  def fold_r(%Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
end

Not So Fast There, Bub

So for Elixir protocols operate on structs, but Maybe isn’t a struct, it’s a sum type made up of Just and Nothing. So we need to implement them with Foldable we need to define the behavior for each branch.

# just.ex
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Just do
  alias FunPark.Monad.Maybe.Just

  def fold_l(%Just{value: value}, just_func, _nothing_func) do
    just_func.(value)
  end

  def fold_r(%Just{} = just, just_func, nothing_func) do
    fold_l(just, just_func, nothing_func)
  end
end

This will allow us to apply the just_func to the value if passed or fold_l to the functions to keep the train going.

# nothing.ex
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Nothing do
  alias FunPark.Monad.Maybe.Nothing

  def fold_l(%Nothing{}, _just_func, nothing_func) do
    nothing_func.()
  end

  def fold_r(%Nothing{} = nothing, just_func, nothing_func) do
    fold_l(nothing, just_func, nothing_func)
  end
end

This is one of the outs of the system, if we get a Nothing struct we will just return the nothing.func().

Run It

iex(5)> good = FunPark.Monad.Maybe.just(10)
%FunPark.Monad.Maybe.Just{value: 10}
iex(6)> bad = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(7)> FunPark.Foldable.fold_l(good, &"Sensor: #{&1}", fn -> "Broken" end)
"Sensor: 10"
iex(8)> FunPark.Foldable.fold_l(bad, &"Sensor: #{&1}", fn -> "Broken" end)
"Broken"

Okay so we are taking a Just and a Nothing and then giving them functions to do if we get a “good” or “bad” value for the Maybe.

Default Value

Okay so the Ride expert wants to make sure that we have a default as some info is better than no info. In this case we want to have it default to 5min if there is no info. We can now create a get_or_else/2 function:

  def get_or_else(maybe, default) do
    fold_l(maybe, fn value -> value end, fn -> default end)
  end

We now have a case to return a default in case we don’t have a real Just

Run It

iex(9)> sensor_a = FunPark.Monad.Maybe.pure(20)
%FunPark.Monad.Maybe.Just{value: 20}
iex(10)> sensor_b = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(11)> sensor_c = FunPark.Monad.Maybe.pure(30)
%FunPark.Monad.Maybe.Just{value: 30}
iex(12)> sensor_d = FunPark.Monad.Maybe.pure(5)
%FunPark.Monad.Maybe.Just{value: 5}
iex(13)> get_or_else_5 = &FunPark.Monad.Maybe.get_or_else(&1, 5)
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(14)> get_or_else_5.(sensor_a)
20
iex(15)> |> FunPark.Math.sum(get_or_else_5.(sensor_b))
25
iex(16)> |> FunPark.Math.sum(get_or_else_5.(sensor_c))
55
iex(17)> |> FunPark.Math.sum(get_or_else_5.(sensor_d))
60

So when we got the results of sensor b we only added 5 instead of the broken branch. Let’s see if we tried to pass the same sensor without the get_or_else/2 instead a get_or_nothing/1

  def get_or_nothing(maybe) do
    fold_l(maybe, fn value -> value end, fn -> nothing() end)
  end

iex> sensor_e = FunPark.Monad.Maybe.just(5)
%FunPark.Monad.Maybe.Just{value: 5}

iex> sensor_f = FunPark.Monad.Maybe.just(10)
%FunPark.Monad.Maybe.Just{value: 10}

iex> sensor_g = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}

iex> get_or_nothing = &FunPark.Monad.Maybe.get_or_nothing(&1)
&FunPark.Monad.Maybe.get_or_nothing/1

iex> get_or_nothing.(sensor_e)
5

iex> get_or_nothing.(sensor_f)
10

iex> get_or_nothing.(sensor_g)
%FunPark.Monad.Maybe.Nothing{}

Lift Other Contexts

Now we need a way to take values and turn them into the Just or Nothing.

Identity

  def lift_identity(%Identity{} = identity) do
    case identity do
      %Identity{value: nil} -> nothing()
      %Identity{value: value} -> just(value)
    end
  end

We now have a way to put a input values into an identity and then pull them out with Maybe.life_identity/1

Run It

iex(28)> person_1 = FunPark.Identity.pure("Dave")
%FunPark.Identity{value: "Dave"}
iex(29)> person_2 = FunPark.Identity.pure(nil)
%FunPark.Identity{value: nil}
iex(30)> maybe_person_1 = FunPark.Monad.Maybe.lift_identity(person_1)
%FunPark.Monad.Maybe.Just{value: "Dave"}
iex(31)> maybe_person_2 = FunPark.Monad.Maybe.lift_identity(person_2)
%FunPark.Monad.Maybe.Nothing{}
iex(32)> FunPark.Monad.Maybe.get_or_else(maybe_person_1, "Missing")
"Dave"
iex(33)> FunPark.Monad.Maybe.get_or_else(maybe_person_2, "Missing")
"Missing"

Great we were able to replace a nil with a default, but we don’t want to just do that. We want to design it out.

Predicate

Okay so we want to now make a MaybeVipPatron that only does anything if the Patron is a VIP. In this case we want to make it so we can send in a bit of data (a struct) and then give it a way to define a good or bad outcome. If it doesn’t pass we return Nothing.

  def lift_predicate(value, predicate) when is_function(predicate, 1) do
    fold_l(
      fn -> predicate.(value) end,
      fn -> just(value) end,
      fn -> nothing() end
    )
  end

fold_l/3 here is taking a structure and saying if true do the first if false do the second.

Run It

iex(34)> tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
%FunPark.Ride{
  id: 20,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 100,
  online: true,
  tags: []
}
iex(35)> FunPark.Ride.suggested?(tea_cup)
false
iex(36)> FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
%FunPark.Monad.Maybe.Nothing{}
iex(37)> tea_cup = FunPark.Ride.update_wait_time(tea_cup, 10)
%FunPark.Ride{
  id: 20,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 10,
  online: true,
  tags: []
}
iex(38)> FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Ride{
    id: 20,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 10,
    online: true,
    tags: []
  }
}

Here is the wrong Domain being passed.

iex(39)> patron = FunPark.Patron.make("alice", 33, 40)
%FunPark.Patron{
  id: 142,
  name: "alice",
  age: 33,
  height: 40,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(40)> FunPark.Monad.Maybe.lift_predicate(patron, &FunPark.Ride.suggested?/1)
** (FunctionClauseError) no function clause matching in FunPark.Ride.suggested?/1    
    
    The following arguments were given to FunPark.Ride.suggested?/1:
    
        # 1
        %FunPark.Patron{
          id: 142,
          name: "alice",
          age: 33,
          height: 40,
          ticket_tier: :basic,
          fast_passes: [],
          reward_points: 0,
          likes: [],
          dislikes: []
        }
    
    Attempted function clauses (showing 1 out of 1):
    
        def suggested?(%FunPark.Ride{} = ride)
    
    (fun_park 0.1.0) lib/fun_park/ride.ex:155: FunPark.Ride.suggested?/1
    (fun_park 0.1.0) lib/fun_park/predicate.ex:46: FunPark.Foldable.Function.fold_l/3
    iex:40: (file)

Bridge Elixir Patterns

Elixir has ways of dealing with missing information, (nil). We use interop functions to bridge the gaps.

# maybe.ex
  def from_nil(nil), do: nothing()
  def from_nil(value), do: just(value)

  def to_nil(%Nothing{}), do: nil
  def to_nil(%Just{value: value}), do: value

We got from a nil to Nothing or Just, or we take a Nothing and go to nil or a Just to a value. Let’s use this in the Ride domain to get a Maybe depending on whether we have a valid FastPass

  def get_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
    Enum.find(
      Patron.get_fast_passes(patron),
      &FastPass.valid?(&1, ride)
    )
    |> Maybe.from_nil()
  end

Once we get a nil or a FastPass we can then return a Maybe

Run It

iex(42)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 270,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(43)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(44)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
  id: 462,
  ride: %FunPark.Ride{
    id: 270,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(45)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
  id: 526,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(46)> FunPark.Ride.get_fast_pass(alice, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(47)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 526,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 462,
      ride: %FunPark.Ride{
        id: 270,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(48)> FunPark.Ride.get_fast_pass(alice, haunted_mansion)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.FastPass{
    id: 462,
    ride: %FunPark.Ride{
      id: 270,
      name: "Haunted Mansion",
      min_age: 14,
      min_height: 0,
      wait_time: 0,
      online: true,
      tags: []
    },
    time: ~U[2025-06-01 13:00:00Z]
  }
}

We now have the ability to work in the Maybe with Elixir built-in functions.

Define Equality

We can now work with the Eq with maybe as there is a way to determine the relationship, as we now have a behavior.

# just.ex
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Just do
  alias FunPark.Monad.Maybe.{Just, Nothing}
  alias FunPark.Eq

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

  def not_eq?(%Just{value: v1}, %Just{value: v2}), do: not Eq.eq?(v1, v2)
  def not_eq?(%Just{}, %Nothing{}), do: true
end

• Just unwraps its value and defers comparison to the underlying Eq imple-
mentation.
• Just is never equal to Nothing.

# nothing.ex
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Nothing do
  alias FunPark.Monad.Maybe.{Nothing, Just}

  def eq?(%Nothing{}, %Nothing{}), do: true
  def eq?(%Nothing{}, %Just{}), do: false

  def not_eq?(%Nothing{}, %Nothing{}), do: false
  def not_eq?(%Nothing{}, %Just{}), do: true
end

• Nothing is always equal to Nothing.
• Nothing is never equal to Just.

Run It

iex(49)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
  id: 590,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(50)> alice_copy = FunPark.Patron.change(alice, %{ticket_tier: :vip})
%FunPark.Patron{
  id: 590,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(51)> FunPark.Eq.Utils.eq?(alice, alice_copy)
true
iex(52)> alice_maybe = FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 590,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(53)> alice_copy_maybe = FunPark.Monad.Maybe.pure(alice_copy)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 590,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(54)> FunPark.Eq.Utils.eq?(alice_maybe, alice_copy_maybe)
true
iex(55)> alice_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
...(55)> alice, &FunPark.Patron.vip?/1
...(55)> )
%FunPark.Monad.Maybe.Nothing{}
iex(56)> alice_copy_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
...(56)> alice_copy, &FunPark.Patron.vip?/1
...(56)> )
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 590,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(57)> FunPark.Eq.Utils.eq?(alice_maybe_vip, alice_copy_maybe_vip)
false

So we have the implementation for the Eq context within the new Maybe modules.

Establish Order

# just.ex
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Just do
  alias FunPark.Monad.Maybe.{Just, Nothing}
  alias FunPark.Ord

  def lt?(%Just{value: v1}, %Just{value: v2}), do: Ord.lt?(v1, v2)
  def lt?(%Just{}, %Nothing{}), do: false

  def le?(%Just{value: v1}, %Just{value: v2}), do: Ord.le?(v1, v2)
  def le?(%Just{}, %Nothing{}), do: false

  def gt?(%Just{value: v1}, %Just{value: v2}), do: Ord.gt?(v1, v2)
  def gt?(%Just{}, %Nothing{}), do: true

  def ge?(%Just{value: v1}, %Just{value: v2}), do: Ord.ge?(v1, v2)
  def ge?(%Just{}, %Nothing{}), do: true
end

• Just unwraps its value and defers comparison to the underlying Ord
implementation.
• Just is always greater than Nothing.

# nothing.ex
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Nothing do
  alias FunPark.Monad.Maybe.{Nothing, Just}

  def lt?(%Nothing{}, %Just{}), do: true
  def lt?(%Nothing{}, %Nothing{}), do: false

  def le?(%Nothing{}, %Just{}), do: true
  def le?(%Nothing{}, %Nothing{}), do: true

  def gt?(%Nothing{}, %Just{}), do: false
  def gt?(%Nothing{}, %Nothing{}), do: false

  def ge?(%Nothing{}, %Just{}), do: false
  def ge?(%Nothing{}, %Nothing{}), do: true
end

• Nothing is always equal to Nothing.
• Nothing is always less than Just.

Run It

iex(72)> alice=FunPark.Patron.make("Alice",15,150,ticket_tier: :vip)
%FunPark.Patron{
  id: 782,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(73)> beth=FunPark.Patron.make("Beth",15,150)
%FunPark.Patron{
  id: 846,
  name: "Beth",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(74)> FunPark.Ord.Utils.compare(alice,beth)
:lt
iex(75)> alice_m=FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 782,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(76)> beth_m=FunPark.Monad.Maybe.pure(beth)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 846,
    name: "Beth",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(77)> FunPark.Ord.Utils.compare(alice_m,beth_m)
:lt
iex(78)> alice_vip=FunPark.Monad.Maybe.lift_predicate(alice,&FunPark.Patron.vip?/1)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 782,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(79)> beth_vip=FunPark.Monad.Maybe.lift_predicate(beth,&FunPark.Patron.vip?/1)
%FunPark.Monad.Maybe.Nothing{}
iex(80)> FunPark.Ord.Utils.compare(alice_vip,beth_vip)
:gt

This works because we now have a value for the Nothing so that it will always be less than a Just and Just can only exist if we have a value to pass. Once we pass the lift_predicate we are going to get a Just or a Nothing depending on the data sent and the check.

Lift Custom Comparisons

Okay so now we want to deal with some custom comparisons. So we need to have them lifted in the Maybe logic.

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

    %{
      eq?: fn
        %Just{value: v1}, %Just{value: v2} -> custom_eq.eq?.(v1, v2)
        %Nothing{}, %Nothing{} -> true
        %Nothing{}, %Just{} -> false
        %Just{}, %Nothing{} -> false
      end,
      not_eq?: fn
        %Just{value: v1}, %Just{value: v2} -> custom_eq.not_eq?.(v1, v2)
        %Nothing{}, %Nothing{} -> false
        %Nothing{}, %Just{} -> true
        %Just{}, %Nothing{} -> true
      end
    }
  end

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

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

We are taking the idea of a custom Ord or an Eq and then making the rules for how they would work. In the case of the Eq we have the rule for the customeq running iff we have a _Just and a Just otherwise we would know the outcome.

Run It

iex(81)> alice=FunPark.Patron.make("Alice",15,150,ticket_tier: :vip)
%FunPark.Patron{
  id: 910,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(82)> beth=FunPark.Patron.make("Beth",15,150)
%FunPark.Patron{
  id: 974,
  name: "Beth",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(83)> ord_by_ticket=FunPark.Patron.ord_by_ticket_tier()
%{
  ge?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  gt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  le?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(84)> FunPark.Ord.Utils.compare(alice,beth,ord_by_ticket)
:gt
iex(85)> alice_maybe=FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 910,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(86)> beth_maybe=FunPark.Monad.Maybe.pure(beth)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 974,
    name: "Beth",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(87)> lifted_ord=FunPark.Monad.Maybe.lift_ord(ord_by_ticket)
%{
  ge?: #Function<12.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
  gt?: #Function<11.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
  le?: #Function<10.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
  lt?: #Function<9.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>
}
iex(88)> FunPark.Ord.Utils.compare(alice_maybe,beth_maybe,lifted_ord)
:gt

Okay so we now can pass in a custom Ord or Eq. It works for both cases but now we can start to add more information to the functions.

Model Absence in a Monoid

• Nothing serves as the identity, explicitly representing the absence of a value.
• Just holds a result that combines according to the underlying monoid.

Let’s now revisit the Patron.max_priority_monoid\0.

# patron.ex
  defp max_priority_monoid do
    %Monoid.Max{
      value: priority_empty(),
      ord: ord_by_priority()
    }
  end

  def max_priority_maybe_monoid do
    %Monoid.Max{
      value: Maybe.nothing(),
      ord: Maybe.lift_ord(ord_by_priority())
    }
  end

  def highest_priority_maybe(patrons) when is_list(patrons) do
    m_concat(
      max_priority_maybe_monoid(),
      patrons |> Enum.map(&Maybe.pure/1)
    )
  end

This allows us to leverage the Maybe to get a real bottom wrung of the totem pole for non-existent value for order.

Run It

iex(90)> alice = FunPark.Patron.make("Alice", 15, 120, reward_points: 50, ticket_tier: :premium)
%FunPark.Patron{
  id: 13,
  name: "Alice",
  age: 15,
  height: 120,
  ticket_tier: :premium,
  fast_passes: [],
  reward_points: 50,
  likes: [],
  dislikes: []
}
iex(91)> beth = FunPark.Patron.make("Beth", 16, 130, reward_points: 20, ticket_tier: :vip)
%FunPark.Patron{
  id: 1038,
  name: "Beth",
  age: 16,
  height: 130,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 20,
  likes: [],
  dislikes: []
}
iex(92)> charles = FunPark.Patron.make("Charles", 14, 135, reward_points: 150, ticket_tier: :premium)
%FunPark.Patron{
  id: 1102,
  name: "Charles",
  age: 14,
  height: 135,
  ticket_tier: :premium,
  fast_passes: [],
  reward_points: 150,
  likes: [],
  dislikes: []
}
iex(93)> FunPark.Patron.highest_priority_maybe([alice, beth, charles])
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 1038,
    name: "Beth",
    age: 16,
    height: 130,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 20,
    likes: [],
    dislikes: []
  }
}
iex(94)> FunPark.Patron.highest_priority_maybe([alice, charles])
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 1102,
    name: "Charles",
    age: 14,
    height: 135,
    ticket_tier: :premium,
    fast_passes: [],
    reward_points: 150,
    likes: [],
    dislikes: []
  }
}
iex(95)> FunPark.Patron.highest_priority_maybe([alice])
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 13,
    name: "Alice",
    age: 15,
    height: 120,
    ticket_tier: :premium,
    fast_passes: [],
    reward_points: 50,
    likes: [],
    dislikes: []
  }
}
iex(96)> FunPark.Patron.highest_priority_maybe([])
%FunPark.Monad.Maybe.Nothing{}

This built us a list of the Patrons in the order_by_priority as we set as the default priority.

Implement the Monadic Behaviors

We need to build the map/2, bind/2 or, ap/2 within the Maybe.

# nothing.ex
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Nothing do
  alias FunPark.Monad.Maybe.Nothing

  def map(%Nothing{}, _func), do: %Nothing{}
  def ap(%Nothing{}, _val), do: %Nothing{}
  def bind(%Nothing{}, _func), do: %Nothing{}
end

# just.ex
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Just do
  alias FunPark.Monad.Maybe.{Just, Nothing}

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

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

  def ap(%Just{}, %Nothing{}), do: %Nothing{}

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

• map/2 applies the transformation and wraps the result back in Just, preserv-
ing structure.
• ap/2 applies the wrapped function to the wrapped value if both are Just;
otherwise, it returns Nothing.
• bind/2 applies a function that returns a Maybe, transforming the value and
potentially changing structure.

Functor Map

We want to only update the ridetime if the _Ride is on-line. So we need to add in this function.

# ride.ex
def update_wait_time_maybe(%__MODULE__{} = ride, wait_time)
    when is_number(wait_time) do
  ride
  |> Maybe.lift_predicate(&online?/1)
  |> map(&update_wait_time(&1, wait_time))
end

Run It

iex(97)> tea_cup = FunPark.Ride.make("Tea Cup", online: false)
%FunPark.Ride{
  id: 1166,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: false,
  tags: []
}
iex(98)> FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
%FunPark.Monad.Maybe.Nothing{}
iex(99)> tea_cup = FunPark.Ride.change(tea_cup, %{online: true})
%FunPark.Ride{
  id: 1166,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(100)> FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Ride{
    id: 1166,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 20,
    online: true,
    tags: []
  }
}

Monad Bind

So now we can a way to forward a Patron based off of their FastPass eligibility for a Ride

  def check_ride_eligibility(%Patron{} = patron, %__MODULE__{} = ride) do
    is_eligible = curry_r(&eligible?/2)
    Maybe.lift_predicate(patron, is_eligible.(ride))
  end

  def check_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
    has_fast_pass = curry_r(&fast_pass?/2)
    Maybe.lift_predicate(patron, has_fast_pass.(ride))
  end

  def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
    check_fast_pass = curry_r(&check_fast_pass/2)

    patron
    |> check_ride_eligibility(ride)
    |> bind(check_fast_pass.(ride))
  end

We are building off the idea that we need to curry the data as we are comparing Ride and Patron here. We build the curry then pass that to the lift_predicate for both instances of the check. Once that is done you can bind to get an actual result.

Run It

iex(101)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 1230,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(102)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(103)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
  id: 1422,
  ride: %FunPark.Ride{
    id: 1230,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(104)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
  id: 1486,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(105)> beth = FunPark.Patron.make("Beth", 13, 130)
%FunPark.Patron{
  id: 1550,
  name: "Beth",
  age: 13,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(106)> FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(107)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 1486,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 1422,
      ride: %FunPark.Ride{
        id: 1230,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(108)> FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 1486,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1422,
        ride: %FunPark.Ride{
          id: 1230,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}
iex(109)> beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
%FunPark.Patron{
  id: 1550,
  name: "Beth",
  age: 13,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 1422,
      ride: %FunPark.Ride{
        id: 1230,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(110)> FunPark.Ride.fast_pass_lane(beth, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}

This allowed us to not only check for VIP and FastPass but also ride_eligibility

Recovery

Okay so we forgot that a VIP doesn’t need a FastPass so we need an orelse that we can pass a check with a fallback function then a new _check_vip_or_fast_pass

# maybe
  def or_else(%Nothing{}, fallback_fun) when is_function(fallback_fun, 0),
    do: fallback_fun.()

  def or_else(%Just{} = just, _fallback_fun), do: just

# ride.ex
  def check_vip_or_fast_pass(patron, ride) do
    is_vip = &Patron.vip?/1

    patron
    |> Maybe.lift_predicate(is_vip)
    |> Maybe.or_else(fn -> check_fast_pass(patron, ride) end)
  end

  def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
    check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2)

    patron
    |> check_ride_eligibility(ride)
    |> bind(check_vip_or_pass.(ride))
  end

Now we are using the new check_vip_or_fast_pass

Run It

iex(111)> charles = FunPark.Patron.make("Charles", 15, 145)
%FunPark.Patron{
  id: 1614,
  name: "Charles",
  age: 15,
  height: 145,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(112)> FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(113)> charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
%FunPark.Patron{
  id: 1614,
  name: "Charles",
  age: 15,
  height: 145,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(114)> FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Patron{
    id: 1614,
    name: "Charles",
    age: 15,
    height: 145,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
}

Okay so we built a Patron checked availability, then added VIP and then checked again.

Refine Lists

Now we can move onto Lists.

Concat

Now we want to take all that and start to build lists. Let’s start with Maybe.concat

# maybe.ex
  def concat(list) when is_list(list) do
    list
    |> fold_l([], fn
      %Just{value: value}, acc -> [value | acc]
      %Nothing{}, acc -> acc
    end)
    |> :lists.reverse()
  end

# ride.ex
  def only_fast_pass_lane(patrons, %__MODULE__{} = ride)
      when is_list(patrons) do
    patrons
    |> Maybe.concat_map(&fast_pass_lane(&1, ride))
  end

We now have the ability to build a map of Patrons take satisfy the conditions. Its basic Enum.reduce style construct.

Run It

iex(115)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 1678,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(116)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(117)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
  id: 1870,
  ride: %FunPark.Ride{
    id: 1678,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(118)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
  id: 1934,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(119)> beth = FunPark.Patron.make("Beth", 13, 135)
%FunPark.Patron{
  id: 1998,
  name: "Beth",
  age: 13,
  height: 135,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(120)> charles = FunPark.Patron.make("Charles", 15, 145)
%FunPark.Patron{
  id: 2062,
  name: "Charles",
  age: 15,
  height: 145,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(121)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 1934,
  name: "Alice",
  age: 15,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 1870,
      ride: %FunPark.Ride{
        id: 1678,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(122)> beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
%FunPark.Patron{
  id: 1998,
  name: "Beth",
  age: 13,
  height: 135,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 1870,
      ride: %FunPark.Ride{
        id: 1678,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(123)> charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
%FunPark.Patron{
  id: 2062,
  name: "Charles",
  age: 15,
  height: 145,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(124)> patrons = [alice, beth, charles]
[
  %FunPark.Patron{
    id: 1934,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1870,
        ride: %FunPark.Ride{
          id: 1678,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 1998,
    name: "Beth",
    age: 13,
    height: 135,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1870,
        ride: %FunPark.Ride{
          id: 1678,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 2062,
    name: "Charles",
    age: 15,
    height: 145,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
]
iex(125)> FunPark.Ride.only_fast_pass_lane_concat(patrons, haunted_mansion)
[
  %FunPark.Patron{
    id: 1934,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1870,
        ride: %FunPark.Ride{
          id: 1678,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 2062,
    name: "Charles",
    age: 15,
    height: 145,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
]

This did exactly what we want but it also added in an extra Enum.map loop. We can do better.

Concat Map

# maybe.ex
  def concat_map(list, func) when is_list(list) and is_function(func, 1) do
    fold_l(list, [], fn item, acc ->
      case func.(item) do
        %Just{value: value} -> [value | acc]
        %Nothing{} -> acc
      end
    end)
    |> :lists.reverse()
  end

# ride
  def only_fast_pass_lane(patrons, %__MODULE__{} = ride)
      when is_list(patrons) do
    patrons
    |> Maybe.concat_map(&fast_pass_lane(&1, ride))
  end

This removes the extra Enum that will go through the values to check and puts it in the front.

Runt It

ex> patrons = [alice, beth, charles]
iex> FunPark.Ride.only_fast_pass_lane(patrons, haunted_mansion)
[
%FunPark.Patron{ name: "Alice", ... },
%FunPark.Patron{ name: "Charles", ... }
]

Sequence

Okay so now we want to work on returning a full list or nothing.

# maybe.ex  
  def sequence([]), do: pure([])

  def sequence([head | tail]) do
    bind(head, fn value ->
      bind(sequence(tail), fn rest ->
        pure([value | rest])
      end)
    end)
  end

# ride.ex
  def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
      when is_list(patrons) do
    patrons
    |> Enum.map(&fast_pass_lane(&1, ride))
    |> Maybe.sequence()
  end

This uses recursion to check for every Patron sent and then break-out if it fails.

Run It

iex(129)> patrons = [alice, beth, charles]
[
  %FunPark.Patron{
    id: 1934,
    name: "Alice",
    age: 15,
    height: 150,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1870,
        ride: %FunPark.Ride{
          id: 1678,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 1998,
    name: "Beth",
    age: 13,
    height: 135,
    ticket_tier: :basic,
    fast_passes: [
      %FunPark.FastPass{
        id: 1870,
        ride: %FunPark.Ride{
          id: 1678,
          name: "Haunted Mansion",
          min_age: 14,
          min_height: 0,
          wait_time: 0,
          online: true,
          tags: []
        },
        time: ~U[2025-06-01 13:00:00Z]
      }
    ],
    reward_points: 0,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 2062,
    name: "Charles",
    age: 15,
    height: 145,
    ticket_tier: :vip,
    fast_passes: [],
    reward_points: 0,
    likes: [],
    dislikes: []
  }
]
iex(130)> FunPark.Ride.group_fast_pass_lane(patrons, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(131)> FunPark.Ride.group_fast_pass_lane([alice, charles], haunted_mansion)
%FunPark.Monad.Maybe.Just{
  value: [
    %FunPark.Patron{
      id: 1934,
      name: "Alice",
      age: 15,
      height: 150,
      ticket_tier: :basic,
      fast_passes: [
        %FunPark.FastPass{
          id: 1870,
          ride: %FunPark.Ride{
            id: 1678,
            name: "Haunted Mansion",
            min_age: 14,
            min_height: 0,
            wait_time: 0,
            online: true,
            tags: []
          },
          time: ~U[2025-06-01 13:00:00Z]
        }
      ],
      reward_points: 0,
      likes: [],
      dislikes: []
    },
    %FunPark.Patron{
      id: 2062,
      name: "Charles",
      age: 15,
      height: 145,
      ticket_tier: :vip,
      fast_passes: [],
      reward_points: 0,
      likes: [],
      dislikes: []
    }
  ]
}

Okay so now we can build based off the group that is asking.

Not so Fast

The current implementation. Using Enum will traverse the entire list instead of stopping prematurely if needed. We can build a better version ourselves.

Traverse

# maybe.ex
  def traverse([], _func), do: pure([])

  def traverse(list, func) when is_list(list) and is_function(func, 1) do
    list
    |> Enum.reduce_while(pure([]), fn item, %Just{value: acc} ->
      case func.(item) do
        %Just{value: value} -> {:cont, pure([value | acc])}
        %Nothing{} -> {:halt, nothing()}
      end
    end)
    |> map(&:lists.reverse/1)
  end

# Ride
  def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
      when is_list(patrons) do
    Maybe.traverse(patrons, &fast_pass_lane(&1, ride))
  end

Okay so now we have a way to break out early.

Filter Within Composition

Let’s start to think about filtering and Filterable.

defprotocol FunPark.Filterable do
  def guard(structure, bool)
  def filter(structure, predicate)
  def filter_map(structure, func)
end

• guard/2 retains the value if the Boolean is true and discards it otherwise.
• filter/2 retains the value if the predicate passes.
• filter_map/2 applies a transformation that may also discard the value.

# nothing.ex
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Nothing do
  alias FunPark.Monad.Maybe.Nothing

  def guard(%Nothing{}, _boolean), do: %Nothing{}
  def filter(%Nothing{}, _predicate), do: %Nothing{}
  def filter_map(%Nothing{}, _func), do: %Nothing{}
end

# just.ex
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Just do
  alias FunPark.Monad.Maybe
  alias FunPark.Monad.Maybe.Just
  alias FunPark.Monad

  def guard(%Just{} = maybe, true), do: maybe
  def guard(%Just{}, false), do: Maybe.nothing()

  def filter(%Just{} = maybe, predicate) do
    Monad.bind(maybe, fn value ->
      if predicate.(value) do
        Maybe.pure(value)
      else
        Maybe.nothing()
      end
    end)
  end

  def filter_map(%Just{value: value}, func) do
    case func.(value) do
      %Just{} = just -> just
      _ -> Maybe.nothing()
    end
  end
end

• guard/2 retains the original Just if the condition is true; otherwise, it returns
Nothing.
• filter/2 applies a predicate to the contained value. If the predicate returns
true, it keeps the value in a Just; if not, it returns Nothing.
• filter_map/2 applies a transformation that returns a Maybe. If the result is a
Just, it’s returned; otherwise, it returns Nothing.

Guard

Okay so this is a way to make sure that we filter out bad values. In this case wait-times that are less than or equal to 0.

  def update_wait_time_maybe(%__MODULE__{} = ride, wait_time)
      when is_number(wait_time) do
    ride
    |> Maybe.lift_predicate(&online?/1)
    |> guard(wait_time >= 0)
    |> map(&update_wait_time(&1, wait_time))
  end

Now we have a way to deal with bad wait times. We check for if it’s online then check if it has a valid wait-time.

Run It

iex(132)> tea_cup = FunPark.Ride.make("Tea Cup")
%FunPark.Ride{
  id: 2126,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(133)> FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
%FunPark.Monad.Maybe.Just{
  value: %FunPark.Ride{
    id: 2126,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 10,
    online: true,
    tags: []
  }
}
iex(134)> FunPark.Ride.update_wait_time_maybe(tea_cup, -10)
%FunPark.Monad.Maybe.Nothing{}
iex(135)> tea_cup = FunPark.Ride.change(tea_cup, %{online: false})
%FunPark.Ride{
  id: 2126,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: false,
  tags: []
}
iex(136)> FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
%FunPark.Monad.Maybe.Nothing{}

This is that same logic in action.

Filter

  def add_fast_pass_maybe(%__MODULE__{} = patron, fast_pass) do
    ride = FastPass.get_ride(fast_pass)
    new_passes = List.union([fast_pass], get_fast_passes(patron))
    update_fast_pass = Utils.curry_r(&change/2)
    eligible = Utils.curry_r(&Ride.eligible?/2)

    patron
    |> Maybe.pure()
    |> filter(eligible.(ride))
    |> map(update_fast_pass.(%{fast_passes: new_passes}))
  end

FilterMap

Now we want to bind/2 and filter_map/2

  def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
    check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2)

    patron
    |> check_ride_eligibility(ride)
    |> bind(check_vip_or_pass.(ride))
  end

  # def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
  #   check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2).(ride)

  #   patron
  #   |> check_ride_eligibility(ride)
  #   |> filter_map(check_vip_or_pass)
  # end

What You’ve Learned

So with this chapter at a close we need to think about all that we have learned. The Maybe is a set of structs that need to be impl at every lever for each type of struct that will be passed. We will need to deal with the Nothing and the Just as these are different types. Once we have built those we can then start to work with the Identity that we can build off of. We then case start to deal with some defaults that will take values as structs and then give us a Just or a Nothing or even a fall-back.


Once this is all set we can then start to deal with the other Context that we have built Eq and Ord to start. Those will need to be defined within the Just and Nothing but once they are defined you can build off of them.


We then started to work with the Maybe version of all the functions that we have built so far. It requires more from you to start but in the end you will deal with more types of functions and you will have better defaults and ways of getting out of loops faster.


There is a lot to talk about here and the Act on it will help with some version of this.