grscheller.fp.woException

Maybe and Either Monads

Functional data types to use in lieu of exceptions.

  1# Copyright 2023-2024 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""
 16#### Maybe and Either Monads
 17
 18Functional data types to use in lieu of exceptions.
 19"""
 20
 21from __future__ import annotations
 22
 23__all__ = [ 'MB', 'XOR', 'mb_to_xor', 'xor_to_mb' ]
 24
 25from typing import Callable, cast, Final, Generic, Iterator, Never, TypeVar
 26from .nada import Nada, nada
 27
 28T = TypeVar('T')
 29S = TypeVar('S')
 30L = TypeVar('L')
 31R = TypeVar('R')
 32
 33class MB(Generic[T]):
 34    """
 35    #### Maybe Monad
 36
 37    Class representing a potentially missing value.
 38
 39    * where `MB(value)` contains a possible value of type `~T`
 40    * `MB( )` semantically represent a non-existent or missing value of type ~T
 41    * immutable, a MB does not change after being created
 42    * immutable semantics, map and flatMap produce new instances
 43    * implementation detail
 44      * `MB( )` contains `nada` as a sentinel value
 45        * as a result, a MB cannot semantically contain `nada`
 46
 47    """
 48    __slots__ = '_value',
 49
 50    def __init__(self, value: T|Nada=nada) -> None:
 51        self._value = value
 52
 53    def __bool__(self) -> bool:
 54        return not self._value is nada
 55
 56    def __iter__(self) -> Iterator[T]:
 57        if self:
 58            yield cast(T, self._value)
 59
 60    def __repr__(self) -> str:
 61        if self:
 62            return 'MB(' + repr(self._value) + ')'
 63        else:
 64            return 'MB()'
 65
 66    def __len__(self) -> int:
 67        return (1 if self else 0)
 68
 69    def __eq__(self, other: object) -> bool:
 70        if not isinstance(other, type(self)):
 71            return False
 72
 73        if self._value is other._value:
 74            return True
 75        return self._value == other._value
 76
 77    def get(self, alt: T|Nada=nada) -> T|Never:
 78        """
 79        ##### Get an alternate value for the non-existent value.
 80
 81        * if given, return an alternate value of type ~T
 82        * otherwise, raises `ValueError`
 83        * will happily return `None` or `()` as sentinel values
 84        """
 85        if self._value is not nada:
 86            return cast(T, self._value)
 87        else:
 88            if alt is not nada:
 89                return cast(T, alt)
 90            else:
 91                raise ValueError('Alternate return type not provided.')
 92
 93    def map(self, f: Callable[[T], S]) -> MB[S]:
 94        """
 95        #### Map over the `MB`
 96
 97        Map MB function f over the 0 or 1 elements of this data structure.
 98        """
 99        return (MB(f(cast(T, self._value))) if self else MB())
100
101    def flatmap(self, f: Callable[[T], MB[S]]) -> MB[S]:
102        """Map MB with function f and flatten."""
103        return (f(cast(T, self._value)) if self else MB())
104
105class XOR(Generic[L, R]):
106    """
107    #### Either Monad
108
109    Class that can semantically contains either a "left" value or "right" value,
110    but not both.
111
112    * implements a left biased Either Monad
113      * `XOR(left, right)` produces a "left" and default potential "right" value
114      * `XOR(left)` produces a "left" value
115      * `XOR(right=right)` produces a "right" value
116    * in a Boolean context, returns True if a "left", False if a "right"
117    * two `XOR` objects compare as equal when
118      * both are left values or both are right values which
119        * contain the same value or
120        * whose values compare as equal
121    * immutable, an XOR does not change after being created
122      * immutable semantics, map & flatMap return new instances
123      * warning: contained values need not be immutable
124    * raises ValueError if both if
125      * a right value is needed but a potential "right" value is not given
126
127    """
128    __slots__ = '_left', '_right'
129
130    def __init__(self
131            , left: L|Nada=nada
132            , right: R|Nada=nada):
133
134        self._left, self._right = left, right
135
136    def __bool__(self) -> bool:
137        """Predicate to determine if the XOR contains a "left" or a "right".
138
139        * true if the XOR is a "left"
140        * false if the XOR is a "right"
141        """
142        return self._left is not nada
143
144    def __iter__(self) -> Iterator[L]:
145        """Yields its value if the XOR is a "left"."""
146        if self._left is not nada:
147            yield cast(L, self._left)
148
149    def __repr__(self) -> str:
150        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
151
152    def __str__(self) -> str:
153        if self:
154            return '< ' + str(self._left) + ' | >'
155        else:
156            return '< | ' + str(self._right) + ' >'
157
158    def __len__(self) -> int:
159        """Semantically, an XOR always contains just one value."""
160        return 1
161
162    def __eq__(self, other: object) -> bool:
163        if not isinstance(other, type(self)):
164            return False
165
166        if self and other:
167            if self._left is other._left:
168                return True
169            return self._left == other._left
170        elif not self and not other:
171            if self._right is other._right:
172                return True
173            return self._right == other._right
174        else:
175            return False
176
177    def get(self, alt: L|Nada=nada) -> L:
178        """
179        ##### Get value if a Left.
180
181        * if the XOR is a left, return its value
182        * otherwise, return alt: L if it is provided
183
184        """
185        if self._left is nada:
186            if alt is nada:
187                msg = 'An alt return value was needed for get, but none was provided.'
188                raise ValueError(msg)
189            else:
190                return cast(L, alt)
191        else:
192            return cast(L, self._left)
193
194    def getRight(self, alt: R|Nada=nada) -> R:
195        """
196        ##### Get value if `XOR` is a Right
197
198        * if XOR is a right, return its value
199        * otherwise return a provided alternate value of type ~R
200        * otherwise return the potential right value
201
202        """
203        if self:
204            if alt is nada:
205                return cast(R, self._right)
206            else:
207                return cast(R, alt)
208        else:
209            return cast(R, self._right)
210
211    def makeRight(self, right: R|Nada=nada) -> XOR[L, R]:
212        """
213        ##### Make right
214
215        Return a new instance transformed into a right `XOR`. Change the right
216        value to `right` if given.
217
218        """
219        if right is nada:
220            right = self.getRight()
221        return cast(XOR[L, R], XOR(right=right))
222
223    def swapRight(self, right: R) -> XOR[L, R]:
224        """
225        ##### Swap in a new right value
226
227        Returns a new instance with a new right (or potential right) value.
228
229        """
230        if self._left is nada:
231            return cast(XOR[L, R], XOR(right=right))
232        else:
233            return XOR(self.get(), right)
234
235    def map(self, f: Callable[[L], S]) -> XOR[S, R]:
236        """
237        ##### Map left
238
239        * if `XOR` is a "left" then map `f` over its value
240          * if `f` successful return a left XOR[S, R]
241          * if `f` unsuccessful return right `XOR`
242            * swallows any exceptions `f` may throw
243            * FUTURE TODO: create class that is a "wrapped" XOR(~T, Exception)
244        * if `XOR` is a "right"
245          * return new `XOR(right=self._right): XOR[S, R]`
246          * use method mapRight to adjust the returned value
247
248        """
249        if self._left is nada:
250            return cast(XOR[S, R], XOR(right=self._right))
251
252        try:
253            applied = f(cast(L, self._left))
254        except Exception:
255            return XOR(right=self._right)
256        else:
257            return XOR(applied, self._right)
258
259    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
260        """
261        ##### Map right
262
263        Map over a right or potential right value.
264
265        """
266        return XOR(self._left, g(cast(R, self._right)))
267
268    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
269        """Map and flatten a Left value, propagate Right values."""
270        if self._left is nada:
271            return XOR(nada, self._right)
272        else:
273            return f(cast(L, self._left))
274
275# Conversion functions
276
277def mb_to_xor(m: MB[T], right: R) -> XOR[T, R]:
278    """
279    #### Convert a MB to an XOR.
280
281    """
282    if m:
283        return XOR(m.get(), right)
284    else:
285        return XOR(nada, right)
286
287def xor_to_mb(e: XOR[T,S]) -> MB[T]:
288    """
289    ####Convert an XOR to a MB.
290
291    """
292    if e:
293        return MB(e.get())
294    else:
295        return MB()
class MB(typing.Generic[~T]):
 34class MB(Generic[T]):
 35    """
 36    #### Maybe Monad
 37
 38    Class representing a potentially missing value.
 39
 40    * where `MB(value)` contains a possible value of type `~T`
 41    * `MB( )` semantically represent a non-existent or missing value of type ~T
 42    * immutable, a MB does not change after being created
 43    * immutable semantics, map and flatMap produce new instances
 44    * implementation detail
 45      * `MB( )` contains `nada` as a sentinel value
 46        * as a result, a MB cannot semantically contain `nada`
 47
 48    """
 49    __slots__ = '_value',
 50
 51    def __init__(self, value: T|Nada=nada) -> None:
 52        self._value = value
 53
 54    def __bool__(self) -> bool:
 55        return not self._value is nada
 56
 57    def __iter__(self) -> Iterator[T]:
 58        if self:
 59            yield cast(T, self._value)
 60
 61    def __repr__(self) -> str:
 62        if self:
 63            return 'MB(' + repr(self._value) + ')'
 64        else:
 65            return 'MB()'
 66
 67    def __len__(self) -> int:
 68        return (1 if self else 0)
 69
 70    def __eq__(self, other: object) -> bool:
 71        if not isinstance(other, type(self)):
 72            return False
 73
 74        if self._value is other._value:
 75            return True
 76        return self._value == other._value
 77
 78    def get(self, alt: T|Nada=nada) -> T|Never:
 79        """
 80        ##### Get an alternate value for the non-existent value.
 81
 82        * if given, return an alternate value of type ~T
 83        * otherwise, raises `ValueError`
 84        * will happily return `None` or `()` as sentinel values
 85        """
 86        if self._value is not nada:
 87            return cast(T, self._value)
 88        else:
 89            if alt is not nada:
 90                return cast(T, alt)
 91            else:
 92                raise ValueError('Alternate return type not provided.')
 93
 94    def map(self, f: Callable[[T], S]) -> MB[S]:
 95        """
 96        #### Map over the `MB`
 97
 98        Map MB function f over the 0 or 1 elements of this data structure.
 99        """
100        return (MB(f(cast(T, self._value))) if self else MB())
101
102    def flatmap(self, f: Callable[[T], MB[S]]) -> MB[S]:
103        """Map MB with function f and flatten."""
104        return (f(cast(T, self._value)) if self else MB())

Maybe Monad

Class representing a potentially missing value.

  • where MB(value) contains a possible value of type ~T
  • MB( ) semantically represent a non-existent or missing value of type ~T
  • immutable, a MB does not change after being created
  • immutable semantics, map and flatMap produce new instances
  • implementation detail
    • MB( ) contains nada as a sentinel value
      • as a result, a MB cannot semantically contain nada
MB(value: Union[~T, grscheller.fp.nada.Nada] = nada)
51    def __init__(self, value: T|Nada=nada) -> None:
52        self._value = value
def get(self, alt: Union[~T, grscheller.fp.nada.Nada] = nada) -> Union[~T, Never]:
78    def get(self, alt: T|Nada=nada) -> T|Never:
79        """
80        ##### Get an alternate value for the non-existent value.
81
82        * if given, return an alternate value of type ~T
83        * otherwise, raises `ValueError`
84        * will happily return `None` or `()` as sentinel values
85        """
86        if self._value is not nada:
87            return cast(T, self._value)
88        else:
89            if alt is not nada:
90                return cast(T, alt)
91            else:
92                raise ValueError('Alternate return type not provided.')
Get an alternate value for the non-existent value.
  • if given, return an alternate value of type ~T
  • otherwise, raises ValueError
  • will happily return None or () as sentinel values
def map(self, f: Callable[[~T], ~S]) -> MB[~S]:
 94    def map(self, f: Callable[[T], S]) -> MB[S]:
 95        """
 96        #### Map over the `MB`
 97
 98        Map MB function f over the 0 or 1 elements of this data structure.
 99        """
100        return (MB(f(cast(T, self._value))) if self else MB())

Map over the MB

Map MB function f over the 0 or 1 elements of this data structure.

def flatmap( self, f: Callable[[~T], MB[~S]]) -> MB[~S]:
102    def flatmap(self, f: Callable[[T], MB[S]]) -> MB[S]:
103        """Map MB with function f and flatten."""
104        return (f(cast(T, self._value)) if self else MB())

Map MB with function f and flatten.

class XOR(typing.Generic[~L, ~R]):
106class XOR(Generic[L, R]):
107    """
108    #### Either Monad
109
110    Class that can semantically contains either a "left" value or "right" value,
111    but not both.
112
113    * implements a left biased Either Monad
114      * `XOR(left, right)` produces a "left" and default potential "right" value
115      * `XOR(left)` produces a "left" value
116      * `XOR(right=right)` produces a "right" value
117    * in a Boolean context, returns True if a "left", False if a "right"
118    * two `XOR` objects compare as equal when
119      * both are left values or both are right values which
120        * contain the same value or
121        * whose values compare as equal
122    * immutable, an XOR does not change after being created
123      * immutable semantics, map & flatMap return new instances
124      * warning: contained values need not be immutable
125    * raises ValueError if both if
126      * a right value is needed but a potential "right" value is not given
127
128    """
129    __slots__ = '_left', '_right'
130
131    def __init__(self
132            , left: L|Nada=nada
133            , right: R|Nada=nada):
134
135        self._left, self._right = left, right
136
137    def __bool__(self) -> bool:
138        """Predicate to determine if the XOR contains a "left" or a "right".
139
140        * true if the XOR is a "left"
141        * false if the XOR is a "right"
142        """
143        return self._left is not nada
144
145    def __iter__(self) -> Iterator[L]:
146        """Yields its value if the XOR is a "left"."""
147        if self._left is not nada:
148            yield cast(L, self._left)
149
150    def __repr__(self) -> str:
151        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
152
153    def __str__(self) -> str:
154        if self:
155            return '< ' + str(self._left) + ' | >'
156        else:
157            return '< | ' + str(self._right) + ' >'
158
159    def __len__(self) -> int:
160        """Semantically, an XOR always contains just one value."""
161        return 1
162
163    def __eq__(self, other: object) -> bool:
164        if not isinstance(other, type(self)):
165            return False
166
167        if self and other:
168            if self._left is other._left:
169                return True
170            return self._left == other._left
171        elif not self and not other:
172            if self._right is other._right:
173                return True
174            return self._right == other._right
175        else:
176            return False
177
178    def get(self, alt: L|Nada=nada) -> L:
179        """
180        ##### Get value if a Left.
181
182        * if the XOR is a left, return its value
183        * otherwise, return alt: L if it is provided
184
185        """
186        if self._left is nada:
187            if alt is nada:
188                msg = 'An alt return value was needed for get, but none was provided.'
189                raise ValueError(msg)
190            else:
191                return cast(L, alt)
192        else:
193            return cast(L, self._left)
194
195    def getRight(self, alt: R|Nada=nada) -> R:
196        """
197        ##### Get value if `XOR` is a Right
198
199        * if XOR is a right, return its value
200        * otherwise return a provided alternate value of type ~R
201        * otherwise return the potential right value
202
203        """
204        if self:
205            if alt is nada:
206                return cast(R, self._right)
207            else:
208                return cast(R, alt)
209        else:
210            return cast(R, self._right)
211
212    def makeRight(self, right: R|Nada=nada) -> XOR[L, R]:
213        """
214        ##### Make right
215
216        Return a new instance transformed into a right `XOR`. Change the right
217        value to `right` if given.
218
219        """
220        if right is nada:
221            right = self.getRight()
222        return cast(XOR[L, R], XOR(right=right))
223
224    def swapRight(self, right: R) -> XOR[L, R]:
225        """
226        ##### Swap in a new right value
227
228        Returns a new instance with a new right (or potential right) value.
229
230        """
231        if self._left is nada:
232            return cast(XOR[L, R], XOR(right=right))
233        else:
234            return XOR(self.get(), right)
235
236    def map(self, f: Callable[[L], S]) -> XOR[S, R]:
237        """
238        ##### Map left
239
240        * if `XOR` is a "left" then map `f` over its value
241          * if `f` successful return a left XOR[S, R]
242          * if `f` unsuccessful return right `XOR`
243            * swallows any exceptions `f` may throw
244            * FUTURE TODO: create class that is a "wrapped" XOR(~T, Exception)
245        * if `XOR` is a "right"
246          * return new `XOR(right=self._right): XOR[S, R]`
247          * use method mapRight to adjust the returned value
248
249        """
250        if self._left is nada:
251            return cast(XOR[S, R], XOR(right=self._right))
252
253        try:
254            applied = f(cast(L, self._left))
255        except Exception:
256            return XOR(right=self._right)
257        else:
258            return XOR(applied, self._right)
259
260    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
261        """
262        ##### Map right
263
264        Map over a right or potential right value.
265
266        """
267        return XOR(self._left, g(cast(R, self._right)))
268
269    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
270        """Map and flatten a Left value, propagate Right values."""
271        if self._left is nada:
272            return XOR(nada, self._right)
273        else:
274            return f(cast(L, self._left))

Either Monad

Class that can semantically contains either a "left" value or "right" value, but not both.

  • implements a left biased Either Monad
    • XOR(left, right) produces a "left" and default potential "right" value
    • XOR(left) produces a "left" value
    • XOR(right=right) produces a "right" value
  • in a Boolean context, returns True if a "left", False if a "right"
  • two XOR objects compare as equal when
    • both are left values or both are right values which
      • contain the same value or
      • whose values compare as equal
  • immutable, an XOR does not change after being created
    • immutable semantics, map & flatMap return new instances
    • warning: contained values need not be immutable
  • raises ValueError if both if
    • a right value is needed but a potential "right" value is not given
XOR( left: Union[~L, grscheller.fp.nada.Nada] = nada, right: Union[~R, grscheller.fp.nada.Nada] = nada)
131    def __init__(self
132            , left: L|Nada=nada
133            , right: R|Nada=nada):
134
135        self._left, self._right = left, right
def get(self, alt: Union[~L, grscheller.fp.nada.Nada] = nada) -> ~L:
178    def get(self, alt: L|Nada=nada) -> L:
179        """
180        ##### Get value if a Left.
181
182        * if the XOR is a left, return its value
183        * otherwise, return alt: L if it is provided
184
185        """
186        if self._left is nada:
187            if alt is nada:
188                msg = 'An alt return value was needed for get, but none was provided.'
189                raise ValueError(msg)
190            else:
191                return cast(L, alt)
192        else:
193            return cast(L, self._left)
Get value if a Left.
  • if the XOR is a left, return its value
  • otherwise, return alt: L if it is provided
def getRight(self, alt: Union[~R, grscheller.fp.nada.Nada] = nada) -> ~R:
195    def getRight(self, alt: R|Nada=nada) -> R:
196        """
197        ##### Get value if `XOR` is a Right
198
199        * if XOR is a right, return its value
200        * otherwise return a provided alternate value of type ~R
201        * otherwise return the potential right value
202
203        """
204        if self:
205            if alt is nada:
206                return cast(R, self._right)
207            else:
208                return cast(R, alt)
209        else:
210            return cast(R, self._right)
Get value if XOR is a Right
  • if XOR is a right, return its value
  • otherwise return a provided alternate value of type ~R
  • otherwise return the potential right value
def makeRight( self, right: Union[~R, grscheller.fp.nada.Nada] = nada) -> XOR[~L, ~R]:
212    def makeRight(self, right: R|Nada=nada) -> XOR[L, R]:
213        """
214        ##### Make right
215
216        Return a new instance transformed into a right `XOR`. Change the right
217        value to `right` if given.
218
219        """
220        if right is nada:
221            right = self.getRight()
222        return cast(XOR[L, R], XOR(right=right))
Make right

Return a new instance transformed into a right XOR. Change the right value to right if given.

def swapRight(self, right: ~R) -> XOR[~L, ~R]:
224    def swapRight(self, right: R) -> XOR[L, R]:
225        """
226        ##### Swap in a new right value
227
228        Returns a new instance with a new right (or potential right) value.
229
230        """
231        if self._left is nada:
232            return cast(XOR[L, R], XOR(right=right))
233        else:
234            return XOR(self.get(), right)
Swap in a new right value

Returns a new instance with a new right (or potential right) value.

def map(self, f: Callable[[~L], ~S]) -> XOR[~S, ~R]:
236    def map(self, f: Callable[[L], S]) -> XOR[S, R]:
237        """
238        ##### Map left
239
240        * if `XOR` is a "left" then map `f` over its value
241          * if `f` successful return a left XOR[S, R]
242          * if `f` unsuccessful return right `XOR`
243            * swallows any exceptions `f` may throw
244            * FUTURE TODO: create class that is a "wrapped" XOR(~T, Exception)
245        * if `XOR` is a "right"
246          * return new `XOR(right=self._right): XOR[S, R]`
247          * use method mapRight to adjust the returned value
248
249        """
250        if self._left is nada:
251            return cast(XOR[S, R], XOR(right=self._right))
252
253        try:
254            applied = f(cast(L, self._left))
255        except Exception:
256            return XOR(right=self._right)
257        else:
258            return XOR(applied, self._right)
Map left
  • if XOR is a "left" then map f over its value
    • if f successful return a left XOR[S, R]
    • if f unsuccessful return right XOR
      • swallows any exceptions f may throw
      • FUTURE TODO: create class that is a "wrapped" XOR(~T, Exception)
  • if XOR is a "right"
    • return new XOR(right=self._right): XOR[S, R]
    • use method mapRight to adjust the returned value
def mapRight(self, g: Callable[[~R], ~R]) -> XOR[~L, ~R]:
260    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
261        """
262        ##### Map right
263
264        Map over a right or potential right value.
265
266        """
267        return XOR(self._left, g(cast(R, self._right)))
Map right

Map over a right or potential right value.

def flatMap( self, f: Callable[[~L], XOR[~S, ~R]]) -> XOR[~S, ~R]:
269    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
270        """Map and flatten a Left value, propagate Right values."""
271        if self._left is nada:
272            return XOR(nada, self._right)
273        else:
274            return f(cast(L, self._left))

Map and flatten a Left value, propagate Right values.

def mb_to_xor( m: MB[~T], right: ~R) -> XOR[~T, ~R]:
278def mb_to_xor(m: MB[T], right: R) -> XOR[T, R]:
279    """
280    #### Convert a MB to an XOR.
281
282    """
283    if m:
284        return XOR(m.get(), right)
285    else:
286        return XOR(nada, right)

Convert a MB to an XOR.

def xor_to_mb( e: XOR[~T, ~S]) -> MB[~T]:
288def xor_to_mb(e: XOR[T,S]) -> MB[T]:
289    """
290    ####Convert an XOR to a MB.
291
292    """
293    if e:
294        return MB(e.get())
295    else:
296        return MB()

Convert an XOR to a MB.