First Steps¶
Now that you've seen Haiway in action, let's dive deeper into the core concepts that make it powerful.
Understanding State¶
In Haiway, everything is built around immutable state objects that ensure thread safety and predictable behavior:
from haiway import State
from typing import Sequence, Mapping
class UserPreferences(State):
theme: str = "light"
notifications: bool = True
languages: Sequence[str] = ("en",) # Becomes tuple
metadata: Mapping[str, str] = {} # Becomes immutable
# State objects are immutable
prefs = UserPreferences()
# prefs.theme = "dark" # This would raise an error!
# Instead, create updated instances through copies
dark_prefs: UserPreferences = prefs.updated(theme="dark")
print(f"Original: {prefs.theme}, Updated: {dark_prefs.theme}")
What's happening here:
- Automatic Immutability:
State
base class prevents modification after creation using__setattr__
blocking - Type Conversion: Abstract collection types are automatically converted to immutable equivalents during validation
- Memory Sharing: The
.updated()
method creates structural sharing - unchanged fields reference the same objects - Type Safety: Field types are validated at runtime, ensuring data integrity
- Default Values: Fields can have defaults, and missing fields use type-appropriate defaults
Important Collection Types¶
Always use abstract collection types to ensure immutability:
Sequence[T]
instead oflist[T]
(becomes tuple)Mapping[K,V]
instead ofdict[K,V]
(becomes immutable)Set[T]
instead ofset[T]
(becomes frozenset)
Why this matters:
- Automatic Conversion: Haiway converts mutable collections (
list
,set
) to immutable equivalents (tuple
,frozenset
) during validation - Interface Flexibility: Abstract types allow callers to pass any compatible collection type
- Memory Efficiency: Immutable collections can be safely shared between state instances
- Thread Safety: Immutable collections eliminate race conditions in concurrent code
Context System¶
The context system provides scoped execution environments that manage state and resources automatically:
from haiway import ctx
import asyncio
async def main():
# Contexts can be nested
async with ctx.scope("database"):
print("database context")
async with ctx.scope("transaction"):
print("Nested context")
# Work happens here
print(f"back to database context")
asyncio.run(main())
What's happening here:
- Context Stack: Each
ctx.scope()
creates a new context that inherits from its parent - Automatic Cleanup: When a scope exits, all resources and tasks are automatically cleaned up
- State Isolation: Each context can contain its own state objects, accessed by type
- Context Variables: Uses Python's
contextvars
under the hood for async-safe state propagation - Nested Scopes: Inner scopes can access outer scope state, but not vice versa
Dependency Injection Pattern¶
Haiway uses protocol-based dependency injection to enable flexible, testable architectures:
from typing import Protocol, runtime_checkable
@runtime_checkable
class EmailSending(Protocol):
async def __call__(self, to: str, subject: str, body: str) -> bool: ...
class NotificationService(State):
email_sending: EmailSending
@classmethod
async def notify_user(cls, user_id: str, message: str) -> bool:
service = ctx.state(cls)
return await service.email_sending(
to=f"user-{user_id}@example.com",
subject="Notification",
body=message
)
# Implementation function
async def smtp_email_sending(to: str, subject: str, body: str) -> bool:
# Send email logic here
print(f"Sending email to {to}: {subject}")
return True
# Factory function
def SMTPNotificationService() -> NotificationService:
return NotificationService(email_sending=smtp_email_sending)
async def main():
service = SMTPNotificationService()
async with ctx.scope("app", service):
success = await NotificationService.notify_user("123", "Welcome!")
print(f"Email sent: {success}")
asyncio.run(main())
What's happening here:
- Protocol Contract:
EmailSending
defines the interface with a single__call__
method for maximum flexibility - Service State:
NotificationService
contains the function implementation and provides a clean API - Implementation Function:
smtp_email_sending
is the concrete function that performs the actual email sending - Factory Pattern:
SMTPNotificationService()
creates a pre-configured service with the implementation wired up - Context Retrieval:
ctx.state(cls)
retrieves the service instance from the current context - Transparent Calling: The class method calls the implementation function seamlessly
- Type Safety:
@runtime_checkable
ensures implementations conform to the protocol at runtime
Resource Management¶
Haiway provides automatic resource cleanup through disposable context managers:
from typing import Mapping
from contextlib import asynccontextmanager
@asynccontextmanager
async def database_connection():
print("Opening database connection")
try:
yield DatabaseState(connection={"status": "connected"})
finally:
print("Closing database connection")
class DatabaseState(State):
connection: Mapping[str, str]
async def main():
async with ctx.scope("app", disposables=(database_connection(),)):
db = ctx.state(DatabaseState)
print(f"Database status: {db.connection['status']}")
# Connection automatically closed here
asyncio.run(main())
What's happening here:
- Disposable Pattern:
disposables=()
parameter accepts async context managers that need cleanup - Resource Lifecycle: Resources are opened when entering the scope and closed when exiting
- State Injection: The yielded state object becomes available through
ctx.state()
- Exception Safety: Resources are cleaned up even if exceptions occur within the scope
- Concurrent Cleanup: Multiple disposables are managed concurrently for efficient resource handling
Next Steps¶
Now that you understand the basics:
- Explore the Functionalities
- Learn about State
- See how to structure Packages