Funx.Eq (funx v0.8.0)

View Source

Run in Livebook

Utilities and DSL for working with the Funx.Eq.Protocol.

This module provides two main capabilities:

  1. Utility functions for working with equality comparisons:

  2. Declarative DSL for building complex equality comparators:

    • eq do ... end - Build comparators with clean syntax
    • Supports on, diff_on, any, and all directives
    • Compiles at compile-time for efficiency

These functions assume that types passed in either support Elixir's equality operator or implement the Funx.Eq.Protocol protocol.

DSL Usage

use Funx.Eq

eq do
  on :name
  on :age
end

Utility Usage

Funx.Eq.contramap(&(&1.age))
Funx.Eq.eq?(value1, value2)

For detailed DSL documentation, see the eq/1 macro below.

Summary

Functions

Combines two equality comparators using the Eq.All monoid.

Combines two equality comparators using the Eq.Any monoid.

Concatenates a list of equality comparators using the Eq.All monoid.

Concatenates a list of equality comparators using the Eq.Any monoid.

Transforms an equality check by applying a projection before comparison.

Creates an equality comparator from a block of projection specifications.

Returns true if two values are equal, using a specified or default Eq.

Checks equality of two values by applying a projection before comparison.

Returns false if two values are not equal, using a specified or default Eq.

Converts an Eq DSL result or projection to an eq_map.

Converts an Eq comparator into a single-argument predicate function for use in Enum functions.

Types

eq_map()

@type eq_map() :: %{
  eq?: (any(), any() -> boolean()),
  not_eq?: (any(), any() -> boolean())
}

eq_t()

@type eq_t() :: Funx.Eq.Protocol.t() | eq_map()

Functions

append_all(a, b)

@spec append_all(eq_t(), eq_t()) :: eq_t()

Combines two equality comparators using the Eq.All monoid.

This function merges two equality comparisons, requiring both to return true for the final result to be considered equal. This enforces a strict equality rule, where all comparators must agree.

Examples

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.append_all(eq1, eq2)
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
true
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
false

append_any(a, b)

@spec append_any(eq_t(), eq_t()) :: eq_t()

Combines two equality comparators using the Eq.Any monoid.

This function merges two equality comparisons, where at least one must return true for the final result to be considered equal.

Examples

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.append_any(eq1, eq2)
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
true
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
false

concat_all(eq_list)

@spec concat_all([eq_t()]) :: eq_t()

Concatenates a list of equality comparators using the Eq.All monoid.

The resulting comparator requires all comparators in the list to agree that two values are equal.

Examples

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.concat_all([eq1, eq2])
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
true
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
false

concat_any(eq_list)

@spec concat_any([eq_t()]) :: eq_t()

Concatenates a list of equality comparators using the Eq.Any monoid.

The resulting comparator allows any comparator in the list to determine equality, making it more permissive.

Examples

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.concat_any([eq1, eq2])
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
true
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
false

contramap(projection, eq \\ Funx.Eq.Protocol)

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

Transforms an equality check by applying a projection before comparison.

The projection must be one of:

  • a function (a -> b) - Applied directly to extract the comparison value
  • a Lens - Uses view!/2 to extract the focused value (raises on missing)
  • a Prism - Uses preview/2 (Nothing == Nothing)
  • a tuple {Prism, default} - Uses preview/2, falling back to default on Nothing
  • a Traversal - Uses to_list_maybe/2, compares all foci element-by-element (both must have all foci)

The eq parameter may be an Eq module or a custom comparator map with :eq? and :not_eq? functions. The projection is applied to both inputs before invoking the underlying comparator.

Examples

Using a projection function:

iex> eq = Funx.Eq.contramap(& &1.age)
iex> eq.eq?.(%{age: 30}, %{age: 30})
true
iex> eq.eq?.(%{age: 30}, %{age: 25})
false

Using a lens for single key access:

iex> eq = Funx.Eq.contramap(Funx.Optics.Lens.key(:age))
iex> eq.eq?.(%{age: 40}, %{age: 40})
true

Using a prism with a default value:

iex> prism = Funx.Optics.Prism.key(:score)
iex> eq = Funx.Eq.contramap({prism, 0})
iex> eq.eq?.(%{score: 10}, %{score: 10})
true
iex> eq.eq?.(%{}, %{score: 0})
true

eq(list)

(macro)

Creates an equality comparator from a block of projection specifications.

Returns a %Funx.Monoid.Eq.All{} struct that can be used with Funx.Eq functions like eq?/3, not_eq?/3, or to_predicate/2.

Directives

  • on - Field/projection must be equal
  • diff_on - Field/projection must be different
  • any - At least one nested check must pass (OR logic)
  • all - All nested checks must pass (AND logic, implicit at top level)

Projection Types

The DSL supports the same projection forms as Ord DSL:

  • Atom - Field access via Prism.key(atom)
  • Atom with or_else - Optional field via {Prism.key(atom), or_else}
  • Function - Direct projection fn x -> ... end or &fun/1
  • Lens - Explicit lens for nested access (raises on missing)
  • Prism - Explicit prism for optional fields
  • Prism with or_else - {Prism.t(), or_else} for optional with fallback
  • Behaviour - Custom equality via Funx.Eq.Dsl.Behaviour.eq/1

Equivalence Relations and diff_on

Core Eq (using only on, all, any) forms an equivalence relation with three properties:

  • Reflexive: eq?(a, a) is always true
  • Symmetric: If eq?(a, b) then eq?(b, a)
  • Transitive: If eq?(a, b) and eq?(b, c) then eq?(a, c)

These properties guarantee that Core Eq partitions values into equivalence classes, making it safe for use with Enum.uniq/2, MapSet, and grouping operations.

Extended Eq (using diff_on) expresses boolean equality predicates and does not guarantee transitivity.

Important: If you need equivalence classes (grouping, uniq, set membership), do not use diff_on.

Examples

Basic multi-field equality:

use Funx.Eq

eq_person = eq do
  on :name
  on :age
end

Using diff_on to check difference:

eq_same_person = eq do
  on :name
  on :email
  diff_on :id
end

Nested any blocks (OR logic):

eq_contact = eq do
  any do
    on :email
    on :username
  end
end

Mixed composition:

eq_mixed = eq do
  on :department
  any do
    on :email
    on :username
  end
end

With nested field paths:

eq_nested = eq do
  on [:user, :profile, :name]
  on [:user, :profile, :age]
end

eq?(a, b, eq \\ Funx.Eq.Protocol)

@spec eq?(a, a, eq_t()) :: boolean() when a: any()

Returns true if two values are equal, using a specified or default Eq.

This function compares the values directly, without applying any projection. For comparisons that require projecting or focusing on part of a structure, use Funx.Eq.eq_by?/4 or Funx.Eq.contramap/2.

Examples

iex> Funx.Eq.eq?(42, 42)
true
iex> Funx.Eq.eq?("foo", "bar")
false

eq_by?(projection, a, b, eq \\ Funx.Eq.Protocol)

@spec eq_by?(
  (a -> b) | Funx.Optics.Lens.t() | {Funx.Optics.Prism.t(), b},
  a,
  a,
  eq_t()
) :: boolean()
when a: any(), b: any()

Checks equality of two values by applying a projection before comparison.

The projection must be one of:

  • a function (a -> b) - Applied directly to extract the comparison value
  • a Lens - Uses view!/2 to extract the focused value (raises on missing)
  • a tuple {Prism, default} - Uses preview/2, falling back to default on Nothing

The eq parameter may be an Eq module or a custom comparator map. The projection is applied to both arguments before invoking the comparator.

Examples

Using a projection function:

iex> Funx.Eq.eq_by?(& &1.age, %{age: 30}, %{age: 30})
true
iex> Funx.Eq.eq_by?(& &1.age, %{age: 30}, %{age: 25})
false

Using a lens for single key access:

iex> Funx.Eq.eq_by?(Funx.Optics.Lens.key(:age), %{age: 40}, %{age: 40})
true

Using a prism with a default value:

iex> prism = Funx.Optics.Prism.key(:score)
iex> Funx.Eq.eq_by?({prism, 0}, %{score: 10}, %{score: 10})
true
iex> Funx.Eq.eq_by?({prism, 0}, %{}, %{score: 0})
true

not_eq?(a, b, eq \\ Funx.Eq.Protocol)

@spec not_eq?(a, a, eq_t()) :: boolean() when a: any()

Returns false if two values are not equal, using a specified or default Eq.

This function compares the values directly, without applying any projection. For comparisons based on a projection, lens, key, or path, use Funx.Eq.eq_by?/4 or a comparator produced by Funx.Eq.contramap/2.

Examples

iex> Funx.Eq.not_eq?(42, 99)
true
iex> Funx.Eq.not_eq?("foo", "foo")
false

to_eq_map(eq_map)

to_eq_map_or_contramap(map, eq)

@spec to_eq_map_or_contramap(any(), eq_t()) :: eq_map()

Converts an Eq DSL result or projection to an eq_map.

If passed a plain map with eq?/2 and not_eq?/2 functions (the result of eq do ... end), returns it directly. Otherwise, delegates to contramap/2.

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

to_predicate(target, eq \\ Funx.Eq.Protocol)

@spec to_predicate(a, eq_t()) :: (a -> boolean()) when a: any()

Converts an Eq comparator into a single-argument predicate function for use in Enum functions.

The resulting predicate takes a single element and returns true if it matches the target based on the specified Eq. If no custom Eq is provided, it defaults to Funx.Eq.Protocol.

Examples

iex> eq = Funx.Eq.contramap(& &1.name)
iex> predicate = Funx.Eq.to_predicate(%{name: "Alice"}, eq)
iex> Funx.Filterable.filter([%{name: "Alice"}, %{name: "Bob"}], predicate)
[%{name: "Alice"}]