dbrattli / Expression

Functional programming for Python
https://expression.readthedocs.io
MIT License
497 stars 32 forks source link

Support for improved client-side ergonomics for building `Try` values #212

Closed vreuter closed 3 months ago

vreuter commented 4 months ago

Right now, it looks like a value of a Try subtype must be created directly, so normal error handling control flow is needed.

What about a constructor that accepts a 0-arg callable (e.g., a fully parameterised function call), which is then invoked inside try...except logic, yielding an Ok-wrapped result if the call succeeds, and an Error-wrapped caught exception in the case of failure?

Hugovdberg commented 3 months ago

In my code I have a decorator for functions that might raise exceptions and converts that into a function that returns a Result:

_Exception = TypeVar("_Exception", bound=Exception)

@expression.curry_flip(1)
def wrap_exception(
    fun: Callable[_P, _a],
    exc: type[_Exception] | tuple[type[_Exception], ...] = Exception,
) -> Callable[_P, Result[_a, _Exception]]:
    """Wrap a function that might raise an Exception in a Result monad

    Args:
        fun (Callable[P, a]):
            The function to be wrapped.
        exc (Union[Tuple[Type[Exception], ...], Type[Exception]], optional):
            The Exception types to be wrapped into the monad. Defaults to Exception.

    Returns:
        Callable[P, Result[a, Exception]]: 
            The decorated function.

    Examples:
        >>> @wrap_exception(ZeroDivisionError)
        ... def inverse(x: int) -> float:
        ...     return 1 / x
        >>> t: Result[float, ZeroDivisionError] = inverse(0)
    """

    @functools.wraps(fun)
    def _wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Result[_a, _Exception]:
        try:
            return Result[_a, _Exception].Ok(fun(*args, **kwargs))
        except exc as e:
            return Result[_a, _Exception].Error(e)

    return _wrapper

By default all exceptions are captured, but you can specify which exceptions should be captured. Would that solve your problem? Note that you can use this on existing functions too, such as pandas.read_excel, like safe_read_excel = wrap_exception(FileNotFoundError)(pandas.read_excel).

vreuter commented 3 months ago

Hey @Hugovdberg , indeed! This looks like a good solution. Mainly, I just want control flow to be by monadic combinators and/or pattern matchig, rather than by error handling, which this will provide :) And fantastic that the universe of caught exceptions is controllable w/ suitable default! Thanks a lot, I'm closing this for now.