Skip to content

Commit

Permalink
Add and_then, join to Expected
Browse files Browse the repository at this point in the history
Refs   #691.
  • Loading branch information
evhub committed Dec 25, 2022
1 parent 78e1cc1 commit 71b6e43
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 7 deletions.
17 changes: 14 additions & 3 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2778,9 +2778,20 @@ data Expected[T](result: T?, error: Exception?):
return self.error is None
def __fmap__[U](self, func: T -> U) -> Expected[U]:
return self.__class__(func(self.result)) if self else self
```

`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.
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].
Implements a monadic bind. Equivalent to fmap ..> .join()."""
return self |> fmap$(func) |> .join()
def join(self: Expected[Expected[T]]) -> Expected[T]:
"""Monadic join. Converts Expected[Expected[T]] to Expected[T]."""
if not self:
return self
if not self.result `isinstance` Expected:
raise TypeError("Expected.join() requires an Expected[Expected[T]]")
return self.result
```

`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`.

##### Example

Expand Down
3 changes: 3 additions & 0 deletions __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ class Expected(_t.Generic[_T], _t.Tuple):
def __getitem__(self, index: _SupportsIndex) -> _T | Exception | None: ...
@_t.overload
def __getitem__(self, index: slice) -> _t.Tuple[_T | Exception | None, ...]: ...
def and_then(self, func: _t.Callable[[_T], Expected[_U]]) -> Expected[_U]: ...
def join(self: Expected[Expected[_T]]) -> Expected[_T]: ...

_coconut_Expected = Expected


Expand Down
28 changes: 25 additions & 3 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -1517,7 +1517,7 @@ def safe_call(_coconut_f{comma_slash}, *args, **kwargs):
except _coconut.Exception as err:
return _coconut_Expected(error=err)
class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){comma_object}):
"""Coconut's Expected built-in is a Coconut data that represents a value
'''Coconut's Expected built-in is a Coconut data that represents a value
that may or may not be an error, similar to Haskell's Either.

Effectively equivalent to:
Expand All @@ -1530,7 +1530,18 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){
return self.error is None
def __fmap__[U](self, func: T -> U) -> Expected[U]:
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].
Implements a monadic bind. Equivalent to fmap ..> .join()."""
return self |> fmap$(func) |> .join()
def join(self: Expected[Expected[T]]) -> Expected[T]:
"""Monadic join. Converts Expected[Expected[T]] to Expected[T]."""
if not self:
return self
if not self.result `isinstance` Expected:
raise TypeError("Expected.join() requires an Expected[Expected[T]]")
return self.result
'''
_coconut_is_data = True
__slots__ = ()
def __add__(self, other): return _coconut.NotImplemented
Expand All @@ -1547,9 +1558,20 @@ class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){
raise _coconut.ValueError("Expected cannot have both a result and an error")
return _coconut.tuple.__new__(cls, (result, error))
def __fmap__(self, func):
return self if self.error is not None else self.__class__(func(self.result))
return self if not self else self.__class__(func(self.result))
def __bool__(self):
return self.error is None
def join(self):
"""Monadic join. Converts Expected[Expected[T]] to Expected[T]."""
if not self:
return self
if not _coconut.isinstance(self.result, _coconut_Expected):
raise _coconut.TypeError("Expected.join() requires an Expected[Expected[T]]")
return self.result
def and_then(self, func):
"""Maps a T -> Expected[U] over an Expected[T] to produce an Expected[U].
Implements a monadic bind. Equivalent to fmap ..> .join()."""
return self.__fmap__(func).join()
class flip(_coconut_base_hashable):
"""Given a function, return a new function with inverse argument order.
If nargs is passed, only the first nargs arguments are reversed."""
Expand Down
2 changes: 1 addition & 1 deletion coconut/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
VERSION = "2.1.1"
VERSION_NAME = "The Spanish Inquisition"
# False for release, int >= 1 for develop
DEVELOP = 40
DEVELOP = 41
ALPHA = False # for pre releases rather than post releases

# -----------------------------------------------------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions coconut/tests/src/cocotest/agnostic/main.coco
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,10 @@ def main_test() -> bool:
res, err = safe_call(-> 1 / 0) |> fmap$(.+1)
assert res is None
assert err `isinstance` ZeroDivisionError
assert Expected(10).and_then(safe_call$(.*2)) == Expected(20)
assert Expected(error=some_err).and_then(safe_call$(.*2)) == Expected(error=some_err)
assert Expected(Expected(10)).join() == Expected(10)
assert Expected(error=some_err).join() == Expected(error=some_err)
recit = ([1,2,3] :: recit) |> map$(.+1)
assert tee(recit)
rawit = (_ for _ in (0, 1))
Expand Down

0 comments on commit 71b6e43

Please sign in to comment.