Funx.Ord (funx v0.8.0)

View Source

Run in Livebook

Provides utilities and DSL for working with the Funx.Ord.Protocol.

This module combines:

  • Utility functions for ordering and comparison
  • A declarative DSL for building complex orderings

Utility Functions

These functions work with types that support Elixir's comparison operators or implement the Funx.Ord.Protocol:

DSL

The DSL provides a declarative syntax for building total orderings over complex data structures.

Use use Funx.Ord to import both utilities and DSL:

use Funx.Ord

ord do
  asc :name
  desc :age
end

The DSL compiles at compile time into efficient compositions using contramap, reverse, and concat, eliminating the need to manually compose ordering functions.

Directions

  • :asc - Ascending order (smallest to largest)
  • :desc - Descending order (largest to smallest)

Projection Types

  • Atom - Field access via Prism.key(atom). Safe for nil values.
  • Atom with or_else - Optional field with fallback value
  • Function - Direct projection fn x -> ... end or &fun/1
  • Lens - Explicit lens for nested access
  • Prism - Explicit prism for optional fields
  • Prism with or_else - Optional with fallback
  • Behaviour - Custom ordering via Funx.Ord.Dsl.Behaviour
  • Ord variable - Existing ord map to compose

See Funx.Ord.Dsl.Behaviour for implementing custom ordering logic.

Summary

Functions

Appends two Ord instances, combining their comparison logic.

Checks if value is between min and max, inclusive, with an optional custom Ord.

Clamps a value between min and max, with an optional custom Ord.

Creates a comparator function from the given Ord module, returning true if a is less than or equal to b according to the module's ordering.

Compares two values and returns :lt, :eq, or :gt, with an optional custom Ord.

Concatenates a list of Ord instances into a single composite comparator.

Transforms an ordering by applying a projection before comparison.

Returns the maximum of two values, with an optional custom Ord.

Returns the minimum of two values, with an optional custom Ord.

Creates an ordering from a block of projection specifications.

Reverses the ordering logic.

Converts an Ord instance into an equality comparator.

Converts an Ord DSL result or projection to an ord_map.

Types

ord_map()

@type ord_map() :: %{
  lt?: (any(), any() -> boolean()),
  le?: (any(), any() -> boolean()),
  gt?: (any(), any() -> boolean()),
  ge?: (any(), any() -> boolean())
}

ord_t()

@type ord_t() :: Funx.Ord.Protocol.t() | ord_map()

Functions

append(a, b)

@spec append(ord_t(), ord_t()) :: ord_t()

Appends two Ord instances, combining their comparison logic.

If the first Ord comparator determines an order, that result is used. If not, the second comparator is used as a fallback.

Examples

iex> ord1 = Funx.Ord.contramap(& &1.age, Funx.Ord.Protocol.Any)
iex> ord2 = Funx.Ord.contramap(& &1.name, Funx.Ord.Protocol.Any)
iex> combined = Funx.Ord.append(ord1, ord2)
iex> combined.lt?.(%{age: 30, name: "Alice"}, %{age: 30, name: "Bob"})
true

between(value, min, max, ord \\ Funx.Ord.Protocol)

@spec between(a, a, a, ord_t()) :: boolean() when a: any()

Checks if value is between min and max, inclusive, with an optional custom Ord.

Examples

iex> Funx.Ord.between(5, 1, 10)
true

iex> Funx.Ord.between(0, 1, 10)
false

iex> Funx.Ord.between(11, 1, 10)
false

clamp(value, min, max, ord \\ Funx.Ord.Protocol)

@spec clamp(a, a, a, ord_t()) :: a when a: any()

Clamps a value between min and max, with an optional custom Ord.

Examples

iex> Funx.Ord.clamp(5, 1, 10)
5

iex> Funx.Ord.clamp(0, 1, 10)
1

iex> Funx.Ord.clamp(15, 1, 10)
10

comparator(ord_module)

@spec comparator(ord_t()) :: (any(), any() -> boolean())

Creates a comparator function from the given Ord module, returning true if a is less than or equal to b according to the module's ordering.

Useful for sorting with Enum.sort/2 or similar functions.

Examples

iex> comparator = Funx.Ord.comparator(Funx.Ord.Protocol.Any)
iex> Enum.sort([3, 1, 2], comparator)
[1, 2, 3]

compare(a, b, ord \\ Funx.Ord.Protocol)

@spec compare(a, a, ord_t()) :: :lt | :eq | :gt when a: any()

Compares two values and returns :lt, :eq, or :gt, with an optional custom Ord.

Examples

iex> Funx.Ord.compare(3, 5)
:lt

iex> Funx.Ord.compare(7, 7)
:eq

iex> Funx.Ord.compare(9, 4)
:gt

concat(ord_list)

@spec concat([ord_t()]) :: ord_t()

Concatenates a list of Ord instances into a single composite comparator.

This function reduces a list of Ord comparators into a single Ord, applying them in sequence until an order is determined.

Examples

iex> ord_list = [
...>   Funx.Ord.contramap(& &1.age, Funx.Ord.Protocol.Any),
...>   Funx.Ord.contramap(& &1.name, Funx.Ord.Protocol.Any)
...> ]
iex> combined = Funx.Ord.concat(ord_list)
iex> combined.gt?.(%{age: 25, name: "Charlie"}, %{age: 25, name: "Bob"})
true

contramap(projection, ord \\ Funx.Ord.Protocol)

@spec contramap(
  (a -> b)
  | Funx.Optics.Lens.t()
  | Funx.Optics.Prism.t()
  | {Funx.Optics.Prism.t(), b},
  ord_t()
) :: ord_map()
when a: any(), b: any()

Transforms an ordering by applying a projection before comparison.

Canonical Normalization Layer

This function defines the single normalization point for all projections in the Ord DSL. Every projection type resolves to one of these four forms:

  • Lens.t() - Uses view!/2 to extract the focused value (raises on missing)
  • Prism.t() - Uses preview/2, returns Maybe, with Nothing < Just(_) ordering
  • {Prism.t(), or_else} - Uses preview/2, falling back to or_else on Nothing
  • (a -> b) - Projection function applied directly

All DSL syntax sugar (atoms, helpers, etc.) normalizes to these types in the parser. This function is the only place that converts optics to executable functions.

The ord parameter may be an Ord module or a custom comparator map with :lt?, :le?, :gt?, and :ge? functions. The projection is applied to both inputs before invoking the underlying comparator.

Examples

Using a projection function:

iex> ord = Funx.Ord.contramap(&String.length/1)
iex> ord.lt?.("cat", "zebra")
true
iex> ord.gt?.("zebra", "cat")
true

Using a lens for single key access:

iex> ord = Funx.Ord.contramap(Funx.Optics.Lens.key(:age))
iex> ord.gt?.(%{age: 40}, %{age: 30})
true
iex> ord.lt?.(%{age: 30}, %{age: 40})
true

Using a bare prism (Nothing < Just):

iex> prism = Funx.Optics.Prism.key(:score)
iex> ord = Funx.Ord.contramap(prism)
iex> ord.lt?.(%{}, %{score: 20})
true
iex> ord.gt?.(%{score: 30}, %{})
true

Using a prism with an or_else value:

iex> prism = Funx.Optics.Prism.key(:score)
iex> ord = Funx.Ord.contramap({prism, 0})
iex> ord.lt?.(%{score: 10}, %{score: 20})
true
iex> ord.lt?.(%{}, %{score: 20})
true
iex> ord.gt?.(%{score: 30}, %{})
true

max(a, b, ord \\ Funx.Ord.Protocol)

@spec max(a, a, ord_t()) :: a when a: any()

Returns the maximum of two values, with an optional custom Ord.

Examples

iex> Funx.Ord.max(3, 5)
5

iex> ord = Funx.Ord.contramap(&String.length/1, Funx.Ord.Protocol.Any)
iex> Funx.Ord.max("cat", "zebra", ord)
"zebra"

min(a, b, ord \\ Funx.Ord.Protocol)

@spec min(a, a, ord_t()) :: a when a: any()

Returns the minimum of two values, with an optional custom Ord.

Examples

iex> Funx.Ord.min(10, 7)
7

iex> ord = Funx.Ord.contramap(&String.length/1, Funx.Ord.Protocol.Any)
iex> Funx.Ord.min("apple", "kiwi", ord)
"kiwi"

ord(list)

(macro)

Creates an ordering from a block of projection specifications.

Returns a %Funx.Monoid.Ord{} struct that can be used with Funx.Ord functions like compare/3, max/3, min/3, or comparator/1.

Examples

ord do
  asc :name
  desc :age
end

ord do
  asc :score, or_else: 0
  desc &String.length(&1.bio)
end

# With nested field paths
ord do
  asc [:user, :profile, :created_at]
  desc [:user, :stats, :score]
end

reverse(ord \\ Funx.Ord.Protocol)

@spec reverse(ord_t()) :: ord_map()

Reverses the ordering logic.

Examples

iex> ord = Funx.Ord.reverse(Funx.Ord.Protocol.Any)
iex> ord.lt?.(10, 5)
true

to_eq(ord \\ Funx.Ord.Protocol)

@spec to_eq(ord_t()) :: Funx.Eq.eq_map()

Converts an Ord instance into an equality comparator.

This function creates a map containing two functions:

  • eq?/2: Returns true if a and b are considered equal by the given Ord.
  • not_eq?/2: Returns true if a and b are not considered equal by the given Ord.

Examples

iex> eq = Funx.Ord.to_eq(Funx.Ord.Protocol.Any)
iex> eq.eq?.(5, 5)
true

to_ord_map(ord_map)

to_ord_map_or_contramap(map, ord)

@spec to_ord_map_or_contramap(any(), ord_t()) :: ord_map()

Converts an Ord DSL result or projection to an ord_map.

If passed a plain map with lt?/2, le?/2, gt?/2, and ge?/2 functions (the result of ord do ... end), returns it directly. Otherwise, delegates to contramap/2.

Used internally by Funx.Macros.ord_for/3 to support both projection-based and DSL-based ordering definitions.