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
- immutable semantics, map & flatMap return new instances
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
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
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)
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 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 & flatMap return new instances
- warning: contained values need not be immutable
- warning: not hashable if value or potential right value mutable
- immutable semantics, map & flatMap return new instances
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':
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)
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
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]':
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
- 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