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 import Funx.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 information
  • right(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, and concat_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 for map, bind, ap and Funx.Foldable for fold_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 and traverse_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] to Either 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 values
  • left/1: Wrap error values with context
  • map/2: Transform success values, preserve errors
  • bind/2: Chain Either-returning operations with fail-fast behavior
  • ap/2: Apply functions across multiple Either values
  • traverse_a/2: Validate with error accumulation
  • sequence/1: Convert [Either e a] to Either 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.