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 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

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 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

# 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.