View Source
Funx.Monad.Either
Usage Rules
LLM Functional Programming Foundation
Key Concepts for LLMs:
CRITICAL Elixir Implementation: All monadic operations are under Funx.Monad
protocol
- NO separate Functor/Applicative protocols - Elixir protocols cannot be extended after definition
- Always use
Monad.map/2
,Monad.bind/2
,Monad.ap/2
or importFunx.Monad
- Different from Haskell's separate Functor, Applicative, Monad typeclasses
Either: Represents immediate success/failure with detailed error context
left(error)
represents failure with error informationright(value)
represents success with the actual value- Right-biased: Operations work on the Right (success) path
- Immediate/Synchronous: Values exist right now, no deferred execution
- No concurrency: All operations are synchronous - use Effect for async operations
Right-biased Monad: Operations transform Right values, preserve Left errors
map/2
,bind/2
,ap/2
only operate on Right values- Left values (errors) pass through unchanged
- Similar to Maybe but with error context preserved
Validation vs Error-handling: Two distinct patterns
- Validation: Use
traverse_a/2
to collect ALL errors - Error-handling: Use
bind/2
chains that stop on first error - Critical difference: validation accumulates, error-handling short-circuits
Kleisli Functions: Functions a -> Either e b
(unwrapped input, wrapped output)
- Primary use:
traverse/2
,traverse_a/2
, andconcat_map/2
for list operations - Individual use:
bind/2
for single Either values - Example:
validate_email :: String -> Either ValidationError Email
Key List Operation Patterns:
concat([Either e a])
→[a]
(extract all Right values, ignore Left)concat_map([a], kleisli_fn)
→[b]
(apply Kleisli, collect Right results)traverse([a], kleisli_fn)
→Either e [b]
(apply Kleisli, all succeed or first Left)traverse_a([a], kleisli_fn)
→Either [e] [b]
(apply Kleisli, all succeed or collect all Left)sequence([Either e a])
→Either e [a]
(like traverse with identity, first Left or all Right)
Sequence (Category Theory): Transform type constructor order
[Either e a]
→Either e [a]
(list of Either becomes Either of list)- Fails fast: first Left value becomes the result
- Success: all Right values collected into Right list
LLM Decision Guide: When to Use Either
✅ Use Either when:
- Need specific error context/details
- Multiple validation steps with different error types
- Business logic with detailed failure messages
- Error recovery or different handling per error type
- User says: "validate", "check", "ensure", "verify", "error details"
❌ Use Effect when:
- Async operations (database calls, HTTP requests, file I/O)
- Need concurrency or deferred execution
- Operations that take significant time
- User says: "async", "concurrent", "fetch", "call API"
❌ Use Maybe when:
- Simple presence/absence (no error context needed)
- "Not found" is sufficient error information
- Optional fields where missing is normal
⚡ Either Strategy Decision:
- Single operation error-handling: Use
bind/2
chains - Multi-field validation: Use
validate/2
to collect all errors - Transform success values: Use
map/2
with regular functions - Combine Either values: Use
ap/2
for applicative patterns - Convert from Maybe: Use
maybe_to_either/2
with error message - Pattern match results: Use
%Left{left: error}
and%Right{right: value}
struct patterns
⚙️ Function Choice Guide (Mathematical Purpose):
- Chain error-prone operations:
bind/2
with Kleisli functions - Transform success values:
map/2
with regular functions - Validate multiple fields:
validate/2
for comprehensive error collection - Apply functions to multiple Either:
ap/2
for combining contexts - Convert lists:
sequence/1
to collect successes or first failure - Handle specific errors: Pattern match Left values for recovery
LLM Context Clues
User language → Either patterns:
- "validate user input" → Use Either for validation with specific error messages
- "parse and validate" → Chain with
bind/2
for step-by-step validation - "check all fields" → Use
validate/2
to collect all validation errors - "detailed error messages" → Left values contain specific error information
- "stop on first error" → Use
bind/2
chains for fail-fast behavior - "collect all errors" → Use
validate/2
for comprehensive validation
Quick Reference
- Use
right(value)
for success,left(error)
for failure - Chain operations with
bind/2
- stops on first Left (error) - Transform success values with
map/2
- leaves Left unchanged - Use
bind/2
with identity to flatten nested Either values - Validate data comprehensively with
validate/2
- collects all errors - Prefer
fold_l/3
over pattern matching for functional case analysis - Import
Funx.Monad
formap
,bind
,ap
andFunx.Foldable
forfold_l
- Convert from Maybe with error context using helper functions
Overview
Funx.Monad.Either
handles success/failure scenarios with detailed error context.
Use Either for:
- Parsing and validation with specific error messages
- Operations that can fail in multiple ways
- Business logic where error details matter for recovery
- API responses where clients need error specifics
Key insight: Either represents "success or failure" with the failure carrying detailed information. Right-biased operations focus on the success path while preserving any errors encountered.
Constructors
right/1
- Wrap a Success Value
Creates an Either representing success:
Either.right(42) # Success: contains 42
Either.right("valid") # Success: contains "valid"
Either.right([1, 2, 3]) # Success: contains [1, 2, 3]
left/1
- Wrap an Error Value
Creates an Either representing failure:
Either.left("error") # Failure: contains error message
Either.left({:validation, "invalid"}) # Failure: structured error
Either.left(%ValidationError{}) # Failure: error struct
pure/1
- Alias for right/1
Alternative constructor for success values:
Either.pure(42) # Same as Either.right(42)
Core Operations
map/2
- Transform Success Values
Applies a function to Right values, leaves Left values unchanged:
import Funx.Monad
import Funx.Foldable
Either.right("hello")
|> map(&String.upcase/1) # right("HELLO")
Either.left("error")
|> map(&String.upcase/1) # left("error") - function never runs
Use map
when:
- You want to transform the success value
- The transformation function returns a plain value (not wrapped in Either)
- You want to preserve the Either structure
bind/2
- Chain Error-Prone Operations
Chains operations that return Either values, for fail-fast error handling:
import Funx.Monad
import Funx.Foldable
# These functions return Either values
parse_int = fn s ->
case Integer.parse(s) do
{int, ""} -> Either.right(int)
_ -> Either.left("Invalid integer: #{s}")
end
end
validate_positive = fn n ->
if n > 0 do
Either.right(n)
else
Either.left("Must be positive: #{n}")
end
end
Either.right("42")
|> bind(parse_int) # right(42)
|> bind(validate_positive) # right(42)
Either.right("invalid")
|> bind(parse_int) # left("Invalid integer: invalid") - chain stops
|> bind(validate_positive) # left("Invalid integer: invalid") - never runs
Use bind
when:
- You're chaining operations that each can fail
- Each step depends on the success of the previous step
- You want fail-fast behavior (stop on first error)
Common bind pattern:
def process_user_input(input) do
Either.right(input)
|> bind(&parse_user_data/1) # String -> Either Error UserData
|> bind(&validate_user_data/1) # UserData -> Either Error ValidUser
|> bind(&save_user/1) # ValidUser -> Either Error SavedUser
end
ap/2
- Apply Functions Across Either Values
Applies a function in an Either to a value in an Either:
import Funx.Monad
import Funx.Foldable
# Apply a wrapped function to wrapped values
Either.right(fn x -> x + 10 end)
|> ap(Either.right(5)) # right(15)
# Combine multiple Either values
add = fn x -> fn y -> x + y end end
Either.right(add)
|> ap(Either.right(3)) # right(fn y -> 3 + y end)
|> ap(Either.right(4)) # right(7)
# If any value is left, result is left
Either.right(add)
|> ap(Either.left("error1")) # left("error1")
|> ap(Either.right(4)) # left("error1")
Use ap
when:
- You want to apply a function to multiple Either values
- You need all values to be Right for the operation to succeed
- You're implementing applicative patterns
Flattening Nested Either Values with bind
Since there's no join/1
function, use bind/2
with the identity function to flatten nested Either values:
import Funx.Monad
import Funx.Foldable
# Flatten nested Right using bind
nested_right = Either.right(Either.right(42))
bind(nested_right, fn inner -> inner end) # right(42)
# Left in outer - stays Left
outer_left = Either.left("outer error")
bind(outer_left, fn inner -> inner end) # left("outer error")
# Left in inner - becomes Left
inner_left = Either.right(Either.left("inner error"))
bind(inner_left, fn inner -> inner end) # left("inner error")
Use this pattern when:
- You have nested Either values that need flattening
- You're implementing monadic operations manually
- You're working with higher-order Either computations
List Operations
concat/1
- Extract All Right Values
Removes all Left values and unwraps Right values from a list:
Either.concat([
Either.right(1),
Either.left("error1"),
Either.right(3),
Either.left("error2")
]) # [1, 3]
validate/2
- Comprehensive Data Validation
The high-level validation function that collects ALL errors from multiple validators:
# Basic validation with error lists
validate_positive = fn n ->
if n > 0, do: Either.right(n), else: Either.left(["Must be positive"])
end
validate_even = fn n ->
if rem(n, 2) == 0, do: Either.right(n), else: Either.left(["Must be even"])
end
Either.validate(3, [validate_positive, validate_even])
# left(["Must be even"])
Either.validate(-2, [validate_positive, validate_even])
# left(["Must be positive"])
Use validate
when:
- You need comprehensive validation with ALL error details
- You're validating forms or user input
- You want to show users all validation problems at once
- You need to apply multiple validation rules to a single value
Validation with ValidationError
For comprehensive domain validation with structured error handling, use Funx.Errors.ValidationError
:
alias Funx.Errors.ValidationError
# Wrap simple errors in ValidationError
validate_age = fn age ->
Either.lift_predicate(age, &(&1 >= 18), "Must be 18 or older")
|> Either.map_left(&ValidationError.new/1)
end
Either.validate(user, [validate_age])
# left(ValidationError{errors: ["Must be 18 or older"]})
See ValidationError
usage rules for advanced patterns:
- Curried validation functions with
curry_r/1
- Fallback validation with
Either.or_else/2
- Error message transformation techniques
- Group validation with
traverse/2
andtraverse_a/2
- Sequential vs comprehensive validation strategies
concat_map/2
- Apply Function and Collect Rights
Applies a function to each element, collecting only Right results:
traverse/2
- Apply Kleisli to List (First Error or All Success)
Applies a Kleisli function to each element, stopping at first Left:
import Funx.Monad
import Funx.Foldable
# Kleisli function: String -> Either String Integer
parse_number = fn str ->
case Integer.parse(str) do
{num, ""} -> Either.right(num)
_ -> Either.left("Invalid number: #{str}")
end
end
# All succeed - get Either list
Either.traverse(["1", "2", "3"], parse_number) # right([1, 2, 3])
# First failure stops processing
Either.traverse(["1", "invalid", "3"], parse_number)
# left("Invalid number: invalid")
Use traverse
when:
- All operations must succeed for meaningful result
- You want fail-fast behavior on lists
- Converting
[a]
toEither e [b]
with validation
traverse_a/2
- Apply Kleisli to List (Collect All Errors)
Applies a Kleisli function to each element, collecting ALL errors:
# Same Kleisli function as above, but returns error lists for accumulation
validate_number = fn str ->
case Integer.parse(str) do
{num, ""} -> Either.right(num)
_ -> Either.left(["Invalid number: #{str}"]) # List for accumulation
end
end
# Collect ALL errors
Either.traverse_a(["1", "invalid", "3", "bad"], validate_number)
# left(["Invalid number: invalid", "Invalid number: bad"])
# All succeed - get Right list
Either.traverse_a(["1", "2", "3"], validate_number) # right([1, 2, 3])
Use traverse_a
when:
- You want to collect ALL errors from validation
- You need comprehensive error reporting
- You're implementing validation that shows all problems at once
concat_map/2
- Apply Kleisli to List (Collect Successes)
Applies a Kleisli function to each element, collecting only successful results:
# Collect only successes - get plain list
Either.concat_map(["1", "invalid", "3", "bad"], parse_number) # [1, 3]
# All succeed - get all results
Either.concat_map(["1", "2", "3"], parse_number) # [1, 2, 3]
# All fail - get empty list
Either.concat_map(["bad", "invalid", "error"], parse_number) # []
Use concat_map
when:
- Partial success is acceptable
- You want to collect all valid results
- You need resilient processing that continues on failure
sequence/1
- Convert List of Either to Either List
Converts [Either e a]
to Either e [a]
- equivalent to traverse
with identity function:
# All success - collect values
Either.sequence([
Either.right(1),
Either.right(2),
Either.right(3)
]) # right([1, 2, 3])
# First failure stops and returns that error
Either.sequence([
Either.right(1),
Either.left("error2"),
Either.left("error3")
]) # left("error2")
# Relationship to traverse
Either.sequence(either_list) == Either.traverse(either_list, fn x -> x end)
Use sequence
when:
- You have a list of Either values from previous computations
- You want all to succeed, or the first failure
- You're collecting results from multiple operations
Operation Comparison
user_data = ["valid@email.com", "invalid-email", "another@valid.com", "bad-format"]
# traverse: Stop at first error
Either.traverse(user_data, &validate_email/1)
# left("Invalid email format: invalid-email")
# traverse_a: Collect all errors
Either.traverse_a(user_data, &validate_email_with_list_error/1)
# left(["Invalid email format: invalid-email", "Invalid email format: bad-format"])
# concat_map: Collect successes, ignore failures
Either.concat_map(user_data, &validate_email/1)
# ["valid@email.com", "another@valid.com"]
Validation
Validation is a specialized use of Either for comprehensive error collection.
See the validate/2
function in the List Operations section above.
Lifting
lift_predicate/3
- Convert Predicate to Either
Converts a predicate function into Either-returning validation:
validate_positive = Either.lift_predicate(&(&1 > 0), "Must be positive")
validate_positive.(5) # right(5)
validate_positive.(-1) # left("Must be positive")
lift_maybe/2
- Convert Maybe to Either
Converts a Maybe to Either with error context:
maybe_user = Maybe.just(%{name: "Alice"})
Either.lift_maybe(maybe_user, "User not found") # right(%{name: "Alice"})
Maybe.nothing() |> Either.lift_maybe("User not found") # left("User not found")
lift_eq/1
and lift_ord/1
- Lift Comparison Functions
Lifts comparison functions for use in Either context:
# Lift equality for Either values
Either.lift_eq(&==/2)
# Lift ordering for Either values
Either.lift_ord(&compare/2)
Elixir Interoperability
from_result/1
- Convert from Result Tuples
# Convert from {:ok, value} | {:error, reason} tuples
Either.from_result({:ok, 42}) # right(42)
Either.from_result({:error, "fail"}) # left("fail")
to_result/1
- Convert to Result Tuples
# Convert to {:ok, value} | {:error, reason} tuples
Either.to_result(Either.right(42)) # {:ok, 42}
Either.to_result(Either.left("fail")) # {:error, "fail"}
from_try/1
- Safe Function Execution
# Run function safely, catching exceptions
Either.from_try(fn -> 42 / 0 end) # left(%ArithmeticError{})
Either.from_try(fn -> 42 / 2 end) # right(21.0)
to_try!/1
- Unwrap or Raise
Either.to_try!(Either.right(42)) # 42
Either.to_try!(Either.left("error")) # raises RuntimeError: "error"
Folding Either Values
Core Concept: Both Left
and Right
implement the Funx.Foldable
protocol, providing fold_l/3
for catamorphism (breaking down data structures).
fold_l/3
- Functional Case Analysis
The fundamental operation for handling Either values without pattern matching:
import Funx.Foldable
# fold_l(either_value, right_function, left_function)
result = fold_l(either_value,
fn success_value -> "Success: #{success_value}" end, # Right case
fn error_value -> "Error: #{error_value}" end # Left case
)
# Examples
fold_l(Either.right(42),
fn value -> value * 2 end, # Runs this: 84
fn error -> 0 end # Never runs
)
fold_l(Either.left("failed"),
fn value -> value * 2 end, # Never runs
fn error -> "Got: #{error}" end # Runs this: "Got: failed"
)
Use fold_l
when:
- You need to convert Either to a different type
- You want functional case analysis without pattern matching
- You're implementing higher-level combinators
- You need to handle both success and error cases
Folding vs Pattern Matching
# ❌ Imperative pattern matching
case either_result do
%Right{right: value} -> "Success: #{value}"
%Left{left: error} -> "Error: #{error}"
end
# ✅ Functional folding
fold_l(either_result,
fn value -> "Success: #{value}" end,
fn error -> "Error: #{error}" end
)
Advanced Folding Patterns
# Convert Either to Result tuple
to_result = fn either ->
fold_l(either,
fn value -> {:ok, value} end,
fn error -> {:error, error} end
)
end
# Extract value with default
get_or_default = fn either, default ->
fold_l(either,
fn value -> value end,
fn _error -> default end
)
end
# Conditional processing based on Either state
process_conditionally = fn either ->
fold_l(either,
fn value -> expensive_success_operation(value) end,
fn error -> log_error_and_return_default(error) end
)
end
When pattern matching is still appropriate:
# Complex data destructuring that fold_l can't handle elegantly
case either_result do
%Right{right: %User{name: name, role: :admin, permissions: perms}} ->
handle_admin(name, perms)
%Right{right: %User{role: :user} = user} ->
handle_regular_user(user)
%Left{left: %ValidationError{field: field, message: msg}} ->
handle_validation_error(field, msg)
%Left{left: error} ->
handle_generic_error(error)
end
Validation Patterns
Error-handling (Fail Fast)
Use bind/2
for operations that should stop on the first error:
def process_payment(payment_data) do
Either.right(payment_data)
|> bind(&validate_card_number/1) # Stop if card invalid
|> bind(&validate_expiry_date/1) # Stop if expiry invalid
|> bind(&validate_cvv/1) # Stop if CVV invalid
|> bind(&charge_card/1) # Stop if charge fails
end
Validation (Collect All Errors)
Use traverse_a/2
to collect all validation errors:
def validate_user_registration(data) do
fields = [data.name, data.email, data.password, data.age]
validators = [
&validate_name/1,
&validate_email/1,
&validate_password/1,
&validate_age/1
]
Either.traverse_a(fields, validators)
|> fold_l(
fn [name, email, password, age] ->
{:ok, %User{name: name, email: email, password: password, age: age}}
end,
fn errors -> {:error, List.flatten(errors)} end
)
end
Refinement
right?/1
and left?/1
- Type Checks
Either.right?(Either.right(42)) # true
Either.right?(Either.left("err")) # false
Either.left?(Either.left("err")) # true
Either.left?(Either.right(42)) # false
Fallback and Extraction
get_or_else/2
- Extract Value with Default
Either.right(42) |> Either.get_or_else(0) # 42
Either.left("error") |> Either.get_or_else(0) # 0
or_else/2
- Fallback on Left
Either.right(42) |> Either.or_else(fn -> Either.right(0) end) # right(42)
Either.left("error") |> Either.or_else(fn -> Either.right(0) end) # right(0)
map_left/2
- Transform Left Values
# Transform error without affecting success
Either.right(42) |> Either.map_left(&String.upcase/1) # right(42)
Either.left("error") |> Either.map_left(&String.upcase/1) # left("ERROR")
flip/1
- Swap Left and Right
Either.flip(Either.right(42)) # left(42)
Either.flip(Either.left("error")) # right("error")
filter_or_else/3
- Conditional Left Conversion
# Convert Right to Left if predicate fails
Either.right(42) |> Either.filter_or_else(&(&1 > 50), "too small") # left("too small")
Either.right(100) |> Either.filter_or_else(&(&1 > 50), "too small") # right(100)
Combining Two Either Values with ap/2
Use the applicative pattern with ap/2
to combine two Either values with a binary function:
import Funx.Monad
import Funx.Foldable
# Combine two Either values using ap
add_fn = Either.right(&+/2)
ap(add_fn, Either.right(3)) |> ap(Either.right(4)) # right(7)
ap(add_fn, Either.right(3)) |> ap(Either.left("error")) # left("error")
ap(add_fn, Either.left("error")) |> ap(Either.right(4)) # left("error")
# More concise with helper function
combine_either = fn ma, mb, f ->
Either.right(f) |> ap(ma) |> ap(mb)
end
combine_either.(Either.right(3), Either.right(4), &+/2) # right(7)
combine_either.(Either.right(3), Either.left("error"), &+/2) # left("error")
# String concatenation
combine_either.(Either.right("Hello, "), Either.right("World!"), &<>/2) # right("Hello, World!")
# Validation combining
combine_either.(
validate_name("Alice"),
validate_age(30),
fn name, age -> %{name: name, age: age} end
) # right(%{name: "Alice", age: 30}) or left(error)
Use this pattern when:
- You need to combine exactly two Either values with a binary function
- You want applicative-style combination that fails fast on first Left
- You're implementing patterns similar to liftA2 from other functional languages
Common Patterns
API Response Handling
def fetch_user_profile(user_id) do
Either.right(user_id)
|> bind(&validate_user_id/1) # Validate ID format
|> bind(&fetch_from_database/1) # Database lookup
|> bind(&check_permissions/1) # Authorization check
|> bind(&format_profile/1) # Format response
|> fold_l(
fn profile -> {:ok, profile} end,
fn error -> {:error, error} end
)
end
Form Validation with Comprehensive Error Collection
# Create individual field validators that work on the whole form
validate_name_field = fn form_data ->
if String.length(form_data.name) > 0 do
Either.right(form_data.name)
else
Either.left(["Name is required"])
end
end
validate_email_field = fn form_data ->
if String.contains?(form_data.email, "@") and String.length(form_data.email) > 5 do
Either.right(form_data.email)
else
Either.left(["Email must be valid"])
end
end
validate_password_field = fn form_data ->
if String.length(form_data.password) >= 8 do
Either.right(form_data.password)
else
Either.left(["Password must be at least 8 characters"])
end
end
# Validate the entire form - collects ALL validation errors
def validate_registration_form(form_data) do
validators = [
validate_name_field,
validate_email_field,
validate_password_field
]
Either.validate(form_data, validators)
|> fold_l(
fn validated_form ->
{:ok, %{
name: validated_form.name,
email: validated_form.email,
password: validated_form.password
}}
end,
fn all_errors ->
{:error, "Registration failed: #{Enum.join(List.flatten(all_errors), ", ")}"}
end
)
end
# Example usage
form_data = %{name: "", email: "invalid", password: "123"}
validate_registration_form(form_data)
# {:error, "Registration failed: Name is required, Email must be valid, Password must be at least 8 characters"}
valid_form = %{name: "Alice", email: "alice@example.com", password: "securepass123"}
validate_registration_form(valid_form)
# {:ok, %{name: "Alice", email: "alice@example.com", password: "securepass123"}}
Configuration Loading
def load_config(config_path) do
Either.right(config_path)
|> bind(&read_config_file/1) # File -> Either Error String
|> bind(&parse_json/1) # String -> Either Error Map
|> bind(&validate_schema/1) # Map -> Either Error ValidConfig
|> bind(&apply_defaults/1) # ValidConfig -> Either Error FinalConfig
end
defp read_config_file(path) do
File.read(path)
|> Either.from_result()
|> Either.map_left(fn reason -> "Failed to read #{path}: #{reason}" end)
end
defp parse_json(content) do
Jason.decode(content)
|> Either.from_result()
|> Either.map_left(fn %Jason.DecodeError{data: data} -> "Invalid JSON: #{data}" end)
end
Integration with Other Modules
With Funx.Utils
# Curry validation functions
validate_range = Utils.curry(fn min, max, value ->
cond do
value < min -> Either.left("Value #{value} below minimum #{min}")
value > max -> Either.left("Value #{value} above maximum #{max}")
true -> Either.right(value)
end
end)
validate_age = validate_range.(0, 150)
validate_percentage = validate_range.(0, 100)
Either.right(25) |> bind(validate_age) # right(25)
Either.right(-5) |> bind(validate_age) # left("Value -5 below minimum 0")
Conversion from Maybe
# Convert Maybe to Either with error context
def maybe_to_either(maybe_value, error_message) do
Maybe.fold_l(maybe_value,
fn value -> Either.right(value) end,
fn -> Either.left(error_message) end
)
end
# Usage in pipeline
def find_and_validate_user(user_id) do
user_id
|> find_user() # Returns Maybe User
|> maybe_to_either("User not found")
|> bind(&validate_user_active/1) # Continue with Either validation
end
With Predicate Logic
# Convert predicates to Either validators
def predicate_to_either(predicate, error_message) do
fn value ->
if predicate.(value) do
Either.right(value)
else
Either.left(error_message)
end
end
end
# Use with validation
is_adult = fn user -> user.age >= 18 end
validate_adult = predicate_to_either(is_adult, "Must be 18 or older")
Either.right(%{age: 25})
|> bind(validate_adult) # right(%{age: 25})
Either.right(%{age: 16})
|> bind(validate_adult) # left("Must be 18 or older")
Advanced Patterns
Error Recovery
def process_with_fallback(data) do
data
|> process_primary_method()
|> fold_l(
fn result -> Either.right(result) end,
fn _error -> data |> process_fallback_method() end
)
end
# Or using a helper function
def either_or_else(either_result, fallback_fn) do
fold_l(either_result, &Either.right/1, fn _error -> fallback_fn.() end)
end
data
|> process_primary_method()
|> either_or_else(fn -> process_fallback_method(data) end)
Error Mapping
def map_error(either_value, error_mapper) do
fold_l(either_value,
&Either.right/1,
fn error -> Either.left(error_mapper.(error)) end
)
end
# Usage: Convert database errors to user-friendly messages
def friendly_database_error(db_error) do
case db_error do
{:constraint, _} -> "Data validation failed"
{:connection, _} -> "Database temporarily unavailable"
_ -> "An unexpected error occurred"
end
end
database_operation()
|> map_error(&friendly_database_error/1)
Testing Strategies
Unit Testing Validation Logic
defmodule ValidationTest do
use ExUnit.Case
import Funx.Monad
test "email validation with detailed errors" do
# Valid email
assert validate_email("user@example.com") == Either.right("user@example.com")
# Invalid formats
assert validate_email("") == Either.left("Email cannot be empty")
assert validate_email("invalid") == Either.left("Email must contain @")
assert validate_email("user@") == Either.left("Invalid domain")
end
test "chaining validations with bind" do
# Successful chain
result = Either.right("123")
|> bind(&parse_integer/1)
|> bind(&validate_positive/1)
assert result == Either.right(123)
# Chain breaks on first error
result = Either.right("invalid")
|> bind(&parse_integer/1) # Fails here
|> bind(&validate_positive/1) # Never runs
assert {:left, _error} = result
end
test "collecting validation errors with traverse_a" do
invalid_data = ["", "not-email", "invalid-age"]
validators = [&validate_name/1, &validate_email/1, &validate_age/1]
case Either.traverse_a(invalid_data, validators) do
{:left, errors} ->
assert length(errors) == 3 # All three validations failed
assert "Name cannot be empty" in errors
{:right, _} ->
flunk("Expected validation errors")
end
end
end
Performance Considerations
Short-Circuiting
# bind chains short-circuit on first Left
# This makes error-handling very efficient
expensive_validation = fn data ->
# This never runs if earlier validation failed
Process.sleep(1000)
Either.right(data)
end
Either.left("early error")
|> bind(&some_validation/1)
|> bind(expensive_validation) # Never executes
|> bind(&another_validation/1)
# Result: left("early error"), computed instantly
Memory Usage
# Either uses minimal memory overhead
# right(value) stores value plus small wrapper
# left(error) stores error plus small wrapper
# Efficient for error handling
validation_result = %{
user: Either.right(%User{id: 1}), # Small overhead
error: Either.left("Validation failed") # Small overhead
}
Troubleshooting Common Issues
Issue: Nested Either Values
# ❌ Problem: Manual nesting creates Either (Either a)
result = Either.right(user_data)
|> map(&validate_user/1) # validate_user returns Either
# Result: Either (Either User) - nested!
# ✅ Solution: Use bind for functions that return Either
result = Either.right(user_data)
|> bind(&validate_user/1) # Automatically flattens to Either User
Issue: Mixing Validation Strategies
# ❌ Problem: Inconsistent error handling approach
def mixed_validation(data) do
Either.right(data)
|> bind(&validate_required_field/1) # Stops on first error
|> Either.validate([&validate_format/1]) # But this tries to collect all
end
# ✅ Solution: Pick one strategy consistently
def fail_fast_validation(data) do
Either.right(data)
|> bind(&validate_required_field/1)
|> bind(&validate_format/1)
|> bind(&validate_business_rules/1)
end
def collect_all_errors_validation(data) do
fields = [data.field1, data.field2, data.field3]
validators = [&validate_field1/1, &validate_field2/1, &validate_field3/1]
Either.traverse_a(fields, validators)
end
Issue: Pattern Matching Confusion
# ❌ Problem: Imperative pattern matching instead of functional folding
case either_value do
%Right{right: value} -> process_success(value)
%Left{left: error} -> handle_error(error)
end
# ✅ Solution: Use functional folding instead
either_value
|> fold_l(
fn value -> process_success(value) end,
fn error -> handle_error(error) end
)
Issue: Over-using Pattern Matching
# ❌ Problem: Manual unwrapping defeats the purpose
case either_value do
%Right{right: value} ->
new_value = transform(value)
Either.right(new_value)
%Left{left: error} -> Either.left(error)
end
# ✅ Solution: Use map to stay in Either context
either_value |> map(&transform/1)
When Not to Use Either
Use Maybe Instead When
# ❌ Either with generic errors loses its advantage
def find_user(id) do
case get_user(id) do
nil -> Either.left("not found") # Generic error
user -> Either.right(user)
end
end
# ✅ Maybe is simpler for basic presence/absence
def find_user(id) do
case get_user(id) do
nil -> Maybe.nothing()
user -> Maybe.just(user)
end
end
Use Plain Values When
# ❌ Either overhead for operations that can't fail
def calculate_tax(amount) do
Either.right(amount)
|> map(fn amt -> amt * 0.1 end)
end
# ✅ Plain calculation for guaranteed operations
def calculate_tax(amount) do
amount * 0.1
end
Use Exceptions When
# ❌ Either for truly exceptional conditions
def divide(a, b) do
if b == 0 do
Either.left("Division by zero")
else
Either.right(a / b)
end
end
# ✅ Exception for programmer errors
def divide(a, b) when b != 0 do
a / b
end
# Let it crash on division by zero - it's a programming error
Summary
Either provides error-safe computation with detailed failure context:
Core Operations:
right/1
: Wrap success valuesleft/1
: Wrap error values with contextmap/2
: Transform success values, preserve errorsbind/2
: Chain Either-returning operations with fail-fast behaviorap/2
: Apply functions across multiple Either valuestraverse_a/2
: Validate with error accumulationsequence/1
: Convert[Either e a]
toEither e [a]
with fail-fast
Key Patterns:
- Chain error-prone operations with
bind/2
for fail-fast - Validate multiple fields with
traverse_a/2
for error collection - Transform success values with
map/2
- Pattern match for specific error handling and recovery
Convert from {:ok, value} | {:error, reason} tuples
Mathematical Properties:
- Functor:
map
preserves structure (Right-biased) - Applicative:
ap
applies functions in context (fails if any Left) - Monad:
bind
enables dependent sequencing with error propagation
Remember: Either represents "success or detailed failure" - use it when error context matters for debugging, user feedback, or recovery strategies.