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

View Source

Behaviour for bind operations across monad DSLs.

This behaviour defines a generic interface for operations that can fail, usable with the bind operation in any monad DSL (Either, Maybe, etc.).

Contract

@callback bind(value :: any(), opts :: keyword(), env :: keyword()) ::
  {:ok, any()}
  | {:error, any()}
  | Either.t()
  | Maybe.t()

The DSL will normalize all these return formats.

Note: Plain values can also be returned and will be treated as success, but using the explicit formats above is preferred for clarity.

Arguments

  • value - The value to operate on
  • 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(value) - Success with value
  • Either.left(error) - Failure with error
  • Maybe.just(value) - Success with value
  • Maybe.nothing() - Failure (no value)

Tagged tuples (supported):

  • {:ok, value} - Operation succeeded with new value
  • {:error, reason} - Operation failed with error

The DSL will normalize all these return values into the appropriate monad type.

Cross-Monad Normalization

When a bind module returns a monad type different from the current DSL context, the result is automatically normalized:

Maybe → Either:

  • Just(value)Right(value)
  • NothingLeft(:nothing) (uses :nothing atom as error)

Either → Maybe:

  • Right(value)Just(value)
  • Left(_error)Nothing (error information is discarded)

This allows Bind modules to be reused across different monad DSLs while maintaining predictable behavior. Note that error information is lost when converting Left to Nothing, as Maybe does not carry error details.

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. Can fail - use this for operations that might not succeed
  4. Returns result in tagged tuple or monad type

Examples

Using Either (Preferred)

defmodule ParseInt do
  @behaviour Funx.Monad.Behaviour.Bind
  import Funx.Monad.Either

  @impl true
  def bind(value, _opts, _env) when is_binary(value) do
    case Integer.parse(value) do
      {int, ""} -> right(int)
      _ -> left("Invalid integer")
    end
  end

  def bind(_value, _opts, _env), do: left("Expected string")
end

# Usage in Either DSL
use Funx.Monad.Either

either "42" do
  bind ParseInt
end
#=> %Right{right: 42}

either "not a number" do
  bind ParseInt
end
#=> %Left{left: "Invalid integer"}

Using Tagged Tuples (Supported)

defmodule ParseIntTuple do
  @behaviour Funx.Monad.Behaviour.Bind

  @impl true
  def bind(value, _opts, _env) when is_binary(value) do
    case Integer.parse(value) do
      {int, ""} -> {:ok, int}
      _ -> {:error, "Invalid integer"}
    end
  end

  def bind(_value, _opts, _env), do: {:error, "Expected string"}
end

# Also works in Either DSL (tuples are normalized)
either "42" do
  bind ParseIntTuple
end
#=> %Right{right: 42}

Using Either Types (Preferred)

defmodule ParseIntEither do
  @behaviour Funx.Monad.Behaviour.Bind
  import Funx.Monad.Either

  @impl true
  def bind(value, _opts, _env) when is_binary(value) do
    case Integer.parse(value) do
      {int, ""} -> right(int)
      _ -> left("Invalid integer")
    end
  end

  def bind(_value, _opts, _env), do: left("Expected string")
end

Using Maybe Types (Preferred)

defmodule ParseIntMaybe do
  @behaviour Funx.Monad.Behaviour.Bind
  import Funx.Monad.Maybe

  @impl true
  def bind(value, _opts, _env) when is_binary(value) do
    case Integer.parse(value) do
      {int, ""} -> just(int)
      _ -> nothing()
    end
  end

  def bind(_value, _opts, _env), do: nothing()
end

# Can be used in Either DSL - Nothing becomes Left(:nothing)
use Funx.Monad.Either

either "42" do
  bind ParseIntMaybe
end
#=> %Right{right: 42}

either "invalid" do
  bind ParseIntMaybe
end
#=> %Left{left: :nothing}

With Options

defmodule ParseIntWithBase do
  @behaviour Funx.Monad.Behaviour.Bind

  @impl true
  def bind(value, opts, _env) when is_binary(value) do
    base = Keyword.get(opts, :base, 10)

    case Integer.parse(value, base) do
      {int, ""} -> {:ok, int}
      _ -> {:error, "Invalid integer for base #{base}"}
    end
  end

  def bind(_value, _opts, _env), do: {:error, "Expected string"}
end

# Usage
either "FF" do
  bind {ParseIntWithBase, base: 16}
end
#=> %Right{right: 255}

Summary

Callbacks

Performs an operation that can fail.

Callbacks

bind(value, opts, env)

@callback bind(value :: any(), opts :: keyword(), env :: keyword()) ::
  {:ok, any()}
  | {:error, any()}
  | Funx.Monad.Either.t(any(), any())
  | Funx.Monad.Maybe.t(any())

Performs an operation that can fail.

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 result indicating success or failure.

Examples:

# Using tagged tuples (generic)
def bind(value, _opts, _env) when is_binary(value) do
  case Integer.parse(value) do
    {int, ""} -> {:ok, int}
    _ -> {:error, "invalid"}
  end
end

# Using Either (monad-specific)
import Funx.Monad.Either

def bind(value, _opts, _env) do
  if valid?(value) do
    right(transform(value))
  else
    left("validation failed")
  end
end

# With options
def bind(value, opts, _env) do
  threshold = Keyword.get(opts, :min, 0)
  if value > threshold do
    {:ok, value}
  else
    {:error, "below threshold"}
  end
end

# Using env for dependency injection
def bind(user_id, _opts, env) do
  database = Keyword.get(env, :database)
  database.fetch_user(user_id)
end