View Source
Funx.Monad.Reader
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
Reader: Represents deferred computation with read-only environment access
pure(value)
creates a Reader that ignores environment, returns valuerun(reader, env)
executes the deferred computation with environmentasks/1
extracts and transforms environment dataask/0
extracts full environment unchanged
Deferred Computation: Define now, run later with environment
- Reader describes computation steps but doesn't execute until
run/2
- Lazy evaluation: Nothing happens until environment is supplied
- Thunk pattern: Functions that defer computation until needed
Environment Threading: Read-only context passed through computation chain
- Environment flows through
map/2
,bind/2
,ap/2
automatically - Each step can access environment via
asks/1
without explicit passing - Key insight: Eliminates prop drilling and explicit parameter passing
LLM Decision Guide: When to Use Reader
Use Reader For
- Dependency injection - swap implementations without changing logic
- Configuration access - shared settings across computation chain
- Avoiding prop drilling - deep access without threading parameters
- Environment-dependent logic - computation that varies by context
Don't Use Reader For
- State modification - Reader is read-only (use Writer or State)
- Error handling - Reader doesn't short-circuit (use Either)
- Optional values - Reader always requires environment (use Maybe)
- Simple value transformation - Reader adds unnecessary complexity
Core Patterns
Construction and Execution
import Funx.Monad, only: [map: 2, bind: 2, ap: 2]
# Create Reader with pure value
reader = Reader.pure(42)
Reader.run(reader, env) # 42
# Create Reader that uses environment
reader = Reader.asks(fn env -> env.api_key end)
Reader.run(reader, %{api_key: "secret"}) # "secret"
# Access full environment
reader = Reader.ask()
Reader.run(reader, %{foo: "bar"}) # %{foo: "bar"}
Dependency Injection Pattern
# Define services
prod_service = fn name -> "Hello #{name} from production!" end
test_service = fn name -> "Hello #{name} from test!" end
# Create computation that depends on injected service
greet_user = fn user ->
Reader.asks(fn service -> service.(user.name) end)
end
# Build deferred computation
user = %{name: "Alice"}
greeting = greet_user.(user)
# Inject different services
Reader.run(greeting, prod_service) # "Hello Alice from production!"
Reader.run(greeting, test_service) # "Hello Alice from test!"
Configuration Access Pattern
# Configuration-dependent computation
create_api_client = Reader.asks(fn config ->
%ApiClient{
endpoint: config.api_endpoint,
timeout: config.timeout,
retries: config.max_retries
}
end)
# Use configuration
config = %{api_endpoint: "https://api.example.com", timeout: 5000, max_retries: 3}
client = Reader.run(create_api_client, config)
Avoid Prop Drilling Pattern
# Without Reader (prop drilling)
square_tunnel = fn {n, user} -> {n * user} end
format_result = fn {n, user} -> "#{user.name} has #{n}" end
{4, user} |> square_tunnel.() |> format_result.()
# With Reader (clean separation)
square = fn n -> n * n end
format_with_user = fn n ->
Reader.asks(fn user -> "#{user.name} has #{n}" end)
end
Reader.pure(4)
|> map(square)
|> bind(format_with_user)
|> Reader.run(user) # "Alice has 16"
Key Rules
- PURE for values - Use
Reader.pure/1
for environment-independent values - ASKS for environment - Use
Reader.asks/1
to access and transform environment - RUN to execute - Always call
Reader.run/2
to resolve deferred computation - LAZY execution - Reader describes steps, nothing happens until run
- READ-ONLY access - Environment cannot be modified, only read
- NO comparison - Reader doesn't implement Eq/Ord (no meaningful comparison of deferred computations)
Monadic Composition
Sequential Computation (bind)
# Chain Reader computations that depend on previous results
fetch_user_config = fn user_id ->
Reader.asks(fn db -> Database.get_user_config(db, user_id) end)
end
apply_defaults = fn config ->
Reader.asks(fn defaults -> Map.merge(defaults, config) end)
end
# Chain operations
user_id = 123
final_config = Reader.pure(user_id)
|> bind(fetch_user_config)
|> bind(apply_defaults)
# Execute with environment
env = %{db: database, defaults: %{theme: "dark", lang: "en"}}
config = Reader.run(final_config, env)
Parallel Computation (ap)
Note: ap/2
applies a wrapped function to a wrapped value, threading environment through both.
# Combine multiple Reader computations
get_name = Reader.asks(fn user -> user.name end)
get_email = Reader.asks(fn user -> user.email end)
format_contact = Reader.pure(fn name -> fn email -> "#{name} <#{email}>" end end)
# Apply pattern for parallel access
contact = format_contact
|> ap(get_name)
|> ap(get_email)
user = %{name: "Alice", email: "alice@example.com"}
Reader.run(contact, user) # "Alice <alice@example.com>"
Transformation (map)
# Transform Reader results
get_age = Reader.asks(fn user -> user.age end)
categorize_age = fn age ->
cond do
age < 18 -> :minor
age < 65 -> :adult
true -> :senior
end
end
age_category = get_age |> map(categorize_age)
Reader.run(age_category, %{age: 25}) # :adult
Advanced Patterns
Nested Environment Access
# Access nested configuration
get_db_config = Reader.asks(fn env -> env.database.connection_string end)
get_cache_config = Reader.asks(fn env -> env.cache.redis_url end)
# Combine nested access
setup_services = ap(
Reader.pure(fn db -> fn cache -> %{database: db, cache: cache} end end),
get_db_config
) |> ap(get_cache_config)
env = %{
database: %{connection_string: "postgres://..."},
cache: %{redis_url: "redis://..."}
}
services = Reader.run(setup_services, env)
Conditional Logic with Environment
# Environment-dependent branching
get_feature_flag = fn feature ->
Reader.asks(fn env -> Map.get(env.features, feature, false) end)
end
conditional_processing = fn data ->
get_feature_flag.(:use_new_algorithm)
|> bind(fn enabled ->
if enabled do
Reader.pure(new_algorithm(data))
else
Reader.pure(legacy_algorithm(data))
end
end)
end
# Usage
env = %{features: %{use_new_algorithm: true}}
result = conditional_processing.(data) |> Reader.run(env)
Reader Composition
# Compose Readers for complex workflows
authenticate_user = fn credentials ->
Reader.asks(fn auth_service -> auth_service.verify(credentials) end)
end
authorize_action = fn user, action ->
Reader.asks(fn authz_service -> authz_service.can?(user, action) end)
end
fetch_data = fn query ->
Reader.asks(fn db -> db.query(query) end)
end
# Compose into workflow
secure_data_access = fn credentials, action, query ->
authenticate_user.(credentials)
|> bind(fn user -> authorize_action.(user, action))
|> bind(fn _authorized -> fetch_data.(query))
end
# Execute with services
services = %{
auth_service: auth_service,
authz_service: authz_service,
db: database
}
data = Reader.run(secure_data_access.(creds, :read, "SELECT * FROM users"), services)
Integration with Other Monads
Reader + Either (Error Handling)
# Reader that might fail
safe_divide = fn x, y ->
Reader.asks(fn precision ->
if y == 0 do
Either.left("Division by zero")
else
Either.right(Float.round(x / y, precision))
end
end)
end
# Chain Reader and Either
result = Reader.run(safe_divide.(10, 3), 2) # Either.right(3.33)
Reader + Maybe (Optional Values)
# Reader with optional results
lookup_config = fn key ->
Reader.asks(fn config ->
case Map.get(config, key) do
nil -> Maybe.nothing()
value -> Maybe.just(value)
end
end)
end
# Usage
config = %{timeout: 5000}
timeout = Reader.run(lookup_config.(:timeout), config) # Maybe.just(5000)
missing = Reader.run(lookup_config.(:retries), config) # Maybe.nothing()
Testing Patterns
# Test Reader computations by providing mock environments
test "dependency injection with Reader" do
mock_service = fn name -> "Mock greeting for #{name}" end
real_service = fn name -> "Real greeting for #{name}" end
greet = fn name ->
Reader.asks(fn service -> service.(name) end)
end
greeting_reader = greet.("Alice")
# Test with mock
assert Reader.run(greeting_reader, mock_service) == "Mock greeting for Alice"
# Test with real service
assert Reader.run(greeting_reader, real_service) == "Real greeting for Alice"
end
# Test configuration access
test "configuration-dependent behavior" do
process_data = fn data ->
Reader.asks(fn config ->
if config.debug do
"Debug: processing #{inspect(data)}"
else
"Processing data"
end
end)
end
processor = process_data.(%{id: 1})
debug_config = %{debug: true}
prod_config = %{debug: false}
assert Reader.run(processor, debug_config) == "Debug: processing %{id: 1}"
assert Reader.run(processor, prod_config) == "Processing data"
end
Anti-Patterns
# L Don't modify environment (Reader is read-only)
bad_reader = Reader.asks(fn env ->
Map.put(env, :modified, true) # Environment change won't persist!
end)
# L Don't use Reader for error handling
bad_error_handling = Reader.asks(fn env ->
if env.error?, do: raise("Error!"), else: "Success" # Use Either instead
end)
# L Don't nest Reader.run calls unnecessarily
bad_nesting = fn env ->
inner = Reader.pure(42)
Reader.run(inner, env) # Unnecessary - just use 42 directly
end
# L Don't compare Readers directly
reader1 = Reader.pure(42)
reader2 = Reader.pure(42)
# reader1 == reader2 # Won't work - Readers don't implement Eq
# Compare results instead
env = %{}
Reader.run(reader1, env) == Reader.run(reader2, env) # true
Performance Considerations
- Reader computations are lazy - no work until
run/2
- Environment is passed through entire computation chain
- Large environments may impact memory usage
- Consider using focused
asks/1
to extract only needed data - Reader composition creates nested function calls - deep nesting may affect stack
Best Practices
- Use Reader for read-only environment access, not state modification
- Keep environments focused - avoid passing entire application state
- Prefer
asks/1
with specific extractors overask/0
with full environment - Test Reader computations by providing different environments
- Combine Reader with Either/Maybe for error handling and optional values
- Use dependency injection pattern to swap implementations for testing
- Document expected environment structure for Reader computations
Common Use Cases
Web Application Configuration
# Request processing with configuration
process_request = fn request ->
Reader.asks(fn config ->
%{
max_upload_size: config.upload.max_size,
allowed_types: config.upload.allowed_types,
timeout: config.request.timeout
}
end)
|> bind(fn settings -> validate_request(request, settings) end)
end
# Execute with app config
app_config = %{
upload: %{max_size: 10_000_000, allowed_types: ["jpg", "png"]},
request: %{timeout: 30_000}
}
result = Reader.run(process_request.(request), app_config)
Database Operations
# Database operations with connection
fetch_user = fn user_id ->
Reader.asks(fn db -> Database.get_user(db, user_id) end)
end
fetch_user_posts = fn user ->
Reader.asks(fn db -> Database.get_posts_by_user(db, user.id) end)
end
# Compose database operations
get_user_data = fn user_id ->
fetch_user.(user_id)
|> bind(fetch_user_posts)
end
# Execute with database connection
db_connection = Database.connect()
user_data = Reader.run(get_user_data.(123), db_connection)
Feature Flag Systems
# Feature-dependent behavior
render_component = fn component_type ->
Reader.asks(fn features ->
if features.new_ui_enabled do
render_new_component(component_type)
else
render_legacy_component(component_type)
end
end)
end
# Usage with feature flags
features = %{new_ui_enabled: true, analytics_enabled: false}
component = Reader.run(render_component.(:navigation), features)
Summary
Funx.Monad.Reader
provides deferred computation with read-only environment access:
- Deferred execution - describe computation steps, execute later with environment
- Environment threading - automatic context passing without prop drilling
- Dependency injection - swap implementations without changing logic
- Configuration access - shared settings across computation chains
- Lazy evaluation - nothing happens until
Reader.run/2
- Read-only access - environment cannot be modified, only accessed
- Monadic composition - chain environment-dependent computations cleanly
Canon: Use Reader for dependency injection, configuration access, and avoiding prop drilling. Always run/2
to execute deferred computations.