Funx.Monad.Behaviour.Ap behaviour (funx v0.8.2)

View Source

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

@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)
  • NothingLeft(: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

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

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

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"}

Summary

Callbacks

Produces a wrapped function based on the input value.

Callbacks

ap(value, opts, env)

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

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