Skip to content

can't compose @future_safe coroutines that specify picky exceptions #2080

Description

@zed

Given coroutines coro1, coro2:

@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
    if c is None:
        raise ConnectionError("not connected")
    await asyncio.sleep(0)  # emulate I/O
    return c


@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
    await asyncio.sleep(0)  # emulate I/O
    return 1 / n

I want to combine them via bind: coro1(c).bind(coro2). It results in type error:

$ uvx --with 'returns[compatible-mypy]==0.25.0' mypy mre.py; ./mre.py
mre.py:30: error: Argument 1 to "bind" of "FutureResult" has incompatible type "Callable[[int], FutureResult[float, ZeroDivisionError]]"; expected "Callable[[int], KindN[FutureResult[Any, Any], float, ConnectionError, Any]]"  [arg-type]

where mre.py:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "returns >=0.25.0",
# ]
# ///
from typing import assert_never
from returns.result import Success, Failure
from returns.io import IOSuccess, IOFailure
from returns.future import future_safe


@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
    if c is None:
        raise ConnectionError("not connected")
    await asyncio.sleep(0)  # emulate I/O
    return c


@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
    await asyncio.sleep(0)  # emulate I/O
    return 1 / n


async def run() -> None:
    for c in [2, 0, None]:
        match await coro1(c).bind(coro2):
            case IOSuccess(Success(r)):
                assert r == 1 / 2, r
            case IOFailure(Failure(ZeroDivisionError(args=(msg,)))):
                assert msg == "division by zero", msg
            case IOFailure(Failure(ConnectionError(args=(msg,)))):
                assert msg == "not connected", msg
            case _ as unreachable:
                assert_never(unreachable)  # type: ignore[arg-type]


if __name__ == "__main__":
    import asyncio

    asyncio.run(run())

If picky exceptions are removed:

@future_safe
async def coro1(c: int | None) -> int: ...

@future_safe
async def coro2(n: int) -> float: ...

I get the desired behavior that coroutines are composed without type errors but these declarations catch too much.

Activity

  1. changed the title [-]can't compose @future_safe coroutine that specify picky exceptions[/-] [+]can't compose @future_safe coroutines that specify picky exceptions[/+] on Mar 23, 2025
  2. sobolevn commented on Mar 23, 2025

    @sobolevn
    Member

    Will https://github.com/dry-python/returns/blob/master/returns/pointfree/unify.py work for you?

    That's correct that you can't unify by default. You either can:

    1. Define common exception type like type MyError = ConnectionError | ZeroDivisionError and use it like x: IOResult[float, MyError] = await coro1(c).bind(coro2)
    2. Use IOResultE if you don't care about precise exceptions types
    3. Use unify :)
  3. zed commented on Mar 23, 2025

    @zed
    Author

    match await unify(coro2)(coro1(c)): passes the type check. The issue is resolved. It would be perfect for my use-case if the type checking worked with lambdas. For now, it is a lot of unify(partial(Class.method, a=a, b=b, c=c))(container_with_self) that works but verbose.

    I tried union type and it worked:

    ErrorType = ConnectionError | ZeroDivisionError
    Coro1Type = Callable[[int | None], FutureResult[int, ErrorType]]
    Coro2Type = Callable[[int], FutureResult[float, ErrorType]]

    but the coupling it introduces between coro1 and coro2 is not always desired.

    Initially, I used FutureResultE that is too permissive.

  4. zed commented on Mar 24, 2025

    @zed
    Author

    The type check passes with the original match await coro1(c).bind(coro2) code on #1975

    uvx --with 'git+https://github.com/dry-python/returns.git@refs/pull/1975/head' --with mypy mypy mre.py; ./mre.py
    Success: no issues found in 1 source file

    No errors on lambdas too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions