Current section
Files
Jump to
Current section
Files
lib/eq/utils.ex
defmodule Funx.Eq.Utils do
@moduledoc """
Utility functions for working with the `Funx.Eq` protocol.
These functions assume that types passed in either support Elixir's equality operator
or implement the `Funx.Eq` protocol.
"""
@type eq_map() :: %{
eq?: (any(), any() -> boolean()),
not_eq?: (any(), any() -> boolean())
}
@type eq_t() :: Funx.Eq.t() | eq_map()
import Funx.Monoid.Utils, only: [m_append: 3, m_concat: 2]
alias Funx.Eq
alias Funx.Monoid
@doc """
Transforms an equality check by applying a function `f` to values before comparison.
The `eq` parameter can be an `Eq` module or a custom comparator map with an `:eq?` function.
If an `Eq` module is provided, it wraps the module’s function to apply `f` to each value before invoking the equality check.
If a custom comparator map is provided, it wraps the function in the map to apply `f` to each value.
## Examples
iex> eq = Funx.Eq.Utils.contramap(& &1.age)
iex> eq.eq?.(%{age: 30}, %{age: 30})
true
iex> eq.eq?.(%{age: 30}, %{age: 25})
false
"""
@spec contramap((a -> b), eq_t()) :: eq_map()
when a: any, b: any
def contramap(f, eq \\ Eq) do
eq = to_eq_map(eq)
%{
eq?: fn a, b -> eq.eq?.(f.(a), f.(b)) end,
not_eq?: fn a, b -> eq.not_eq?.(f.(a), f.(b)) end
}
end
@doc """
Checks equality of values by applying a projection function, using a specified or default `Eq`.
The `eq` parameter can be an `Eq` module or a custom comparator map with an `:eq?` function.
## Examples
iex> Funx.Eq.Utils.eq_by?(& &1.age, %{age: 30}, %{age: 30})
true
iex> Funx.Eq.Utils.eq_by?(& &1.age, %{age: 30}, %{age: 25})
false
"""
@spec eq_by?((a -> b), a, a, eq_t()) :: boolean()
when a: any, b: any
def eq_by?(f, a, b, eq \\ Eq) do
eq = to_eq_map(eq)
eq.eq?.(f.(a), f.(b))
end
@doc """
Returns true if two values are equal, using a specified or default `Eq`.
## Examples
iex> Funx.Eq.Utils.eq?(42, 42)
true
iex> Funx.Eq.Utils.eq?("foo", "bar")
false
"""
@spec eq?(a, a, eq_t()) :: boolean()
when a: any
def eq?(a, b, eq \\ Eq) do
eq = to_eq_map(eq)
eq.eq?.(a, b)
end
@doc """
Returns false if two values are not equal, using a specified or default `Eq`.
## Examples
iex> Funx.Eq.Utils.not_eq?(42, 99)
true
iex> Funx.Eq.Utils.not_eq?("foo", "foo")
false
"""
@spec not_eq?(a, a, eq_t()) :: boolean()
when a: any
def not_eq?(a, b, eq \\ Eq) do
eq = to_eq_map(eq)
eq.not_eq?.(a, b)
end
@doc """
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.Utils.contramap(& &1.name)
iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
iex> combined = Funx.Eq.Utils.append_all(eq1, eq2)
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
true
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
false
"""
@spec append_all(Monoid.Eq.All.t(), Monoid.Eq.All.t()) :: Monoid.Eq.All.t()
def append_all(a, b) do
m_append(%Monoid.Eq.All{}, a, b)
end
@doc """
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.Utils.contramap(& &1.name)
iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
iex> combined = Funx.Eq.Utils.append_any(eq1, eq2)
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
true
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
false
"""
@spec append_any(Monoid.Eq.Any.t(), Monoid.Eq.Any.t()) :: Monoid.Eq.Any.t()
def append_any(a, b) do
m_append(%Monoid.Eq.Any{}, a, b)
end
@doc """
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.Utils.contramap(& &1.name)
iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
iex> combined = Funx.Eq.Utils.concat_all([eq1, eq2])
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
true
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
false
"""
@spec concat_all([Monoid.Eq.All.t()]) :: Monoid.Eq.All.t()
def concat_all(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.All{}, eq_list)
end
@doc """
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.Utils.contramap(& &1.name)
iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
iex> combined = Funx.Eq.Utils.concat_any([eq1, eq2])
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
true
iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
false
"""
@spec concat_any([Monoid.Eq.Any.t()]) :: Monoid.Eq.Any.t()
def concat_any(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.Any{}, eq_list)
end
@doc """
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`.
## Examples
iex> eq = Funx.Eq.Utils.contramap(& &1.name)
iex> predicate = Funx.Eq.Utils.to_predicate(%{name: "Alice"}, eq)
iex> Funx.Filterable.filter([%{name: "Alice"}, %{name: "Bob"}], predicate)
[%{name: "Alice"}]
"""
@spec to_predicate(a, eq_t()) :: (a -> boolean())
when a: any
def to_predicate(target, eq \\ Eq) do
eq = to_eq_map(eq)
fn elem -> eq.eq?.(elem, target) end
end
def to_eq_map(%{eq?: eq_fun, not_eq?: not_eq_fun} = eq_map)
when is_function(eq_fun, 2) and is_function(not_eq_fun, 2) do
eq_map
end
def to_eq_map(module) when is_atom(module) do
%{
eq?: &module.eq?/2,
not_eq?: &module.not_eq?/2
}
end
end