Task Monad
Lazy computations with automatic memoization
The Task monad represents lazy computations that can be executed later. Once executed, the result is automatically memoized (cached) for subsequent accesses, making it ideal for expensive operations.
Task[T] wraps a computation that produces a value of type T. The computation is deferred until run() is called, and the result is cached after the first execution.
Creating Tasks
Wrapping Computations
The Task constructor wraps a function that will execute lazily when run.
from better_py import Task
task = Task(lambda: expensive_computation())
result = task.run() # Executes and caches resultUse the constructor when you need to defer expensive operations.
Lifting Pure Values
The Task.pure method lifts a pure value into a Task context.
from better_py import Task
Task.pure(42).run() # 42Use this when you need to create a Task from an already-computed value.
Creating Delayed Computations
The Task.delay method creates a delayed computation that executes when run.
from better_py import Task
# Delay a value
Task.delay(42).run() # 42
# Delay a function
Task.delay(lambda: expensive_computation()).run()Use delay to defer expensive operations until they're actually needed.
Running Tasks
Executing and Caching
The run method executes the Task and caches the result for subsequent calls.
from better_py import Task
task = Task(lambda: 42)
result = task.run() # 42
# Subsequent calls return cached result
result2 = task.run() # 42 (from cache)This is the primary way to execute Task computations.
Checking Cache Status
The is_cached method checks whether the Task has been executed.
from better_py import Task
task = Task(lambda: 42)
task.is_cached() # False
task.run()
task.is_cached() # TrueUse this to check if a Task has already been computed.
Peeking at Cached Values
The peek method returns the cached value without executing if not yet run.
from better_py import Task
task = Task(lambda: 42)
task.peek() # None (not yet executed)
task.run() # 42
task.peek() # 42 (cached)Use peek to access cached values when you're not sure if the Task has been run.
Transforming Tasks
Mapping Results
The map method transforms the result of a Task computation.
from better_py import Task
Task(lambda: 5).map(lambda x: x * 2).run() # 10The transformation is only applied when the Task is run, not when it's created.
Chaining Operations
The flat_map method chains Task operations sequentially.
from better_py import Task
def fetch_data():
return Task(lambda: [1, 2, 3])
def process_data(data):
return Task(lambda: sum(data))
result = fetch_data().flat_map(process_data)
result.run() # 6Use flat_map when the second computation depends on the result of the first.
Combining Tasks
The zip method combines two Tasks into a tuple of their results.
from better_py import Task
result = Task(lambda: 5).zip(Task(lambda: "hello"))
result.run() # (5, "hello")Use zip when you need to run multiple independent Tasks together.
Filtering Results
The filter method conditionally returns the result or None based on a predicate.
from better_py import Task
Task(lambda: 5).filter(lambda x: x > 3).run() # 5
Task(lambda: 2).filter(lambda x: x > 3).run() # NoneUse filter to conditionally keep or discard results.
Sequencing Operations
The and_then method runs two Tasks in order, discarding the first result.
from better_py import Task
(Task(lambda: print("First"))
.and_then(Task(lambda: print("Second")))
.run())
# Prints:
# First
# SecondUse and_then when you need to run side effects in sequence but don't need the first result.
Real-World Pattern: Lazy Data Fetching
from better_py import Task
def fetch_user(user_id: int) -> Task[dict]:
return Task(lambda: database.query(f"SELECT * FROM users WHERE id = {user_id}"))
def fetch_orders(user: dict) -> Task[list]:
return Task(lambda: api.get(f"/users/{user['id']}/orders"))
# Chain tasks
user_task = fetch_user(1)
# Task not executed yet
# Use the task later
orders = (user_task
.flat_map(lambda user: fetch_orders(user))
.run())
# Executes both queries, results are cached
# Use the same user_task again
user2 = user_task.run() # Returns cached resultThis pattern shows Task's power: expensive operations are deferred until needed, and results are automatically cached for subsequent accesses.
When to Use
Use Task when:
- You want lazy evaluation
- You need memoization/caching
- You want to defer expensive computations
- You're building pipelines
- You need automatic caching
- You want to avoid redundant computations
Don't use Task when:
- You need immediate execution (use plain functions)
- You don't need caching (use plain functions)
- Side effects should happen immediately (use
IO) - The computation is trivial
Comparison with IO
| Feature | Task | IO |
|---|---|---|
| Execution | Lazy | Deferred but explicit |
| Caching | Automatic memoization | No caching |
| Use case | Expensive computations | Side effects |
| Multiple runs | Returns cached value | Re-executes |
Rule of thumb: Use Task for expensive pure computations that benefit from caching. Use IO for side effects that should execute each time.