Funx.Ord (funx v0.8.2)

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

append(a, b) deprecated

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.

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

Composes two Ord instances, combining their comparison logic.

concat(ord_list) deprecated

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.

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)

This function is deprecated. Use compose/2 instead.
@spec append(ord_t(), ord_t()) :: ord_t()

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

compose(ord_list)

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

Composes 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.compose(ord_list)
iex> combined.gt?.(%{age: 25, name: "Charlie"}, %{age: 25, name: "Bob"})
true

compose(a, b)

@spec compose(ord_t(), ord_t()) :: ord_t()

Composes 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.compose(ord1, ord2)
iex> combined.lt?.(%{age: 30, name: "Alice"}, %{age: 30, name: "Bob"})
true

concat(ord_list)

This function is deprecated. Use compose/1 instead.
@spec concat([ord_t()]) :: ord_t()

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)