grscheller.fp.err_handling

Module fp.err_handling - monadic error handling

Functional data types to use in lieu of exceptions.

Error handling types:

  • class MB: Maybe (Optional) monad
  • class XOR: Left biased Either monad
  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"""### Module fp.err_handling - monadic error handling
 16
 17Functional data types to use in lieu of exceptions.
 18
 19#### Error handling types:
 20
 21* **class MB:** Maybe (Optional) monad
 22* **class XOR:** Left biased Either monad
 23
 24"""
 25from __future__ import annotations
 26
 27__all__ = [ 'MB', 'XOR' ]
 28
 29from collections.abc import Callable, Iterator, Sequence
 30from typing import cast, Final, Never, overload
 31from .singletons import Sentinel
 32
 33class MB[D]():
 34    """Maybe monad - class wrapping a potentially missing value.
 35
 36    * where `MB(value)` contains a possible value of type `~D`
 37    * `MB()` semantically represent a non-existent or missing value of type `~D`
 38    * immutable, a `MB` does not change after being created
 39      * immutable semantics, map & flatMap return new instances
 40      * warning: contained values need not be immutable
 41      * warning: not hashable if a mutable value is contained
 42    * raises `ValueError` if get method not given default value & one is needed
 43    * immutable, a `MB` does not change after being created
 44      * immutable semantics, map & flatMap return new instances
 45        * warning: contained values need not be immutable
 46        * warning: not hashable if contained value is mutable
 47
 48    """
 49    __slots__ = '_value',
 50    __match_args__ = '_value',
 51
 52    @overload
 53    def __init__(self) -> None: ...
 54    @overload
 55    def __init__(self, value: MB[D]) -> None: ...
 56    @overload
 57    def __init__(self, value: D) -> None: ...
 58
 59    def __init__(self, value: D|MB[D]|Sentinel=Sentinel()) -> None:
 60        self._value: D|Sentinel
 61        match value:
 62            case MB(d):
 63                self._value = d
 64            case MB():
 65                self._value = Sentinel()
 66            case s if s is Sentinel():
 67                self._value = s
 68            case d:
 69                self._value = d
 70
 71    def __bool__(self) -> bool:
 72        return self._value is not Sentinel()
 73
 74    def __iter__(self) -> Iterator[D]:
 75        if self:
 76            yield cast(D, self._value)
 77
 78    def __repr__(self) -> str:
 79        if self:
 80            return 'MB(' + repr(self._value) + ')'
 81        else:
 82            return 'MB()'
 83
 84    def __len__(self) -> int:
 85        return (1 if self else 0)
 86
 87    def __eq__(self, other: object) -> bool:
 88        if not isinstance(other, type(self)):
 89            return False
 90
 91        if self._value is other._value:
 92            return True
 93        elif self._value == other._value:
 94            return True
 95        else:
 96            return False
 97
 98    def get(self, alt: D|Sentinel=Sentinel()) -> D|Never:
 99        """Return the contained value if it exists, otherwise an alternate value.
100
101        * alternate value must me of type `~D`
102        * raises `ValueError` if an alternate value is not provided but needed
103
104        """
105        if self._value is not Sentinel():
106            return cast(D, self._value)
107        else:
108            if alt is not Sentinel():
109                return cast(D, alt)
110            else:
111                msg = 'An alternate return type not provided.'
112                raise ValueError(msg)
113
114    def map[U](self, f: Callable[[D], U]) -> MB[U]:
115        """Map function `f` over the 0 or 1 elements of this data structure.
116
117        * if `f` should fail, return a MB()
118
119        """
120        if self._value is Sentinel():
121            return cast(MB[U], self)
122        else:
123            try:
124                return MB(f(cast(D, self._value)))
125            except Exception:
126                return MB()
127
128    def flatmap[U](self, f: Callable[[D], MB[U]]) -> MB[U]:
129        """Map `MB` with function `f` and flatten."""
130        try:
131            return (f(cast(D, self._value)) if self else MB())
132        except Exception:
133            return MB()
134
135    @staticmethod
136    def call[U, V](f: Callable[[U], V], u: U) -> MB[V]:
137        """Return an function call wrapped in a MB"""
138        try:
139            return MB(f(u))
140        except Exception:
141            return MB()
142
143    @staticmethod
144    def lz_call[U, V](f: Callable[[U], V], u: U) -> Callable[[], MB[V]]:
145        def ret() -> MB[V]:
146            return MB.call(f, u)
147        return ret
148
149    @staticmethod
150    def idx[V](v: Sequence[V], ii: int) -> MB[V]:
151        """Return an indexed value wrapped in a MB"""
152        try:
153            return MB(v[ii])
154        except IndexError:
155            return MB()
156
157    @staticmethod
158    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], MB[V]]:
159        def ret() -> MB[V]:
160            return MB.idx(v, ii)
161        return ret
162
163    @staticmethod
164    def sequence[T](seq_mb_d: Sequence[MB[T]]) -> MB[Sequence[T]]:
165        """Sequence an indexable container of `MB[~D]`
166
167        * if all the contained `MB` values in the container are not empty,
168          * return a `MB` of a container containing the values contained
169          * otherwise return an empty `MB`
170
171        """
172        l: list[T] = []
173
174        for mb_d in seq_mb_d:
175            if mb_d:
176                l.append(mb_d.get())
177            else:
178                return MB()
179
180        ds = cast(Sequence[T], type(seq_mb_d)(l))  # type: ignore # will be a subclass at runtime
181        return MB(ds)
182
183class XOR[L, R]():
184    """Either monad - class semantically containing either a left or a right
185    value, but not both.
186
187    * implements a left biased Either Monad
188    * `XOR(left: ~L, right: ~R)` produces a left `XOR` which
189      * contains a value of type `~L`
190      * and a potential right value of type `~R`
191    * `XOR(MB(), right)` produces a right `XOR`
192    * in a Boolean context
193      * `True` if a left `XOR`
194      * `False` if a right `XOR`
195    * two `XOR` objects compare as equal when
196      * both are left values or both are right values whose values
197        * are the same object
198        * compare as equal
199    * immutable, an `XOR` does not change after being created
200      * immutable semantics, map & flatMap return new instances
201        * warning: contained values need not be immutable
202        * warning: not hashable if value or potential right value mutable
203
204    """
205    __slots__ = '_left', '_right'
206    __match_args__ = ('_left', '_right')
207
208    @overload
209    def __init__(self, left: L, right: R) -> None: ...
210    @overload
211    def __init__(self, left: MB[L], right: R) -> None: ...
212
213    def __init__(self, left: L|MB[L], right: R) -> None:
214        self._left: L|Sentinel
215        self._right: R
216        match left:
217            case MB(l):
218                self._left, self._right = l, right
219            case MB():
220                self._left, self._right = Sentinel(), right
221            case l:
222                self._left, self._right = l, right
223
224    def __bool__(self) -> bool:
225        return self._left is not Sentinel()
226
227    def __iter__(self) -> Iterator[L]:
228        if self:
229            yield cast(L, self._left)
230
231    def __repr__(self) -> str:
232        if self:
233            return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
234        else:
235            return 'XOR(MB(), ' + repr(self._right) + ')'
236
237    def __str__(self) -> str:
238        if self:
239            return '< ' + str(self._left) + ' | >'
240        else:
241            return '< | ' + str(self._right) + ' >'
242
243    def __len__(self) -> int:
244        # Semantically, an XOR always contains just one value.
245        return 1
246
247    def __eq__(self, other: object) -> bool:
248        if not isinstance(other, type(self)):
249            return False
250
251        if self and other:
252            if self._left is other._left:
253                return True
254            elif self._left == other._left:
255                return True
256            else:
257                return False
258
259        if not self and not other:
260            if self._right is other._right:
261                return True
262            elif self._right == other._right:
263                return True
264            else:
265                return False
266
267        return False
268
269    @overload
270    def getLeft(self) -> L|Never: ...
271    @overload
272    def getLeft(self, altLeft: L) -> L: ...
273
274    def getLeft(self, altLeft: L|Sentinel=Sentinel()) -> L|Never:
275        """Get value if a left.
276
277        * if the `XOR` is a left, return its value
278        * otherwise, return `alt: ~L` if it is provided
279        * alternate value must me of type `~L`
280        * raises `ValueError` if an alternate value is not provided but needed
281
282        """
283        if self._left is Sentinel():
284            if altLeft is Sentinel():
285                msg1 = 'XOR: getLeft method called on a right XOR '
286                msg2 = 'without an alternate left return value.'
287                raise(ValueError(msg1+msg2))
288            else:
289                return cast(L, altLeft)
290        else:
291            return cast(L, self._left)
292
293    def getRight(self) -> R:
294        """Get value of `XOR` if a right, potential right value if a left.
295
296        * if `XOR` is a right, return its value
297        * if `XOR` is a left, return the potential right value
298
299        """
300        return self._right
301
302    def makeRight(self) -> XOR[L, R]:
303        """Make a right based on the `XOR`.
304
305        * return a right based on potential right value
306        * returns itself if already a right
307
308        """
309        if self._left is Sentinel():
310            return self
311        else:
312            return cast(XOR[L, R], XOR(Sentinel(), self._right))
313
314    def newRight(self, right: R) -> XOR[L, R]:
315        """Swap in a right value. 
316
317        * returns a new instance with a new right (or potential right) value.
318        """
319        if self._left is Sentinel():
320            return cast(XOR[L, R], XOR(MB(), right))
321        else:
322            return cast(XOR[L, R], XOR(self._left, right))
323
324    def map[U](self, f: Callable[[L], U]) -> XOR[U, R]:
325        """Map over if a left value.
326
327        * if `XOR` is a left then map `f` over its value
328          * if `f` successful return a left `XOR[S, R]`
329          * if `f` unsuccessful return right `XOR[S, R]`
330            * swallows any exceptions `f` may throw
331        * if `XOR` is a right
332          * return new `XOR(right=self._right): XOR[S, R]`
333          * use method `mapRight` to adjust the returned value
334
335        """
336        if self._left is Sentinel():
337            return cast(XOR[U, R], self)
338        try:
339            applied = f(cast(L, self._left))
340        except Exception:
341            return cast(XOR[U, R], XOR(MB(), self._right))
342        else:
343            return XOR(applied, self._right)
344
345    def mapRight(self, g: Callable[[R], R], altRight: R) -> XOR[L, R]:
346        """Map over a right or potential right value."""
347        try:
348            applied = g(self._right)
349            right = applied
350        except:
351            right = altRight
352
353        if self:
354            left: L|MB[L] = cast(L, self._left)
355        else:
356            left = MB()
357
358        return XOR(left, right)
359
360    def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
361        """Flatmap - bind
362
363        * map over then flatten left values
364        * propagate right values
365
366        """
367        if self._left is Sentinel():
368            return cast(XOR[U, R], self)
369        else:
370            return f(cast(L, self._left))
371
372    @staticmethod
373    def call[U, V](f: Callable[[U], V], left: U) -> XOR[V, MB[Exception]]:
374        try:
375            return XOR(f(left), MB())
376        except Exception as esc:
377            return XOR(MB(), MB(esc))
378
379    @staticmethod
380    def lz_call[U, V](f: Callable[[U], V], left: U) -> Callable[[], XOR[V, MB[Exception]]]:
381        def ret() -> XOR[V, MB[Exception]]:
382            return XOR.call(f, left)
383        return ret
384
385    @staticmethod
386    def idx[V](v: Sequence[V], ii: int) -> XOR[V, MB[Exception]]:
387        try:
388            return XOR(v[ii], MB())
389        except Exception as esc:
390            return XOR(MB(), MB(esc))
391
392    @staticmethod
393    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], XOR[V, MB[Exception]]]:
394        def ret() -> XOR[V, MB[Exception]]:
395            return XOR.idx(v, ii)
396        return ret
397
398    @staticmethod
399    def sequence(seq_xor_lr: Sequence[XOR[L, R]], potential_right: R) -> XOR[Sequence[L], R]:
400        """Sequence an indexable container of `XOR[L, R]`
401
402        * if all the `XOR` values contained in the container are lefts, then
403          * return an `XOR` of the same type container of all the left values
404          * setting the potential right `potential_right`
405        * if at least one of the `XOR` values contained in the container is a right,
406          * return a right XOR containing the right value of the first right
407
408        """
409        l: list[L] = []
410
411        for xor_lr in seq_xor_lr:
412            if xor_lr:
413                l.append(xor_lr.getLeft())
414            else:
415                return cast(XOR[Sequence[L], R], XOR(Sentinel(), xor_lr.getRight()))
416
417        ds = cast(Sequence[L], type(seq_xor_lr)(l))  # type: ignore # will be a subclass at runtime
418        return XOR(ds, potential_right)
class MB(typing.Generic[D]):
 34class MB[D]():
 35    """Maybe monad - class wrapping a potentially missing value.
 36
 37    * where `MB(value)` contains a possible value of type `~D`
 38    * `MB()` semantically represent a non-existent or missing value of type `~D`
 39    * immutable, a `MB` does not change after being created
 40      * immutable semantics, map & flatMap return new instances
 41      * warning: contained values need not be immutable
 42      * warning: not hashable if a mutable value is contained
 43    * raises `ValueError` if get method not given default value & one is needed
 44    * immutable, a `MB` does not change after being created
 45      * immutable semantics, map & flatMap return new instances
 46        * warning: contained values need not be immutable
 47        * warning: not hashable if contained value is mutable
 48
 49    """
 50    __slots__ = '_value',
 51    __match_args__ = '_value',
 52
 53    @overload
 54    def __init__(self) -> None: ...
 55    @overload
 56    def __init__(self, value: MB[D]) -> None: ...
 57    @overload
 58    def __init__(self, value: D) -> None: ...
 59
 60    def __init__(self, value: D|MB[D]|Sentinel=Sentinel()) -> None:
 61        self._value: D|Sentinel
 62        match value:
 63            case MB(d):
 64                self._value = d
 65            case MB():
 66                self._value = Sentinel()
 67            case s if s is Sentinel():
 68                self._value = s
 69            case d:
 70                self._value = d
 71
 72    def __bool__(self) -> bool:
 73        return self._value is not Sentinel()
 74
 75    def __iter__(self) -> Iterator[D]:
 76        if self:
 77            yield cast(D, self._value)
 78
 79    def __repr__(self) -> str:
 80        if self:
 81            return 'MB(' + repr(self._value) + ')'
 82        else:
 83            return 'MB()'
 84
 85    def __len__(self) -> int:
 86        return (1 if self else 0)
 87
 88    def __eq__(self, other: object) -> bool:
 89        if not isinstance(other, type(self)):
 90            return False
 91
 92        if self._value is other._value:
 93            return True
 94        elif self._value == other._value:
 95            return True
 96        else:
 97            return False
 98
 99    def get(self, alt: D|Sentinel=Sentinel()) -> D|Never:
100        """Return the contained value if it exists, otherwise an alternate value.
101
102        * alternate value must me of type `~D`
103        * raises `ValueError` if an alternate value is not provided but needed
104
105        """
106        if self._value is not Sentinel():
107            return cast(D, self._value)
108        else:
109            if alt is not Sentinel():
110                return cast(D, alt)
111            else:
112                msg = 'An alternate return type not provided.'
113                raise ValueError(msg)
114
115    def map[U](self, f: Callable[[D], U]) -> MB[U]:
116        """Map function `f` over the 0 or 1 elements of this data structure.
117
118        * if `f` should fail, return a MB()
119
120        """
121        if self._value is Sentinel():
122            return cast(MB[U], self)
123        else:
124            try:
125                return MB(f(cast(D, self._value)))
126            except Exception:
127                return MB()
128
129    def flatmap[U](self, f: Callable[[D], MB[U]]) -> MB[U]:
130        """Map `MB` with function `f` and flatten."""
131        try:
132            return (f(cast(D, self._value)) if self else MB())
133        except Exception:
134            return MB()
135
136    @staticmethod
137    def call[U, V](f: Callable[[U], V], u: U) -> MB[V]:
138        """Return an function call wrapped in a MB"""
139        try:
140            return MB(f(u))
141        except Exception:
142            return MB()
143
144    @staticmethod
145    def lz_call[U, V](f: Callable[[U], V], u: U) -> Callable[[], MB[V]]:
146        def ret() -> MB[V]:
147            return MB.call(f, u)
148        return ret
149
150    @staticmethod
151    def idx[V](v: Sequence[V], ii: int) -> MB[V]:
152        """Return an indexed value wrapped in a MB"""
153        try:
154            return MB(v[ii])
155        except IndexError:
156            return MB()
157
158    @staticmethod
159    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], MB[V]]:
160        def ret() -> MB[V]:
161            return MB.idx(v, ii)
162        return ret
163
164    @staticmethod
165    def sequence[T](seq_mb_d: Sequence[MB[T]]) -> MB[Sequence[T]]:
166        """Sequence an indexable container of `MB[~D]`
167
168        * if all the contained `MB` values in the container are not empty,
169          * return a `MB` of a container containing the values contained
170          * otherwise return an empty `MB`
171
172        """
173        l: list[T] = []
174
175        for mb_d in seq_mb_d:
176            if mb_d:
177                l.append(mb_d.get())
178            else:
179                return MB()
180
181        ds = cast(Sequence[T], type(seq_mb_d)(l))  # type: ignore # will be a subclass at runtime
182        return MB(ds)

Maybe monad - class wrapping a potentially missing value.

  • where MB(value) contains a possible value of type ~D
  • MB() semantically represent a non-existent or missing value of type ~D
  • immutable, a MB does not change after being created
    • immutable semantics, map & flatMap return new instances
    • warning: contained values need not be immutable
    • warning: not hashable if a mutable value is contained
  • raises ValueError if get method not given default value & one is needed
  • immutable, a MB does not change after being created
    • immutable semantics, map & flatMap return new instances
      • warning: contained values need not be immutable
      • warning: not hashable if contained value is mutable
MB(value: 'D | MB[D] | Sentinel' = Sentinel())
60    def __init__(self, value: D|MB[D]|Sentinel=Sentinel()) -> None:
61        self._value: D|Sentinel
62        match value:
63            case MB(d):
64                self._value = d
65            case MB():
66                self._value = Sentinel()
67            case s if s is Sentinel():
68                self._value = s
69            case d:
70                self._value = d
def get(self, alt: 'D | Sentinel' = Sentinel()) -> 'D | Never':
 99    def get(self, alt: D|Sentinel=Sentinel()) -> D|Never:
100        """Return the contained value if it exists, otherwise an alternate value.
101
102        * alternate value must me of type `~D`
103        * raises `ValueError` if an alternate value is not provided but needed
104
105        """
106        if self._value is not Sentinel():
107            return cast(D, self._value)
108        else:
109            if alt is not Sentinel():
110                return cast(D, alt)
111            else:
112                msg = 'An alternate return type not provided.'
113                raise ValueError(msg)

Return the contained value if it exists, otherwise an alternate value.

  • alternate value must me of type ~D
  • raises ValueError if an alternate value is not provided but needed
def map(self, f: 'Callable[[D], U]') -> 'MB[U]':
115    def map[U](self, f: Callable[[D], U]) -> MB[U]:
116        """Map function `f` over the 0 or 1 elements of this data structure.
117
118        * if `f` should fail, return a MB()
119
120        """
121        if self._value is Sentinel():
122            return cast(MB[U], self)
123        else:
124            try:
125                return MB(f(cast(D, self._value)))
126            except Exception:
127                return MB()

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

  • if f should fail, return a MB()
def flatmap(self, f: 'Callable[[D], MB[U]]') -> 'MB[U]':
129    def flatmap[U](self, f: Callable[[D], MB[U]]) -> MB[U]:
130        """Map `MB` with function `f` and flatten."""
131        try:
132            return (f(cast(D, self._value)) if self else MB())
133        except Exception:
134            return MB()

Map MB with function f and flatten.

@staticmethod
def call(f: 'Callable[[U], V]', u: 'U') -> 'MB[V]':
136    @staticmethod
137    def call[U, V](f: Callable[[U], V], u: U) -> MB[V]:
138        """Return an function call wrapped in a MB"""
139        try:
140            return MB(f(u))
141        except Exception:
142            return MB()

Return an function call wrapped in a MB

@staticmethod
def lz_call(f: 'Callable[[U], V]', u: 'U') -> 'Callable[[], MB[V]]':
144    @staticmethod
145    def lz_call[U, V](f: Callable[[U], V], u: U) -> Callable[[], MB[V]]:
146        def ret() -> MB[V]:
147            return MB.call(f, u)
148        return ret
@staticmethod
def idx(v: 'Sequence[V]', ii: int) -> 'MB[V]':
150    @staticmethod
151    def idx[V](v: Sequence[V], ii: int) -> MB[V]:
152        """Return an indexed value wrapped in a MB"""
153        try:
154            return MB(v[ii])
155        except IndexError:
156            return MB()

Return an indexed value wrapped in a MB

@staticmethod
def lz_idx(v: 'Sequence[V]', ii: int) -> 'Callable[[], MB[V]]':
158    @staticmethod
159    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], MB[V]]:
160        def ret() -> MB[V]:
161            return MB.idx(v, ii)
162        return ret
@staticmethod
def sequence(seq_mb_d: 'Sequence[MB[T]]') -> 'MB[Sequence[T]]':
164    @staticmethod
165    def sequence[T](seq_mb_d: Sequence[MB[T]]) -> MB[Sequence[T]]:
166        """Sequence an indexable container of `MB[~D]`
167
168        * if all the contained `MB` values in the container are not empty,
169          * return a `MB` of a container containing the values contained
170          * otherwise return an empty `MB`
171
172        """
173        l: list[T] = []
174
175        for mb_d in seq_mb_d:
176            if mb_d:
177                l.append(mb_d.get())
178            else:
179                return MB()
180
181        ds = cast(Sequence[T], type(seq_mb_d)(l))  # type: ignore # will be a subclass at runtime
182        return MB(ds)

Sequence an indexable container of MB[~D]

  • if all the contained MB values in the container are not empty,
    • return a MB of a container containing the values contained
    • otherwise return an empty MB
class XOR(typing.Generic[L, R]):
184class XOR[L, R]():
185    """Either monad - class semantically containing either a left or a right
186    value, but not both.
187
188    * implements a left biased Either Monad
189    * `XOR(left: ~L, right: ~R)` produces a left `XOR` which
190      * contains a value of type `~L`
191      * and a potential right value of type `~R`
192    * `XOR(MB(), right)` produces a right `XOR`
193    * in a Boolean context
194      * `True` if a left `XOR`
195      * `False` if a right `XOR`
196    * two `XOR` objects compare as equal when
197      * both are left values or both are right values whose values
198        * are the same object
199        * compare as equal
200    * immutable, an `XOR` does not change after being created
201      * immutable semantics, map & flatMap return new instances
202        * warning: contained values need not be immutable
203        * warning: not hashable if value or potential right value mutable
204
205    """
206    __slots__ = '_left', '_right'
207    __match_args__ = ('_left', '_right')
208
209    @overload
210    def __init__(self, left: L, right: R) -> None: ...
211    @overload
212    def __init__(self, left: MB[L], right: R) -> None: ...
213
214    def __init__(self, left: L|MB[L], right: R) -> None:
215        self._left: L|Sentinel
216        self._right: R
217        match left:
218            case MB(l):
219                self._left, self._right = l, right
220            case MB():
221                self._left, self._right = Sentinel(), right
222            case l:
223                self._left, self._right = l, right
224
225    def __bool__(self) -> bool:
226        return self._left is not Sentinel()
227
228    def __iter__(self) -> Iterator[L]:
229        if self:
230            yield cast(L, self._left)
231
232    def __repr__(self) -> str:
233        if self:
234            return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
235        else:
236            return 'XOR(MB(), ' + repr(self._right) + ')'
237
238    def __str__(self) -> str:
239        if self:
240            return '< ' + str(self._left) + ' | >'
241        else:
242            return '< | ' + str(self._right) + ' >'
243
244    def __len__(self) -> int:
245        # Semantically, an XOR always contains just one value.
246        return 1
247
248    def __eq__(self, other: object) -> bool:
249        if not isinstance(other, type(self)):
250            return False
251
252        if self and other:
253            if self._left is other._left:
254                return True
255            elif self._left == other._left:
256                return True
257            else:
258                return False
259
260        if not self and not other:
261            if self._right is other._right:
262                return True
263            elif self._right == other._right:
264                return True
265            else:
266                return False
267
268        return False
269
270    @overload
271    def getLeft(self) -> L|Never: ...
272    @overload
273    def getLeft(self, altLeft: L) -> L: ...
274
275    def getLeft(self, altLeft: L|Sentinel=Sentinel()) -> L|Never:
276        """Get value if a left.
277
278        * if the `XOR` is a left, return its value
279        * otherwise, return `alt: ~L` if it is provided
280        * alternate value must me of type `~L`
281        * raises `ValueError` if an alternate value is not provided but needed
282
283        """
284        if self._left is Sentinel():
285            if altLeft is Sentinel():
286                msg1 = 'XOR: getLeft method called on a right XOR '
287                msg2 = 'without an alternate left return value.'
288                raise(ValueError(msg1+msg2))
289            else:
290                return cast(L, altLeft)
291        else:
292            return cast(L, self._left)
293
294    def getRight(self) -> R:
295        """Get value of `XOR` if a right, potential right value if a left.
296
297        * if `XOR` is a right, return its value
298        * if `XOR` is a left, return the potential right value
299
300        """
301        return self._right
302
303    def makeRight(self) -> XOR[L, R]:
304        """Make a right based on the `XOR`.
305
306        * return a right based on potential right value
307        * returns itself if already a right
308
309        """
310        if self._left is Sentinel():
311            return self
312        else:
313            return cast(XOR[L, R], XOR(Sentinel(), self._right))
314
315    def newRight(self, right: R) -> XOR[L, R]:
316        """Swap in a right value. 
317
318        * returns a new instance with a new right (or potential right) value.
319        """
320        if self._left is Sentinel():
321            return cast(XOR[L, R], XOR(MB(), right))
322        else:
323            return cast(XOR[L, R], XOR(self._left, right))
324
325    def map[U](self, f: Callable[[L], U]) -> XOR[U, R]:
326        """Map over if a left value.
327
328        * if `XOR` is a left then map `f` over its value
329          * if `f` successful return a left `XOR[S, R]`
330          * if `f` unsuccessful return right `XOR[S, R]`
331            * swallows any exceptions `f` may throw
332        * if `XOR` is a right
333          * return new `XOR(right=self._right): XOR[S, R]`
334          * use method `mapRight` to adjust the returned value
335
336        """
337        if self._left is Sentinel():
338            return cast(XOR[U, R], self)
339        try:
340            applied = f(cast(L, self._left))
341        except Exception:
342            return cast(XOR[U, R], XOR(MB(), self._right))
343        else:
344            return XOR(applied, self._right)
345
346    def mapRight(self, g: Callable[[R], R], altRight: R) -> XOR[L, R]:
347        """Map over a right or potential right value."""
348        try:
349            applied = g(self._right)
350            right = applied
351        except:
352            right = altRight
353
354        if self:
355            left: L|MB[L] = cast(L, self._left)
356        else:
357            left = MB()
358
359        return XOR(left, right)
360
361    def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
362        """Flatmap - bind
363
364        * map over then flatten left values
365        * propagate right values
366
367        """
368        if self._left is Sentinel():
369            return cast(XOR[U, R], self)
370        else:
371            return f(cast(L, self._left))
372
373    @staticmethod
374    def call[U, V](f: Callable[[U], V], left: U) -> XOR[V, MB[Exception]]:
375        try:
376            return XOR(f(left), MB())
377        except Exception as esc:
378            return XOR(MB(), MB(esc))
379
380    @staticmethod
381    def lz_call[U, V](f: Callable[[U], V], left: U) -> Callable[[], XOR[V, MB[Exception]]]:
382        def ret() -> XOR[V, MB[Exception]]:
383            return XOR.call(f, left)
384        return ret
385
386    @staticmethod
387    def idx[V](v: Sequence[V], ii: int) -> XOR[V, MB[Exception]]:
388        try:
389            return XOR(v[ii], MB())
390        except Exception as esc:
391            return XOR(MB(), MB(esc))
392
393    @staticmethod
394    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], XOR[V, MB[Exception]]]:
395        def ret() -> XOR[V, MB[Exception]]:
396            return XOR.idx(v, ii)
397        return ret
398
399    @staticmethod
400    def sequence(seq_xor_lr: Sequence[XOR[L, R]], potential_right: R) -> XOR[Sequence[L], R]:
401        """Sequence an indexable container of `XOR[L, R]`
402
403        * if all the `XOR` values contained in the container are lefts, then
404          * return an `XOR` of the same type container of all the left values
405          * setting the potential right `potential_right`
406        * if at least one of the `XOR` values contained in the container is a right,
407          * return a right XOR containing the right value of the first right
408
409        """
410        l: list[L] = []
411
412        for xor_lr in seq_xor_lr:
413            if xor_lr:
414                l.append(xor_lr.getLeft())
415            else:
416                return cast(XOR[Sequence[L], R], XOR(Sentinel(), xor_lr.getRight()))
417
418        ds = cast(Sequence[L], type(seq_xor_lr)(l))  # type: ignore # will be a subclass at runtime
419        return XOR(ds, potential_right)

Either monad - class semantically containing either a left or a right value, but not both.

  • implements a left biased Either Monad
  • XOR(left: ~L, right: ~R) produces a left XOR which
    • contains a value of type ~L
    • and a potential right value of type ~R
  • XOR(MB(), right) produces a right XOR
  • in a Boolean context
    • True if a left XOR
    • False if a right XOR
  • two XOR objects compare as equal when
    • both are left values or both are right values whose values
      • are the same object
      • 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
      • warning: not hashable if value or potential right value mutable
XOR(left: 'L | MB[L]', right: 'R')
214    def __init__(self, left: L|MB[L], right: R) -> None:
215        self._left: L|Sentinel
216        self._right: R
217        match left:
218            case MB(l):
219                self._left, self._right = l, right
220            case MB():
221                self._left, self._right = Sentinel(), right
222            case l:
223                self._left, self._right = l, right
def getLeft(self, altLeft: 'L | Sentinel' = Sentinel()) -> 'L | Never':
275    def getLeft(self, altLeft: L|Sentinel=Sentinel()) -> L|Never:
276        """Get value if a left.
277
278        * if the `XOR` is a left, return its value
279        * otherwise, return `alt: ~L` if it is provided
280        * alternate value must me of type `~L`
281        * raises `ValueError` if an alternate value is not provided but needed
282
283        """
284        if self._left is Sentinel():
285            if altLeft is Sentinel():
286                msg1 = 'XOR: getLeft method called on a right XOR '
287                msg2 = 'without an alternate left return value.'
288                raise(ValueError(msg1+msg2))
289            else:
290                return cast(L, altLeft)
291        else:
292            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
  • alternate value must me of type ~L
  • raises ValueError if an alternate value is not provided but needed
def getRight(self) -> 'R':
294    def getRight(self) -> R:
295        """Get value of `XOR` if a right, potential right value if a left.
296
297        * if `XOR` is a right, return its value
298        * if `XOR` is a left, return the potential right value
299
300        """
301        return self._right

Get value of XOR if a right, potential right value if a left.

  • if XOR is a right, return its value
  • if XOR is a left, return the potential right value
def makeRight(self) -> 'XOR[L, R]':
303    def makeRight(self) -> XOR[L, R]:
304        """Make a right based on the `XOR`.
305
306        * return a right based on potential right value
307        * returns itself if already a right
308
309        """
310        if self._left is Sentinel():
311            return self
312        else:
313            return cast(XOR[L, R], XOR(Sentinel(), self._right))

Make a right based on the XOR.

  • return a right based on potential right value
  • returns itself if already a right
def newRight(self, right: 'R') -> 'XOR[L, R]':
315    def newRight(self, right: R) -> XOR[L, R]:
316        """Swap in a right value. 
317
318        * returns a new instance with a new right (or potential right) value.
319        """
320        if self._left is Sentinel():
321            return cast(XOR[L, R], XOR(MB(), right))
322        else:
323            return cast(XOR[L, R], XOR(self._left, right))

Swap in a right value.

  • returns a new instance with a new right (or potential right) value.
def map(self, f: 'Callable[[L], U]') -> 'XOR[U, R]':
325    def map[U](self, f: Callable[[L], U]) -> XOR[U, R]:
326        """Map over if a left value.
327
328        * if `XOR` is a left then map `f` over its value
329          * if `f` successful return a left `XOR[S, R]`
330          * if `f` unsuccessful return right `XOR[S, R]`
331            * swallows any exceptions `f` may throw
332        * if `XOR` is a right
333          * return new `XOR(right=self._right): XOR[S, R]`
334          * use method `mapRight` to adjust the returned value
335
336        """
337        if self._left is Sentinel():
338            return cast(XOR[U, R], self)
339        try:
340            applied = f(cast(L, self._left))
341        except Exception:
342            return cast(XOR[U, R], XOR(MB(), self._right))
343        else:
344            return XOR(applied, self._right)

Map over if a left value.

  • 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[S, R]
      • swallows any exceptions f may throw
  • 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]', altRight: 'R') -> 'XOR[L, R]':
346    def mapRight(self, g: Callable[[R], R], altRight: R) -> XOR[L, R]:
347        """Map over a right or potential right value."""
348        try:
349            applied = g(self._right)
350            right = applied
351        except:
352            right = altRight
353
354        if self:
355            left: L|MB[L] = cast(L, self._left)
356        else:
357            left = MB()
358
359        return XOR(left, right)

Map over a right or potential right value.

def flatMap(self, f: 'Callable[[L], XOR[U, R]]') -> 'XOR[U, R]':
361    def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
362        """Flatmap - bind
363
364        * map over then flatten left values
365        * propagate right values
366
367        """
368        if self._left is Sentinel():
369            return cast(XOR[U, R], self)
370        else:
371            return f(cast(L, self._left))

Flatmap - bind

  • map over then flatten left values
  • propagate right values
@staticmethod
def call(f: 'Callable[[U], V]', left: 'U') -> 'XOR[V, MB[Exception]]':
373    @staticmethod
374    def call[U, V](f: Callable[[U], V], left: U) -> XOR[V, MB[Exception]]:
375        try:
376            return XOR(f(left), MB())
377        except Exception as esc:
378            return XOR(MB(), MB(esc))
@staticmethod
def lz_call( f: 'Callable[[U], V]', left: 'U') -> 'Callable[[], XOR[V, MB[Exception]]]':
380    @staticmethod
381    def lz_call[U, V](f: Callable[[U], V], left: U) -> Callable[[], XOR[V, MB[Exception]]]:
382        def ret() -> XOR[V, MB[Exception]]:
383            return XOR.call(f, left)
384        return ret
@staticmethod
def idx(v: 'Sequence[V]', ii: int) -> 'XOR[V, MB[Exception]]':
386    @staticmethod
387    def idx[V](v: Sequence[V], ii: int) -> XOR[V, MB[Exception]]:
388        try:
389            return XOR(v[ii], MB())
390        except Exception as esc:
391            return XOR(MB(), MB(esc))
@staticmethod
def lz_idx(v: 'Sequence[V]', ii: int) -> 'Callable[[], XOR[V, MB[Exception]]]':
393    @staticmethod
394    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], XOR[V, MB[Exception]]]:
395        def ret() -> XOR[V, MB[Exception]]:
396            return XOR.idx(v, ii)
397        return ret
@staticmethod
def sequence( seq_xor_lr: 'Sequence[XOR[L, R]]', potential_right: 'R') -> 'XOR[Sequence[L], R]':
399    @staticmethod
400    def sequence(seq_xor_lr: Sequence[XOR[L, R]], potential_right: R) -> XOR[Sequence[L], R]:
401        """Sequence an indexable container of `XOR[L, R]`
402
403        * if all the `XOR` values contained in the container are lefts, then
404          * return an `XOR` of the same type container of all the left values
405          * setting the potential right `potential_right`
406        * if at least one of the `XOR` values contained in the container is a right,
407          * return a right XOR containing the right value of the first right
408
409        """
410        l: list[L] = []
411
412        for xor_lr in seq_xor_lr:
413            if xor_lr:
414                l.append(xor_lr.getLeft())
415            else:
416                return cast(XOR[Sequence[L], R], XOR(Sentinel(), xor_lr.getRight()))
417
418        ds = cast(Sequence[L], type(seq_xor_lr)(l))  # type: ignore # will be a subclass at runtime
419        return XOR(ds, potential_right)

Sequence an indexable container of XOR[L, R]

  • if all the XOR values contained in the container are lefts, then
    • return an XOR of the same type container of all the left values
    • setting the potential right potential_right
  • if at least one of the XOR values contained in the container is a right,
    • return a right XOR containing the right value of the first right