option icon indicating copy to clipboard operation
option copied to clipboard

Introduce a safe eval function

Open mahmoudimus opened this issue 3 years ago • 2 comments

Hi there - I love this little library, but I think some utility functions would really be beneficial for migrating exception code to using results.

Some thoughts, similar to the dry-returns library, we should introduce a safe function for a following use case:

Imagine refactoring this piece of (very hypothetical code):

def foo(a, b, c):
    try:
        a(b, c)
    except ZeroDivisionError:
        raise ValueError("cannot divide by zero")
    except Exception:
        raise TypeError("What?")

It can be very easily refactored with the introduction of safe function:

from functools import wraps
from option.result import Result

def _safe(func):
    @wraps(func)
    def safe_chain(*args, **kwargs):
        z = Result.Ok(func)
        try:
            return z.map(lambda x: x(*args, **kwargs))
        except Exception as e:  # noqa
            return Result.Err(e)
    return safe_chain

then refactoring foo becomes:


def foo(a, b, c):
  a = _safe(a)
  rval = a(b, c)
  if rval.is_ok:
      return rval
  if rval.unwrap_err() is ZeroDivisionError:
      return Result.Err("cannot divide by zero")
  else:
      return Result.Err("what?")

This works very well for languages that do not support try/except for control flows, which is something that can be mapped to non-Turing complete language subsets such as Starlark.

Thoughts?

mahmoudimus avatar May 29 '21 16:05 mahmoudimus

The code sample you provided is a little confusing to read, but are you suggesting that there should be a decorator that converts exceptional code into one that returns Result?

MaT1g3R avatar Mar 30 '22 19:03 MaT1g3R

Love this idea, safe would be very cool, but the return type would be Result[T, Exception], which is not the finest grain for the error type.

I ended up having something similar, which I call handle:

E = TypeVar('E', bound=BaseException)
T = TypeVar('T')

def handle(err_sumtype: type[E], fn: Callable[[], T]) -> Result[T, E]:
    """
    Encapsulates a function call that expects to return `T` but may fail with
    exceptions `err_sumtype` (`E`) to return an `option.Result`
    """
    try:
        return Result.Ok(fn())
    except err_sumtype as e:
        return Result.Err(e)

In this case, your foo becomes:

def foo_result(may_err_fn, x, y) -> Result[T, str]:
  # output: Result[T, ZeroDivisionError | Exception]
  output = handle(ZeroDivisionError | Exception, lambda: may_err_fn(x, y))
  if output.is_ok:
    return output
  match output.unwrap_err():
    case ZeroDivisionError(_):
      return Result.Err("cannot divide by zero")
    case _:
      return Result.Err("what?")

def foo(may_err_fn, x, y):
  return foo_result(may_err_fn, x, y).unwrap()

Pegasust avatar Mar 15 '23 23:03 Pegasust