Do Notation
@do binds values from one family inside a generator. Each yield unwraps a success, and the first Err, Nothing or Failure ends the block and becomes its result.
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'")Option and Try work the same way:
from pyrula import Option, Nothing
users = {"ada": {"email": "ada@example.com"}}
@dodef lookup(uid: str): user = yield Option.of(users.get(uid)) email = yield Option.of(user.get("email")) return email
assert lookup("ada").get_or_else("") == "ada@example.com"assert lookup("missing") is NothingLimits
Section titled “Limits”- A block yields one family. Mixing families raises
TypeError. Validatedis not supported; usezip,sequenceortraverseto accumulate errors.@dodoes not capture exceptions raised in the body, including in aTryblock. Wrap them withTry.applyfirst.
@do_async drives an async generator the same way. Python forbids return value in an async generator, so the result is the last yielded value:
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")In a Try block, @do_async captures Exception from the body or a yielded awaitable as Failure.