IO Side-Effects
Effect System
Effect System make side effects explicit and trackable, originating from functional programming. Instead of allowing implicit side effects, an effect system explicitly declares, tracks, and controls the side effects of operations throughout the system. This pattern establishes a disciplined approach to handling operations that interact with external systems, modify state, or otherwise have observable effects beyond pure computation.
Key Concepts
- Effect Type: Categorization of the kind of side effect (IO, state mutation, API call)
- Effect Description: Declaration of what an operation might do and its parameters
- Effect Handler: Component that processes effects when they are executed
- Effect Chain: Composition of effectful operations that preserve effect metadata
- Effect Isolation: Containing effects to specific scopes for better control
- Effect Tracking: Recording all effects throughout the system lifecycle
Benefits
- Transparency: Clear documentation of what operations do and their side effects
- Predictability: Easier reasoning about program behavior with explicit effects
- Testability: Side effects can be mocked, inspected, or verified independently
- Composition: Complex operations built from simple ones with controlled effects
- Debugging: Track the source and impact of effects throughout the system
- Separation: Cleanly separate business logic from effect implementation
Implementation Considerations
- Performance Overhead: Balancing explicitness with runtime cost
- Granularity: Right level of detail for effect descriptions
- Handler Strategy: Global vs. contextual handlers for different environments
- Effect Scope: Local vs. propagating effects across component boundaries
- Error Handling: Strategy for effect failures and recovery mechanisms
- Concurrency Model: How effects interact with parallel execution
Core Interfaces
Effectful[V]
├── with_effect(effect) -> Effectful[V] # Add an effect
├── map(fn) -> Effectful[Any] # Transform value
├── bind(fn) -> Effectful[Any] # Chain with another effectful operation
├── run(handler) -> V # Execute with handler
└── get_effects() -> List[Effect] # Inspect effects
Implementation with Effect Library
NERV implements the Effect System pattern using the Effect library, which provides a functional approach to effect handling:
Core Library Components
Component | Description | NERV Integration |
---|---|---|
Effect base class | Base for all effect representations | Extended for domain-specific effects |
TypedEffect class | Type-aware effect representation | Used for strongly-typed effects |
perform() function | Central effect execution mechanism | Core runtime for effect processing |
sync_performer decorator | Registers synchronous handlers | Used for most effect handlers |
async_performer decorator | Registers asynchronous handlers | Used for IO/network effects |
ComposedPerformer class | Combines multiple effect handlers | Used to build handler registry |
TypeDispatcher class | Routes effects to correct handlers | Used for effect routing |
@do decorator | Provides do-notation for effects | Used for sequential effect composition |
parallel_all() function | Executes effects in parallel | Used for concurrent effect processing |
retry() function | Retries failed effects | Used for resilient effect execution |
Core Effect Types
from enum import Enum, auto
from dataclasses import dataclass
from typing import Any, Dict, Optional
class EffectType(Enum):
"""Types of side effects in the system."""
# I/O effects
FILE_READ = auto()
FILE_WRITE = auto()
NETWORK_REQUEST = auto()
DATABASE_QUERY = auto()
# External service effects
MODEL_CALL = auto()
TOOL_INVOKE = auto()
API_CALL = auto()
# State effects
STATE_READ = auto()
STATE_MODIFY = auto()
@dataclass
class EffectIntent:
"""Description of an intended side effect."""
type: EffectType
payload: Any = None
description: str = ""
Effect Handlers
The Effect library enables specialized handlers for different effect types:
from effect import sync_performer, Effect, ComposedPerformer, TypeDispatcher
# FileReadEffect handler
@sync_performer
def perform_file_read(dispatcher, effect):
"""Performer for file read operations."""
path = effect.payload.get("path")
mode = effect.payload.get("mode", "r")
with open(path, mode) as f:
return f.read()
# ApiCallEffect handler
@sync_performer
def perform_api_call(dispatcher, effect):
"""Performer for API call operations."""
import requests
url = effect.payload.get("url")
method = effect.payload.get("method", "GET")
data = effect.payload.get("data")
return requests.request(method, url, json=data).json()
# Compose handlers into a dispatcher
dispatcher = TypeDispatcher({
FileReadEffect: perform_file_read,
ApiCallEffect: perform_api_call,
# Additional handlers...
})
Integration Patterns
The Effect System integrates with other NERV patterns:
- With Reactive Event Mesh: Effects can emit events through the EventBus
- With Temporal Versioning: Effect history can be versioned in TemporalStore
- With State Projection: Effects can trigger state changes through projections
- With Quantum Partitioning: Effects can be distributed across execution units
Pattern Variations
Monadic Effects
Functional approach using monads to encapsulate and compose effects. Best for pure functional systems, with strong type safety and composition capabilities.
# Example monadic effect sequence
result = (
fetch_user(user_id)
.bind(lambda user: fetch_posts(user['id']))
.map(lambda posts: [post['title'] for post in posts])
)
Tagged Effects
Lightweight approach using type tags or metadata to describe effects. Simpler but less powerful, better suited for smaller systems or specific domains.
# Example tagged effect
@effect_type("file_read")
def read_file(path):
# Implementation...
Capability-Based Effects
Effects constrained by explicit capabilities passed to functions. Provides fine-grained control over what operations can perform which effects.
# Example capability-based effect
def process_data(data, io_capability=None):
if io_capability:
io_capability.write_file("result.txt", data)
return process(data)
Library-Specific Implementation Details
The Effect library provides several key mechanisms used in NERV:
Effect Performers
# Register performers for different effect types
performers = ComposedPerformer([
(FileReadEffect, perform_file_read),
(ApiCallEffect, perform_api_call),
(DatabaseEffect, perform_database_query)
])
# Execute effects with the performer
result = perform(effect, performers)
Async Effect Support
from effect import async_performer
@async_performer
async def perform_async_api_call(dispatcher, effect):
"""Async performer for API call operations."""
import aiohttp
async with aiohttp.ClientSession() as session:
method = getattr(session, effect.method.lower())
async with method(effect.url) as response:
return await response.json()
Effect Composition with Do Notation
The Effect library provides a powerful “do notation” for composing multiple effects:
from effect.do import do
@do
def fetch_user_with_posts(user_id):
"""Compose multiple effects with the do notation."""
user = yield fetch_user_effect(user_id)
posts = yield fetch_posts_effect(user['id'])
return {'user': user, 'posts': posts}
Parallel Effect Execution
The Effect library supports parallel execution of independent effects:
from effect import parallel_all
def fetch_dashboard_data(user_id):
"""Execute multiple effects in parallel."""
effects = [
user_info_effect(user_id),
recent_posts_effect(user_id),
notification_count_effect(user_id),
recommended_users_effect(user_id)
]
return parallel_all(effects)
Performance Considerations
Effect systems can introduce overhead, but several optimization strategies are available:
- Effect Batching: Group similar effects to reduce handler overhead
- Lazy Evaluation: Defer effect execution until results are needed
- Caching: Cache results of pure operations with the same parameters
- Effect Fusion: Combine multiple effects into a single operation where possible
- Selective Tracking: Only track effects that are relevant to current context
Integration with Atlas
The Effect System integrates with multiple Atlas subsystems:
Provider System: Tracking API calls to LLM providers
pythonfetch_completion = provider_request_effect( provider="anthropic", model="claude-3-haiku", prompt="Generate a story" )
Knowledge System: Monitoring database operations and document storage
pythonretrieve_docs = database_query_effect( collection="documents", query={"keywords": ["architecture", "patterns"]} )
Agent System: Tracking agent actions and decision processes
pythonagent_action = agent_effect( agent_id="researcher", action="search", parameters={"query": "NERV architecture"} )
Tool System: Recording tool invocations and their effects
pythontool_result = tool_invoke_effect( tool_name="web_search", parameters={"query": "effect system pattern"} )
File System: Monitoring file operations and resource usage
pythonfile_content = file_read_effect( path="/docs/architecture.md" )
Related Patterns
- Monad Pattern: Foundation for effect composition
- Command Pattern: Similar encapsulation of operations
- Decorator Pattern: Alternative for adding effect tracking
- Interpreter Pattern: For effect interpretation
- Middleware Pattern: For effect processing pipelines