# `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 import `Funx.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 value
- `run(reader, env)` executes the deferred computation with environment
- `asks/1` extracts and transforms environment data
- `ask/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
```elixir
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
```elixir
# 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
```elixir
# 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
```elixir
# 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)
```elixir
# 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.
```elixir
# 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)
```elixir
# 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
```elixir
# 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
```elixir
# 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
```elixir
# 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)
```elixir
# 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)
```elixir
# 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
```elixir
# 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
```elixir
# 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 over `ask/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
```elixir
# 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
```elixir
# 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
```elixir
# 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.