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