defmodule BoundCond do @moduledoc """ `bound_cond/1` — a `cond` whose clauses can thread interim variables. Plain `cond` runs the first truthy clause's body, but each clause is isolated: a value computed while testing one condition can't be reused by the next. `with` *can* thread state, but it models a single happy path with fallbacks — not several branches of equal priority. `bound_cond/1` fills that gap. Clauses are tried top to bottom, exactly like `cond`. To introduce variables for the clauses that follow, add a `:bind ->` step: import BoundCond bound_cond do in_range?( x, y) -> n :bind -> last = get_last( x) pos = get_pos( y) pos > last -> last true -> pos end A `:bind ->` clause never "matches": its body runs only when execution falls through to it, and the variables it binds stay in scope for every clause below it — but, like `cond`, nothing bound inside a `bound_cond` leaks out of it (not even an unconditional `:bind`). If no clause matches, a `CondClauseError` is raised, just like `cond`. It expands to nested `if`/`else` wrapped in a `case` (used only to open a scope, so the bindings can't leak), so there is effectively no runtime overhead. """ @doc """ A `cond` whose clauses can share interim variables. Use one or more `:bind -> ...` steps to bind variables for the clauses that follow them. ## Examples iex> import BoundCond BoundCond iex> x = 7 7 iex> bound_cond do ...> x < 0 -> ...> :negative ...> ...> :bind -> ...> doubled = x * 2 ...> ...> doubled > 10 -> ...> { :big, doubled} ...> ...> true -> ...> { :small, doubled} ...> end {:big, 14} """ defmacro bound_cond( do: clauses) do body = clauses |> List.wrap() |> build() # The `case` is there to open a scope: keeps everything bound inside. quote do case nil do _ -> unquote( body) end end end # `:bind ->` — run the body for its bindings, then continue. The body is # spliced into the surrounding block (a bare `__block__` is not a new scope), # so anything it binds is visible to every clause built into `rest`. defp build([{ :->, _, [[ :bind], body]} | rest]) do quote do unquote( body) unquote( build( rest)) end end # `true ->` — the unconditional catch-all; clauses after it are unreachable. defp build([{ :->, _, [[ true], body]} | _rest]) do body end # `condition -> body` defp build([{ :->, _, [[ condition], body]} | rest]) do quote do if unquote( condition) do unquote( body) else unquote( build( rest)) end end end # No clause matched — mirror `cond`. defp build( []) do quote do raise CondClauseError end end # A clause head that isn't a single condition (or `:bind`). defp build([{ :->, _, [ head, _body]} | _rest]) do raise ArgumentError, "bound_cond: each clause needs exactly one condition or `:bind`, got: " <> Macro.to_string({ :->, [], [ head, :...]}) end end