Powered by AppSignal & Oban Pro

Advanced Functional Programming

JKBook/chap2.livemd

Advanced Functional Programming

Section

This notebook contains the code and exercises I type for the book https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir/

Define protocols

defprotocol Fun.Eq do
  @fallback_to_any True

  def eq?(a, b)

  def not_eq?(a, b)
end

defimpl Fun.Eq, for: Any do
  def eq?(a, b), do: a == b
  def not_eq?(a, b), do: a != b
end


defprotocol Fun.Ord do
    @fallback_to_any true
    
    def lt?(a, b)
    def le?(a, b)
    def gt?(a, b)
    def ge?(a, b)
end



defmodule Fun.Utils do
  alias Fun.Eq

  def contramap(f, eq \\ Eq) do
    eq = to_eq_map(eq)

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

  def to_eq_map(%{eq?: f1, not_eq?: f2} = input) when is_function(f1, 2) and is_function(f2, 2) do
    input
  end

  def to_eq_map(module) when is_atom(module) do
    %{
      eq?: &module.eq?/2,
      not_eq?: &module.not_eq?/2
    }
  end

  def eq(a, b, eq \\ Eq) do
    eq = to_eq_map(eq)
    eq.eq?.(a, b)
  end
end

defmodule Fun.Utils.Ord do
  alias Fun.Ord

  def contramap(f, ord \\ Ord) do
    ord_map = Fun.Utils.Ord.to_ord_map(ord)

    %{
      lt?: fn a, b -> ord_map.lt?.(f.(a), f.(b)) end,
      le?: fn a, b -> ord_map.le?.(f.(a), f.(b)) end,
      gt?: fn a, b -> ord_map.gt?.(f.(a), f.(b)) end,
      ge?: fn a, b -> ord_map.ge?.(f.(a), f.(b)) end
    }
  end

  def to_ord_map(%{lt: f1, le: f2, gt: f3, ge: f4} = input)
      when is_function(f1) and is_function(f2) and is_function(f3) and is_function(f4) do
    input
  end

  def to_ord_map(module) when is_atom(module) do
    %{
      lt?: &module.lt?/2,
      le?: &module.le?/2,
      gt?: &module.gt?/2,
      ge?: &module.gt?/2
    }
  end

  def compare(a, b, ord \\ Ord) do
    ord = Fun.Utils.Ord.to_ord_map(ord)

    cond do
      ord.lt?.(a, b) -> :lt
      ord.gt?.(a, b) -> :gt
      true -> :eq
    end
  end

  def comparator(ord_module) do
    fn a, b -> Fun.Utils.Ord.compare(a, b, ord_module) != :gt end
  end

  def to_eq(ord \\ Fun.Ord) do
    %{
      eq?: fn a, b -> Fun.Utils.Ord.compare(a, b, ord) == :eq end,
      not_eq?: fn a, b -> Fun.Utils.Ord.compare(a, b, ord) != :eq end
    }
  end

  def reverse(ord \\ Fun.Ord) do
    ord_map = Fun.Utils.Ord.contramap(ord)

    %{
      lt?: ord.gt?,
      le?: ord.ge?,
      gt?: ord.lt?,
      ge?: ord.le?
    }
  end
end

List Utility modules

defmodule Fun.List do
  # module for list tasks
  alias Fun.Utils

  def unique(list, eq \\ Fun.Eq) when is_list(list) do
    list
    |> Enum.reduce([], fn item, acc ->
      if Enum.any?(acc, &Fun.Utils.eq(item, &1, eq)), do: acc, else: [item | acc]
    end)
    |> :lists.reverse()
  end

  def union(l1, l2) when is_list(l2) and is_list(l2) do
    (l1 ++ l2) |> Fun.List.unique()
  end

  def intersection(l1, l2, eq \\ Fun.Eq) when is_list(l1) and is_list(l2) do
    l1
    |> Enum.filter(fn item ->
      Enum.any?(l2, &Fun.Utils.eq(item, &1, eq))
    end)
    |> Fun.List.unique(eq)
  end

  def difference(l1, l2, eq \\ Fun.Eq) when is_list(l1) and is_list(l2) do
    l1
    |> Enum.reject(fn item ->
      Enum.any?(l2, &Utils.eq(item, &1, eq))
    end)
    |> Fun.List.unique()
  end

  def simetric_difference(l1, l2, eq \\ Fun.Eq) when is_list(l1) and is_list(l2) do
    (Fun.List.difference(l1, l2, eq) ++ Fun.List.difference(l2, l1, eq)) |> Fun.List.unique()
  end

  def subset?(small, large, eq \\ Fun.Eq) when is_list(small) and is_list(large) do
    Enum.all?(small, fn item ->
      Enum.any?(large, &Utils.eq(item, &1, eq))
    end)
  end

  def superset(large, small, eq \\ Fun.Eq) when is_list(large) and is_list(small) do
    Fun.List.subset?(small, large, eq)
  end

  def sort(list, ord \\ Fun.Ord) do
    Enum.sort(list, Fun.Utils.Ord.comparator(ord))
  end

  def stric_sort(list, ord \\ Fun.Ord) do
    list |> Fun.List.unique(Fun.Utils.Ord.to_eq(ord)) |> Fun.List.sort(ord)
  end
end

Patron module

defmodule Fun.Patron do
  defstruct id: nil,
            name: nil,
            age: 0,
            height: 0,
            ticket_tier: :basic,
            fast_passes: [],
            reward_points: 0,
            likes: [],
            dislikes: []

  defp tier_priority(:vip), do: 3
  defp tier_priority(:premium), do: 2
  defp tier_priority(:basic), do: 1
  defp tier_priority(_), do: 0

  defp get_priority(%__MODULE__{ticket_tier: ticket}) do
    tier_priority(ticket)
  end
  def order_by_ticket_tier do 
   Fun.Utils.Ord.contramap(&get_priority/1) 
  end
  def make(name, age, height, opts \\ [])
      when is_bitstring(name) and is_integer(age) and is_integer(height) and age > 0 and
             height > 0 do
    %__MODULE__{
      id: :erlang.unique_integer([:positive]),
      name: name,
      age: age,
      height: height,
      ticket_tier: Keyword.get( opts , :ticket_tier , :basic),
      fast_passes: Keyword.get(opts , :fast_passes , []) , 
      reward_points: Keyword.get(opts , :reward_points , 0),
      likes: Keyword.get(opts , :fast_passes , []),
      dislikes: Keyword.get(opts , :fast_passes , []),
         
    }
  end

  def change(%__MODULE__{} = patron , changes) when is_map(changes) do
    changes = Map.delete(changes , :id)

    struct( patron, changes) 
  end
end


Ride module

defmodule Fun.Ride do
    defstruct id: nil,
            name: "Unknown Ride",
            min_age: 0,
            min_height: 0,
            wait_time: 0,
            online: true,
            tags: []

  def make(name, opts \\ [] ) when is_binary(name) do
    %__MODULE__{
      name: name, 
      min_age: Keyword.get(opts, :min_age , 0),
      min_height: Keyword.get(opts, :min_height , 0),
      wait_time: Keyword.get(opts, :wait_time , 0),
      online: Keyword.get(opts, :online , true),
      tags: Keyword.get(opts, :tags , []),

    }
  end
  
  def change(%__MODULE__{} = ride , changes) when is_map(changes) do
    changes = Map.delete(changes , :id)

    struct( ride, changes) 
  end

end

Pass module

defmodule Fun.Pass do
  alias Fun.Ride
  
  defstruct id: nil,
    ride: nil,
    time: nil

  def make( %Ride{} = ride, %DateTime{} = time) do 
    %__MODULE__{
      id: :erlang.unique_integer([:positive]),
      ride: ride,
      time: time
    }
  end
    
  def change(%__MODULE__{} = pass , changes) when is_map(changes) do
    changes = Map.delete(changes , :id)

    struct( pass, changes) 
  end

  def get_time( %__MODULE__{ time: time} ) do
    time
  end

  def eq_time( ) do 
    Fun.Utils.contramap(&get_time/1)
  end
end

Protocol implementations

defimpl Fun.Eq, for: Fun.Patron do
  alias Fun.Eq
  alias Fun.Patron
  def eq?(%Patron{id: id1}, %Patron{id: id2}), do: Eq.eq?(id1, id2)
  def not_eq?(%Patron{id: id1}, %Patron{id: id2}), do: Eq.not_eq?(id1, id2)
end

# Implement the equiality property for rides
defimpl Fun.Eq, for: Fun.Ride do
  alias Fun.Eq
  alias Fun.Ride

  def eq?(%Ride{id: id1}, %Ride{id: id2}), do: Eq.eq?(id1, id2)
  def not_eq?(%Ride{id: id1}, %Ride{id: id2}), do: Eq.not_eq?(id1, id2)
end

defimpl Fun.Eq, for: Fun.Pass do
  alias Fun.Eq
  alias Fun.Pass

  def eq?(%Pass{id: id1}, %Pass{id: id2}), do: Eq.eq?(id1, id2)
  def not_eq?(%Pass{id: id1}, %Pass{id: id2}), do: Eq.not_eq?(id1, id2)
end

defimpl Fun.Ord, for: Any do
  def lt?(a, b), do: a < b
  def le?(a, b), do: a <= b
  def gt?(a, b), do: a > b
  def ge?(a, b), do: a >= b
end

defimpl Fun.Ord, for: Fun.Ride do
  alias Fun.Ord
  alias Fun.Ride
  def lt?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.lt?(v1, v2)
  def le?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.le?(v1, v2)
  def gt?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.gt?(v1, v2)
  def ge?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.ge?(v1, v2)
end

defimpl Fun.Ord, for: DateTime do
  def lt?(a, b), do: DateTime.compare(a, b) == :lt
  def le?(a, b), do: match?(x when x in [:lt, :eq], DateTime.compare(a, b))
  def gt?(a, b), do: DateTime.compare(a, b) == :gt
  def ge?(a, b), do: match?(x when x in [:gt, :eq], DateTime.compare(a, b))
end

defimpl Fun.Ord, for: Fun.Pass do
  alias Fun.Pass

  def lt?(%Pass{time: t1}, %Pass{time: t2}), do: Fun.Ord.lt?(t1, t2)
  def le?(%Pass{time: t1}, %Pass{time: t2}), do: Fun.Ord.le?(t1, t2)
  def gt?(%Pass{time: t1}, %Pass{time: t2}), do: Fun.Ord.gt?(t1, t2)
  def ge?(%Pass{time: t1}, %Pass{time: t2}), do: Fun.Ord.ge?(t1, t2)
end
defprotocol Fun.Foldable do
  def fold_l(structures, transformation_fn, base)
  def fold_r(structures, transformation_fn, base)
end

defimpl Fun.Foldable, for: Fun.List do
  def fold_l(list, acc, fun), do: :lists.foldl(fun, acc, list)
  def fold_r(list, acc, fun), do: :lists.foldr(fun, acc, list)
end

defimpl Fun.Foldable, for: List do
  def fold_l(list, acc, fun), do: :lists.foldl(fun, acc, list)
  def fold_r(list, acc, fun), do: :lists.foldr(fun, acc, list)
end

Playing with Monoids

defprotocol Fun.Monoid do
  def empty(empty_monoid)
  def append(monoid_struct_a, monoid_struct_b)
  def wrap(monoid_struct, value)
  def unwrap(monoid_struct)
end

defmodule Fun.Monoid.Sum do
  defstruct value: 0
end

defimpl Fun.Monoid, for: Fun.Monoid.Sum do
  alias Fun.Monoid.Sum

  def empty(_), do: %Sum{}
  def append(%Sum{value: a}, %Sum{value: b}), do: %Sum{value: a + b}
  def wrap(%Sum{}, value) when is_number(value), do: %Sum{value: value}
  def unwrap(%Sum{value: v}) when is_number(v), do: v
end

defmodule Fun.Monoid.Utils do
  import Fun.Monoid, only: [empty: 1, append: 2, wrap: 2, unwrap: 1]
  import Fun.Foldable, only: [fold_l: 3]

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

  def m_concat(monoid, list) when is_struct(monoid) and is_list(list) do
    fold_l(list, empty(monoid), fn value, acc -> append(acc, wrap(monoid, value)) end) |> unwrap()
  end
end
defmodule Fun.Math do
  alias Fun.Monoid
  import Fun.Monoid.Utils
  def sum(a, b) do
      m_append( %Monoid.Sum{} , a ,b )
  end

  def sum( list) when is_list(list) do 
    m_concat( %Monoid.Sum{} , list)
  end
end
# Testing monoid sum
Fun.Math.sum(1,2)
Fun.Math.sum([])
sum_1 = Fun.Monoid.wrap(%Fun.Monoid.Sum{} , 10)

sum_2 = Fun.Monoid.wrap(%Fun.Monoid.Sum{} , 20)

res = Fun.Monoid.append(sum_1, sum_2)
Fun.Monoid.unwrap(res)
Fun.Monoid.Utils.m_append( %Fun.Monoid.Sum{} , 10 ,20 )

Utils

Testing Things

alice = Fun.Patron.make("alice"  , 15 , 100 )
alice_b = Fun.Patron.change(alice , %{ ticket_tier: :premium} )
alice == alice_b
mansion = Fun.Ride.make("Dark Mansion", min_age: 14, tags: [:dark])
tea = Fun.Ride.make("Tea", min_age: 14, tags: [:happy])
datetime = DateTime.new!( ~D[2025-01-01], ~T[13:00:00])
new_pass = Fun.Pass.make(mansion,datetime)
otro_pass = Fun.Pass.make(tea, datetime)

Fun.Pass.eq_time.eq?.( new_pass , otro_pass)
alice_a = Fun.Patron.make("Alice", 15, 50)
alice_b = Fun.Patron.change(  alice_a , %{ticket_tier: :premium} )

ll = [ alice_a , alice_b]

Fun.List.unique(ll)
alice = Fun.Patron.make("Alice", 15, 50, ticket_tier: :premium)
manfred = Fun.Patron.make("manfred", 15, 50, ticket_tier: :basic)
ticket_ord_f = Fun.Patron.order_by_ticket_tier()
ticket_ord_f.gt?.(manfred , alice )
manfred = Fun.Patron.change( manfred , %{ ticket_tier: :vip})
ticket_ord_f.gt?.(manfred, alice)
Fun.Utils.Ord.compare(10,3)
apple_cart = Fun.Ride.make("Apple Cart")
manfred_ride = Fun.Ride.make("Manfred Ride")
Fun.Utils.Ord.compare(apple_cart, manfred_ride)
l = [:banana , :pan, :arepa]

Fun.List.stric_sort(l)