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 aMB
cannot contain a MBMB(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
- immutable semantics, map & bind return new instances
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
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
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)
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 leftXOR
which- contains a value of type
~L
- and a potential right value of type
~R
- contains a value of type
XOR(MB(), right)
produces a rightXOR
- in a Boolean context
- 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
- both are left values or both are right values whose values
- 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
- immutable semantics, map & bind return new instances
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':
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)
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
lz_call( f: 'Callable[[U], V]', left: 'U') -> 'Callable[[], XOR[V, MB[Exception]]]':
@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
- return an
- 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