defmodule Funx.Monad.Maybe do @moduledoc """ [![Run in Livebook](https://livebook.dev/badge/v1/black.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FJKWA%2Ffunx%2Fblob%2Fmain%2Flivebooks%2Fmonad%2Fmaybe%2Fmaybe.livemd) The `Funx.Monad.Maybe` module provides an implementation of the `Maybe` monad, a functional abstraction used to represent optional values in Elixir. A `Maybe` represents one of two possibilities: - `Just(value)`: the presence of a value - `Nothing`: the absence of a value This pattern is useful for eliminating `nil` checks and handling missing data explicitly and safely in functional pipelines. ### Constructors - `just/1`: Wraps a value in the `Just` variant. - `nothing/0`: Returns a `Nothing` value. - `pure/1`: Alias for `just/1`. ### Refinement - `just?/1`: Returns `true` if the value is a `Just`. - `nothing?/1`: Returns `true` if the value is a `Nothing`. ### Fallback and Extraction - `get_or_else/2`: Returns the value from a `Just`, or a default if `Nothing`. - `or_else/2`: Returns the original `Just`, or invokes a fallback function if `Nothing`. ### List Operations - `concat/1`: Removes all `Nothing` values and unwraps the `Just` values from a list. - `concat_map/2`: Applies a function and collects only `Just` results. - `sequence/1`: Converts a list of `Maybe` values into a single `Maybe` of list. - `traverse/2`: Applies a function to each element in a list and sequences the results. ### Lifting - `lift_predicate/2`: Converts a value to `Just` if it meets a predicate, otherwise `Nothing`. - `lift_identity/1`: Converts an `Identity` to a `Maybe`. - `lift_either/1`: Converts an `Either` to a `Maybe`. - `lift_eq/1`: Lifts an equality function for use in the `Maybe` context. - `lift_ord/1`: Lifts an ordering function for use in the `Maybe` context. ### Elixir Interoperability - `nil_iso/0`: Returns the isomorphism between `Maybe` and nil values. - `from_nil/1`: Converts `nil` to `Nothing`, otherwise wraps the value in `Just` (via `nil_iso`). - `to_nil/1`: Returns the underlying value or `nil` (via `nil_iso`). - `result_iso/0`: Returns the isomorphism between `Maybe` and result tuples. - `from_result/1`: Converts `{:ok, val}` or `{:error, _}` into a `Maybe` (via `result_iso`). - `to_result/1`: Converts a `Maybe` to a result tuple (via `result_iso`). - `from_try/1`: Runs a function and returns `Just` on success, or `Nothing` if an exception is raised. - `to_try!/2`: Unwraps a `Just`, or raises an error if `Nothing`. ## Protocols The `Just` and `Nothing` structs implement the following protocols, making the `Maybe` abstraction composable and extensible: - `Funx.Eq`: Enables equality comparisons between `Maybe` values. - `Funx.Foldable`: Implements `fold_l/3` and `fold_r/3` for reducing over the value or fallback. - `Funx.Filterable`: Supports conditional retention with `filter/2`, `guard/2`, and `filter_map/2`. - `Funx.Monad`: Provides `map/2`, `ap/2`, and `bind/2` for monadic composition. - `Funx.Ord`: Defines ordering behavior between `Just` and `Nothing` values. - `Funx.Tappable`: Executes side effects on `Just` values via `Funx.Tappable.tap/2`, leaving `Nothing` unchanged. Although these implementations are defined per constructor (`Just` and `Nothing`), the behavior is consistent across the `Maybe` abstraction. This module helps you represent optional data explicitly, structure conditional logic safely, and eliminate reliance on `nil` in functional pipelines. ## DSL Usage You can use the Maybe DSL for clean monadic composition: use Funx.Monad.Maybe maybe input do bind ParseInt bind PositiveNumber map Double end """ defmacro __using__(_opts) do quote do import Funx.Monad.Maybe import Funx.Monad.Maybe.Dsl end end import Funx.Monad, only: [map: 2] import Funx.Foldable, only: [fold_l: 3] alias Funx.Monad.{Either, Identity, Maybe} alias Funx.Optics.Iso alias Either.{Left, Right} alias Maybe.{Just, Nothing} @type t(value) :: Just.t(value) | Nothing.t() @doc """ Wraps a value in `Just`. ## Examples iex> Funx.Monad.Maybe.just(2) %Funx.Monad.Maybe.Just{value: 2} """ @spec just(any()) :: Just.t(any()) def just(value), do: Just.pure(value) @doc """ Returns a `Nothing` value. ## Examples iex> Funx.Monad.Maybe.nothing() %Funx.Monad.Maybe.Nothing{} """ @spec nothing() :: Nothing.t() def nothing, do: Nothing.pure() @doc """ Alias for `just/1`. ## Examples iex> Funx.Monad.Maybe.pure(5) %Funx.Monad.Maybe.Just{value: 5} """ @spec pure(any()) :: Just.t(any()) def pure(value), do: just(value) @doc """ Returns `true` if the `Maybe` is `Just`, otherwise `false`. ## Examples iex> Funx.Monad.Maybe.just?(Funx.Monad.Maybe.just(5)) true iex> Funx.Monad.Maybe.just?(Funx.Monad.Maybe.nothing()) false """ @spec just?(t(any())) :: boolean() def just?(%Just{}), do: true def just?(_), do: false @doc """ Returns `true` if the `Maybe` is `Nothing`, otherwise `false`. ## Examples iex> Funx.Monad.Maybe.nothing?(Funx.Monad.Maybe.nothing()) true iex> Funx.Monad.Maybe.nothing?(Funx.Monad.Maybe.just(5)) false """ @spec nothing?(t(any())) :: boolean() def nothing?(%Nothing{}), do: true def nothing?(_), do: false @doc """ Retrieves the value from a `Maybe`, returning `default` if `Nothing`. ## Examples iex> Funx.Monad.Maybe.get_or_else(Funx.Monad.Maybe.just(5), 0) 5 iex> Funx.Monad.Maybe.get_or_else(Funx.Monad.Maybe.nothing(), 0) 0 """ @spec get_or_else(t(value), value) :: value when value: var def get_or_else(maybe, default) do fold_l(maybe, fn value -> value end, fn -> default end) end @doc """ Returns the current `Just` value or invokes the `fallback_fun` if `Nothing`. ## Examples iex> Funx.Monad.Maybe.or_else(Funx.Monad.Maybe.nothing(), fn -> Funx.Monad.Maybe.just(42) end) %Funx.Monad.Maybe.Just{value: 42} iex> Funx.Monad.Maybe.or_else(Funx.Monad.Maybe.just(10), fn -> Funx.Monad.Maybe.just(42) end) %Funx.Monad.Maybe.Just{value: 10} """ @spec or_else(t(value), (-> t(value))) :: t(value) when value: var def or_else(%Nothing{}, fallback_fun) when is_function(fallback_fun, 0), do: fallback_fun.() def or_else(%Just{} = just, _fallback_fun), do: just @doc """ Lifts an equality function to compare `Maybe` values: - `Just` vs `Just`: Uses the custom equality function. - `Nothing` vs `Nothing`: Always `true`. - `Just` vs `Nothing` or vice versa: Always `false`. ## Examples iex> eq = Funx.Monad.Maybe.lift_eq(%{ ...> eq?: fn x, y -> x == y end, ...> not_eq?: fn x, y -> x != y end ...> }) iex> eq.eq?.(Funx.Monad.Maybe.just(5), Funx.Monad.Maybe.just(5)) true iex> eq.eq?.(Funx.Monad.Maybe.just(5), Funx.Monad.Maybe.just(10)) false iex> eq.eq?.(Funx.Monad.Maybe.nothing(), Funx.Monad.Maybe.nothing()) true iex> eq.eq?.(Funx.Monad.Maybe.just(5), Funx.Monad.Maybe.nothing()) false """ @spec lift_eq(Funx.Eq.eq_t()) :: Funx.Eq.eq_map() def lift_eq(custom_eq) do custom_eq = Funx.Eq.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 @doc """ Adapts an ordering function to compare `Maybe` values: - `Nothing` is considered less than any `Just`. - Two `Just` values are compared by the provided function. ## Examples iex> ord = Funx.Monad.Maybe.lift_ord(%{ ...> lt?: & le?: &<=/2, ...> gt?: &>/2, ...> ge?: &>=/2 ...> }) iex> ord.lt?.(Funx.Monad.Maybe.just(3), Funx.Monad.Maybe.just(5)) true iex> ord.lt?.(Funx.Monad.Maybe.nothing(), Funx.Monad.Maybe.just(5)) true """ @spec lift_ord(Funx.Ord.ord_t()) :: Funx.Ord.ord_map() def lift_ord(custom_ord) do custom_ord = Funx.Ord.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 @doc """ Removes `Nothing` values from a list of `Maybe` and returns a list of unwrapped `Just` values. ## Examples iex> Funx.Monad.Maybe.concat([Funx.Monad.Maybe.pure(1), Funx.Monad.Maybe.nothing(), Funx.Monad.Maybe.pure(2)]) [1, 2] iex> Funx.Monad.Maybe.concat([Funx.Monad.Maybe.nothing(), Funx.Monad.Maybe.nothing()]) [] iex> Funx.Monad.Maybe.concat([Funx.Monad.Maybe.pure("a"), Funx.Monad.Maybe.pure("b"), Funx.Monad.Maybe.pure("c")]) ["a", "b", "c"] """ @spec concat([t(output)]) :: [output] when output: any() 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 @doc """ Maps a function over a list, collecting unwrapped `Just` values and ignoring `Nothing` in a single pass. ## Examples iex> Funx.Monad.Maybe.concat_map([1, 2, 3, 4], fn x -> ...> if rem(x, 2) == 0, do: Funx.Monad.Maybe.pure(x), else: Funx.Monad.Maybe.nothing() ...> end) [2, 4] iex> Funx.Monad.Maybe.concat_map([1, nil, 3], fn ...> nil -> Funx.Monad.Maybe.nothing() ...> x -> Funx.Monad.Maybe.pure(x * 2) ...> end) [2, 6] iex> Funx.Monad.Maybe.concat_map([1, 2, 3], fn x -> Funx.Monad.Maybe.pure(x + 1) end) [2, 3, 4] iex> Funx.Monad.Maybe.concat_map([], fn x -> Funx.Monad.Maybe.pure(x) end) [] """ @spec concat_map([input], (input -> t(output))) :: [output] when input: any(), output: any() 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 @doc """ Converts a list of `Maybe` values into a `Maybe` containing a list. If any element is `Nothing`, the entire result is `Nothing`. ## Examples iex> Funx.Monad.Maybe.sequence([Funx.Monad.Maybe.just(1), Funx.Monad.Maybe.just(2)]) %Funx.Monad.Maybe.Just{value: [1, 2]} iex> Funx.Monad.Maybe.sequence([Funx.Monad.Maybe.just(1), Funx.Monad.Maybe.nothing()]) %Funx.Monad.Maybe.Nothing{} """ @spec sequence([t(value)]) :: t([value]) when value: any() def sequence(list) when is_list(list), do: traverse(list, fn x -> x end) # def sequence([]), do: pure([]) # def sequence([head | tail]) do # bind(head, fn value -> # bind(sequence(tail), fn rest -> # pure([value | rest]) # end) # end) # end @doc """ Applies a function to each element of a list, collecting results into a single `Maybe`. If any call returns `Nothing`, the operation halts and returns `Nothing`. ## Examples iex> Funx.Monad.Maybe.traverse([1, 2], fn x -> Funx.Monad.Maybe.just(x * 2) end) %Funx.Monad.Maybe.Just{value: [2, 4]} iex> Funx.Monad.Maybe.traverse([1, nil, 3], fn ...> nil -> Funx.Monad.Maybe.nothing() ...> x -> Funx.Monad.Maybe.just(x * 2) ...> end) %Funx.Monad.Maybe.Nothing{} """ @spec traverse([input], (input -> t(output))) :: t([output]) when input: any(), output: any() 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 @doc """ Converts an `Identity` value into a `Maybe`. If the value is `nil`, returns `Nothing`; otherwise `Just`. ## Examples iex> Funx.Monad.Maybe.lift_identity(Funx.Monad.Identity.pure(5)) %Funx.Monad.Maybe.Just{value: 5} iex> Funx.Monad.Maybe.lift_identity(Funx.Monad.Identity.pure(nil)) %Funx.Monad.Maybe.Nothing{} """ def lift_identity(%Identity{} = identity) do case identity do %Identity{value: nil} -> nothing() %Identity{value: value} -> just(value) end end @doc """ Converts an `Either` to a `Maybe`. `Right` becomes `Just`, and `Left` becomes `Nothing`. ## Examples iex> Funx.Monad.Maybe.lift_either(Funx.Monad.Either.right(5)) %Funx.Monad.Maybe.Just{value: 5} iex> Funx.Monad.Maybe.lift_either(Funx.Monad.Either.left("Error")) %Funx.Monad.Maybe.Nothing{} """ def lift_either(either) do case either do %Right{right: value} -> just(value) %Left{} -> nothing() end end @doc """ Lifts a value into `Maybe` based on a predicate. If `predicate.(value)` is `true`, returns `Just(value)`; otherwise `Nothing`. ## Examples iex> Funx.Monad.Maybe.lift_predicate(5, fn x -> x > 3 end) %Funx.Monad.Maybe.Just{value: 5} iex> Funx.Monad.Maybe.lift_predicate(2, fn x -> x > 3 end) %Funx.Monad.Maybe.Nothing{} """ @spec lift_predicate(term(), (term() -> boolean())) :: t(term()) 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 @doc """ Returns the isomorphism between Maybe and nil values. This iso witnesses that `Maybe` and `nil | value` are equivalent representations. ## Examples iex> iso = Funx.Monad.Maybe.nil_iso() iex> Funx.Optics.Iso.view(nil, iso) %Funx.Monad.Maybe.Nothing{} iex> Funx.Optics.Iso.view(42, iso) %Funx.Monad.Maybe.Just{value: 42} iex> Funx.Optics.Iso.review(%Funx.Monad.Maybe.Just{value: 42}, iso) 42 iex> Funx.Optics.Iso.review(%Funx.Monad.Maybe.Nothing{}, iso) nil """ @spec nil_iso() :: Iso.t() def nil_iso do Iso.make( fn nil -> %Nothing{} value -> %Just{value: value} end, fn %Just{value: value} -> value %Nothing{} -> nil end ) end @doc """ Returns the isomorphism between Maybe and Elixir result tuples. This iso witnesses that `Maybe` and `{:ok, _} | {:error, :nothing}` are equivalent representations. ## Examples iex> iso = Funx.Monad.Maybe.result_iso() iex> Funx.Optics.Iso.view({:ok, 42}, iso) %Funx.Monad.Maybe.Just{value: 42} iex> Funx.Optics.Iso.view({:error, :nothing}, iso) %Funx.Monad.Maybe.Nothing{} iex> Funx.Optics.Iso.review(%Funx.Monad.Maybe.Just{value: 42}, iso) {:ok, 42} iex> Funx.Optics.Iso.review(%Funx.Monad.Maybe.Nothing{}, iso) {:error, :nothing} """ @spec result_iso() :: Iso.t() def result_iso do Iso.make( fn {:ok, value} -> %Just{value: value} {:error, _} -> %Nothing{} end, fn %Just{value: value} -> {:ok, value} %Nothing{} -> {:error, :nothing} end ) end @doc """ Returns `true` if the given `Maybe` is a `Just`, or `false` if it is `Nothing`. This provides a simple way to treat a `Maybe` as a boolean condition, useful when filtering or making branching decisions based on presence. ## Examples iex> Funx.Monad.Maybe.to_predicate(Funx.Monad.Maybe.just(42)) true iex> Funx.Monad.Maybe.to_predicate(Funx.Monad.Maybe.nothing()) false Raises an error if the input is not a `Just` or `Nothing`. """ @spec to_predicate(t(any())) :: boolean() def to_predicate(maybe) when is_struct(maybe, Just) or is_struct(maybe, Nothing) do fold_l(maybe, fn _value -> true end, fn -> false end) end @doc """ Converts `nil` to `Nothing`; any other value becomes `Just`. Implemented via the `Maybe <-> nil` isomorphism. ## Examples iex> Funx.Monad.Maybe.from_nil(nil) %Funx.Monad.Maybe.Nothing{} iex> Funx.Monad.Maybe.from_nil(5) %Funx.Monad.Maybe.Just{value: 5} """ @spec from_nil(nil | value) :: t(value) when value: term() def from_nil(value), do: Iso.view(value, nil_iso()) @doc """ Converts a `Maybe` to its wrapped value or `nil`. Implemented via the `Maybe <-> nil` isomorphism. ## Examples iex> Funx.Monad.Maybe.to_nil(Funx.Monad.Maybe.just(5)) 5 iex> Funx.Monad.Maybe.to_nil(Funx.Monad.Maybe.nothing()) nil """ @spec to_nil(t(value)) :: nil | value when value: term() def to_nil(maybe), do: Iso.review(maybe, nil_iso()) @doc """ Executes a function within a `Maybe` context, returning `Nothing` if an exception occurs. ## Examples iex> Funx.Monad.Maybe.from_try(fn -> 5 end) %Funx.Monad.Maybe.Just{value: 5} iex> Funx.Monad.Maybe.from_try(fn -> raise "error" end) %Funx.Monad.Maybe.Nothing{} """ @spec from_try((-> right)) :: t(right) when right: term() def from_try(func) do try do result = func.() just(result) rescue _exception -> nothing() end end @doc """ Extracts a value from a `Maybe`, raising an exception if `Nothing`. The second parameter can be: - A string message (raises `RuntimeError`) - An exception module (e.g., `Enum.EmptyError`) - An exception struct (e.g., `%ArgumentError{message: "custom"}`) ## Examples iex> Funx.Monad.Maybe.to_try!(Funx.Monad.Maybe.just(5)) 5 iex> Funx.Monad.Maybe.to_try!(Funx.Monad.Maybe.nothing(), "No value found") ** (RuntimeError) No value found iex> Funx.Monad.Maybe.to_try!(Funx.Monad.Maybe.nothing(), Enum.EmptyError) ** (Enum.EmptyError) empty error iex> Funx.Monad.Maybe.to_try!(Funx.Monad.Maybe.nothing(), %ArgumentError{message: "missing value"}) ** (ArgumentError) missing value """ @spec to_try!(t(right), String.t() | atom() | Exception.t()) :: right | no_return when right: term() def to_try!(maybe, error_or_message \\ "Nothing value encountered") def to_try!(%Just{value: value}, _error_or_message), do: value def to_try!(%Nothing{}, exception) when is_atom(exception) do raise exception end def to_try!(%Nothing{}, %{__exception__: true} = exception) do raise exception end def to_try!(%Nothing{}, message) when is_binary(message) do raise message end def to_try!(maybe, _message) do raise ArgumentError, "First argument must be a Maybe (Just or Nothing), got: #{inspect(maybe)}" end @doc """ Converts a result tuple to a `Maybe`. `{:ok, value}` becomes `Just(value)`, while `{:error, _}` becomes `Nothing`. Implemented via the `Maybe <-> result tuple` isomorphism. ## Examples iex> Funx.Monad.Maybe.from_result({:ok, 5}) %Funx.Monad.Maybe.Just{value: 5} iex> Funx.Monad.Maybe.from_result({:error, :something}) %Funx.Monad.Maybe.Nothing{} """ @spec from_result({:ok, right} | {:error, term()}) :: t(right) when right: term() def from_result(result), do: Iso.view(result, result_iso()) @doc """ Converts a `Maybe` to a result tuple. `Just(value)` becomes `{:ok, value}`, while `Nothing` becomes `{:error, :nothing}`. Implemented via the `Maybe <-> result tuple` isomorphism. ## Examples iex> Funx.Monad.Maybe.to_result(Funx.Monad.Maybe.just(5)) {:ok, 5} iex> Funx.Monad.Maybe.to_result(Funx.Monad.Maybe.nothing()) {:error, :nothing} """ @spec to_result(t(right)) :: {:ok, right} | {:error, :nothing} when right: term() def to_result(maybe), do: Iso.review(maybe, result_iso()) end