Skip to content

Runtime

Functions for executing effects and obtaining their results.

Use the run_sync / run_async variants when you are comfortable letting errors propagate as exceptions. Use the _exit variants when you want to handle errors as values using Exit.

pyfect.runtime.run_sync(effect)

Execute a synchronous effect and return its value.

Raises if the effect fails or contains async primitives.

Example
from pyfect import effect
result = effect.run_sync(effect.succeed(42))  # 42

Raises:

Type Description
BaseException

If the effect fails with an exception error value (re-raised as-is)

RuntimeError

If the effect fails with a non-exception error value, or contains async

Source code in src/pyfect/runtime.py
def run_sync[A, E](effect: Effect[A, E, None]) -> A:  # noqa: PLR0911, PLR0912
    """
    Execute a synchronous effect and return its value.

    Raises if the effect fails or contains async primitives.

    Example:
        ```python
        from pyfect import effect
        result = effect.run_sync(effect.succeed(42))  # 42
        ```

    Raises:
        BaseException: If the effect fails with an exception error value (re-raised as-is)
        RuntimeError: If the effect fails with a non-exception error value, or contains async
        primitives
    """
    match effect:
        case Succeed(value):
            return value
        case Sync(thunk):
            return thunk()
        case Fail(error):
            if isinstance(error, BaseException):
                raise error
            msg = f"effect failed: {error}"
            raise RuntimeError(msg)
        case Tap(inner_effect, f):
            # Run the inner effect
            result = run_sync(inner_effect)
            # Run the tap function for side effects (ignore result)
            run_sync(f(result))
            # Return the original value
            return result
        case Map(inner_effect, f):
            # Run the inner effect and transform the result
            result = run_sync(inner_effect)
            return f(result)
        case FlatMap(inner_effect, f):
            # Run the inner effect, then run the effect returned by f
            result = run_sync(inner_effect)
            next_effect = f(result)
            return run_sync(next_effect)
        case Ignore(inner_effect):
            # Run the effect and ignore both success and failure
            with contextlib.suppress(BaseException):
                run_sync(inner_effect)
            return cast(A, None)
        case MapError(inner_effect, f):
            # Run the effect and transform errors
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    return value
                case exit.Failure(error):
                    # Transform the error and re-raise
                    transformed = f(cast(Any, error))
                    if isinstance(transformed, BaseException):
                        # Preserve exception chain if original error was an exception
                        if isinstance(error, BaseException):
                            raise transformed from error
                        raise transformed
                    msg = f"effect failed: {transformed}"
                    raise RuntimeError(msg)
        case TapError(inner_effect, f):
            # Try to run the inner effect
            try:
                return run_sync(inner_effect)
            except BaseException as e:
                # Run the tap function for side effects (ignore result)
                with contextlib.suppress(BaseException):
                    run_sync(f(cast(Any, e)))
                # Re-raise the original error
                raise
        case Suspend(thunk):
            # Execute thunk to get effect, then run it
            return run_sync(thunk())
        case TrySync(thunk):
            # Execute thunk - exceptions propagate
            return thunk()
        case _:
            msg = f"Cannot run {type(effect).__name__} synchronously"
            raise RuntimeError(msg)

pyfect.runtime.run_sync_exit(effect)

Execute a synchronous effect and return Exit instead of throwing.

Returns Success on success or Failure on error. This keeps errors as values all the way through.

Example
result = effect.run_sync_exit(effect.succeed(42))
match result:
    case effect.Success(value):
        print(f"Success: {value}")
    case effect.Failure(error):
        print(f"Error: {error}")

Raises:

Type Description
RuntimeError

If the effect cannot be run synchronously

Source code in src/pyfect/runtime.py
def run_sync_exit[A, E](effect: Effect[A, E, None]) -> Exit[A, E]:  # noqa: PLR0911, PLR0912
    """
    Execute a synchronous effect and return Exit instead of throwing.

    Returns Success on success or Failure on error.
    This keeps errors as values all the way through.

    Example:
        ```python
        result = effect.run_sync_exit(effect.succeed(42))
        match result:
            case effect.Success(value):
                print(f"Success: {value}")
            case effect.Failure(error):
                print(f"Error: {error}")
        ```

    Raises:
        RuntimeError: If the effect cannot be run synchronously
    """
    match effect:
        case Succeed(value):
            return exit.succeed(value)
        case Sync(thunk):
            return exit.succeed(thunk())
        case Fail(error):
            return exit.fail(error)
        case Tap(inner_effect, f):
            # Run the inner effect
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    # Run tap for side effects (ignore result)
                    run_sync(f(value))
                    return exit.succeed(value)
                case exit.Failure(error):
                    return exit.fail(error)
        case Map(inner_effect, f):
            # Run the inner effect and transform successful result
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    return exit.succeed(f(value))
                case exit.Failure(error):
                    return exit.fail(error)
        case FlatMap(inner_effect, f):
            # Run the inner effect, then run the effect returned by f
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    next_effect = f(value)
                    return run_sync_exit(next_effect)
                case exit.Failure(error):
                    return exit.fail(error)
        case Ignore(inner_effect):
            # Run the effect and ignore both success and failure
            run_sync_exit(inner_effect)  # Ignore the result
            return exit.succeed(cast(A, None))
        case MapError(inner_effect, f):
            # Run the effect and transform errors
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    return exit.succeed(value)
                case exit.Failure(error):
                    return exit.fail(f(cast(Any, error)))
        case TapError(inner_effect, f):
            # Run the inner effect
            inner_result = run_sync_exit(inner_effect)
            match inner_result:
                case exit.Success(value):
                    return exit.succeed(value)
                case exit.Failure(error):
                    # Run tap_error for side effects (ignore result)
                    with contextlib.suppress(BaseException):
                        run_sync(f(cast(Any, error)))
                    return exit.fail(error)
        case Suspend(thunk):
            # Execute thunk to get effect, then run it
            return run_sync_exit(thunk())
        case TrySync(thunk):
            # Execute and catch exceptions
            try:
                return exit.succeed(thunk())
            except Exception as e:
                return exit.fail(cast(E, e))
        case _:
            msg = f"Cannot run {type(effect).__name__} synchronously"
            raise RuntimeError(msg)

pyfect.runtime.run_async(effect)

Execute an effect asynchronously and return an awaitable.

Handles both synchronous and asynchronous primitives.

Example
import asyncio
from pyfect import effect
result = await effect.run_async(effect.async_(lambda: asyncio.sleep(0.1)))

Raises:

Type Description
BaseException

If the effect fails with an exception error value (re-raised as-is)

RuntimeError

If the effect fails with a non-exception error value

Source code in src/pyfect/runtime.py
def run_async[A, E](effect: Effect[A, E, None]) -> Awaitable[A]:
    """
    Execute an effect asynchronously and return an awaitable.

    Handles both synchronous and asynchronous primitives.

    Example:
        ```python
        import asyncio
        from pyfect import effect
        result = await effect.run_async(effect.async_(lambda: asyncio.sleep(0.1)))
        ```

    Raises:
        BaseException: If the effect fails with an exception error value (re-raised as-is)
        RuntimeError: If the effect fails with a non-exception error value
    """

    async def execute() -> A:  # noqa: PLR0911, PLR0912
        match effect:
            case Succeed(value):
                return value
            case Sync(thunk):
                return thunk()
            case Async(thunk):
                return await thunk()
            case Fail(error):
                if isinstance(error, BaseException):
                    raise error
                msg = f"effect failed: {error}"
                raise RuntimeError(msg)
            case Tap(inner_effect, f):
                # Run the inner effect
                result = await run_async(inner_effect)
                # Run the tap function for side effects (ignore result)
                await run_async(f(result))
                # Return the original value
                return result
            case Map(inner_effect, f):
                # Run the inner effect and transform the result
                result = await run_async(inner_effect)
                return f(result)
            case FlatMap(inner_effect, f):
                # Run the inner effect, then run the effect returned by f
                result = await run_async(inner_effect)
                next_effect = f(result)
                return await run_async(next_effect)
            case Ignore(inner_effect):
                # Run the effect and ignore both success and failure
                with contextlib.suppress(BaseException):
                    await run_async(inner_effect)
                return cast(A, None)
            case MapError(inner_effect, f):
                # Run the effect and transform errors
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        return value
                    case exit.Failure(error):
                        # Transform the error and re-raise
                        transformed = f(cast(Any, error))
                        if isinstance(transformed, BaseException):
                            # Preserve exception chain if original error was an exception
                            if isinstance(error, BaseException):
                                raise transformed from error
                            raise transformed
                        msg = f"effect failed: {transformed}"
                        raise RuntimeError(msg)
            case TapError(inner_effect, f):
                # Try to run the inner effect
                try:
                    return await run_async(inner_effect)
                except BaseException as e:
                    # Run the tap function for side effects (ignore result)
                    with contextlib.suppress(BaseException):
                        await run_async(f(cast(Any, e)))
                    # Re-raise the original error
                    raise
            case Suspend(thunk):
                # Execute thunk to get effect, then run it
                return await run_async(thunk())
            case TrySync(thunk):
                # Execute sync thunk - exceptions propagate
                return thunk()
            case TryAsync(thunk):
                # Execute async thunk - exceptions propagate
                return await thunk()

    return execute()

pyfect.runtime.run_async_exit(effect)

Execute an effect asynchronously and return Exit instead of throwing.

Returns Success on success or Failure on error. This can run both synchronous and asynchronous effects.

Example
result = await effect.run_async_exit(effect.succeed(42))
match result:
    case effect.Success(value):
        print(f"Success: {value}")
    case effect.Failure(error):
        print(f"Error: {error}")
Source code in src/pyfect/runtime.py
def run_async_exit[A, E](effect: Effect[A, E, None]) -> Awaitable[Exit[A, E]]:
    """
    Execute an effect asynchronously and return Exit instead of throwing.

    Returns Success on success or Failure on error.
    This can run both synchronous and asynchronous effects.

    Example:
        ```python
        result = await effect.run_async_exit(effect.succeed(42))
        match result:
            case effect.Success(value):
                print(f"Success: {value}")
            case effect.Failure(error):
                print(f"Error: {error}")
        ```
    """

    async def execute() -> Exit[A, E]:  # noqa: PLR0911, PLR0912
        match effect:
            case Succeed(value):
                return exit.succeed(value)
            case Sync(thunk):
                return exit.succeed(thunk())
            case Async(thunk):
                return exit.succeed(await thunk())
            case Fail(error):
                return exit.fail(error)
            case Tap(inner_effect, f):
                # Run the inner effect
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        # Run tap for side effects (ignore result)
                        await run_async(f(value))
                        return exit.succeed(value)
                    case exit.Failure(error):
                        return exit.fail(error)
            case Map(inner_effect, f):
                # Run the inner effect and transform successful result
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        return exit.succeed(f(value))
                    case exit.Failure(error):
                        return exit.fail(error)
            case FlatMap(inner_effect, f):
                # Run the inner effect, then run the effect returned by f
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        next_effect = f(value)
                        return await run_async_exit(next_effect)
                    case exit.Failure(error):
                        return exit.fail(error)
            case Ignore(inner_effect):
                # Run the effect and ignore both success and failure
                await run_async_exit(inner_effect)  # Ignore the result
                return exit.succeed(cast(A, None))
            case MapError(inner_effect, f):
                # Run the effect and transform errors
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        return exit.succeed(value)
                    case exit.Failure(error):
                        return exit.fail(f(cast(Any, error)))
            case TapError(inner_effect, f):
                # Run the inner effect
                inner_result = await run_async_exit(inner_effect)
                match inner_result:
                    case exit.Success(value):
                        return exit.succeed(value)
                    case exit.Failure(error):
                        # Run tap_error for side effects (ignore result)
                        with contextlib.suppress(BaseException):
                            await run_async(f(cast(Any, error)))
                        return exit.fail(error)
            case Suspend(thunk):
                # Execute thunk to get effect, then run it
                return await run_async_exit(thunk())
            case TrySync(thunk):
                # Execute sync thunk and catch exceptions
                try:
                    return exit.succeed(thunk())
                except Exception as e:
                    return exit.fail(cast(E, e))
            case TryAsync(thunk):
                # Execute async thunk and catch exceptions
                try:
                    return exit.succeed(await thunk())
                except Exception as e:
                    return exit.fail(cast(E, e))

    return execute()