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 importFunx.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
andconcat_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
andnothing/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) orconcat_map/2
(collect successes) - Convert lists: Use
sequence/1
to flip[Maybe a]
toMaybe [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 onnothing
- Transform values with
map/2
- leavesnothing
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]
toMaybe [a]
withsequence/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
formap
,bind
,ap
andFunx.Foldable
forfold_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]
toMaybe [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 valuesnothing/0
: Represent absencemap/2
: Transform present values, skip absentbind/2
: Chain Maybe-returning operations with automatic flatteningap/2
: Apply functions across multiple Maybe valuessequence/1
: Convert[Maybe a]
toMaybe [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.