Funx.Monad.Behaviour.Bind behaviour (funx v0.8.0)
View SourceBehaviour 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 onopts- 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 valueEither.left(error)- Failure with errorMaybe.just(value)- Success with valueMaybe.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)Nothing→Left(:nothing)(uses:nothingatom 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
- Arguments strictly ordered: value, opts, env
- May use env for Reader-like dependency injection (only way to access env - functions cannot)
- Can fail - use this for operations that might not succeed
- 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")
endUsing 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
@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