diff --git a/DOCS.md b/DOCS.md index f4e6d8153..a514f1999 100644 --- a/DOCS.md +++ b/DOCS.md @@ -3157,6 +3157,10 @@ data Expected[T](result: T? = None, error: BaseException? = None): def __bool__(self) -> bool: return self.error is None def __fmap__[U](self, func: T -> U) -> Expected[U]: + """Maps func over the result if it exists. + + __fmap__ should be used directly only when fmap is not available (e.g. when consuming an Expected in vanilla Python). + """ return self.__class__(func(self.result)) if self else self def and_then[U](self, func: T -> Expected[U]) -> Expected[U]: """Maps a T -> Expected[U] over an Expected[T] to produce an Expected[U]. @@ -3172,27 +3176,43 @@ data Expected[T](result: T? = None, error: BaseException? = None): def map_error(self, func: BaseException -> BaseException) -> Expected[T]: """Maps func over the error if it exists.""" return self if self else self.__class__(error=func(self.error)) + def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: + """Recover from the given err_type by calling handler on the error to determine the result.""" + if not self and isinstance(self.error, err_type): + return self.__class__(handler(self.error)) + return self + def expect_error(self, *err_types: BaseException) -> Expected[T]: + """Raise any errors that do not match the given error types.""" + if not self and not isinstance(self.error, err_types): + raise self.error + return self + def unwrap(self) -> T: + """Unwrap the result or raise the error.""" + if not self: + raise self.error + return self.result def or_else[U](self, func: BaseException -> Expected[U]) -> Expected[T | U]: """Return self if no error, otherwise return the result of evaluating func on the error.""" return self if self else func(self.error) - def result_or[U](self, default: U) -> T | U: - """Return the result if it exists, otherwise return the default.""" - return self.result if self else default def result_or_else[U](self, func: BaseException -> U) -> T | U: """Return the result if it exists, otherwise return the result of evaluating func on the error.""" return self.result if self else func(self.error) - def unwrap(self) -> T: - """Unwrap the result or raise the error.""" - if not self: - raise self.error - return self.result - def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: - if not self and _coconut.isinstance(self.error, err_type): - return self.__class__(handler(self.error)) - return self + def result_or[U](self, default: U) -> T | U: + """Return the result if it exists, otherwise return the default. + + Since .result_or() completely silences errors, it is highly recommended that you + call .expect_error() first to explicitly declare what errors you are okay silencing. + """ + return self.result if self else default ``` -`Expected` is primarily used as the return type for [`safe_call`](#safe_call). Generally, the best way to use `Expected` is with [`fmap`](#fmap), which will apply a function to the result if it exists, or otherwise retain the error. If you want to sequence multiple `Expected`-returning operations, `.and_then` should be used instead of `fmap`. +`Expected` is primarily used as the return type for [`safe_call`](#safe_call). + +Generally, the best way to use `Expected` is with [`fmap`](#fmap), which will apply a function to the result if it exists, or otherwise retain the error. If you want to sequence multiple `Expected`-returning operations, `.and_then` should be used instead of `fmap`. To handle specific errors, the following patterns are equivalent: +``` +safe_call(might_raise_IOError).handle(IOError, const 10).unwrap() +safe_call(might_raise_IOError).expect_error(IOError).result_or(10) +``` To match against an `Expected`, just: ``` diff --git a/__coconut__/__init__.pyi b/__coconut__/__init__.pyi index def115f7b..8f60cc7e7 100644 --- a/__coconut__/__init__.pyi +++ b/__coconut__/__init__.pyi @@ -325,6 +325,10 @@ class Expected(_BaseExpected[_T]): def __bool__(self) -> bool: return self.error is None def __fmap__[U](self, func: T -> U) -> Expected[U]: + """Maps func over the result if it exists. + + __fmap__ should be used directly only when fmap is not available (e.g. when consuming an Expected in vanilla Python). + """ return self.__class__(func(self.result)) if self else self def and_then[U](self, func: T -> Expected[U]) -> Expected[U]: """Maps a T -> Expected[U] over an Expected[T] to produce an Expected[U]. @@ -340,24 +344,34 @@ class Expected(_BaseExpected[_T]): def map_error(self, func: BaseException -> BaseException) -> Expected[T]: """Maps func over the error if it exists.""" return self if self else self.__class__(error=func(self.error)) + def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: + """Recover from the given err_type by calling handler on the error to determine the result.""" + if not self and isinstance(self.error, err_type): + return self.__class__(handler(self.error)) + return self + def expect_error(self, *err_types: BaseException) -> Expected[T]: + """Raise any errors that do not match the given error types.""" + if not self and not isinstance(self.error, err_types): + raise self.error + return self + def unwrap(self) -> T: + """Unwrap the result or raise the error.""" + if not self: + raise self.error + return self.result def or_else[U](self, func: BaseException -> Expected[U]) -> Expected[T | U]: """Return self if no error, otherwise return the result of evaluating func on the error.""" return self if self else func(self.error) - def result_or[U](self, default: U) -> T | U: - """Return the result if it exists, otherwise return the default.""" - return self.result if self else default def result_or_else[U](self, func: BaseException -> U) -> T | U: """Return the result if it exists, otherwise return the result of evaluating func on the error.""" return self.result if self else func(self.error) - def unwrap(self) -> T: - """Unwrap the result or raise the error.""" - if not self: - raise self.error - return self.result - def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: - if not self and _coconut.isinstance(self.error, err_type): - return self.__class__(handler(self.error)) - return self + def result_or[U](self, default: U) -> T | U: + """Return the result if it exists, otherwise return the default. + + Since .result_or() completely silences errors, it is highly recommended that you + call .expect_error() first to explicitly declare what errors you are okay silencing. + """ + return self.result if self else default ''' __slots__ = () _coconut_is_data = True @@ -408,20 +422,27 @@ class Expected(_BaseExpected[_T]): def map_error(self, func: _t.Callable[[BaseException], BaseException]) -> Expected[_T]: """Maps func over the error if it exists.""" ... + def handle(self, err_type: _t.Type[BaseException], handler: _t.Callable[[BaseException], _T]) -> Expected[_T]: + """Recover from the given err_type by calling handler on the error to determine the result.""" + ... + def expect_error(self, *err_types: BaseException) -> Expected[_T]: + """Raise any errors that do not match the given error types.""" + ... + def unwrap(self) -> _T: + """Unwrap the result or raise the error.""" + ... def or_else(self, func: _t.Callable[[BaseException], Expected[_U]]) -> Expected[_T | _U]: """Return self if no error, otherwise return the result of evaluating func on the error.""" ... - def result_or(self, default: _U) -> _T | _U: - """Return the result if it exists, otherwise return the default.""" - ... def result_or_else(self, func: _t.Callable[[BaseException], _U]) -> _T | _U: """Return the result if it exists, otherwise return the result of evaluating func on the error.""" ... - def unwrap(self) -> _T: - """Unwrap the result or raise the error.""" - ... - def handle(self, err_type: _t.Type[BaseException], handler: _t.Callable[[BaseException], _T]) -> Expected[_T]: - """Recover from the given err_type by calling handler on the error to determine the result.""" + def result_or(self, default: _U) -> _T | _U: + """Return the result if it exists, otherwise return the default. + + Since .result_or() completely silences errors, it is highly recommended that you + call .expect_error() first to explicitly declare what errors you are okay silencing. + """ ... _coconut_Expected = Expected @@ -1574,7 +1595,7 @@ def lift(func: _t.Callable[[_T, _U], _W]) -> _coconut_lifted_2[_T, _U, _W]: ... def lift(func: _t.Callable[[_T, _U, _V], _W]) -> _coconut_lifted_3[_T, _U, _V, _W]: ... @_t.overload def lift(func: _t.Callable[..., _W]) -> _t.Callable[..., _t.Callable[..., _W]]: - """Lifts a function up so that all of its arguments are functions. + """Lift a function up so that all of its arguments are functions. For a binary function f(x, y) and two unary functions g(z) and h(z), lift works as the S' combinator: lift(f)(g, h)(z) == f(g(z), h(z)) diff --git a/coconut/compiler/templates/header.py_template b/coconut/compiler/templates/header.py_template index 4edc7ce34..bde083643 100644 --- a/coconut/compiler/templates/header.py_template +++ b/coconut/compiler/templates/header.py_template @@ -1712,6 +1712,10 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){ def __bool__(self) -> bool: return self.error is None def __fmap__[U](self, func: T -> U) -> Expected[U]: + """Maps func over the result if it exists. + + __fmap__ should be used directly only when fmap is not available (e.g. when consuming an Expected in vanilla Python). + """ return self.__class__(func(self.result)) if self else self def and_then[U](self, func: T -> Expected[U]) -> Expected[U]: """Maps a T -> Expected[U] over an Expected[T] to produce an Expected[U]. @@ -1727,24 +1731,34 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){ def map_error(self, func: BaseException -> BaseException) -> Expected[T]: """Maps func over the error if it exists.""" return self if self else self.__class__(error=func(self.error)) + def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: + """Recover from the given err_type by calling handler on the error to determine the result.""" + if not self and isinstance(self.error, err_type): + return self.__class__(handler(self.error)) + return self + def expect_error(self, *err_types: BaseException) -> Expected[T]: + """Raise any errors that do not match the given error types.""" + if not self and not isinstance(self.error, err_types): + raise self.error + return self + def unwrap(self) -> T: + """Unwrap the result or raise the error.""" + if not self: + raise self.error + return self.result def or_else[U](self, func: BaseException -> Expected[U]) -> Expected[T | U]: """Return self if no error, otherwise return the result of evaluating func on the error.""" return self if self else func(self.error) - def result_or[U](self, default: U) -> T | U: - """Return the result if it exists, otherwise return the default.""" - return self.result if self else default def result_or_else[U](self, func: BaseException -> U) -> T | U: """Return the result if it exists, otherwise return the result of evaluating func on the error.""" return self.result if self else func(self.error) - def unwrap(self) -> T: - """Unwrap the result or raise the error.""" - if not self: - raise self.error - return self.result - def handle(self, err_type, handler: BaseException -> T) -> Expected[T]: - if not self and _coconut.isinstance(self.error, err_type): - return self.__class__(handler(self.error)) - return self + def result_or[U](self, default: U) -> T | U: + """Return the result if it exists, otherwise return the default. + + Since .result_or() completely silences errors, it is highly recommended that you + call .expect_error() first to explicitly declare what errors you are okay silencing. + """ + return self.result if self else default ''' __slots__ = () {is_data_var} = True @@ -1788,6 +1802,21 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){ def map_error(self, func): """Maps func over the error if it exists.""" return self if self else self.__class__(error=func(self.error)) + def handle(self, err_type, handler): + """Recover from the given err_type by calling handler on the error to determine the result.""" + if not self and _coconut.isinstance(self.error, err_type): + return self.__class__(handler(self.error)) + return self + def expect_error(self, *err_types): + """Raise any errors that do not match the given error types.""" + if not self and not _coconut.isinstance(self.error, err_types): + raise self.error + return self + def unwrap(self): + """Unwrap the result or raise the error.""" + if not self: + raise self.error + return self.result def or_else(self, func): """Return self if no error, otherwise return the result of evaluating func on the error.""" if self: @@ -1796,22 +1825,16 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){ if not _coconut.isinstance(got, {_coconut_}Expected): raise _coconut.TypeError("Expected.or_else() requires a function that returns an Expected") return got - def result_or(self, default): - """Return the result if it exists, otherwise return the default.""" - return self.result if self else default def result_or_else(self, func): """Return the result if it exists, otherwise return the result of evaluating func on the error.""" return self.result if self else func(self.error) - def unwrap(self): - """Unwrap the result or raise the error.""" - if not self: - raise self.error - return self.result - def handle(self, err_type, handler): - """Recover from the given err_type by calling handler on the error to determine the result.""" - if not self and _coconut.isinstance(self.error, err_type): - return self.__class__(handler(self.error)) - return self + def result_or(self, default): + """Return the result if it exists, otherwise return the default. + + Since .result_or() completely silences errors, it is highly recommended that you + call .expect_error() first to explicitly declare what errors you are okay silencing. + """ + return self.result if self else default class flip(_coconut_base_callable): """Given a function, return a new function with inverse argument order. If nargs is passed, only the first nargs arguments are reversed.""" @@ -1858,7 +1881,7 @@ class _coconut_lifted(_coconut_base_callable): def __repr__(self): return "lift(%r)(%s%s)" % (self.func, ", ".join(_coconut.repr(g) for g in self.func_args), ", ".join(k + "=" + _coconut.repr(h) for k, h in self.func_kwargs.items())) class lift(_coconut_base_callable): - """Lifts a function up so that all of its arguments are functions. + """Lift a function up so that all of its arguments are functions. For a binary function f(x, y) and two unary functions g(z) and h(z), lift works as the S' combinator: lift(f)(g, h)(z) == f(g(z), h(z)) diff --git a/coconut/tests/src/cocotest/agnostic/suite.coco b/coconut/tests/src/cocotest/agnostic/suite.coco index 64da67027..1ac462e6d 100644 --- a/coconut/tests/src/cocotest/agnostic/suite.coco +++ b/coconut/tests/src/cocotest/agnostic/suite.coco @@ -1065,8 +1065,8 @@ forward 2""") == 900 haslocobj = hasloc([[1, 2]]) haslocobj |>= .iloc$[0]$[1] assert haslocobj == 2 - assert safe_raise_exc().error `isinstance` Exception - assert safe_raise_exc().handle(Exception, const 10).result == 10 + assert safe_raise_exc(IOError).error `isinstance` IOError + assert safe_raise_exc(IOError).handle(IOError, const 10).unwrap() == 10 == safe_raise_exc(IOError).expect_error(IOError).result_or(10) # must come at end assert fibs_calls[0] == 1 diff --git a/coconut/tests/src/cocotest/agnostic/util.coco b/coconut/tests/src/cocotest/agnostic/util.coco index efa5b681d..0cb370c59 100644 --- a/coconut/tests/src/cocotest/agnostic/util.coco +++ b/coconut/tests/src/cocotest/agnostic/util.coco @@ -1015,8 +1015,8 @@ def raise_exc(): raise Exception("raise_exc") @safe_call$ -def safe_raise_exc() = - raise_exc() +def safe_raise_exc(exc_cls = Exception): + raise exc_cls() def does_raise_exc(func): try: