Current section

Files

Jump to
funx lib appendable.ex
Raw

lib/appendable.ex

defprotocol Funx.Appendable do
@moduledoc """
A protocol for combining values in a generic, extensible way.
The `Appendable` protocol defines how two values of the same type can be combined. It is
used throughout Funx in functions like `traverse_a/2` and `wither_a/2` to accumulate
intermediate results without coupling logic to a specific type.
This protocol enables functions to remain flexible and composable when reducing,
aggregating, or accumulating values across a wide variety of domains.
## Required functions
* `coerce/1` – Normalizes an input value into a form suitable for aggregation.
* `append/2` – Combines two values of the same type into one.
### Default – Flat list aggregation
A fallback implementation is provided for all types that do not define a specific
`Appendable` instance. This default uses list concatenation as a universal aggregation
strategy: all inputs are coerced into lists (if not already), and combined using `++`.
When using the default aggregation strategy, values are collected in a plain list:
```elixir
validate_positive = fn x ->
Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
end
validate_even = fn x ->
Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
end
Funx.Monad.Either.validate(4, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Right{right: 4}
Funx.Monad.Either.validate(3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{left: ["Value must be even: 3"]}
Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{left: ["Value must be positive: -3", "Value must be even: -3"]}
```
### Structured aggregation with `ValidationError`
You can also use a custom struct to hold errors. This example uses `ValidationError`:
```elixir
alias Funx.Errors.ValidationError
validate_positive = fn x ->
Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
validate_even = fn x ->
Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{
# left: %ValidationError{
# errors: ["Value must be positive: -3", "Value must be even: -3"]
# }
# }
```
"""
@fallback_to_any true
@doc """
Normalizes a single input value into a form suitable for accumulation.
"""
def coerce(term)
@doc """
Combines two values into a single result.
Implementations must ensure the operation is associative within their type. For types
that require disambiguation or structural control, define a custom implementation.
"""
def append(accumulator, coerced)
end
defimpl Funx.Appendable, for: Any do
@spec coerce(term()) :: list()
def coerce(value) when is_list(value), do: value
def coerce(value), do: [value]
@spec append(term(), term()) :: list()
def append(acc, coerced), do: coerce(acc) ++ coerce(coerced)
end