Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 44 additions & 28 deletions lib/eq.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ defmodule Funx.Eq do
1. **Utility functions** for working with equality comparisons:
- `contramap/2` - Transform equality checks via projections
- `eq?/3`, `not_eq?/3` - Direct equality checks
- `append_all/2`, `append_any/2` - Combine comparators
- `concat_all/1`, `concat_any/1` - Combine lists of comparators
- `compose_all/2`, `compose_any/2` - Combine comparators
- `compose_all/1`, `compose_any/1` - Combine lists of comparators
- `to_predicate/2` - Convert to single-argument predicates

2. **Declarative DSL** for building complex equality comparators:
Expand Down Expand Up @@ -375,7 +375,7 @@ defmodule Funx.Eq do
end

@doc """
Combines two equality comparators using the `Eq.All` monoid.
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,
Expand All @@ -385,61 +385,61 @@ defmodule Funx.Eq do

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.append_all(eq1, eq2)
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
"""
@spec append_all(eq_t(), eq_t()) :: eq_t()
def append_all(a, b) do
@spec compose_all(eq_t(), eq_t()) :: eq_t()
def compose_all(a, b) do
m_append(%Monoid.Eq.All{}, a, b)
end

@doc """
Combines two equality comparators using the `Eq.Any` monoid.
Composes a list of equality comparators using the `Eq.All` monoid.

This function merges two equality comparisons, where **at least one**
must return `true` for the final result to be considered equal.
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.append_any(eq1, eq2)
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
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: "Bob", age: 25}, combined)
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
false
"""
@spec append_any(eq_t(), eq_t()) :: eq_t()
def append_any(a, b) do
m_append(%Monoid.Eq.Any{}, a, b)
@spec compose_all([eq_t()]) :: eq_t()
def compose_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.All` monoid.
Composes two equality comparators using the `Eq.Any` monoid.

The resulting comparator requires **all** comparators in the list to agree
that two values are equal.
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.concat_all([eq1, eq2])
iex> Funx.Eq.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
true
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
"""
@spec concat_all([eq_t()]) :: eq_t()
def concat_all(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.All{}, eq_list)
@spec compose_any(eq_t(), eq_t()) :: eq_t()
def compose_any(a, b) do
m_append(%Monoid.Eq.Any{}, a, b)
end

@doc """
Concatenates a list of equality comparators using the `Eq.Any` monoid.
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.
Expand All @@ -448,17 +448,33 @@ defmodule Funx.Eq do

iex> eq1 = Funx.Eq.contramap(& &1.name)
iex> eq2 = Funx.Eq.contramap(& &1.age)
iex> combined = Funx.Eq.concat_any([eq1, eq2])
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
"""
@spec concat_any([eq_t()]) :: eq_t()
def concat_any(eq_list) when is_list(eq_list) do
@spec compose_any([eq_t()]) :: eq_t()
def compose_any(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.Any{}, eq_list)
end

@deprecated "Use compose_all/2 instead"
@spec append_all(eq_t(), eq_t()) :: eq_t()
def append_all(a, b), do: compose_all(a, b)

@deprecated "Use compose_any/2 instead"
@spec append_any(eq_t(), eq_t()) :: eq_t()
def append_any(a, b), do: compose_any(a, b)

@deprecated "Use compose_all/1 instead"
@spec concat_all([eq_t()]) :: eq_t()
def concat_all(eq_list) when is_list(eq_list), do: compose_all(eq_list)

@deprecated "Use compose_any/1 instead"
@spec concat_any([eq_t()]) :: eq_t()
def concat_any(eq_list) when is_list(eq_list), do: compose_any(eq_list)

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

Expand Down
4 changes: 2 additions & 2 deletions lib/eq/dsl/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ defmodule Funx.Eq.Dsl.Block do
# ## Purpose
#
# Blocks group multiple equality checks with a composition strategy:
# - `:all` → All children must pass (AND logic) via concat_all
# - `:any` → At least one child must pass (OR logic) via concat_any
# - `:all` → All children must pass (AND logic) via compose_all
# - `:any` → At least one child must pass (OR logic) via compose_any
#
# ## Structure
#
Expand Down
14 changes: 7 additions & 7 deletions lib/eq/dsl/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ defmodule Funx.Eq.Dsl.Executor do
#
# The executor recursively walks the node tree:
# - Step nodes → Generate contramap/to_eq_map calls
# - Block nodes → Generate concat_all/concat_any calls
# - Block nodes → Generate compose_all/compose_any calls
# - Negate flag → Swap eq?/not_eq? functions
#
# Top-level nodes are implicitly combined with concat_all (AND logic).
# Top-level nodes are implicitly combined with compose_all (AND logic).

alias Funx.Eq
alias Funx.Eq.Dsl.{Block, Step}
Expand All @@ -43,10 +43,10 @@ defmodule Funx.Eq.Dsl.Executor do
Each node is converted to:
- Step (on) → `contramap(projection, eq)`
- Step (not_on) → `contramap(projection, negate(eq))`
- Block (all) → `concat_all([children...])`
- Block (any) → `concat_any([children...])`
- Block (all) → `compose_all([children...])`
- Block (any) → `compose_any([children...])`

Top-level nodes are combined with `concat_all` (implicit all strategy).
Top-level nodes are combined with `compose_all` (implicit all strategy).
"""
@spec execute_nodes(list(Step.t() | Block.t())) :: Macro.t()
def execute_nodes([]) do
Expand All @@ -60,15 +60,15 @@ defmodule Funx.Eq.Dsl.Executor do
eq_asts = Enum.map(nodes, &node_to_ast/1)

quote do
Eq.concat_all([unquote_splicing(eq_asts)])
Eq.compose_all([unquote_splicing(eq_asts)])
end
end

defp build_any_ast(nodes) do
eq_asts = Enum.map(nodes, &node_to_ast/1)

quote do
Eq.concat_any([unquote_splicing(eq_asts)])
Eq.compose_any([unquote_splicing(eq_asts)])
end
end

Expand Down
26 changes: 17 additions & 9 deletions lib/ord.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Funx.Ord do
- `reverse/1` - Reverse ordering logic
- `comparator/1` - Convert to Elixir comparator for `Enum.sort/2`
- `to_eq/1` - Convert to equality comparator
- `append/2`, `concat/1` - Combine multiple orderings
- `compose/2`, `compose/1` - Combine multiple orderings

## DSL

Expand Down Expand Up @@ -392,7 +392,7 @@ defmodule Funx.Ord do
end

@doc """
Appends two `Ord` instances, combining their comparison logic.
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.
Expand All @@ -401,17 +401,17 @@ defmodule Funx.Ord do

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 = Funx.Ord.compose(ord1, ord2)
iex> combined.lt?.(%{age: 30, name: "Alice"}, %{age: 30, name: "Bob"})
true
"""
@spec append(ord_t(), ord_t()) :: ord_t()
def append(a, b) do
@spec compose(ord_t(), ord_t()) :: ord_t()
def compose(a, b) do
m_append(%Funx.Monoid.Ord{}, a, b)
end

@doc """
Concatenates a list of `Ord` instances into a single composite comparator.
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.
Expand All @@ -422,15 +422,23 @@ defmodule Funx.Ord do
...> 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 = Funx.Ord.compose(ord_list)
iex> combined.gt?.(%{age: 25, name: "Charlie"}, %{age: 25, name: "Bob"})
true
"""
@spec concat([ord_t()]) :: ord_t()
def concat(ord_list) when is_list(ord_list) do
@spec compose([ord_t()]) :: ord_t()
def compose(ord_list) when is_list(ord_list) do
m_concat(%Funx.Monoid.Ord{}, ord_list)
end

@deprecated "Use compose/2 instead"
@spec append(ord_t(), ord_t()) :: ord_t()
def append(a, b), do: compose(a, b)

@deprecated "Use compose/1 instead"
@spec concat([ord_t()]) :: ord_t()
def concat(ord_list) when is_list(ord_list), do: compose(ord_list)

def to_ord_map(%{lt?: lt_fun, le?: le_fun, gt?: gt_fun, ge?: ge_fun} = ord_map)
when is_function(lt_fun, 2) and
is_function(le_fun, 2) and
Expand Down
6 changes: 3 additions & 3 deletions lib/ord/dsl/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule Funx.Ord.Dsl.Executor do
- `:asc` → `contramap(projection, ord)`
- `:desc` → `reverse(contramap(projection, ord))`

Multiple steps are combined with `concat([...])` (monoid append).
Multiple steps are combined with `compose([...])` (monoid append).

If two values are equal on all specified fields, they compare as equal.
Users can add an explicit tiebreaker if needed (e.g., `asc &Function.identity/1`).
Expand All @@ -50,7 +50,7 @@ defmodule Funx.Ord.Dsl.Executor do
ord_asts = Enum.map(steps, &step_to_ord_ast/1)

quote do
Ord.concat([unquote_splicing(ord_asts)])
Ord.compose([unquote_splicing(ord_asts)])
end
end

Expand Down Expand Up @@ -166,7 +166,7 @@ defmodule Funx.Ord.Dsl.Executor do
- ord do ... end
- Ord.contramap(...)
- Ord.reverse(...)
- Ord.concat([...])
- Ord.compose([...])
"""
end
end
Expand Down
32 changes: 16 additions & 16 deletions livebooks/eq/eq.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ eq_by?(& &1.age, %{age: 30}, %{age: 25})

## Monoid Operations

### append_all/2
### compose_all/2

Combines two equality comparators using the `Eq.All` monoid.
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,
Expand All @@ -139,77 +139,77 @@ where all comparators must agree.
```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = append_all(eq1, eq2)
combined = compose_all(eq1, eq2)
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
```

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = append_all(eq1, eq2)
combined = compose_all(eq1, eq2)
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
```

### append_any/2
### compose_any/2

Combines two equality comparators using the `Eq.Any` monoid.
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.

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = append_any(eq1, eq2)
combined = compose_any(eq1, eq2)
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
```

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = append_any(eq1, eq2)
combined = compose_any(eq1, eq2)
eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
```

### concat_all/1
### compose_all/1

Concatenates a list of equality comparators using the `Eq.All` monoid.
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.

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = concat_all([eq1, eq2])
combined = compose_all([eq1, eq2])
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
```

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = concat_all([eq1, eq2])
combined = compose_all([eq1, eq2])
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
```

### concat_any/1
### compose_any/1

Concatenates a list of equality comparators using the `Eq.Any` monoid.
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.

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = concat_any([eq1, eq2])
combined = compose_any([eq1, eq2])
eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
```

```elixir
eq1 = contramap(& &1.name)
eq2 = contramap(& &1.age)
combined = concat_any([eq1, eq2])
combined = compose_any([eq1, eq2])
eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
```

Expand Down
Loading