Funx.Macros (funx v0.8.0)
View SourceProvides macros for automatically implementing Funx.Eq and Funx.Ord protocols
for structs based on field projections.
The Funx.Macros module generates protocol implementations at compile time,
eliminating boilerplate while providing flexible projection options for both
equality and ordering comparisons. The macros support simple field access,
nested structures, optional fields, and custom projections through a unified interface.
This module is useful for:
- Implementing
Funx.Eqprotocol for structs with projection-based equality - Implementing
Funx.Ordprotocol with various projection strategies - Handling optional fields with safe defaults via
or_else - Accessing nested structures through Lens and Prism optics
- Custom comparison logic via projection functions
Macros
eq_for/2- GenerateFunx.Eqprotocol implementation (basic)eq_for/3- GenerateFunx.Eqprotocol with options (e.g.,or_else,eq)ord_for/2- GenerateFunx.Ordprotocol implementation (basic)ord_for/3- GenerateFunx.Ordprotocol with options (e.g.,or_else)
Projection Types
Both eq_for and ord_for macros support multiple projection types, all normalized at compile time:
- Atom - Converted to
Prism.key(atom). Safe for nil values withNothing < Justsemantics. - Atom with or_else -
ord_for(Struct, :field, or_else: default)→{Prism.key(:field), default}. - Lens - Total access via
Lens.key/1orLens.path/1. RaisesKeyErroron missing keys. - Prism - Partial access via
Prism.key/1orPrism.path/1. ReturnsMaybewithNothing < Justsemantics. - Prism with or_else -
ord_for(Struct, Prism.key(:field), or_else: default)→{prism, default}. - {Prism, default} - Tuple syntax for partial access with explicit fallback value.
- Traversal - Multiple foci via
Traversal.combine/1. All foci must match for equality. - Function - Custom projection
fn x -> ... endor&fun/1. Must return a comparable value. - Eq DSL - A pre-built equality comparator from
eq do ... end. Used directly byeq_for. - Ord DSL - A pre-built ordering from
ord do ... end. Used directly byord_for.
Note: Atoms use Prism by default for safety. Use explicit
Lens.key(:field)when you need total access that raises on missing keys or nil intermediate values.
or_else Option
The or_else option provides fallback values for optional fields:
- Valid with: Atoms, Prisms, and helper functions returning Prisms
- Invalid with: Lens (always returns a value), Traversal (focuses on multiple elements),
functions (must handle own defaults), struct literals, or
{Prism, default}tuples (redundant)
When or_else is used with an incompatible projection type, a clear compile-time
error is raised with actionable guidance.
Examples
Simple equality by field:
iex> defmodule Person do
...> defstruct [:name, :age]
...>
...> require Funx.Macros
...> Funx.Macros.eq_for(Person, :age)
...> end
iex> alias Funx.Eq
iex> Eq.eq?(%Person{name: "Alice", age: 30}, %Person{name: "Bob", age: 30})
trueEquality with optional field:
iex> defmodule Item do
...> defstruct [:name, :score]
...>
...> require Funx.Macros
...> Funx.Macros.eq_for(Item, :score, or_else: 0)
...> end
iex> alias Funx.Eq
iex> i1 = %Item{name: "A", score: nil}
iex> i2 = %Item{name: "B", score: 0}
iex> Eq.eq?(i1, i2) # nil becomes 0, so equal
trueOrdering by field with Prism (safe for nil):
iex> defmodule Product do
...> defstruct [:name, :rating]
...>
...> require Funx.Macros
...> Funx.Macros.ord_for(Product, :rating)
...> end
iex> alias Funx.Ord
iex> p1 = %Product{name: "Widget", rating: 4}
iex> p2 = %Product{name: "Gadget", rating: 5}
iex> Ord.lt?(p1, p2)
trueOptional field with or_else:
iex> defmodule Item do
...> defstruct [:name, :score]
...>
...> require Funx.Macros
...> Funx.Macros.ord_for(Item, :score, or_else: 0)
...> end
iex> alias Funx.Ord
iex> i1 = %Item{name: "A", score: nil}
iex> i2 = %Item{name: "B", score: 10}
iex> Ord.lt?(i1, i2) # nil becomes 0, so 0 < 10
trueNested structure access with Lens:
iex> defmodule Address, do: defstruct [:city, :state]
iex> defmodule Customer do
...> defstruct [:name, :address]
...>
...> require Funx.Macros
...> alias Funx.Optics.Lens
...> Funx.Macros.ord_for(Customer, Lens.path([:address, :city]))
...> end
iex> alias Funx.Ord
iex> c1 = %Customer{name: "Alice", address: %Address{city: "Austin", state: "TX"}}
iex> c2 = %Customer{name: "Bob", address: %Address{city: "Boston", state: "MA"}}
iex> Ord.lt?(c1, c2) # "Austin" < "Boston"
trueFunction projection:
iex> defmodule Article do
...> defstruct [:title, :content]
...>
...> require Funx.Macros
...> Funx.Macros.ord_for(Article, &String.length(&1.title))
...> end
iex> alias Funx.Ord
iex> a1 = %Article{title: "Short", content: "..."}
iex> a2 = %Article{title: "Very Long Title", content: "..."}
iex> Ord.lt?(a1, a2) # length("Short") < length("Very Long Title")
trueProtocol Dispatch
The generated Ord implementations leverage the Funx.Ord protocol for projected values.
Any type implementing Ord can be used as a projection target:
defmodule Priority do
defstruct [:level]
end
defimpl Funx.Ord, for: Priority do
def lt?(a, b), do: a.level < b.level
def le?(a, b), do: a.level <= b.level
def gt?(a, b), do: a.level > b.level
def ge?(a, b), do: a.level >= b.level
end
defmodule Task do
defstruct [:title, :priority]
require Funx.Macros
Funx.Macros.ord_for(Task, :priority) # Uses Funx.Ord.Priority
endCompile-Time Behavior
All macros expand at compile time into direct protocol implementations with zero
runtime overhead. The ord_for macro normalizes all projection types into one of
four canonical forms that Funx.Ord.contramap/2 accepts:
Lens.t()- Bare Lens structPrism.t()- Bare Prism struct (usesMaybe.lift_ord){Prism.t(), or_else}- Prism with fallback value(a -> b)- Projection function
Example expansion:
Funx.Macros.ord_for(Product, :rating, or_else: 0)Compiles to:
defimpl Funx.Ord, for: Product do
defp __ord_map__ do
Funx.Ord.contramap({Prism.key(:rating), 0})
end
def lt?(a, b) when is_struct(a, Product) and is_struct(b, Product) do
__ord_map__().lt?.(a, b)
end
# ... other comparison functions
endError Handling
The macros provide clear compile-time errors for invalid configurations:
- Using
or_elsewith Lens (total access doesn't need fallback) - Using
or_elsewith functions (functions must handle own defaults) - Using
or_elsewith{Prism, default}tuple (redundant) - Using
or_elsewith struct literals (ambiguous semantics)
All error messages include actionable guidance and examples of correct usage.
Summary
Functions
Generates an implementation of the Funx.Eq protocol for the given struct,
using the specified projection as the basis for equality comparison.
Projection Types
The macro supports the same projection types as ord_for:
- Atom - Converted to
Prism.key(atom). Safe for nil values. - Atom with or_else -
eq_for(Struct, :field, or_else: default)→{Prism.key(:field), default}. - Lens - Total access via
Lens.key/1orLens.path/1. Raises on missing values. - Prism - Partial access via
Prism.key/1orPrism.path/1. - Prism with or_else -
eq_for(Struct, Prism.key(:field), or_else: default)→{prism, default}. - {Prism, default} - Partial access with fallback value.
- Traversal - Multiple foci via
Traversal.combine/1. All foci must match. - Function - Custom projection function
(struct -> value). - Eq DSL - A pre-built equality comparator from
eq do ... end. Used directly without contramap.
Options
:or_else- Fallback value for optional fields. Only valid with atoms and Prisms.:eq- Custom Eq module or map for comparison. Defaults toFunx.Eq.Protocol.
Examples
# Atom (backward compatible)
defmodule Person do
defstruct [:name, :age]
end
Funx.Macros.eq_for(Person, :age)
# Atom with or_else
Funx.Macros.eq_for(Person, :score, or_else: 0)
# Lens - total access
Funx.Macros.eq_for(Customer, Lens.path([:address, :city]))
# Prism - partial access
Funx.Macros.eq_for(Item, Prism.key(:rating))
# Traversal - multiple foci
Funx.Macros.eq_for(Person, Traversal.combine([Lens.key(:name), Lens.key(:age)]))
# Function projection
Funx.Macros.eq_for(Article, &String.length(&1.title))
# Custom Eq module
Funx.Macros.eq_for(Person, :name, eq: CaseInsensitiveEq)
# Eq DSL - complex equality with multiple fields
use Funx.Eq
Funx.Macros.eq_for(Person, eq do
on :name
on :age
end)
Generates an implementation of the Funx.Ord protocol for the given struct,
using the specified projection as the basis for ordering comparisons.
Projection Types
The macro supports multiple projection types:
- Atom - Converted to
Prism.key(atom). Safe for nil values (Nothing < Just). - Atom with or_else -
ord_for(Struct, :field, or_else: default)→{Prism.key(:field), default}. - Lens - Total access via
Lens.key/1orLens.path/1. Raises on missing values. - Prism - Partial access via
Prism.key/1orPrism.path/1. Nothing < Just semantics. - Prism with or_else -
ord_for(Struct, Prism.key(:field), or_else: default)→{prism, default}. - {Prism, default} - Partial access with fallback value for Nothing.
- Function - Custom projection function
(struct -> comparable). - Ord DSL - A pre-built ordering from
ord do ... end. Used directly without contramap.
Options
:or_else- Fallback value for optional fields. Only valid with atoms and Prisms.:ord- Custom Ord module or map for comparison. Defaults toFunx.Ord.Protocol.
Examples
# Atom - uses Prism.key (safe for nil)
defmodule Product do
defstruct [:name, :rating]
end
Funx.Macros.ord_for(Product, :rating)
# Atom with or_else - provides default for nil values
Funx.Macros.ord_for(Product, :rating, or_else: 0)
# Lens - total access (raises on nil)
defmodule Customer do
defstruct [:name, :address]
end
Funx.Macros.ord_for(Customer, Lens.path([:address, :city]))
# Prism - partial access
Funx.Macros.ord_for(Item, Prism.key(:score))
# Prism with or_else
Funx.Macros.ord_for(Item, Prism.key(:score), or_else: 0)
# Prism with default tuple (alternative to or_else)
Funx.Macros.ord_for(Task, {Prism.key(:priority), 0})
# Function projection
Funx.Macros.ord_for(Article, &String.length(&1.title))
# Ord DSL - complex ordering with multiple fields
use Funx.Ord
Funx.Macros.ord_for(Person, ord do
asc :name
desc :age
end)