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