grscheller.fp.err_handling
Maybe and Either Monads.
Monadic functional data types to use in lieu of exceptions.
Monadic types:
- MB: Maybe monad
- 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"""### Maybe and Either Monads. 16 17Monadic functional data types to use in lieu of exceptions. 18 19##### Monadic types: 20 21* **MB:** Maybe monad 22* **XOR:** Left biased Either monad 23 24""" 25from __future__ import annotations 26 27__all__ = [ 'MB', 'XOR', 'mb_to_xor', 'xor_to_mb' ] 28 29from typing import Callable, cast, Final, Iterator, Never 30from .nothingness import _NoValue, noValue 31 32class MB[D](): 33 """#### Maybe Monad 34 35 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 * implementation detail: 44 * `MB( )` contains `sentinel` as a sentinel value 45 * as a result, a MB cannot semantically contain the `sentinel` value 46 * raises `ValueError` if get method not given a default value 47 48 """ 49 __slots__ = '_value', 50 51 def __init__(self, value: D|_NoValue=noValue) -> None: 52 self._value = value 53 54 def __bool__(self) -> bool: 55 return not self._value is noValue 56 57 def __iter__(self) -> Iterator[D]: 58 if self: 59 yield cast(D, self._value) 60 61 def __repr__(self) -> str: 62 if self: 63 return 'MB(' + repr(self._value) + ')' 64 else: 65 return 'MB()' 66 67 def __len__(self) -> int: 68 return (1 if self else 0) 69 70 def __eq__(self, other: object) -> bool: 71 if not isinstance(other, type(self)): 72 return False 73 74 if self._value is other._value: 75 return True 76 return self._value == other._value 77 78 def get(self, alt: D|_NoValue=noValue) -> D|Never: 79 """Return the contained value if it exists, otherwise an alternate value. 80 81 * alternate value must me of type ~D 82 * raises `ValueError` if an alternate value is not provided but needed 83 84 """ 85 if self._value is not noValue: 86 return cast(D, self._value) 87 else: 88 if alt is not noValue: 89 return cast(D, alt) 90 else: 91 msg = 'An alternate return type not provided.' 92 raise ValueError(msg) 93 94 def map[U](self, f: Callable[[D], U]) -> MB[U]: 95 """Map function f over the 0 or 1 elements of this data structure.""" 96 return (MB(f(cast(D, self._value))) if self else MB()) 97 98 def flatmap[U](self, f: Callable[[D], MB[U]]) -> MB[U]: 99 """Map MB with function f and flatten.""" 100 return (f(cast(D, self._value)) if self else MB()) 101 102class XOR[L,R](): 103 """#### Either Monad 104 105 Class semantically containing either a "left" or a "right" value, 106 but not both. 107 108 * implements a left biased Either Monad 109 * `XOR(left: L, right: R)` produces a "left" value 110 * with a default potential "right" value 111 * `XOR(left)` produces a "left" value 112 * `XOR(right=right)` produces a "right" value 113 * in a Boolean context, returns True if a "left", False if a "right" 114 * two `XOR` objects compare as equal when 115 * both are left values or both are right values which 116 * contain the same value or 117 * whose values compare as equal 118 * immutable, an `XOR` does not change after being created 119 * immutable semantics, map & flatMap return new instances 120 * warning: contained values need not be immutable 121 * warning: not hashable if value or potential right value mutable 122 * `get` and `getRight` methods can raises `ValueError` when 123 * a "right" value is needed but a potential "right" value was not given 124 125 """ 126 __slots__ = '_left', '_right' 127 128 def __init__(self, left: L|_NoValue=noValue, right: R|_NoValue=noValue) -> None: 129 self._left, self._right = left, right 130 131 def __bool__(self) -> bool: 132 return self._left is not noValue 133 134 def __iter__(self) -> Iterator[L]: 135 if self._left is not noValue: 136 yield cast(L, self._left) 137 138 def __repr__(self) -> str: 139 if self._left is noValue: 140 return 'XOR(right=' + repr(self._right) + ')' 141 else: 142 return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')' 143 144 def __str__(self) -> str: 145 if self: 146 return '< ' + str(self._left) + ' | >' 147 else: 148 return '< | ' + str(self._right) + ' >' 149 150 def __len__(self) -> int: 151 # Semantically, an XOR always contains just one value. 152 return 1 153 154 def __eq__(self, other: object) -> bool: 155 if not isinstance(other, type(self)): 156 return False 157 158 if self and other: 159 if self._left is other._left: 160 return True 161 return self._left == other._left 162 elif not self and not other: 163 if self._right is other._right: 164 return True 165 return self._right == other._right 166 else: 167 return False 168 169 def get(self, alt: L|_NoValue=noValue) -> L|Never: 170 """Get value if a Left. 171 172 * if the XOR is a left, return its value 173 * otherwise, return alt: L if it is provided 174 * alternate value must me of type ~L 175 * raises `ValueError` if an alternate value is not provided but needed 176 177 """ 178 if self._left is noValue: 179 if alt is noValue: 180 msg = 'An alt return value was needed by get, but none was provided.' 181 raise ValueError(msg) 182 else: 183 return cast(L, alt) 184 else: 185 return cast(L, self._left) 186 187 def getRight(self, alt: R|_NoValue=noValue) -> R|Never: 188 """Get value of `XOR` if a Right, potential right value if a left. 189 190 * if XOR is a right, return its value 191 * otherwise return a provided alternate value of type ~R 192 * if XOR is a left, return the potential right value 193 * raises `ValueError` if a potential right value was not provided 194 195 """ 196 if self: 197 if alt is noValue: 198 if self._right is noValue: 199 msg = 'A potential right was needed by get, but none was provided.' 200 raise ValueError(msg) 201 else: 202 return cast(R, self._right) 203 else: 204 return cast(R, alt) 205 else: 206 return cast(R, self._right) 207 208 def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]: 209 """Make right 210 211 Return a new instance transformed into a right `XOR`. Change the right 212 value to `right` if given. 213 """ 214 if right is noValue: 215 right = self.getRight() 216 return cast(XOR[L, R], XOR(right=right)) 217 218 def swapRight(self, right: R) -> XOR[L, R]: 219 """Swap in a new right value, returns a new instance with a new right 220 (or potential right) value. 221 """ 222 if self._left is noValue: 223 return cast(XOR[L, R], XOR(right=right)) 224 else: 225 return XOR(self.get(), right) 226 227 def map[U](self, f: Callable[[L], U]) -> XOR[U, R]: 228 """Map over if a left value. 229 230 * if `XOR` is a "left" then map `f` over its value 231 * if `f` successful return a left XOR[S, R] 232 * if `f` unsuccessful return right `XOR` 233 * swallows any exceptions `f` may throw 234 * if `XOR` is a "right" 235 * return new `XOR(right=self._right): XOR[S, R]` 236 * use method mapRight to adjust the returned value 237 238 """ 239 if self._left is noValue: 240 return cast(XOR[U, R], XOR(right=self._right)) 241 242 try: 243 applied = f(cast(L, self._left)) 244 except Exception: 245 return XOR(right=self._right) 246 else: 247 return XOR(applied, self._right) 248 249 def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]: 250 """Map over a right or potential right value.""" 251 return XOR(self._left, g(cast(R, self._right))) 252 253 def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]: 254 """Flatmap - Monadically bind 255 256 * map over then flatten left values 257 * propagate right values 258 259 """ 260 if self._left is noValue: 261 return XOR(noValue, self._right) 262 else: 263 return f(cast(L, self._left)) 264 265# Conversion functions 266 267def mb_to_xor[D,R](m: MB[D], right: R) -> XOR[D, R]: 268 """Convert a MB to an XOR.""" 269 if m: 270 return XOR(m.get(), right) 271 else: 272 return XOR(noValue, right) 273 274def xor_to_mb[D,U](e: XOR[D,U]) -> MB[D]: 275 """Convert an XOR to a MB.""" 276 if e: 277 return MB(e.get()) 278 else: 279 return MB()
class
MB(typing.Generic[D]):
33class MB[D](): 34 """#### Maybe Monad 35 36 Class wrapping a potentially missing value. 37 38 * where `MB(value)` contains a possible value of type `~D` 39 * `MB( )` semantically represent a non-existent or missing value of type ~D 40 * immutable, a `MB` does not change after being created 41 * immutable semantics, map & flatMap return new instances 42 * warning: contained values need not be immutable 43 * warning: not hashable if a mutable value is contained 44 * implementation detail: 45 * `MB( )` contains `sentinel` as a sentinel value 46 * as a result, a MB cannot semantically contain the `sentinel` value 47 * raises `ValueError` if get method not given a default value 48 49 """ 50 __slots__ = '_value', 51 52 def __init__(self, value: D|_NoValue=noValue) -> None: 53 self._value = value 54 55 def __bool__(self) -> bool: 56 return not self._value is noValue 57 58 def __iter__(self) -> Iterator[D]: 59 if self: 60 yield cast(D, self._value) 61 62 def __repr__(self) -> str: 63 if self: 64 return 'MB(' + repr(self._value) + ')' 65 else: 66 return 'MB()' 67 68 def __len__(self) -> int: 69 return (1 if self else 0) 70 71 def __eq__(self, other: object) -> bool: 72 if not isinstance(other, type(self)): 73 return False 74 75 if self._value is other._value: 76 return True 77 return self._value == other._value 78 79 def get(self, alt: D|_NoValue=noValue) -> D|Never: 80 """Return the contained value if it exists, otherwise an alternate value. 81 82 * alternate value must me of type ~D 83 * raises `ValueError` if an alternate value is not provided but needed 84 85 """ 86 if self._value is not noValue: 87 return cast(D, self._value) 88 else: 89 if alt is not noValue: 90 return cast(D, alt) 91 else: 92 msg = 'An alternate return type not provided.' 93 raise ValueError(msg) 94 95 def map[U](self, f: Callable[[D], U]) -> MB[U]: 96 """Map function f over the 0 or 1 elements of this data structure.""" 97 return (MB(f(cast(D, self._value))) if self else MB()) 98 99 def flatmap[U](self, f: Callable[[D], MB[U]]) -> MB[U]: 100 """Map MB with function f and flatten.""" 101 return (f(cast(D, self._value)) if self else MB())
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
- implementation detail:
MB( )
containssentinel
as a sentinel value- as a result, a MB cannot semantically contain the
sentinel
value
- as a result, a MB cannot semantically contain the
- raises
ValueError
if get method not given a default value
def
get(self, alt: 'D | _NoValue' = noValue) -> 'D | Never':
79 def get(self, alt: D|_NoValue=noValue) -> D|Never: 80 """Return the contained value if it exists, otherwise an alternate value. 81 82 * alternate value must me of type ~D 83 * raises `ValueError` if an alternate value is not provided but needed 84 85 """ 86 if self._value is not noValue: 87 return cast(D, self._value) 88 else: 89 if alt is not noValue: 90 return cast(D, alt) 91 else: 92 msg = 'An alternate return type not provided.' 93 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
class
XOR(typing.Generic[L, R]):
103class XOR[L,R](): 104 """#### Either Monad 105 106 Class semantically containing either a "left" or a "right" value, 107 but not both. 108 109 * implements a left biased Either Monad 110 * `XOR(left: L, right: R)` produces a "left" value 111 * with a default potential "right" value 112 * `XOR(left)` produces a "left" value 113 * `XOR(right=right)` produces a "right" value 114 * in a Boolean context, returns True if a "left", False if a "right" 115 * two `XOR` objects compare as equal when 116 * both are left values or both are right values which 117 * contain the same value or 118 * whose values compare as equal 119 * immutable, an `XOR` does not change after being created 120 * immutable semantics, map & flatMap return new instances 121 * warning: contained values need not be immutable 122 * warning: not hashable if value or potential right value mutable 123 * `get` and `getRight` methods can raises `ValueError` when 124 * a "right" value is needed but a potential "right" value was not given 125 126 """ 127 __slots__ = '_left', '_right' 128 129 def __init__(self, left: L|_NoValue=noValue, right: R|_NoValue=noValue) -> None: 130 self._left, self._right = left, right 131 132 def __bool__(self) -> bool: 133 return self._left is not noValue 134 135 def __iter__(self) -> Iterator[L]: 136 if self._left is not noValue: 137 yield cast(L, self._left) 138 139 def __repr__(self) -> str: 140 if self._left is noValue: 141 return 'XOR(right=' + repr(self._right) + ')' 142 else: 143 return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')' 144 145 def __str__(self) -> str: 146 if self: 147 return '< ' + str(self._left) + ' | >' 148 else: 149 return '< | ' + str(self._right) + ' >' 150 151 def __len__(self) -> int: 152 # Semantically, an XOR always contains just one value. 153 return 1 154 155 def __eq__(self, other: object) -> bool: 156 if not isinstance(other, type(self)): 157 return False 158 159 if self and other: 160 if self._left is other._left: 161 return True 162 return self._left == other._left 163 elif not self and not other: 164 if self._right is other._right: 165 return True 166 return self._right == other._right 167 else: 168 return False 169 170 def get(self, alt: L|_NoValue=noValue) -> L|Never: 171 """Get value if a Left. 172 173 * if the XOR is a left, return its value 174 * otherwise, return alt: L if it is provided 175 * alternate value must me of type ~L 176 * raises `ValueError` if an alternate value is not provided but needed 177 178 """ 179 if self._left is noValue: 180 if alt is noValue: 181 msg = 'An alt return value was needed by get, but none was provided.' 182 raise ValueError(msg) 183 else: 184 return cast(L, alt) 185 else: 186 return cast(L, self._left) 187 188 def getRight(self, alt: R|_NoValue=noValue) -> R|Never: 189 """Get value of `XOR` if a Right, potential right value if a left. 190 191 * if XOR is a right, return its value 192 * otherwise return a provided alternate value of type ~R 193 * if XOR is a left, return the potential right value 194 * raises `ValueError` if a potential right value was not provided 195 196 """ 197 if self: 198 if alt is noValue: 199 if self._right is noValue: 200 msg = 'A potential right was needed by get, but none was provided.' 201 raise ValueError(msg) 202 else: 203 return cast(R, self._right) 204 else: 205 return cast(R, alt) 206 else: 207 return cast(R, self._right) 208 209 def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]: 210 """Make right 211 212 Return a new instance transformed into a right `XOR`. Change the right 213 value to `right` if given. 214 """ 215 if right is noValue: 216 right = self.getRight() 217 return cast(XOR[L, R], XOR(right=right)) 218 219 def swapRight(self, right: R) -> XOR[L, R]: 220 """Swap in a new right value, returns a new instance with a new right 221 (or potential right) value. 222 """ 223 if self._left is noValue: 224 return cast(XOR[L, R], XOR(right=right)) 225 else: 226 return XOR(self.get(), right) 227 228 def map[U](self, f: Callable[[L], U]) -> XOR[U, R]: 229 """Map over if a left value. 230 231 * if `XOR` is a "left" then map `f` over its value 232 * if `f` successful return a left XOR[S, R] 233 * if `f` unsuccessful return right `XOR` 234 * swallows any exceptions `f` may throw 235 * if `XOR` is a "right" 236 * return new `XOR(right=self._right): XOR[S, R]` 237 * use method mapRight to adjust the returned value 238 239 """ 240 if self._left is noValue: 241 return cast(XOR[U, R], XOR(right=self._right)) 242 243 try: 244 applied = f(cast(L, self._left)) 245 except Exception: 246 return XOR(right=self._right) 247 else: 248 return XOR(applied, self._right) 249 250 def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]: 251 """Map over a right or potential right value.""" 252 return XOR(self._left, g(cast(R, self._right))) 253 254 def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]: 255 """Flatmap - Monadically bind 256 257 * map over then flatten left values 258 * propagate right values 259 260 """ 261 if self._left is noValue: 262 return XOR(noValue, self._right) 263 else: 264 return f(cast(L, self._left))
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" value- with a default potential "right" value
XOR(left)
produces a "left" valueXOR(right=right)
produces a "right" value
- in a Boolean context, returns True if a "left", False if a "right"
- two
XOR
objects compare as equal when- both are left values or both are right values which
- contain the same value or
- whose values compare as equal
- both are left values or both are right values which
- 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
get
andgetRight
methods can raisesValueError
when- a "right" value is needed but a potential "right" value was not given
def
get(self, alt: 'L | _NoValue' = noValue) -> 'L | Never':
170 def get(self, alt: L|_NoValue=noValue) -> L|Never: 171 """Get value if a Left. 172 173 * if the XOR is a left, return its value 174 * otherwise, return alt: L if it is provided 175 * alternate value must me of type ~L 176 * raises `ValueError` if an alternate value is not provided but needed 177 178 """ 179 if self._left is noValue: 180 if alt is noValue: 181 msg = 'An alt return value was needed by get, but none was provided.' 182 raise ValueError(msg) 183 else: 184 return cast(L, alt) 185 else: 186 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, alt: 'R | _NoValue' = noValue) -> 'R | Never':
188 def getRight(self, alt: R|_NoValue=noValue) -> R|Never: 189 """Get value of `XOR` if a Right, potential right value if a left. 190 191 * if XOR is a right, return its value 192 * otherwise return a provided alternate value of type ~R 193 * if XOR is a left, return the potential right value 194 * raises `ValueError` if a potential right value was not provided 195 196 """ 197 if self: 198 if alt is noValue: 199 if self._right is noValue: 200 msg = 'A potential right was needed by get, but none was provided.' 201 raise ValueError(msg) 202 else: 203 return cast(R, self._right) 204 else: 205 return cast(R, alt) 206 else: 207 return cast(R, self._right)
Get value of XOR
if a Right, potential right value if a left.
- if XOR is a right, return its value
- otherwise return a provided alternate value of type ~R
- if XOR is a left, return the potential right value
- raises
ValueError
if a potential right value was not provided
- raises
def
makeRight(self, right: 'R | _NoValue' = noValue) -> 'XOR[L, R]':
209 def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]: 210 """Make right 211 212 Return a new instance transformed into a right `XOR`. Change the right 213 value to `right` if given. 214 """ 215 if right is noValue: 216 right = self.getRight() 217 return cast(XOR[L, R], XOR(right=right))
Make right
Return a new instance transformed into a right XOR
. Change the right
value to right
if given.
def
swapRight(self, right: 'R') -> 'XOR[L, R]':
219 def swapRight(self, right: R) -> XOR[L, R]: 220 """Swap in a new right value, returns a new instance with a new right 221 (or potential right) value. 222 """ 223 if self._left is noValue: 224 return cast(XOR[L, R], XOR(right=right)) 225 else: 226 return XOR(self.get(), right)
Swap in a new right value, returns a new instance with a new right (or potential right) value.
def
map(self, f: 'Callable[[L], U]') -> 'XOR[U, R]':
228 def map[U](self, f: Callable[[L], U]) -> XOR[U, R]: 229 """Map over if a left value. 230 231 * if `XOR` is a "left" then map `f` over its value 232 * if `f` successful return a left XOR[S, R] 233 * if `f` unsuccessful return right `XOR` 234 * swallows any exceptions `f` may throw 235 * if `XOR` is a "right" 236 * return new `XOR(right=self._right): XOR[S, R]` 237 * use method mapRight to adjust the returned value 238 239 """ 240 if self._left is noValue: 241 return cast(XOR[U, R], XOR(right=self._right)) 242 243 try: 244 applied = f(cast(L, self._left)) 245 except Exception: 246 return XOR(right=self._right) 247 else: 248 return XOR(applied, self._right)
def
mapRight(self, g: 'Callable[[R], R]') -> 'XOR[L, R]':
250 def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]: 251 """Map over a right or potential right value.""" 252 return XOR(self._left, g(cast(R, self._right)))
Map over a right or potential right value.
def
flatMap(self, f: 'Callable[[L], XOR[U, R]]') -> 'XOR[U, R]':
254 def flatMap[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]: 255 """Flatmap - Monadically bind 256 257 * map over then flatten left values 258 * propagate right values 259 260 """ 261 if self._left is noValue: 262 return XOR(noValue, self._right) 263 else: 264 return f(cast(L, self._left))
Flatmap - Monadically bind
- map over then flatten left values
- propagate right values
def
mb_to_xor(m: 'MB[D]', right: 'R') -> 'XOR[D, R]':
268def mb_to_xor[D,R](m: MB[D], right: R) -> XOR[D, R]: 269 """Convert a MB to an XOR.""" 270 if m: 271 return XOR(m.get(), right) 272 else: 273 return XOR(noValue, right)
Convert a MB to an XOR.
def
xor_to_mb(e: 'XOR[D, U]') -> 'MB[D]':
275def xor_to_mb[D,U](e: XOR[D,U]) -> MB[D]: 276 """Convert an XOR to a MB.""" 277 if e: 278 return MB(e.get()) 279 else: 280 return MB()
Convert an XOR to a MB.