dbrattli / Expression

Pragmatic functional programming for Python inspired by F#
https://expression.readthedocs.io
MIT License
466 stars 31 forks source link

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

Closed vreuter closed 1 month ago

vreuter commented 1 month 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 1 month 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 1 month 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.