View Source Funx.Monad.Maybe 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

Kleisli Function: A function a -> Maybe b (takes unwrapped value, returns wrapped value)

  • Primary use: traverse/2 and concat_map/2 for list operations
  • Individual use: Monad.bind/2 for single Maybe values
  • Example: find_user :: UserId -> Maybe User

Key List Operation Patterns:

  • concat([Maybe a])[a] (extract all Just values, ignore Nothing)
  • concat_map([a], kleisli_fn)[b] (apply Kleisli, collect Just results)
  • traverse([a], kleisli_fn)Maybe [b] (apply Kleisli, all succeed or Nothing)
  • sequence([Maybe a])Maybe [a] (like traverse with identity function)

Functor: Something you can map over while preserving structure

  • Monad.map/2 :: (a -> b) -> Maybe a -> Maybe b
  • Transforms the present value, leaves Nothing unchanged

Applicative: Allows applying functions inside a context

  • Monad.ap/2 :: Maybe (a -> b) -> Maybe a -> Maybe b
  • Can combine multiple Maybe values

Monad: Supports bind for chaining dependent computations

  • Monad.bind/2 :: Maybe a -> (a -> Maybe b) -> Maybe b
  • Flattens nested Maybe values automatically

Sequence (Category Theory): Swap the order of two type constructors

  • [Maybe a]Maybe [a] (list of Maybe becomes Maybe of list)
  • Not about sequential processing - about type transformation

Maybe: Represents immediate presence/absence of values

  • Presence: Value exists and is usable (just(value))
  • Absence: Value is missing, incomplete, or unavailable (nothing())
  • Immediate/Synchronous: Values exist right now, no deferred execution
  • No concurrency: All operations are synchronous - use Effect for async operations

LLM Decision Guide: When to Use Maybe

✅ Use Maybe when:

  • Simple presence/absence (user profile, config value)
  • No error context needed ("not found" is sufficient)
  • Chaining operations that should skip on absence
  • Optional fields or nullable database columns
  • User says: "optional", "might not exist", "could be missing"

❌ 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 Either when:

  • Need specific error context ("user not found", "validation failed")
  • Multiple error types or recovery strategies
  • Business validation with detailed failure messages
  • User says: "validate", "check requirements", "ensure valid"

⚡ Maybe Strategy Decision:

  • Simple presence check: Use just/1 and nothing/0 constructors
  • Chain operations: Use bind/2 for individual Maybe sequencing
  • Transform present values: Use map/2 with regular functions
  • Combine multiple Maybe values: Use ap/2 for applicative pattern
  • Apply Kleisli to lists: Use traverse/2 (all must succeed) or concat_map/2 (collect successes)
  • Convert lists: Use sequence/1 to flip [Maybe a] to Maybe [a]
  • Pattern match results: Use case with %Just{value: value} and %Nothing{}

⚙️ Function Choice Guide (Mathematical Purpose):

  • Chain dependent lookups: bind/2 with functions returning Maybe
  • Transform present values: map/2 with functions returning plain values
  • Apply functions to multiple Maybe values: ap/2 for combining contexts
  • Handle missing values: Pattern match or use from_nil/1, to_nil/1
  • Work with lists: sequence/1, traverse/2, traverse_a/2

LLM Context Clues

User language → Maybe patterns:

  • "optional user profile" → find_user/1 returning Maybe User
  • "might not have email" → Maybe String for optional email field
  • "chain lookups" → bind/2 with multiple Maybe-returning functions
  • "transform if present" → map/2 to modify just the value
  • "combine optional values" → ap/2 to apply function across Maybe values
  • "list of optional items" → sequence/1 to collect all present values

Quick Reference

  • Use just(value) for present values, nothing() for absence
  • Chain operations with bind/2 - they skip automatically on nothing
  • Transform values with map/2 - leaves nothing unchanged
  • Combine multiple Maybe values with ap/2
  • Use bind/2 with identity to flatten nested Maybes: bind(nested_maybe, fn x -> x end)
  • Convert [Maybe a] to Maybe [a] with sequence/1
  • Prefer fold_l/3 over pattern matching for functional case analysis
  • Note: Maybe values are structs %Just{value: ...} or %Nothing{}, not tagged tuples
  • Import Funx.Monad for map, bind, ap and Funx.Foldable for fold_l

Overview

Funx.Monad.Maybe handles presence and absence without explicit null checks.

Use Maybe for:

  • Optional fields and nullable database columns
  • Operations that might not return a value
  • Chaining computations that should skip on missing data
  • Simple presence/absence (no detailed error context needed)

Key insight: Maybe represents "optional" - either there's a value (just) or there isn't (nothing). All operations respect this, automatically skipping work when there's nothing to work with.

Constructors

just/1 - Wrap a Present Value

Creates a Maybe containing a value:

Maybe.just(42)        # Present: contains 42
Maybe.just("hello")   # Present: contains "hello"
Maybe.just([1, 2, 3]) # Present: contains [1, 2, 3]

nothing/0 - Represent Absence

Creates a Maybe representing absence:

Maybe.nothing()       # Absent: contains no value

pure/1 - Alias for just/1

Alternative constructor for present values:

Maybe.pure(42)    # Same as Maybe.just(42)

Core Operations

map/2 - Transform Present Values

Applies a function to the value inside a just, leaves nothing unchanged:

import Funx.Monad
import Funx.Foldable

Maybe.just(5)
|> map(fn x -> x * 2 end)    # just(10)

Maybe.nothing()
|> map(fn x -> x * 2 end)    # nothing() - function never runs

Use map when:

  • You want to transform the value if it exists
  • The transformation function returns a plain value (not wrapped in Maybe)
  • You want to preserve the Maybe structure

bind/2 - Chain Dependent Operations

Chains operations that return Maybe values, automatically flattening nested Maybe:

import Funx.Monad
import Funx.Foldable

# These functions return Maybe values
find_user = fn id -> if id > 0, do: Maybe.just(%{id: id}), else: Maybe.nothing() end
get_email = fn user -> if user.id == 1, do: Maybe.just("user@example.com"), else: Maybe.nothing() end

Maybe.just(1)
|> bind(find_user)    # just(%{id: 1})
|> bind(get_email)    # just("user@example.com")

Maybe.just(-1)
|> bind(find_user)    # nothing() - chain stops here
|> bind(get_email)    # nothing() - this never runs

Use bind when:

  • You're chaining operations that each return Maybe
  • Each step depends on the result of the previous step
  • You want automatic short-circuiting on nothing

Common bind pattern:

def process_user_id(user_id) do
  Maybe.just(user_id)
  |> bind(&find_user/1)         # UserId -> Maybe User
  |> bind(&get_user_profile/1)  # User -> Maybe Profile  
  |> bind(&format_name/1)       # Profile -> Maybe String
end

ap/2 - Apply Functions Across Maybe Values

Applies a function in a Maybe to a value in a Maybe:

import Funx.Monad
import Funx.Foldable

# Apply a wrapped function to wrapped values
Maybe.just(fn x -> x + 10 end)
|> ap(Maybe.just(5))          # just(15)

# Combine multiple Maybe values
add = fn x -> fn y -> x + y end end

Maybe.just(add)
|> ap(Maybe.just(3))          # just(fn y -> 3 + y end)
|> ap(Maybe.just(4))          # just(7)

# If any value is nothing, result is nothing
Maybe.just(add)
|> ap(Maybe.nothing())        # nothing()
|> ap(Maybe.just(4))          # nothing()

Use ap when:

  • You want to apply a function to multiple Maybe values
  • You need all values to be present for the operation to succeed
  • You're implementing applicative patterns

Folding Maybe Values

Core Concept: Both Just and Nothing 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 Maybe values without pattern matching:

import Funx.Foldable

# fold_l(maybe_value, just_function, nothing_function)
result = fold_l(maybe_value, 
  fn value -> "Found: #{value}" end,  # Just case
  fn -> "Not found" end               # Nothing case
)

# Examples
fold_l(Maybe.just(42), 
  fn value -> value * 2 end,    # Runs this: 84
  fn -> 0 end                   # Never runs
)

fold_l(Maybe.nothing(), 
  fn value -> value * 2 end,    # Never runs
  fn -> "No value" end          # Runs this: "No value"
)

Use fold_l when:

  • You need to convert Maybe to a different type
  • You want functional case analysis without pattern matching
  • You're implementing higher-level combinators
  • You need to handle both present and absent cases

Folding vs Pattern Matching

# ❌ Imperative pattern matching
case maybe_value do
  %Just{value: value} -> "Found: #{value}"
  %Nothing{} -> "Not found"
end

# ✅ Functional folding
fold_l(maybe_value,
  fn value -> "Found: #{value}" end,
  fn -> "Not found" end
)

Advanced Folding Patterns

# Extract value with default
get_or_default = fn maybe, default ->
  fold_l(maybe,
    fn value -> value end,
    fn -> default end
  )
end

# Convert Maybe to result tuple
to_result = fn maybe ->
  fold_l(maybe,
    fn value -> {:ok, value} end,
    fn -> {:error, :not_found} end
  )
end

# Conditional processing
process_if_present = fn maybe ->
  fold_l(maybe,
    fn value -> expensive_operation(value) end,
    fn -> :skipped end
  )
end

Flattening Nested Maybe Values with bind

Since there's no join/1 function, use bind/2 with the identity function to flatten nested Maybe values:

import Funx.Monad

# Flatten nested Maybe using bind
nested = Maybe.just(Maybe.just(42))
bind(nested, fn inner -> inner end)    # just(42)

# Nothing in outer - stays nothing
outer_nothing = Maybe.nothing()
bind(outer_nothing, fn inner -> inner end)    # nothing()

# Nothing in inner - becomes nothing  
inner_nothing = Maybe.just(Maybe.nothing())
bind(inner_nothing, fn inner -> inner end)    # nothing()

Use this pattern when:

  • You have nested Maybe values that need flattening
  • You're implementing monadic operations manually
  • You're working with higher-order Maybe computations

Functional Error Handling

Important: Maybe values are structs %Just{value: ...} or %Nothing{}, not tagged tuples. Pattern matching must respect this shape.

Maybe values are best handled with functional folding:

fold_l(maybe_value,
  fn value -> "Found: #{value}" end,
  fn -> "Not found" end
)

Common patterns:

# Extract with default
value = fold_l(maybe_user,
  fn user -> user.name end,
  fn -> "Guest" end
)

# Process only if present
fold_l(maybe_config,
  fn config -> apply_config(config) end,
  fn -> :ok end
)

Refinement

just?/1 and nothing?/1 - Type Checks

Maybe.just?(Maybe.just(42))      # true
Maybe.just?(Maybe.nothing())     # false

Maybe.nothing?(Maybe.nothing())  # true
Maybe.nothing?(Maybe.just(42))   # false

Fallback and Extraction

get_or_else/2 - Extract Value with Default

Maybe.just(42) |> Maybe.get_or_else(0)        # 42
Maybe.nothing() |> Maybe.get_or_else(0)       # 0

or_else/2 - Fallback on Nothing

Maybe.just(42) |> Maybe.or_else(fn -> Maybe.just(0) end)     # just(42)
Maybe.nothing() |> Maybe.or_else(fn -> Maybe.just(0) end)    # just(0)

Combining Two Maybe Values with ap/2

Use the applicative pattern with ap/2 to combine two Maybe values with a binary function:

import Funx.Monad

# Combine two Maybe values using ap
add_fn = Maybe.just(&+/2)
ap(add_fn, Maybe.just(3)) |> ap(Maybe.just(4))     # just(7)
ap(add_fn, Maybe.just(3)) |> ap(Maybe.nothing())   # nothing()
ap(add_fn, Maybe.nothing()) |> ap(Maybe.just(4))   # nothing()

# More concise with helper function
combine_maybe = fn ma, mb, f ->
  Maybe.just(f) |> ap(ma) |> ap(mb)
end

combine_maybe.(Maybe.just(3), Maybe.just(4), &+/2)         # just(7)
combine_maybe.(Maybe.just(3), Maybe.nothing(), &+/2)       # nothing()

# String concatenation
combine_maybe.(Maybe.just("Hello, "), Maybe.just("World!"), &<>/2)  # just("Hello, World!")

# Working with structs
combine_maybe.(
  Maybe.just(%{name: "Alice"}),
  Maybe.just(%{age: 30}),
  fn user, age_info -> Map.merge(user, age_info) end
)  # just(%{name: "Alice", age: 30})

Use this pattern when:

  • You need to combine exactly two Maybe values with a binary function
  • You want applicative-style combination that fails fast on first Nothing
  • You're implementing patterns similar to liftA2 from other functional languages

Common Patterns

Safe Navigation

Instead of nested null checks:

# Imperative style with null checks
def get_user_city(user_id) do
  fold_l(find_user(user_id),
    fn user ->
      fold_l(get_address(user),
        fn address -> address.city end,
        fn -> nil end
      )
    end,
    fn -> nil end
  )
end

# Functional style with Maybe
def get_user_city(user_id) do
  Maybe.just(user_id)
  |> bind(&find_user_maybe/1)     # Returns Maybe User
  |> bind(&get_address_maybe/1)   # Returns Maybe Address  
  |> map(& &1.city)               # Extract city if present
end

Optional Field Processing

# Process optional email field
def send_welcome_email(user) do
  user.email
  |> Maybe.from_nil()
  |> map(&normalize_email/1)
  |> bind(&validate_email/1)      # Returns Maybe valid_email
  |> map(&send_email/1)           # Send if valid
  |> Either.lift_maybe("No valid email")
end

Collecting Optional Values

# Gather optional settings
def load_user_preferences(user_id) do
  preferences = [
    get_theme_preference(user_id),      # Maybe String
    get_language_preference(user_id),   # Maybe String  
    get_timezone_preference(user_id)    # Maybe String
  ]
  
  Maybe.sequence(preferences)
  |> map(fn [theme, lang, tz] -> 
    %{theme: theme, language: lang, timezone: tz}
  end)
end

Integration with Other Modules

With Funx.Utils

# Curry functions for Maybe operations
find_by_id = Utils.curry(&find_user/1)
user_finder = find_by_id.(42)

Maybe.just(database)
|> bind(user_finder)

# Compose Maybe-returning functions
compose_maybe = Utils.compose([
  &Maybe.from_nil/1,
  &get_user_profile/1,  # Returns Maybe
  &format_display_name/1
])

With Predicate Logic

# Convert predicates to Maybe values
def predicate_to_maybe(predicate, value) do
  if predicate.(value) do
    Maybe.just(value)
  else
    Maybe.nothing()
  end
end

# Use with validation
is_adult = fn user -> user.age >= 18 end

Maybe.just(user)
|> bind(fn u -> predicate_to_maybe(is_adult, u) end)
|> map(&process_adult_user/1)

Conversions Between Types

Conversion to Either

# Convert Maybe to Either with error context using the built-in function
Either.lift_maybe(maybe_value, "Error message")

# Usage in validation pipeline
Maybe.just(user_input)
|> bind(&parse_user_id/1)
|> maybe_to_either("Invalid user ID")
|> Either.bind(&detailed_validation/1)

List Operations

concat/1 - Extract All Just Values

Removes all Nothing values and unwraps Just values from a list:

Maybe.concat([
  Maybe.just(1),
  Maybe.nothing(),
  Maybe.just(3),
  Maybe.nothing()
])                              # [1, 3]

concat_map/2 - Apply Function and Collect Just Results

traverse/2 - Apply Kleisli to List (All Must Succeed)

Applies a Kleisli function to each element, requiring all operations to succeed:

import Funx.Monad
import Funx.Foldable

# Kleisli function: String -> Maybe Integer
parse_number = fn str ->
  case Integer.parse(str) do
    {num, ""} -> Maybe.just(num)
    _ -> Maybe.nothing()
  end
end

# All succeed - get Maybe list
Maybe.traverse(["1", "2", "3"], parse_number)  # just([1, 2, 3])

# Any fail - get Nothing
Maybe.traverse(["1", "invalid", "3"], parse_number)  # nothing()

Use traverse when:

  • All operations must succeed for meaningful result
  • You need fail-fast behavior on lists
  • Converting [a] to Maybe [b] with validation

concat_map/2 - Apply Kleisli to List (Collect Successes)

Applies a Kleisli function to each element, collecting only successful results:

# Same Kleisli function as above
parse_number = fn str ->
  case Integer.parse(str) do
    {num, ""} -> Maybe.just(num)
    _ -> Maybe.nothing()
  end
end

# Collect only successes - get plain list
Maybe.concat_map(["1", "invalid", "3", "bad"], parse_number)  # [1, 3]

# All succeed - get all results
Maybe.concat_map(["1", "2", "3"], parse_number)  # [1, 2, 3]

# All fail - get empty list
Maybe.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 Maybe to Maybe List

Converts [Maybe a] to Maybe [a] - equivalent to traverse with identity function:

# All present - success
Maybe.sequence([
  Maybe.just(1),
  Maybe.just(2), 
  Maybe.just(3)
])                            # just([1, 2, 3])

# Any absent - failure  
Maybe.sequence([
  Maybe.just(1),
  Maybe.nothing(),
  Maybe.just(3)  
])                            # nothing()

# Relationship to traverse  
Maybe.sequence(maybe_list) == Maybe.traverse(maybe_list, fn x -> x end)

Use sequence when:

  • You have a list of Maybe values from previous computations
  • You want all values to be present, or nothing at all
  • You're collecting results from multiple optional operations

List Operations Comparison

user_ids = [1, 2, 999, 4]  # 999 is invalid ID

# traverse: All must succeed or nothing
Maybe.traverse(user_ids, &find_user/1)
# nothing() - because user 999 doesn't exist

# concat_map: Collect successes, ignore failures  
Maybe.concat_map(user_ids, &find_user/1)
# [user1, user2, user4] - got the valid users

# sequence: All existing values must be present
existing_maybes = [Maybe.just(user1), Maybe.nothing(), Maybe.just(user3)]
Maybe.sequence(existing_maybes)  # nothing() - because one is Nothing

Lifting

lift_predicate/2 - Convert Value Based on Predicate

Converts a value to Just if it meets a predicate, otherwise Nothing:

validate_positive = Maybe.lift_predicate(&(&1 > 0))

validate_positive.(5)   # just(5)
validate_positive.(-1)  # nothing()

lift_either/1 - Convert Either to Maybe

Converts an Either to Maybe, discarding Left error information:

Maybe.lift_either(Either.right(42))    # just(42)
Maybe.lift_either(Either.left("error")) # nothing()

lift_identity/1 - Convert Identity to Maybe

Converts an Identity monad to Maybe:

Maybe.lift_identity(Identity.pure(42))  # just(42)

lift_eq/1 and lift_ord/1 - Lift Comparison Functions

Lifts comparison functions for use in Maybe context:

# Lift equality for Maybe values
Maybe.lift_eq(&==/2)

# Lift ordering for Maybe values  
Maybe.lift_ord(&compare/2)

Elixir Interoperability

from_nil/1 - Convert Nil to Maybe

Maybe.from_nil(42)      # just(42)
Maybe.from_nil(nil)     # nothing()

to_nil/1 - Convert Maybe to Nil

Maybe.to_nil(Maybe.just(42))    # 42
Maybe.to_nil(Maybe.nothing())   # nil

from_result/1 - Convert Result Tuple to Maybe

Maybe.from_result({:ok, 42})        # just(42)
Maybe.from_result({:error, "fail"}) # nothing()

to_result/1 - Convert Maybe to Result Tuple

Maybe.to_result(Maybe.just(42))    # {:ok, 42}
Maybe.to_result(Maybe.nothing())   # {:error, nil}

from_try/1 - Safe Function Execution

# Run function safely, returning Nothing on exception
Maybe.from_try(fn -> 42 / 0 end)  # nothing()
Maybe.from_try(fn -> 42 / 2 end)  # just(21.0)

to_try!/2 - Unwrap or Raise with Custom Error

Maybe.to_try!(Maybe.just(42), "No value")     # 42
Maybe.to_try!(Maybe.nothing(), "No value")   # raises RuntimeError: "No value"

Testing Strategies

Property-Based Testing

defmodule MaybePropertyTest do
  use ExUnit.Case
  use StreamData

  property "map preserves just structure" do
    check all value <- term(),
              f <- StreamData.constant(fn x -> x + 1 end) do
      result = Maybe.just(value) |> Monad.map(f)
      assert Maybe.just?(result)
    end
  end

  property "map on nothing returns nothing" do
    check all f <- StreamData.constant(fn x -> x + 1 end) do
      result = Maybe.nothing() |> Monad.map(f)
      assert result == Maybe.nothing()
    end
  end

  property "bind with just applies function" do
    check all value <- integer(),
              result_value <- integer() do
      f = fn _x -> Maybe.just(result_value) end
      result = Maybe.just(value) |> Monad.bind(f)
      assert result == Maybe.just(result_value)
    end
  end
end

Unit Testing Common Patterns

defmodule MaybeTest do
  use ExUnit.Case
  import Funx.Monad

  test "chaining operations with bind" do
    # Successful chain
    result = Maybe.just(5)
    |> bind(fn x -> Maybe.just(x * 2) end)
    |> bind(fn x -> Maybe.just(x + 1) end)
    
    assert result == Maybe.just(11)
    
    # Chain breaks on nothing
    result = Maybe.just(5)
    |> bind(fn _x -> Maybe.nothing() end)
    |> bind(fn x -> Maybe.just(x + 1) end)  # Never executed
    
    assert result == Maybe.nothing()
  end

  test "combining values with ap" do
    add = fn x -> fn y -> x + y end end
    
    result = Maybe.just(add)
    |> ap(Maybe.just(10))
    |> ap(Maybe.just(5))
    
    assert result == Maybe.just(15)
    
    # Fails if any value is nothing
    result = Maybe.just(add)
    |> ap(Maybe.nothing())
    |> ap(Maybe.just(5))
    
    assert result == Maybe.nothing()
  end

  test "sequence converts list of Maybe to Maybe list" do
    # All present
    result = Maybe.sequence([
      Maybe.just(1),
      Maybe.just(2),
      Maybe.just(3)
    ])
    assert result == Maybe.just([1, 2, 3])
    
    # Any absent
    result = Maybe.sequence([
      Maybe.just(1),
      Maybe.nothing(),
      Maybe.just(3)
    ])
    assert result == Maybe.nothing()
  end
end

Performance Considerations

Lazy Evaluation

# Operations on nothing are essentially no-ops
# This makes Maybe chains very efficient when they short-circuit early

expensive_computation = fn x ->
  # This never runs if we start with nothing
  Process.sleep(1000)
  x * 2
end

Maybe.nothing()
|> map(expensive_computation)    # Returns immediately
|> bind(fn x -> Maybe.just(x + 1) end)
# Result: nothing(), computed instantly

Memory Usage

# Maybe uses minimal memory overhead
# just(value) stores the value plus a small wrapper
# nothing() is a singleton, shared across all nothing instances

# Efficient for optional fields
user = %{
  id: 1,
  name: "Alice",
  email: Maybe.just("alice@example.com"),  # Small overhead
  phone: Maybe.nothing()                   # Shared singleton
}

Troubleshooting Common Issues

Issue: Nested Maybe Values

# ❌ Problem: Manual nesting creates Maybe Maybe a
result = Maybe.just(user_id)
|> map(&find_user/1)  # find_user returns Maybe User
# Result: Maybe (Maybe User) - nested!

# ✅ Solution: Use bind for functions that return Maybe
result = Maybe.just(user_id) 
|> bind(&find_user/1)  # Automatically flattens to Maybe User

Issue: Mixing Maybe with Nil

# ❌ Problem: Inconsistent nil/Maybe usage
def process_data(data) do
  fold_l(get_user(data),
    fn user -> Maybe.just(user) end,
    fn -> Maybe.nothing() end
  )
  |> map(&transform_user/1)
end

# ✅ Solution: Convert early, stay in Maybe context
def process_data(data) do
  get_user(data)
  |> Maybe.from_nil()      # Convert nil -> Maybe early
  |> map(&transform_user/1)
end

Issue: Pattern Matching Confusion

# ❌ Problem: Imperative pattern matching instead of functional folding
case maybe_user do
  %Just{value: user} -> process_user(user)
  %Nothing{} -> handle_missing()
end

# ✅ Solution: Use functional folding
fold_l(maybe_user,
  fn user -> process_user(user) end,
  fn -> handle_missing() end
)

Issue: Over-using Pattern Matching

# ❌ Problem: Manual unwrapping defeats the purpose
fold_l(maybe_value,
  fn value -> 
    new_value = transform(value)
    Maybe.just(new_value)
  end,
  fn -> Maybe.nothing() end
)

# ✅ Solution: Use map to stay in Maybe context
maybe_value |> map(&transform/1)

When Not to Use Maybe

Use Either Instead When

# ❌ Maybe loses error context
def validate_email(email) do
  if valid_email_format?(email) do
    Maybe.just(email)
  else
    Maybe.nothing()  # Lost: why did validation fail?
  end
end

# ✅ Either preserves error context  
def validate_email(email) do
  cond do
    String.length(email) == 0 -> Either.left("Email cannot be empty")
    not String.contains?(email, "@") -> Either.left("Email must contain @")
    not valid_domain?(email) -> Either.left("Invalid email domain")
    true -> Either.right(email)
  end
end

Use Plain Values When

# ❌ Maybe overhead for always-present values
def calculate_tax(amount) do
  # Tax rate is always known, no need for Maybe
  Maybe.just(amount)
  |> map(fn amt -> amt * 0.1 end)
end

# ✅ Plain calculation for guaranteed values
def calculate_tax(amount) do
  amount * 0.1
end

Summary

Maybe provides null-safe computation for optional values:

Core Operations:

  • just/1: Wrap present values
  • nothing/0: Represent absence
  • map/2: Transform present values, skip absent
  • bind/2: Chain Maybe-returning operations with automatic flattening
  • ap/2: Apply functions across multiple Maybe values
  • sequence/1: Convert [Maybe a] to Maybe [a]

Key Patterns:

  • Chain dependent lookups with bind/2
  • Transform values with map/2
  • Combine multiple optional values with ap/2
  • Collect all-or-nothing results with sequence/1
  • Pattern match for final handling

Mathematical Properties:

  • Functor: map preserves structure
  • Applicative: ap applies functions in context
  • Monad: bind enables dependent sequencing with flattening

Remember: Maybe represents "optional" - use it when absence is a valid state that should be handled gracefully, without needing specific error information.