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