View Source
Funx.Monad
Usage Rules
LLM Functional Programming Foundation
Key Concepts for LLMs:
Monad: A design pattern that provides a structured way to handle computations with context
- Bind operation: Chains computations that might fail, transform, or have side effects
- Map operation: Transforms the wrapped value with a regular function
- Mathematical foundation: Satisfies identity and associativity laws
- Three fundamental operations:
bind/2
,map/2
, andap/2
Kleisli Functions: Functions that return values wrapped in a monadic context
- Type signature:
a -> M<b>
(takes regular value, returns monadic value) - Compose with
bind/2
rather than regular function composition - Enable chaining of operations that might fail or transform context
- Example:
fn x -> {:ok, x * 2} end
is a Kleisli function for Result monad
Monadic vs. Applicative: Different styles for handling multiple wrapped values
- Monadic (sequential): Later computations depend on earlier results
- Applicative (independent): All computations are independent and can run in parallel
- Concurrency note: Parallel execution only applies to Effect monad - Maybe/Either are synchronous
- Rule: Use monadic when you need the result of one computation to determine the next
Bind vs. Map: Different operations for different transformations
- Map: Transform the value inside the monad (
a -> b
functions) - Bind: Chain monadic operations (
a -> M<b>
functions) - Key insight: Bind flattens nested monads, map doesn't
Context Preservation: Monads maintain computational context through transformations
- Maybe monad: Preserves presence/absence context (synchronous)
- Either monad: Preserves success/failure with error information (synchronous)
- Effect monad: Preserves async computation with Reader environment (deferred/concurrent)
- Identity monad: Transparent context for learning and composition (synchronous)
- Context flows: Through bind chains automatically
LLM Decision Guide: When to Use Monad Protocol
✅ Use Monad Protocol when:
- Building generic functions that work with any monad
- Creating monad-agnostic algorithms
- Need to compose different monadic operations
- Want polymorphic code that works with Maybe, Either, Identity, etc.
- User says: "generic", "polymorphic", "works with any monad", "monad-agnostic"
❌ Don't use Protocol when:
- Working with specific monad types only
- Performance is critical (protocol dispatch has overhead)
- Simple transformations that don't need genericity
- User is asking for specific Maybe or Either operations
⚡ Protocol vs. Direct Module Decision:
- Protocol: Generic code that works with multiple monad types
- Direct module: Type-specific optimized operations
- Rule: Start with direct modules, extract to protocol when you need genericity
⚙️ Operation Choice Guide:
- bind/2: Chain operations that return monadic values
- map/2: Transform values inside the monadic context
- ap/2: Apply functions in monadic context to values in monadic context
- Law verification: Always test monad laws in generic code
LLM Context Clues
User language → Monad protocol patterns:
- "generic monad function" → Use protocol operations
- "works with any monad" → Protocol-based implementation
- "polymorphic over monads" → Protocol with type constraints
- "chain operations" →
bind/2
sequences - "transform value" →
map/2
- "apply function" →
ap/2
- "sequence computations" → Bind chains with error handling
- "monad laws" → Identity and associativity verification
Quick Reference
- Use
bind/2
to chain Kleisli functions (operations returning monadic values) - Use
map/2
to transform values inside the monadic context - Use
ap/2
for applicative style when computations are independent - Use constructor functions like
Maybe.just/1
,Either.right/1
to create monadic values - Use
bind/2
to naturally flatten nested monads through composition - All monads support
map/2
andap/2
via protocol or import - Monad laws: left identity, right identity, associativity
- Protocol enables polymorphic functions working across monad types
Overview
Funx.Monad
defines the core protocol for monadic operations in Elixir. It provides the essential operations that all monads must implement: bind/2
, map/2
, and ap/2
. This protocol enables writing generic, reusable functions that work with any monadic type.
The protocol is the foundation for functional composition patterns, enabling you to chain computations while preserving context (whether that's handling optional values, errors, transformations, or other computational contexts).
Protocol Operations
Operation | Type Signature | Purpose |
---|---|---|
bind/2 | M<a> -> (a -> M<b>) -> M<b> | Chain monadic computations |
map/2 | M<a> -> (a -> b) -> M<b> | Transform the wrapped value |
ap/2 | M<(a -> b)> -> M<a> -> M<b> | Apply a wrapped function to wrapped value |
The protocol requires implementing three operations: bind/2
for chaining monadic functions, map/2
for transforming wrapped values, and ap/2
for applicative-style function application.
LLM Monad Laws Foundation
Mathematical Requirements:
All monad implementations must satisfy three fundamental laws:
Left Identity: Constructor.new(a) |> bind(f) == f.(a)
- Creating a monad then binding should equal direct application
- The monadic wrapper doesn't interfere with computation
Right Identity: m |> bind(fn x -> Constructor.new(x) end) == m
- Binding with constructor should leave the monad unchanged
- Constructor functions are neutral elements for bind
Associativity: (m |> bind(f)) |> bind(g) == m |> bind(fn x -> bind(f.(x), g) end)
- Order of binding operations doesn't matter
- Enables reliable composition and refactoring
Why Laws Matter for LLMs:
- Predictability: Laws ensure consistent behavior across implementations
- Composition safety: Can refactor and compose without breaking semantics
- Generic algorithms: Enable writing functions that work with any lawful monad
- Debugging confidence: Law violations indicate implementation bugs
Usage Patterns
Basic Monadic Pipeline
# Generic function working with any monad
def transform_generically(monad_value) do
monad_value
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x * 2) end)
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
|> Monad.bind(fn x ->
if x > 10 do
SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc."large: #{x}")
else
SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc."small: #{x}")
end
end)
end
# Works with Maybe
transform_generically(Maybe.just(5)) # => Just("large: 11")
transform_generically(Maybe.nothing()) # => Nothing
# Works with Either
transform_generically(Either.right(5)) # => Right("large: 11")
transform_generically(Either.left("error")) # => Left("error")
Kleisli Function Composition
# Kleisli functions for various monads
def safe_divide(a, b) do
if b != 0 do
Maybe.just(a / b)
else
Maybe.nothing()
end
end
def validate_positive(x) do
if x > 0 do
Either.right(x)
else
Either.left("must be positive")
end
end
# Generic composition using protocol
def compose_kleisli(value, kleisli_fns) do
Enum.reduce(kleisli_fns, SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.value), fn f, acc ->
Monad.bind(acc, f)
end)
end
# Use with different monads
compose_kleisli(10, [safe_divide(20), &Maybe.just(&1 + 1)])
compose_kleisli(10, [validate_positive, fn x -> Either.right(x * 2) end])
Map for Transforming Values
# Transform values inside monadic context
import Funx.Monad
# Works with any monad
Maybe.just(5) |> map(&(&1 * 2)) # Just(10)
Either.right(5) |> map(&(&1 * 2)) # Right(10)
Identity.identity(5) |> map(&(&1 * 2)) # Identity(10)
# Chain transformations
Maybe.just("hello")
|> map(&String.upcase/1) # Just("HELLO")
|> map(&String.length/1) # Just(5)
Ap for Applying Wrapped Functions
Applies a monadic function (e.g., M<(a -> b)>
) to a monadic argument (M<a>
), returning M<b>
. This enables applicative style when computations are independent.
import Funx.Monad
# Basic function application
add = fn x -> fn y -> x + y end end
return(add)
|> ap(return(3))
|> ap(return(4)) # Works with any monad
# With Maybe
Maybe.just(add)
|> ap(Maybe.just(3))
|> ap(Maybe.just(4)) # Just(7)
# With Either
Either.right(add)
|> ap(Either.right(3))
|> ap(Either.right(4)) # Right(7)
# String concatenation
concat3 = fn a -> fn b -> fn c -> a <> b <> c end end end
Maybe.just(concat3)
|> ap(Maybe.just("Hello, "))
|> ap(Maybe.just("World"))
|> ap(Maybe.just("!")) # Just("Hello, World!")
Flattening Nested Monads
# When you have nested monads - use bind to flatten naturally
nested_maybe = Maybe.just(Maybe.just(42))
flattened = Monad.bind(nested_maybe, fn inner -> inner end) # => Just(42)
nested_either = Either.right(Either.right("success"))
flattened = Monad.bind(nested_either, fn inner -> inner end) # => Right("success")
Guidelines for Generic Monad Functions
When writing functions that work with any monad:
- Use protocol operations only:
Monad.bind/2
,Monad.map/2
,Monad.ap/2
- Avoid specific monad constructors: Don't use
Maybe.just/1
in generic code - Choose the right operation:
map
for transforming,bind
for chaining,ap
for independent computations - Test monad laws: Verify your functions preserve monadic structure
- Consider performance: Protocol dispatch has overhead vs direct module calls
- Document type constraints: Make clear which monads your function supports
- Handle all cases: Generic code should work correctly with all monadic types
LLM Code Templates
Basic Monadic Pipeline Template
def build_monadic_pipeline() do
fn initial_value, monad_type ->
initial_value
|> monad_type.return()
|> Monad.bind(fn x ->
# First transformation (returns monadic value)
transformed = transform_step_1(x)
monad_type.return(transformed)
end)
|> Monad.bind(fn x ->
# Second transformation with possible failure
case validate_step_2(x) do
:ok -> monad_type.return(x)
{:error, reason} -> monad_type.error(reason) # Assumes error constructor
end
end)
|> Monad.bind(fn x ->
# Final transformation
result = finalize_step(x)
monad_type.return(result)
end)
end
end
# Usage with different monads
pipeline = build_monadic_pipeline()
pipeline.(42, Maybe) # Works with Maybe monad
pipeline.(42, Either) # Works with Either monad
Monad Law Verification Template
def verify_monad_laws(monad_module, test_value, test_function) do
# Left Identity Law: return(a) >>= f === f(a)
left_identity = fn ->
left = monad_module.return(test_value) |> Monad.bind(test_function)
right = test_function.(test_value)
left == right
end
# Right Identity Law: m >>= return === m
right_identity = fn ->
test_monad = monad_module.return(test_value)
left = test_monad |> Monad.bind(&monad_module.return/1)
right = test_monad
left == right
end
# Associativity Law: (m >>= f) >>= g === m >>= (\x -> f(x) >>= g)
associativity = fn ->
test_monad = monad_module.return(test_value)
second_function = fn x -> monad_module.return(x * 3) end
left = test_monad
|> Monad.bind(test_function)
|> Monad.bind(second_function)
right = test_monad |> Monad.bind(fn x ->
test_function.(x) |> Monad.bind(second_function)
end)
left == right
end
%{
left_identity: left_identity.(),
right_identity: right_identity.(),
associativity: associativity.()
}
end
# Test all laws for a monad
def test_monad_implementation(monad_module) do
test_fn = fn x -> monad_module.return(x + 1) end
results = verify_monad_laws(monad_module, 42, test_fn)
all_passed = Enum.all?(Map.values(results))
{all_passed, results}
end
Kleisli Function Factory Template
def build_kleisli_factory(monad_module) do
%{
# Safe mathematical operations
safe_divide: fn a, b ->
if b != 0 do
monad_module.return(a / b)
else
monad_module.error("division by zero")
end
end,
safe_sqrt: fn x ->
if x >= 0 do
monad_module.return(:math.sqrt(x))
else
monad_module.error("negative square root")
end
end,
# Validation operations
validate_range: fn min, max ->
fn value ->
if value >= min and value <= max do
monad_module.return(value)
else
monad_module.error("value #{value} not in range #{min}-#{max}")
end
end
end,
# Transformation operations
transform_with: fn transformer ->
fn value ->
try do
result = transformer.(value)
monad_module.return(result)
rescue
error -> monad_module.error("transformation failed: #{inspect(error)}")
end
end
end,
# Conditional operations
when_condition: fn predicate, then_fn, else_fn ->
fn value ->
if predicate.(value) do
then_fn.(value)
else
else_fn.(value)
end
end
end
}
end
# Usage with different monads
maybe_ops = build_kleisli_factory(Maybe)
either_ops = build_kleisli_factory(Either)
# Chain operations
42
|> maybe_ops.safe_divide.(6)
|> Monad.bind(maybe_ops.safe_sqrt)
|> Monad.bind(maybe_ops.validate_range.(0, 10))
Applicative vs. Monadic Style Template
def comparison_template() do
import Funx.Monad
# Sample data
maybe_a = Maybe.just(5)
maybe_b = Maybe.just(10)
maybe_c = Maybe.just(2)
# Applicative style - all computations are independent
add3 = fn a -> fn b -> fn c -> a + b + c end end end
applicative_result =
return(add3)
|> ap(maybe_a)
|> ap(maybe_b)
|> ap(maybe_c) # Just(17)
# Monadic style - later computations depend on earlier ones
monadic_result =
maybe_a
|> bind(fn a ->
maybe_b |> bind(fn b ->
# This computation depends on both a and b
if a + b > 10 do
maybe_c |> bind(fn c ->
return(a + b + c + 100) # Bonus for large values
end)
else
maybe_c |> bind(fn c ->
return(a + b + c)
end)
end
end)
end)
# When to use each:
# - Applicative: When operations are independent (use ap)
# - Monadic: When later operations depend on earlier results (use bind)
%{
applicative: applicative_result, # Just(17)
monadic: monadic_result # Just(117) - includes bonus
}
end
Monad Transformer Pattern Template
def build_transformer_stack() do
# Example: Maybe + IO monad transformer
# MaybeT IO a = IO (Maybe a)
# Basic operations for the transformer
def lift_io(io_action) do
fn ->
result = io_action.()
Maybe.just(result)
end
end
def lift_maybe(maybe_value) do
fn -> maybe_value end
end
# Bind for the transformer stack
def bind_transformer(transformer_action, kleisli_fn) do
fn ->
case transformer_action.() do
Maybe.Nothing -> Maybe.nothing()
Maybe.Just(value) ->
next_action = kleisli_fn.(value)
next_action.()
end
end
end
# Example usage
def fetch_and_process_user(user_id) do
# This would be a complex operation involving both IO and Maybe
lift_io(fn -> fetch_user_from_db(user_id) end)
|> bind_transformer(fn user ->
if user.active do
lift_maybe(Maybe.just(user))
else
lift_maybe(Maybe.nothing())
end
end)
|> bind_transformer(fn user ->
lift_io(fn ->
processed_user = process_user_data(user)
Maybe.just(processed_user)
end)
end)
end
# Run the transformer stack
user_result = fetch_and_process_user(123)
final_result = user_result.() # IO (Maybe User)
end
Utils Integration Template
def build_utils_integration() do
# Combine Monad protocol with Utils currying
# Curry monadic operations for pipeline use
bind_with = Funx.Utils.curry_r(&Monad.bind/2)
# Create reusable monadic operations
def create_monadic_validators() do
%{
positive: bind_with.(fn x ->
if x > 0 do
Maybe.just(x)
else
Maybe.nothing()
end
end),
even: bind_with.(fn x ->
if rem(x, 2) == 0 do
Either.right(x)
else
Either.left("must be even")
end
end),
in_range: fn min, max ->
bind_with.(fn x ->
if x >= min and x <= max do
Maybe.just(x)
else
Maybe.nothing()
end
end)
end
}
end
# Use in pipelines
validators = create_monadic_validators()
# Pipeline with curried monadic operations
result = Maybe.just(42)
|> validators.positive.()
|> bind_with.(fn x -> Maybe.just(x * 2) end).()
|> validators.in_range.(50, 100).()
# Compose multiple validators
validate_positive_even = fn monad_value ->
monad_value
|> validators.positive.()
|> validators.even.()
end
Either.right(20) |> validate_positive_even.()
end
LLM Testing Guidance
Test Protocol Implementation
defmodule MonadLawTest do
use ExUnit.Case
# Test that a monad implementation satisfies the laws
def test_monad_laws(monad_module, sample_values, sample_functions) do
Enum.each(sample_values, fn value ->
Enum.each(sample_functions, fn f ->
test_left_identity(monad_module, value, f)
test_right_identity(monad_module, value)
test_associativity(monad_module, value, f)
end)
end)
end
defp test_left_identity(monad_module, value, kleisli_fn) do
# return(a) >>= f === f(a)
left = monad_module.return(value) |> Monad.bind(kleisli_fn)
right = kleisli_fn.(value)
assert left == right, """
Left identity law failed for #{inspect(monad_module)}
Value: #{inspect(value)}
Function: #{inspect(kleisli_fn)}
"""
end
defp test_right_identity(monad_module, value) do
# m >>= return === m
monad_value = monad_module.return(value)
left = monad_value |> Monad.bind(&monad_module.return/1)
right = monad_value
assert left == right, """
Right identity law failed for #{inspect(monad_module)}
Value: #{inspect(value)}
"""
end
defp test_associativity(monad_module, value, f) do
# (m >>= f) >>= g === m >>= (\x -> f(x) >>= g)
g = fn x -> monad_module.return(x * 3) end
m = monad_module.return(value)
left = m |> Monad.bind(f) |> Monad.bind(g)
right = m |> Monad.bind(fn x -> f.(x) |> Monad.bind(g) end)
assert left == right, """
Associativity law failed for #{inspect(monad_module)}
Value: #{inspect(value)}
"""
end
test "Maybe satisfies monad laws" do
sample_values = [1, 2, 0, -1, 100]
sample_functions = [
fn x -> Maybe.just(x + 1) end,
fn x -> if x > 0, do: Maybe.just(x), else: Maybe.nothing() end,
fn x -> Maybe.just(x * 2) end
]
test_monad_laws(Maybe, sample_values, sample_functions)
end
test "Either satisfies monad laws" do
sample_values = [1, 2, 0, -1]
sample_functions = [
fn x -> Either.right(x + 1) end,
fn x -> if x >= 0, do: Either.right(x), else: Either.left("negative") end,
fn x -> Either.right(x * 2) end
]
test_monad_laws(Either, sample_values, sample_functions)
end
end
Test Generic Monadic Functions
defmodule GenericMonadTest do
use ExUnit.Case
# Test that generic functions work with multiple monad types
def generic_double_and_add(monad_value, amount) do
monad_value
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x * 2) end)
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + amount) end)
end
test "generic functions work with Maybe" do
result = generic_double_and_add(Maybe.just(5), 10)
assert result == Maybe.just(20)
result = generic_double_and_add(Maybe.nothing(), 10)
assert result == Maybe.nothing()
end
test "generic functions work with Either" do
result = generic_double_and_add(Either.right(5), 10)
assert result == Either.right(20)
result = generic_double_and_add(Either.left("error"), 10)
assert result == Either.left("error")
end
test "generic functions work with Identity" do
result = generic_double_and_add(Identity.new(5), 10)
assert result == Identity.new(20)
end
end
Test Join Operation
defmodule MonadJoinTest do
use ExUnit.Case
test "join flattens nested Maybe" do
nested = Maybe.just(Maybe.just(42))
flattened = Monad.bind(nested, fn inner -> inner end)
assert flattened == Maybe.just(42)
nested_nothing_inner = Maybe.just(Maybe.nothing())
flattened = Monad.bind(nested_nothing_inner, fn inner -> inner end)
assert flattened == Maybe.nothing()
nothing_outer = Maybe.nothing()
flattened = Monad.bind(nothing_outer, fn inner -> inner end)
assert flattened == Maybe.nothing()
end
test "join flattens nested Either" do
nested_right = Either.right(Either.right("success"))
flattened = Monad.bind(nested_right, fn inner -> inner end)
assert flattened == Either.right("success")
nested_left_inner = Either.right(Either.left("inner error"))
flattened = Monad.bind(nested_left_inner, fn inner -> inner end)
assert flattened == Either.left("inner error")
left_outer = Either.left("outer error")
flattened = Monad.bind(left_outer, fn inner -> inner end)
assert flattened == Either.left("outer error")
end
end
LLM Debugging Tips
Trace Monadic Operations
def trace_monad_operations(monad_value, operations) do
IO.puts("Starting with: #{inspect(monad_value)}")
result = Enum.reduce(operations, {monad_value, 0}, fn operation, {current, step} ->
IO.puts("Step #{step}: Input = #{inspect(current)}")
next = case operation do
{:bind, f} ->
result = Monad.bind(current, f)
IO.puts("Step #{step}: bind -> #{inspect(result)}")
result
{:map, f} ->
result = Monad.bind(current, fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.f.(x)) end)
IO.puts("Step #{step}: map -> #{inspect(result)}")
result
end
{next, step + 1}
end)
{traced_result, _} = result
IO.puts("Final result: #{inspect(traced_result)}")
traced_result
end
# Usage
trace_monad_operations(Maybe.just(10), [
{:bind, fn x -> Maybe.just(x * 2) end},
{:map, fn x -> x + 5 end},
{:bind, fn x -> if x > 20, do: Maybe.just(x), else: Maybe.nothing() end}
])
Verify Law Compliance
def debug_monad_laws(monad_module, value, function) do
IO.puts("Testing monad laws for #{inspect(monad_module)} with value #{inspect(value)}")
# Left Identity
left_identity_left = monad_module.return(value) |> Monad.bind(function)
left_identity_right = function.(value)
left_identity_ok = left_identity_left == left_identity_right
IO.puts("Left Identity: #{left_identity_ok}")
IO.puts(" return(#{inspect(value)}) >>= f = #{inspect(left_identity_left)}")
IO.puts(" f(#{inspect(value)}) = #{inspect(left_identity_right)}")
# Right Identity
m = monad_module.return(value)
right_identity_left = m |> Monad.bind(&monad_module.return/1)
right_identity_right = m
right_identity_ok = right_identity_left == right_identity_right
IO.puts("Right Identity: #{right_identity_ok}")
IO.puts(" m >>= return = #{inspect(right_identity_left)}")
IO.puts(" m = #{inspect(right_identity_right)}")
# Associativity
g = fn x -> monad_module.return(x + 100) end
assoc_left = m |> Monad.bind(function) |> Monad.bind(g)
assoc_right = m |> Monad.bind(fn x -> function.(x) |> Monad.bind(g) end)
associativity_ok = assoc_left == assoc_right
IO.puts("Associativity: #{associativity_ok}")
IO.puts(" (m >>= f) >>= g = #{inspect(assoc_left)}")
IO.puts(" m >>= (\\x -> f(x) >>= g) = #{inspect(assoc_right)}")
%{
left_identity: left_identity_ok,
right_identity: right_identity_ok,
associativity: associativity_ok,
all_pass: left_identity_ok and right_identity_ok and associativity_ok
}
end
Monitor Performance
def benchmark_monadic_operations() do
# Compare protocol vs direct module performance
test_value = Maybe.just(42)
iterations = 100_000
# Protocol-based version
protocol_time = :timer.tc(fn ->
for _ <- 1..iterations do
test_value
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x * 2) end)
end
end)
# Direct module version
direct_time = :timer.tc(fn ->
for _ <- 1..iterations do
test_value
|> Maybe.bind(fn x -> Maybe.just(x + 1) end)
|> Maybe.bind(fn x -> Maybe.just(x * 2) end)
end
end)
IO.puts("Protocol time: #{elem(protocol_time, 0)} microseconds")
IO.puts("Direct time: #{elem(direct_time, 0)} microseconds")
IO.puts("Overhead: #{(elem(protocol_time, 0) / elem(direct_time, 0) - 1) * 100}%")
end
LLM Common Mistakes to Avoid
❌ Don't Mix Protocol and Direct Calls
# ❌ Wrong: mixing protocol and module-specific calls
def bad_generic_function(monad_value) do
monad_value
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
|> Maybe.map(fn x -> x * 2 end) # Breaks genericity!
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x - 1) end)
end
# ✅ Correct: use protocol operations consistently
def good_generic_function(monad_value) do
monad_value
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x * 2) end) # Map via bind + return
|> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x - 1) end)
end
❌ Don't Assume Specific Monad Constructors
# ❌ Wrong: using specific constructors in generic code
def bad_validate_positive(monad_value) do
Monad.bind(monad_value, fn x ->
if x > 0 do
Maybe.just(x) # Assumes Maybe monad!
else
Maybe.nothing()
end
end)
end
# ✅ Correct: use protocol operations and pass monad module
def good_validate_positive(monad_value, monad_module) do
Monad.bind(monad_value, fn x ->
if x > 0 do
monad_module.return(x)
else
monad_module.empty() # Assumes empty/error constructor
end
end)
end
❌ Don't Ignore Monad Laws
# ❌ Wrong: implementing bind that violates laws
defmodule BadMonad do
defstruct [:value, :extra]
defimpl Funx.Monad do
def bind(%BadMonad{value: value, extra: extra}, f) do
# This breaks associativity by adding extra each time!
result = f.(value)
%{result | extra: extra + 1}
end
def return(value), do: %BadMonad{value: value, extra: 0}
def join(%BadMonad{value: %BadMonad{} = inner}), do: inner
end
end
# ✅ Correct: law-abiding implementation
defmodule GoodMonad do
defstruct [:value]
defimpl Funx.Monad do
def bind(%GoodMonad{value: value}, f) do
f.(value) # Simple, law-abiding bind
end
def return(value), do: %GoodMonad{value: value}
def join(%GoodMonad{value: %GoodMonad{} = inner}), do: inner
end
end
❌ Don't Use Return Inside Bind Unnecessarily
# ❌ Wrong: unnecessary return wrapping
def bad_chain(monad_value) do
monad_value
|> Monad.bind(fn x ->
result = x + 1
SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.result) # This is just map!
end)
end
# ✅ Better: recognize this is mapping, not binding
def good_chain(monad_value) do
# If your monad has map, use it
monad_value |> SomeMonad.map(fn x -> x + 1 end)
# Or if you need generic protocol:
monad_value |> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
end
# ✅ Best: only use bind when you need to flatten
def best_chain(monad_value) do
monad_value
|> Monad.bind(fn x ->
# This function returns a monad, so bind is correct
if x > 0 do
SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x)
else
SomeMonad.empty()
end
end)
end
❌ Don't Nest Binds When You Can Chain
# ❌ Wrong: nested bind calls (hard to read)
def bad_nested_operations(monad_value) do
Monad.bind(monad_value, fn x ->
Monad.bind(SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1), fn y ->
Monad.bind(SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.y * 2), fn z ->
SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.z - 1)
end)
end)
end)
end
# ✅ Correct: chain bind operations with pipe
def good_chained_operations(monad_value) do
monad_value
|> Monad.bind(fn x -> SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
|> Monad.bind(fn y -> SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.y * 2) end)
|> Monad.bind(fn z -> SomeSpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.z - 1) end)
end
❌ Don't Forget Error Propagation
# ❌ Wrong: not handling error cases in generic code
def bad_error_handling(monad_value) do
Monad.bind(monad_value, fn x ->
# What if this operation can fail?
result = risky_operation(x) # Might raise exception
SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.result)
end)
end
# ✅ Correct: handle errors appropriately
def good_error_handling(monad_value, monad_module) do
Monad.bind(monad_value, fn x ->
try do
result = risky_operation(x)
monad_module.return(result)
rescue
error -> monad_module.error("operation failed: #{inspect(error)}")
end
end)
end
LLM Integration with Other Modules
With Predicate Logic
def build_predicate_monadic_filters() do
# Combine predicates with monadic validation
# Create monadic validators from predicates
def predicate_to_monad_validator(predicate, error_msg) do
fn value ->
if Predicate.test(predicate, value) do
Maybe.just(value)
else
Either.left(error_msg)
end
end
end
# Build complex validation chains
age_predicate = Predicate.Utils.and_all([
Predicate.Utils.greater_than(0),
Predicate.Utils.less_than(150)
])
email_predicate = Predicate.Utils.matches(~r/@/)
validators = %{
age: predicate_to_monad_validator(age_predicate, "invalid age"),
email: predicate_to_monad_validator(email_predicate, "invalid email")
}
# Use in monadic pipeline
def validate_user(user_data) do
{:ok, user_data}
|> Either.from_result()
|> Monad.bind(fn data ->
validators.age.(data.age)
|> Monad.bind(fn _ -> validators.email.(data.email))
|> Monad.bind(fn _ -> Either.right(data))
end)
end
validate_user(%{age: 25, email: "user@example.com"})
end
With Utils Currying
def build_curried_monadic_operations() do
# Create curried monadic operations for pipeline use
# Curry bind for different argument orders
bind_with_fn = Funx.Utils.curry_r(&Monad.bind/2)
bind_with_monad = Funx.Utils.curry(&Monad.bind/2)
# Create reusable monadic transformations
def create_transformers() do
%{
validate_positive: bind_with_fn.(fn x ->
if x > 0 do
Maybe.just(x)
else
Maybe.nothing()
end
end),
safe_divide_by: fn divisor ->
bind_with_fn.(fn x ->
if divisor != 0 do
Maybe.just(x / divisor)
else
Maybe.nothing()
end
end)
end,
transform_with: fn transformer ->
bind_with_fn.(fn x ->
try do
result = transformer.(x)
Maybe.just(result)
rescue
_ -> Maybe.nothing()
end
end)
end
}
end
# Use in functional pipelines
transformers = create_transformers()
Maybe.just(42)
|> transformers.validate_positive.()
|> transformers.safe_divide_by.(6).()
|> transformers.transform_with.(&round/1).()
end
With Eq for Custom Equality
def build_monadic_equality_operations() do
# Use custom Eq instances in monadic contexts
# Create monadic equality testers
def create_equality_validators(eq_instance) do
%{
equals: fn expected_value ->
fn monad_value ->
Monad.bind(monad_value, fn actual_value ->
if Eq.Utils.eq?(actual_value, expected_value, eq_instance) do
Maybe.just(actual_value)
else
Maybe.nothing()
end
end)
end
end,
not_equals: fn forbidden_value ->
fn monad_value ->
Monad.bind(monad_value, fn actual_value ->
if Eq.Utils.not_eq?(actual_value, forbidden_value, eq_instance) do
Maybe.just(actual_value)
else
Maybe.nothing()
end
end)
end
end
}
end
# Use with custom Eq instances
by_id_eq = Eq.Utils.contramap(fn user -> user.id end)
validators = create_equality_validators(by_id_eq)
target_user = %{id: 123, name: "Alice"}
forbidden_user = %{id: 999, name: "Admin"}
Maybe.just(%{id: 123, name: "Alice Updated"})
|> validators.equals.(target_user).() # Passes (same ID)
|> validators.not_equals.(forbidden_user).() # Passes (different ID)
end
Cross-Module Composition
def build_comprehensive_pipeline() do
# Combine Monad, Predicate, Utils, and Eq in one pipeline
# Setup components
age_predicate = Predicate.Utils.between(18, 65)
email_predicate = Predicate.Utils.matches(~r/\A[^@\s]+@[^@\s]+\z/)
user_eq = Eq.Utils.contramap(fn user -> {user.id, user.email} end)
# Curried operations
validate_with = Funx.Utils.curry_r(fn predicate, value ->
if Predicate.test(predicate, value) do
Either.right(value)
else
Either.left("validation failed")
end
end)
transform_with = Funx.Utils.curry_r(fn transformer, monad_value ->
Monad.bind(monad_value, fn value ->
Either.right(transformer.(value))
end)
end)
# Build comprehensive validation pipeline
def validate_and_transform_user(user_data, existing_users) do
user_data
|> Either.right()
|> Monad.bind(validate_with.(age_predicate).(user_data.age))
|> Monad.bind(validate_with.(email_predicate).(user_data.email))
|> transform_with.(fn data ->
%{data | name: String.upcase(data.name)}
end).()
|> Monad.bind(fn processed_user ->
# Check for duplicates using custom Eq
duplicate = Enum.find(existing_users, fn existing ->
Eq.Utils.eq?(processed_user, existing, user_eq)
end)
if duplicate do
Either.left("duplicate user")
else
Either.right(processed_user)
end
end)
end
# Usage
new_user = %{id: 123, name: "alice", age: 25, email: "alice@example.com"}
existing = [%{id: 456, name: "bob", age: 30, email: "bob@example.com"}]
validate_and_transform_user(new_user, existing)
end
Performance Considerations
Protocol Overhead
Protocol dispatch has performance overhead compared to direct module calls:
# Benchmark protocol vs direct calls
def benchmark_monad_operations() do
test_value = Maybe.just(42)
iterations = 100_000
# Protocol version
protocol_fun = fn ->
Enum.reduce(1..iterations, test_value, fn _, acc ->
acc |> Monad.bind(fn x -> SpecificMonad.new( # Use constructor like Maybe.just(, Either.right(, etc.x + 1) end)
end)
end
# Direct version
direct_fun = fn ->
Enum.reduce(1..iterations, test_value, fn _, acc ->
acc |> Maybe.bind(fn x -> Maybe.just(x + 1) end)
end)
end
{protocol_time, _} = :timer.tc(protocol_fun)
{direct_time, _} = :timer.tc(direct_fun)
overhead_percent = (protocol_time / direct_time - 1) * 100
IO.puts("Protocol overhead: #{overhead_percent}%")
end
Use protocols when:
- Genericity is valuable: Function works with multiple monad types
- Performance is acceptable: Not in critical hot paths
- Code reuse matters: Avoiding duplication across monad types
Use direct module calls when:
- Performance is critical: Hot paths or tight loops
- Single monad type: Only working with one specific monad
- Simple operations: Basic transformations that don't benefit from genericity
Anti-Patterns
Avoid these common mistakes when working with the Monad protocol:
- Mixing protocol and module calls in the same generic function
- Using specific constructors like
Maybe.just/1
in protocol-based code - Ignoring monad laws when implementing custom monads
- Overusing protocol when direct module calls would be more efficient
- Nested bind calls instead of chaining with pipe operator
- Not testing law compliance for custom monad implementations
When to Use
Use the Monad protocol when you want to:
- Write generic functions that work with multiple monad types
- Create reusable algorithms independent of specific monad implementation
- Build libraries that support various monadic computations
- Ensure your code follows mathematical monad laws
- Enable composition patterns that work across different contexts
- Abstract over computational patterns (error handling, optional values, etc.)
Built-in Behavior
- Protocol dispatch: Runtime type-based method selection
- Law enforcement: Implementations should satisfy monad laws
- Composition support: Operations designed for chaining and nesting
- Type flexibility: Works with any type implementing the protocol
Summary
Funx.Monad
provides the essential protocol for monadic programming in Elixir. It enables writing generic, reusable functions that work with any monadic type while preserving the mathematical properties that make monads reliable and composable.
The protocol supports three core operations - bind
, return
, and join
- that together provide a foundation for functional composition patterns. When combined with other Funx modules like Utils, Predicate, and Eq, it enables powerful functional programming abstractions.
Key Points:
- Generic programming: Write functions that work with any monad
- Mathematical foundation: Based on category theory laws for reliability
- Composition patterns: Chain computations while preserving context
- Cross-module integration: Combines with other Funx utilities
- Performance trade-offs: Consider overhead vs. genericity benefits
- Law compliance: Always verify implementations satisfy monad laws
Canon: Use for generic monadic algorithms, test law compliance, prefer direct modules for performance-critical code.