Current section
Files
Jump to
Current section
Files
lib/monad/behaviour/ap.ex
defmodule Funx.Monad.Behaviour.Ap do
@moduledoc """
Behaviour for applicative operations across monad DSLs.
Ap (apply) is the applicative functor operation that produces a wrapped function
to be applied to wrapped values. This allows for function application within
a computational context.
## Contract
```elixir
@callback ap(value :: any(), opts :: keyword(), env :: keyword()) ::
{:ok, (any() -> any())}
| {:error, any()}
| Either.t((any() -> any()), any())
| Maybe.t((any() -> any()))
```
## Arguments
- `value` - The value to use for producing the function
- `opts` - Keyword list of options (module-specific configuration)
- `env` - Environment/context from DSL (for Reader-like dependency injection)
## Return Values
**Monad types (preferred)**:
- `Either.right(fn)` - Success with a function
- `Either.left(error)` - Failure with error
- `Maybe.just(fn)` - Success with a function
- `Maybe.nothing()` - Failure (no function)
**Tagged tuples (supported)**:
- `{:ok, fn}` - Operation succeeded with a function
- `{:error, reason}` - Operation failed with error
The DSL will normalize all these return values into the appropriate monad type.
## Cross-Monad Normalization
When an `ap` module returns a monad type different from the current DSL context,
the result is automatically normalized:
**Maybe → Either**:
- `Just(fn)` → `Right(fn)`
- `Nothing` → `Left(:nothing)` (uses `:nothing` atom as error)
**Either → Maybe**:
- `Right(fn)` → `Just(fn)`
- `Left(_error)` → `Nothing` (error information is discarded)
## Semantic Rules
1. **Arguments strictly ordered**: value, opts, env
2. **May use env** for Reader-like dependency injection (only way to access env - functions cannot)
3. **Produces a function** - returns a wrapped function, not a wrapped value
4. **Can fail** - use this for operations that might not produce a function
## Examples
### Basic Applicative
```elixir
defmodule Multiplier do
@behaviour Funx.Monad.Behaviour.Ap
import Funx.Monad.Either
@impl true
def ap(factor, _opts, _env) when is_number(factor) do
right(fn x -> x * factor end)
end
def ap(_value, _opts, _env), do: left("Expected number for factor")
end
# Usage in Either DSL
use Funx.Monad.Either
either 5 do
ap Multiplier
end
# First evaluates Multiplier.ap(5, []) -> Right(fn x -> x * 5 end)
# Then applies: fn x -> x * 5 end to 5 -> 25
#=> %Right{right: 25}
```
### With Options
```elixir
defmodule ConditionalOperator do
@behaviour Funx.Monad.Behaviour.Ap
import Funx.Monad.Either
@impl true
def ap(value, opts, _env) do
op = Keyword.get(opts, :operation, :add)
case op do
:add -> right(fn x -> x + value end)
:multiply -> right(fn x -> x * value end)
:subtract -> right(fn x -> x - value end)
_ -> left("Unknown operation: \#{op}")
end
end
end
# Usage
either 10 do
ap {ConditionalOperator, operation: :multiply}
end
#=> %Right{right: 100}
```
### Failure Cases
```elixir
defmodule ValidatedOperator do
@behaviour Funx.Monad.Behaviour.Ap
import Funx.Monad.Either
@impl true
def ap(value, _opts, _env) when is_number(value) and value > 0 do
right(fn x -> x + value end)
end
def ap(value, _opts, _env) when is_number(value) do
left("Factor must be positive, got: \#{value}")
end
def ap(_value, _opts, _env), do: left("Expected number")
end
# Success
either 5 do
ap ValidatedOperator
end
#=> %Right{right: 10}
# Failure
either -3 do
ap ValidatedOperator
end
#=> %Left{left: "Factor must be positive, got: -3"}
```
"""
@doc """
Produces a wrapped function based on the input value.
Arguments:
* value - The current value in the pipeline
* opts - Module-specific options passed in the DSL
* env - Environment/context from the DSL (for dependency injection)
Returns a wrapped function that will be applied to the value.
Examples:
# Using Either
import Funx.Monad.Either
def ap(multiplier, _opts, _env) when is_number(multiplier) do
right(fn x -> x * multiplier end)
end
# Using tagged tuples
def ap(multiplier, _opts, _env) when is_number(multiplier) do
{:ok, fn x -> x * multiplier end}
end
# With failure case
def ap(value, _opts, _env) do
if valid?(value) do
right(fn x -> transform(x, value) end)
else
left("validation failed")
end
end
# With options
def ap(value, opts, _env) do
operation = Keyword.get(opts, :operation, :default)
right(create_function(operation, value))
end
# Using env for dependency injection
def ap(value, _opts, env) do
transformer = Keyword.get(env, :transformer)
right(fn x -> transformer.transform(x, value) end)
end
"""
@callback ap(value :: any(), opts :: keyword(), env :: keyword()) ::
{:ok, (any() -> any())}
| {:error, any()}
| Funx.Monad.Either.t((any() -> any()), any())
| Funx.Monad.Maybe.t((any() -> any()))
end