Skip to content

Commit e1d5bb2

Browse files
committed
Add docs. Fix type errors
1 parent 774f142 commit e1d5bb2

File tree

5 files changed

+100
-20
lines changed

5 files changed

+100
-20
lines changed

README.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,78 @@ def fn5() -> Generator[int, int, int]:
461461
pinned to `Exception` i.e., `Result[TSource, Exception]`.
462462
"""
463463

464+
# %% [markdown]
465+
"""
466+
### AsyncResult
467+
468+
The `AsyncResult[T, TError]` type is the asynchronous version of `Result`. It allows you
469+
to compose asynchronous operations that may fail, using the Result type. This is
470+
particularly useful for handling errors in asynchronous code, such as API calls,
471+
database operations, or any other I/O-bound tasks.
472+
473+
Similar to the `Result` effect, AsyncResult enables "railway oriented programming" but
474+
for asynchronous operations. If any part of the function yields an `Error`, the function
475+
is short-circuited and the following statements will never be executed.
476+
"""
477+
478+
# %%
479+
from collections.abc import AsyncGenerator
480+
481+
from expression import Error, Ok, effect
482+
483+
484+
@effect.async_result[int, str]()
485+
async def fn() -> AsyncGenerator[int, int]:
486+
x: int = yield 42 # Regular value
487+
y: int = yield await Ok(43) # Awaitable Ok value
488+
489+
# Short-circuit if condition is met
490+
if x + y > 80:
491+
z: int = yield await Error("Value too large") # This will short-circuit
492+
else:
493+
z: int = yield 44
494+
495+
yield x + y + z # Final value
496+
497+
498+
# This would be run in an async context
499+
# result = await fn()
500+
# assert result == Error("Value too large")
501+
502+
# %% [markdown]
503+
"""
504+
AsyncResult works well with other async functions and can be nested:
505+
"""
506+
507+
508+
# %%
509+
@effect.async_result[int, str]()
510+
async def inner(x: int) -> AsyncGenerator[int, int]:
511+
y: int = yield x + 1
512+
yield y + 1 # Final value is y + 1
513+
514+
515+
@effect.async_result[int, str]()
516+
async def outer() -> AsyncGenerator[int, int]:
517+
x: int = yield 40
518+
519+
# Call inner and await its result
520+
inner_result = await inner(x)
521+
y: int = yield await inner_result
522+
523+
yield y # Final value is y
524+
525+
526+
# This would be run in an async context
527+
# result = await outer()
528+
# assert result == Ok(42) # 40 -> 41 -> 42
529+
530+
# %% [markdown]
531+
"""
532+
A simplified type called `AsyncTry` is also available. It's an async result type that is
533+
pinned to `Exception` i.e., `AsyncResult[TSource, Exception]`.
534+
"""
535+
464536
# %% [markdown]
465537
"""
466538
### Sequence

expression/core/async_builder.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from .error import EffectError
1515

1616

17-
_T = TypeVar("_T") # for value type
18-
_M = TypeVar("_M") # for monadic type
17+
_T = TypeVar("_T") # The container item type
18+
_M = TypeVar("_M") # for container type
1919
_P = ParamSpec("_P")
2020

2121

@@ -82,12 +82,10 @@ async def _send(
8282
# Effect errors (Nothing, Error, etc) short circuits
8383
state.is_done = True
8484
return await self.return_from(cast("_M", error.args[0]))
85-
except StopAsyncIteration as ex:
86-
print("StopAsyncIteration occurred", ex)
85+
except StopAsyncIteration:
8786
state.is_done = True
8887
raise
89-
except Exception as ex:
90-
print("Exception occurred", ex)
88+
except Exception:
9189
state.is_done = True
9290
raise
9391

expression/effect/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""A collection of computational expression effects."""
22

3+
from .async_result import AsyncResultBuilder as async_result
4+
from .async_result import AsyncTryBuilder as async_try
35
from .option import OptionBuilder as option
46
from .result import ResultBuilder as result
57
from .result import TryBuilder as try_
68
from .seq import SeqBuilder as seq_builder
7-
from .async_result import AsyncResultBuilder as async_result
8-
from .async_result import AsyncTryBuilder as async_try
99

1010

1111
seq = seq_builder
1212

1313

14-
__all__ = ["option", "result", "seq", "try_", "async_result", "async_try"]
14+
__all__ = ["async_result", "async_try", "option", "result", "seq", "try_"]

expression/effect/async_result.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
works with async operations.
66
"""
77

8-
from collections.abc import Awaitable, Callable
9-
from typing import Any, TypeVar
8+
from collections.abc import AsyncGenerator, Awaitable, Callable
9+
from typing import Any, ParamSpec, TypeVar
1010

1111
from expression.core import Ok, Result
1212
from expression.core.async_builder import AsyncBuilder
@@ -15,6 +15,7 @@
1515
_TSource = TypeVar("_TSource")
1616
_TResult = TypeVar("_TResult")
1717
_TError = TypeVar("_TError")
18+
_P = ParamSpec("_P")
1819

1920

2021
class AsyncResultBuilder(AsyncBuilder[_TSource, Result[Any, _TError]]):
@@ -119,6 +120,16 @@ async def run(self, computation: Result[_TSource, _TError]) -> Result[_TSource,
119120
"""
120121
return computation
121122

123+
def __call__(
124+
self,
125+
fn: Callable[
126+
_P,
127+
AsyncGenerator[_TSource, _TSource] | AsyncGenerator[_TSource, None],
128+
],
129+
) -> Callable[_P, Awaitable[Result[_TSource, _TError]]]:
130+
"""The builder decorator."""
131+
return super().__call__(fn)
132+
122133

123134
# Create singleton instances
124135
async_result: AsyncResultBuilder[Any, Any] = AsyncResultBuilder()

tests/test_async_result_builder.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,12 @@ async def fn() -> AsyncGenerator[int, int]:
233233
case _:
234234
assert False
235235

236-
237236
@pytest.mark.asyncio
238237
async def test_async_result_builder_return_error_wrapped():
239238
"""Test that async_result builder properly handles returning Error directly."""
240239

241240
@effect.async_result[Result[int, str], str]()
242-
async def fn() -> AsyncGenerator[Result[int, str], int]:
241+
async def fn() -> AsyncGenerator[Result[int, str], Result[int, str]]:
243242
yield Error("error") # Use yield await instead of return
244243
# No need for additional yield
245244

@@ -360,13 +359,13 @@ async def fn() -> AsyncGenerator[int, int]:
360359
async def test_async_result_builder_with_async_result_functions():
361360
"""Test that async_result builder properly works with functions returning async Results."""
362361

363-
async def async_ok_function(x: int) -> Result[int, str]:
362+
async def async_ok_function(x: int) -> int:
364363
await asyncio.sleep(0.01) # Small delay to simulate async work
365-
return Ok(x * 2)
364+
return await Ok(x * 2)
366365

367-
async def async_error_function(msg: str) -> Result[int, str]:
366+
async def async_error_function(msg: str) -> int:
368367
await asyncio.sleep(0.01) # Small delay to simulate async work
369-
return Error(msg)
368+
return await Error(msg)
370369

371370
@effect.async_result[int, str]()
372371
async def success_fn() -> AsyncGenerator[int, int]:
@@ -375,18 +374,18 @@ async def success_fn() -> AsyncGenerator[int, int]:
375374
# Call async function and get its result
376375
result = await async_ok_function(x)
377376
# Then yield the result
378-
y: int = yield await result
377+
y: int = yield result
379378

380379
yield y
381380

382381
@effect.async_result[int, str]()
383382
async def error_fn() -> AsyncGenerator[int, int]:
384-
x: int = yield 21
383+
_: int = yield 21
385384

386385
# Call async function and get its result
387386
result = await async_error_function("async error")
388387
# Then yield it - this should short-circuit
389-
y: int = yield await result
388+
y: int = yield result
390389

391390
yield y # Should not reach here
392391

0 commit comments

Comments
 (0)