Do Notation
@do binds values from one sum-type family inside a generator. Each yield
extracts a successful or present value. The first Err, Nothing, or Failure
ends the block and becomes its result.
The following example parses two integers and skips the second parse if the first fails:
from pyrula import Either, do, Ok, Err
def parse_int(raw: str) -> Either[int, str]: try: return Ok(int(raw)) except ValueError: return Err(f"not an int: {raw!r}")
@dodef total(a: str, b: str): x = yield parse_int(a) y = yield parse_int(b) return x + y
total("2", "3") # Ok(5)total("nope", "3") # Err("not an int: 'nope'")Each yield unwraps an Ok and binds the value, or stops at the first Err and
returns it. The body reads like straight-line code while keeping the error handling.
@do also accepts Option and Try.
An Option block returns Nothing when a lookup is absent:
from pyrula import Option, Nothing
@dodef lookup(uid: str): user = yield Option.of(users.get(uid)) email = yield Option.of(user.get("email")) return email
assert lookup("missing") is NothingFamily boundaries
Section titled “Family boundaries”Each block must yield one family: Either, Option, or
Try. Mixing families raises TypeError. Convert between families before
yielding, or use separate blocks.
Validated is not supported. Use Validated.zip, sequence, or traverse
for independent checks that accumulate errors. Use @do for dependent steps
that must stop after an unsuccessful result.
@do wraps the generator’s returned value in its yielded family. It does not
capture exceptions raised in the generator body, including a Try block.
Wrap exception-raising operations with Try.apply before yielding them.
Async composition
Section titled “Async composition”@do_async drives an async generator with the same single-family short-circuiting
behavior. Python forbids return value in an async generator, so the result is
the last successfully yielded value, wrapped in that family. End with a yield:
import asyncio
from pyrula import Either, Ok, Err, do_async
async def fetch_name(uid: str) -> Either[str, str]: return Ok("Ada") if uid == "ada" else Err("unknown user")
@do_asyncasync def greeting(uid: str): name = yield await fetch_name(uid) yield Ok(f"Hello, {name}")
assert asyncio.run(greeting("ada")) == Ok("Hello, Ada")assert asyncio.run(greeting("missing")) == Err("unknown user")After establishing the Try family, @do_async captures Exception from the
async body or a yielded awaitable as Failure. Exceptions outside that hierarchy
propagate.
Scala equivalent
Section titled “Scala equivalent”@do is Scala’s for-comprehension. x = yield e is x <- e and return is yield,
with the first failure short-circuiting, exactly as a for { ... } yield over
Either/Option/Try desugars to flatMap and map.