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
 41    * implementation wise `MB( )` contains an inaccessible sentinel value
 42    * immutable, a MB does not change after being created
 43    * immutable semantics, map and flatMap produce new instances
 44    * two MB values only return equal if both ...
 45
 46    """
 47    __slots__ = '_value',
 48
 49    def __init__(self, value: T|Nada=nada) -> None:
 50        self._value = value
 51
 52    def __bool__(self) -> bool:
 53        return not self._value is nada
 54
 55    def __iter__(self) -> Iterator[T]:
 56        if self:
 57            yield cast(T, self._value)
 58
 59    def __repr__(self) -> str:
 60        if self:
 61            return 'MB(' + repr(self._value) + ')'
 62        else:
 63            return 'MB()'
 64
 65    def __len__(self) -> int:
 66        return (1 if self else 0)
 67
 68    def __eq__(self, other: object) -> bool:
 69        if not isinstance(other, type(self)):
 70            return False
 71
 72        if self._value is other._value:
 73            return True
 74        return self._value == other._value
 75
 76    def get(self, alt: T|Nada=nada) -> T|Never:
 77        """
 78        ##### Get an alternate value for the non-existent value.
 79
 80        * if given, return an alternate value of type ~T
 81        * otherwise, raises `ValueError`
 82        * will happily return `None` or `()` as sentinel values
 83        """
 84        if self._value is not nada:
 85            return cast(T, self._value)
 86        else:
 87            if alt is not nada:
 88                return cast(T, alt)
 89            else:
 90                raise ValueError('Alternate return type not provided.')
 91
 92    def map(self, f: Callable[[T], S]) -> MB[S]:
 93        """
 94        #### Map over the `MB`
 95
 96        Map MB function f over the 0 or 1 elements of this data structure.
 97        """
 98        return (MB(f(cast(T, self._value))) if self else MB())
 99
100    def flatmap(self, f: Callable[[T], MB[S]]) -> MB[S]:
101        """Map MB with function f and flatten."""
102        return (f(cast(T, self._value)) if self else MB())
103
104class XOR(Generic[L, R]):
105    """
106    #### Either Monad
107
108    Class that can semantically contains either a "left" value or "right" value,
109    but not both.
110
111    * implements a left biased Either Monad
112    * `XOR(left, right)` produces a "left" and default potential "right" value
113    * `XOR(left)` produces a "left" value
114    * `XOR(right=right)` produces a "right" value
115    * in a Boolean context, returns True if a "left", False if a "right"
116    * immutable, an XOR does not change after being created
117    * immutable semantics, map & flatMap return new instances
118    * raises ValueError if both "left" and "right" values are not given
119    * raises ValueError if both if a potential "right" value is not given
120
121    """
122    __slots__ = '_left', '_right'
123
124    def __init__(self
125            , left: L|Nada=nada
126            , right: R|Nada=nada):
127
128        if left == nada == right:
129            raise ValueError('XOR: neither left nor right values provided')
130        self._left, self._right = left, right
131
132    def __bool__(self) -> bool:
133        """Predicate to determine if the XOR contains a "left" or a "right".
134
135        * true if the XOR is a "left"
136        * false if the XOR is a "right"
137        """
138        return self._left is not nada
139
140    def __iter__(self) -> Iterator[L]:
141        """Yields its value if the XOR is a "left"."""
142        if self._left is not nada:
143            yield cast(L, self._left)
144
145    def __repr__(self) -> str:
146        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
147
148    def __str__(self) -> str:
149        if self:
150            return '< ' + str(self._left) + ' | >'
151        else:
152            return '< | ' + str(self._right) + ' >'
153
154    def __len__(self) -> int:
155        """Semantically, an XOR always contains just one value."""
156        return 1
157
158    def __eq__(self, other: object) -> bool:
159        if not isinstance(other, type(self)):
160            return False
161
162        if self and other:
163            if self._left is other._left:
164                return True
165            return self._left == other._left
166        elif not self and not other:
167            if self._right is other._right:
168                return True
169            return self._right == other._right
170        else:
171            return False
172
173    def get(self, alt: L|Nada=nada) -> L:
174        """
175        ##### Get value if a Left.
176
177        * if the XOR is a left, return its value
178        * otherwise, return alt if it is provided
179        * raises `ValueError` if alternate value needed but not provided
180
181        """
182        if self._left is nada:
183            if alt is nada:
184                raise ValueError('Alternate return value needed but not provided.')
185            else:
186                return cast(L, alt)
187        else:
188            return cast(L, self._left)
189
190    def getRight(self, alt: R|Nada=nada) -> R:
191        """
192        ##### Get value if `XOR` is a Right.
193
194        * if XOR is a right, return its value
195        * otherwise return an alternate value of type ~R
196        * raises `ValueError` if alternate value needed but not provided
197
198        """
199        if not self:
200            if self._right is not nada:
201                return cast(R, self._right)
202            elif alt is not nada:
203                return cast(R, alt)
204            else:
205                raise ValueError('Alternate return type needed but not provided.')
206        else:
207            if alt is not nada:
208                return cast(R, alt)
209            else:
210                raise ValueError('Alternate return type needed but not provided.')
211
212    def getDefaultRight(self) -> R:
213        """
214        ##### Get default right value
215
216        Get value if a "right" otherwise get the default "right" value.
217        """
218        return cast(R, self._right)
219
220    def map(self, f: Callable[[L], S], right: R|Nada=nada) -> XOR[S, R]:
221        """
222        ##### Map over an XOR.
223
224        TODO: catch any errors `f` may throw
225
226        * if `XOR` is a "left" then map `f`
227          * if `f` successful, return a "left" XOR[S, R]
228          * if `f` unsuccessful and `right` given, return `XOR(right=right)`
229          * if `f` unsuccessful and `right` not given, return `XOR(right=self.right)`
230        * if `XOR` is a "right"
231          * if `right` is given, return `XOR(right=right)`
232          * if `right` is not given, return `XOR(right=self.right)`
233
234        """
235        if self._left is nada:
236            if right is nada:
237                return XOR(right=self._right)
238            else:
239                return XOR(right=right)
240
241        if right is nada:
242            return XOR(f(cast(L, self._left)), self._right)
243        else:
244            return XOR(f(cast(L, self._left)), right)
245
246    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
247        """Map over a "right" value."""
248        if self._left is nada:
249            return XOR(nada, g(cast(R,self._right)))
250        return self
251
252    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
253        """Map and flatten a Left value, propagate Right values."""
254        if self._left is nada:
255            return XOR(nada, self._right)
256        else:
257            return f(cast(L, self._left))
258
259# Conversion functions
260
261def mb_to_xor(m: MB[T], right: R) -> XOR[T, R]:
262    """
263    #### Convert a MB to an XOR.
264
265    """
266    if m:
267        return XOR(m.get(), right)
268    else:
269        return XOR(nada, right)
270
271def xor_to_mb(e: XOR[T,S]) -> MB[T]:
272    """
273    ####Convert an XOR to a MB.
274
275    """
276    if e:
277        return MB(e.get())
278    else:
279        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
 42    * implementation wise `MB( )` contains an inaccessible sentinel value
 43    * immutable, a MB does not change after being created
 44    * immutable semantics, map and flatMap produce new instances
 45    * two MB values only return equal if both ...
 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())

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
  • implementation wise MB( ) contains an inaccessible sentinel value
  • immutable, a MB does not change after being created
  • immutable semantics, map and flatMap produce new instances
  • two MB values only return equal if both ...
MB(value: Union[~T, grscheller.fp.nada.Nada] = nada)
50    def __init__(self, value: T|Nada=nada) -> None:
51        self._value = value
def get(self, alt: Union[~T, grscheller.fp.nada.Nada] = nada) -> Union[~T, Never]:
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.')
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]:
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())

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]:
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())

Map MB with function f and flatten.

class XOR(typing.Generic[~L, ~R]):
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    * immutable, an XOR does not change after being created
118    * immutable semantics, map & flatMap return new instances
119    * raises ValueError if both "left" and "right" values are not given
120    * raises ValueError if both if a potential "right" value is not given
121
122    """
123    __slots__ = '_left', '_right'
124
125    def __init__(self
126            , left: L|Nada=nada
127            , right: R|Nada=nada):
128
129        if left == nada == right:
130            raise ValueError('XOR: neither left nor right values provided')
131        self._left, self._right = left, right
132
133    def __bool__(self) -> bool:
134        """Predicate to determine if the XOR contains a "left" or a "right".
135
136        * true if the XOR is a "left"
137        * false if the XOR is a "right"
138        """
139        return self._left is not nada
140
141    def __iter__(self) -> Iterator[L]:
142        """Yields its value if the XOR is a "left"."""
143        if self._left is not nada:
144            yield cast(L, self._left)
145
146    def __repr__(self) -> str:
147        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
148
149    def __str__(self) -> str:
150        if self:
151            return '< ' + str(self._left) + ' | >'
152        else:
153            return '< | ' + str(self._right) + ' >'
154
155    def __len__(self) -> int:
156        """Semantically, an XOR always contains just one value."""
157        return 1
158
159    def __eq__(self, other: object) -> bool:
160        if not isinstance(other, type(self)):
161            return False
162
163        if self and other:
164            if self._left is other._left:
165                return True
166            return self._left == other._left
167        elif not self and not other:
168            if self._right is other._right:
169                return True
170            return self._right == other._right
171        else:
172            return False
173
174    def get(self, alt: L|Nada=nada) -> L:
175        """
176        ##### Get value if a Left.
177
178        * if the XOR is a left, return its value
179        * otherwise, return alt if it is provided
180        * raises `ValueError` if alternate value needed but not provided
181
182        """
183        if self._left is nada:
184            if alt is nada:
185                raise ValueError('Alternate return value needed but not provided.')
186            else:
187                return cast(L, alt)
188        else:
189            return cast(L, self._left)
190
191    def getRight(self, alt: R|Nada=nada) -> R:
192        """
193        ##### Get value if `XOR` is a Right.
194
195        * if XOR is a right, return its value
196        * otherwise return an alternate value of type ~R
197        * raises `ValueError` if alternate value needed but not provided
198
199        """
200        if not self:
201            if self._right is not nada:
202                return cast(R, self._right)
203            elif alt is not nada:
204                return cast(R, alt)
205            else:
206                raise ValueError('Alternate return type needed but not provided.')
207        else:
208            if alt is not nada:
209                return cast(R, alt)
210            else:
211                raise ValueError('Alternate return type needed but not provided.')
212
213    def getDefaultRight(self) -> R:
214        """
215        ##### Get default right value
216
217        Get value if a "right" otherwise get the default "right" value.
218        """
219        return cast(R, self._right)
220
221    def map(self, f: Callable[[L], S], right: R|Nada=nada) -> XOR[S, R]:
222        """
223        ##### Map over an XOR.
224
225        TODO: catch any errors `f` may throw
226
227        * if `XOR` is a "left" then map `f`
228          * if `f` successful, return a "left" XOR[S, R]
229          * if `f` unsuccessful and `right` given, return `XOR(right=right)`
230          * if `f` unsuccessful and `right` not given, return `XOR(right=self.right)`
231        * if `XOR` is a "right"
232          * if `right` is given, return `XOR(right=right)`
233          * if `right` is not given, return `XOR(right=self.right)`
234
235        """
236        if self._left is nada:
237            if right is nada:
238                return XOR(right=self._right)
239            else:
240                return XOR(right=right)
241
242        if right is nada:
243            return XOR(f(cast(L, self._left)), self._right)
244        else:
245            return XOR(f(cast(L, self._left)), right)
246
247    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
248        """Map over a "right" value."""
249        if self._left is nada:
250            return XOR(nada, g(cast(R,self._right)))
251        return self
252
253    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
254        """Map and flatten a Left value, propagate Right values."""
255        if self._left is nada:
256            return XOR(nada, self._right)
257        else:
258            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"
  • immutable, an XOR does not change after being created
  • immutable semantics, map & flatMap return new instances
  • raises ValueError if both "left" and "right" values are not given
  • raises ValueError if both if a potential "right" value is not given
XOR( left: Union[~L, grscheller.fp.nada.Nada] = nada, right: Union[~R, grscheller.fp.nada.Nada] = nada)
125    def __init__(self
126            , left: L|Nada=nada
127            , right: R|Nada=nada):
128
129        if left == nada == right:
130            raise ValueError('XOR: neither left nor right values provided')
131        self._left, self._right = left, right
def get(self, alt: Union[~L, grscheller.fp.nada.Nada] = nada) -> ~L:
174    def get(self, alt: L|Nada=nada) -> L:
175        """
176        ##### Get value if a Left.
177
178        * if the XOR is a left, return its value
179        * otherwise, return alt if it is provided
180        * raises `ValueError` if alternate value needed but not provided
181
182        """
183        if self._left is nada:
184            if alt is nada:
185                raise ValueError('Alternate return value needed but not provided.')
186            else:
187                return cast(L, alt)
188        else:
189            return cast(L, self._left)
Get value if a Left.
  • if the XOR is a left, return its value
  • otherwise, return alt if it is provided
  • raises ValueError if alternate value needed but not provided
def getRight(self, alt: Union[~R, grscheller.fp.nada.Nada] = nada) -> ~R:
191    def getRight(self, alt: R|Nada=nada) -> R:
192        """
193        ##### Get value if `XOR` is a Right.
194
195        * if XOR is a right, return its value
196        * otherwise return an alternate value of type ~R
197        * raises `ValueError` if alternate value needed but not provided
198
199        """
200        if not self:
201            if self._right is not nada:
202                return cast(R, self._right)
203            elif alt is not nada:
204                return cast(R, alt)
205            else:
206                raise ValueError('Alternate return type needed but not provided.')
207        else:
208            if alt is not nada:
209                return cast(R, alt)
210            else:
211                raise ValueError('Alternate return type needed but not provided.')
Get value if XOR is a Right.
  • if XOR is a right, return its value
  • otherwise return an alternate value of type ~R
  • raises ValueError if alternate value needed but not provided
def getDefaultRight(self) -> ~R:
213    def getDefaultRight(self) -> R:
214        """
215        ##### Get default right value
216
217        Get value if a "right" otherwise get the default "right" value.
218        """
219        return cast(R, self._right)
Get default right value

Get value if a "right" otherwise get the default "right" value.

def map( self, f: Callable[[~L], ~S], right: Union[~R, grscheller.fp.nada.Nada] = nada) -> XOR[~S, ~R]:
221    def map(self, f: Callable[[L], S], right: R|Nada=nada) -> XOR[S, R]:
222        """
223        ##### Map over an XOR.
224
225        TODO: catch any errors `f` may throw
226
227        * if `XOR` is a "left" then map `f`
228          * if `f` successful, return a "left" XOR[S, R]
229          * if `f` unsuccessful and `right` given, return `XOR(right=right)`
230          * if `f` unsuccessful and `right` not given, return `XOR(right=self.right)`
231        * if `XOR` is a "right"
232          * if `right` is given, return `XOR(right=right)`
233          * if `right` is not given, return `XOR(right=self.right)`
234
235        """
236        if self._left is nada:
237            if right is nada:
238                return XOR(right=self._right)
239            else:
240                return XOR(right=right)
241
242        if right is nada:
243            return XOR(f(cast(L, self._left)), self._right)
244        else:
245            return XOR(f(cast(L, self._left)), right)
Map over an XOR.

TODO: catch any errors f may throw

  • if XOR is a "left" then map f
    • if f successful, return a "left" XOR[S, R]
    • if f unsuccessful and right given, return XOR(right=right)
    • if f unsuccessful and right not given, return XOR(right=self.right)
  • if XOR is a "right"
    • if right is given, return XOR(right=right)
    • if right is not given, return XOR(right=self.right)
def mapRight(self, g: Callable[[~R], ~R]) -> XOR[~L, ~R]:
247    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
248        """Map over a "right" value."""
249        if self._left is nada:
250            return XOR(nada, g(cast(R,self._right)))
251        return self

Map over a "right" value.

def flatMap( self, f: Callable[[~L], XOR[~S, ~R]]) -> XOR[~S, ~R]:
253    def flatMap(self, f: Callable[[L], XOR[S, R]]) -> XOR[S, R]:
254        """Map and flatten a Left value, propagate Right values."""
255        if self._left is nada:
256            return XOR(nada, self._right)
257        else:
258            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]:
262def mb_to_xor(m: MB[T], right: R) -> XOR[T, R]:
263    """
264    #### Convert a MB to an XOR.
265
266    """
267    if m:
268        return XOR(m.get(), right)
269    else:
270        return XOR(nada, right)

Convert a MB to an XOR.

def xor_to_mb( e: XOR[~T, ~S]) -> MB[~T]:
272def xor_to_mb(e: XOR[T,S]) -> MB[T]:
273    """
274    ####Convert an XOR to a MB.
275
276    """
277    if e:
278        return MB(e.get())
279    else:
280        return MB()

Convert an XOR to a MB.