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

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

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

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

  • if f should fail, return a MB()
def bind(self, f: 'Callable[[D], MB[U]]') -> 'MB[U]':
134    def bind[U](self, f: Callable[[D], MB[U]]) -> MB[U]:
135        """Map `MB` with function `f` and flatten."""
136        try:
137            return (f(cast(D, self._value)) if self else MB())
138        except Exception:
139            return MB()

Map MB with function f and flatten.

@staticmethod
def call(f: 'Callable[[U], V]', u: 'U') -> 'MB[V]':
141    @staticmethod
142    def call[U, V](f: Callable[[U], V], u: U) -> MB[V]:
143        """Return an function call wrapped in a MB"""
144        try:
145            return MB(f(u))
146        except Exception:
147            return MB()

Return an function call wrapped in a MB

@staticmethod
def lz_call(f: 'Callable[[U], V]', u: 'U') -> 'Callable[[], MB[V]]':
149    @staticmethod
150    def lz_call[U, V](f: Callable[[U], V], u: U) -> Callable[[], MB[V]]:
151        def ret() -> MB[V]:
152            return MB.call(f, u)
153        return ret
@staticmethod
def idx(v: 'Sequence[V]', ii: int) -> 'MB[V]':
155    @staticmethod
156    def idx[V](v: Sequence[V], ii: int) -> MB[V]:
157        """Return an indexed value wrapped in a MB"""
158        try:
159            return MB(v[ii])
160        except IndexError:
161            return MB()

Return an indexed value wrapped in a MB

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

Get value if a left.

  • if the XOR is a left, return its value
  • if a right, return an alternate value of type ~L` if it is provided
    • alternate value provided directly
    • or optionally provided with a MB
  • returns a MB[L] for when an altLeft value is needed but not provided
def getRight(self) -> 'R':
310    def getRight(self) -> R:
311        """Get value of `XOR` if a right, potential right value if a left.
312
313        * if `XOR` is a right, return its value
314        * if `XOR` is a left, return the potential right value
315
316        """
317        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]':
319    def makeRight(self) -> XOR[L, R]:
320        """Make a right based on the `XOR`.
321
322        * return a right based on potential right value
323        * returns itself if already a right
324
325        """
326        if self._left == MB():
327            return self
328        else:
329            return cast(XOR[L, R], XOR(MB(), 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]':
331    def newRight(self, right: R) -> XOR[L, R]:
332        """Swap in a right value.
333
334        * returns a new instance with a new right (or potential right) value.
335
336        """
337        if self._left == MB():
338            return cast(XOR[L, R], XOR(MB(), right))
339        else:
340            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]':
342    def map[U](self, f: Callable[[L], U]) -> XOR[U, R]:
343        """Map over if a left value.
344
345        * if `XOR` is a left then map `f` over its value
346          * if `f` successful return a left `XOR[S, R]`
347          * if `f` unsuccessful return right `XOR[S, R]`
348            * swallows any exceptions `f` may throw
349        * if `XOR` is a right
350          * return new `XOR(right=self._right): XOR[S, R]`
351          * use method `mapRight` to adjust the returned value
352
353        """
354        if self._left == MB():
355            return cast(XOR[U, R], self)
356        try:
357            applied = f(cast(L, self._left))
358        except Exception:
359            return cast(XOR[U, R], XOR(MB(), self._right))
360        else:
361            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]':
363    def mapRight(self, g: Callable[[R], R], altRight: R) -> XOR[L, R]:
364        """Map over a right or potential right value."""
365        try:
366            applied = g(self._right)
367            right = applied
368        except:
369            right = altRight
370
371        if self:
372            left: L|MB[L] = cast(L, self._left)
373        else:
374            left = MB()
375
376        return XOR(left, right)

Map over a right or potential right value.

def bind(self, f: 'Callable[[L], XOR[U, R]]') -> 'XOR[U, R]':
378    def bind[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
379        """Flatmap - bind
380
381        * map over then flatten left values
382        * propagate right values
383
384        """
385        if self._left == MB():
386            return cast(XOR[U, R], self)
387        else:
388            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]]':
390    @staticmethod
391    def call[U, V](f: Callable[[U], V], left: U) -> XOR[V, MB[Exception]]:
392        try:
393            return XOR(f(left), MB())
394        except Exception as esc:
395            return XOR(MB(), MB(esc))
@staticmethod
def lz_call( f: 'Callable[[U], V]', left: 'U') -> 'Callable[[], XOR[V, MB[Exception]]]':
397    @staticmethod
398    def lz_call[U, V](f: Callable[[U], V], left: U) -> Callable[[], XOR[V, MB[Exception]]]:
399        def ret() -> XOR[V, MB[Exception]]:
400            return XOR.call(f, left)
401        return ret
@staticmethod
def idx(v: 'Sequence[V]', ii: int) -> 'XOR[V, MB[Exception]]':
403    @staticmethod
404    def idx[V](v: Sequence[V], ii: int) -> XOR[V, MB[Exception]]:
405        try:
406            return XOR(v[ii], MB())
407        except Exception as esc:
408            return XOR(MB(), MB(esc))
@staticmethod
def lz_idx(v: 'Sequence[V]', ii: int) -> 'Callable[[], XOR[V, MB[Exception]]]':
410    @staticmethod
411    def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], XOR[V, MB[Exception]]]:
412        def ret() -> XOR[V, MB[Exception]]:
413            return XOR.idx(v, ii)
414        return ret
@staticmethod
def sequence( seq_xor_lr: 'Sequence[XOR[L, R]]', potential_right: 'R') -> 'XOR[Sequence[L], R]':
416    @staticmethod
417    def sequence(seq_xor_lr: Sequence[XOR[L, R]], potential_right: R) -> XOR[Sequence[L], R]:
418        """Sequence an indexable container of `XOR[L, R]`
419
420        * if all the `XOR` values contained in the container are lefts, then
421          * return an `XOR` of the same type container of all the left values
422          * setting the potential right `potential_right`
423        * if at least one of the `XOR` values contained in the container is a right,
424          * return a right XOR containing the right value of the first right
425
426        """
427        l: list[L] = []
428
429        for xor_lr in seq_xor_lr:
430            if xor_lr:
431                l.append(xor_lr.getLeft().get())
432            else:
433                return XOR(MB(), xor_lr.getRight())
434
435        ds = cast(Sequence[L], type(seq_xor_lr)(l))  # type: ignore # will be a subclass at runtime
436        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