Skip to content

Commit

Permalink
Add Expected.expect_error
Browse files Browse the repository at this point in the history
  • Loading branch information
evhub committed Nov 2, 2023
1 parent 0c7a5af commit c13fa4c
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 64 deletions.
46 changes: 33 additions & 13 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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:
```
Expand Down
63 changes: 42 additions & 21 deletions __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
75 changes: 49 additions & 26 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions coconut/tests/src/cocotest/agnostic/suite.coco
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions coconut/tests/src/cocotest/agnostic/util.coco
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit c13fa4c

Please sign in to comment.