Funx.Eq (funx v0.8.2)

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

append_all(a, b) deprecated
append_any(a, b) deprecated

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

Composes two equality comparators using the Eq.All monoid.

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

Composes two equality comparators using the Eq.Any monoid.

concat_all(eq_list) deprecated
concat_any(eq_list) deprecated

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 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)

This function is deprecated. Use compose_all/2 instead.
@spec append_all(eq_t(), eq_t()) :: eq_t()

append_any(a, b)

This function is deprecated. Use compose_any/2 instead.
@spec append_any(eq_t(), eq_t()) :: eq_t()

compose_all(eq_list)

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

Composes 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.compose_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

compose_all(a, b)

@spec compose_all(eq_t(), eq_t()) :: eq_t()

Composes 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.compose_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

compose_any(eq_list)

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

Composes 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.compose_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

compose_any(a, b)

@spec compose_any(eq_t(), eq_t()) :: eq_t()

Composes 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.compose_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)

This function is deprecated. Use compose_all/1 instead.
@spec concat_all([eq_t()]) :: eq_t()

concat_any(eq_list)

This function is deprecated. Use compose_any/1 instead.
@spec concat_any([eq_t()]) :: eq_t()

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)

Bare Eq Maps

Eq maps can be composed directly without the on directive:

  • Variable - Eq map stored in a variable
  • Helper call - 0-arity function returning an Eq map (e.g., EqHelpers.by_name())
  • Behaviour module - Module implementing Funx.Eq.Dsl.Behaviour
  • Behaviour with options - Tuple syntax {Module, opts}

Projection Types (with on directive)

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

Bare eq maps (without on directive):

# Using a helper function
eq_helper = eq do
  EqHelpers.name_case_insensitive()
end

# Using a behaviour module
eq_behaviour = eq do
  UserById
end

# Behaviour with options
eq_opts = eq do
  {UserByName, case_sensitive: false}
end

# Mixing bare eq with projections
eq_mixed = eq do
  UserById
  on :department
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_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"}]