grscheller.fp.singletons
Module fp.singletons - collection of singleton classes
Classes permitting at most only one instantiation. Safer, but not as performant, than a non-exported module level global. Difficult, but not impossible, for a typical end-user to exploit. Different versions tailored for different use cases.
Singleton types:
- class NoValue: singleton instance representing the absence of a value
- Class Sentinel: singleton instances used as a "hidden" sentinel value
- class Nada: singleton instance representing & propagating failure
NoValue was designed as a None replacement
While None
represents "returned no values," NoValue()
represents the absence
of a value. Non-existing values should not be comparable to anything, even
themselves. End-users may use both None
and ()
as sentinel values which
colliding with using either to represent "nothingness."
Sentinel value used as a hidden implementation detailed
- Here is another implementation for Sentinel:
- on GitHub: taleinat/python-stdlib-sentinels
- on PyPI: Project: Sentinels
- see: PEP 661
This one is quite close to mine, but enables different sentinels with different names, and can be pickled. These last two extra features I feel make this implementation overly complicated. Mind is intended to be used as a "hidden" implementation detail. Hidden from both end users and other library modules. Sometimes cast or override may be needed when over zealous typing tools get confused.
Nada propagates failure
Nada is a singleton representing & propagating failure. Failure just blissfully propagates down "the happy path." For almost everything you do with it, it just returns itself. The maintainer has not used this construct enough yet to determine if it is a brilliant idea or a horrible blunder.
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.singletons - collection of singleton classes 16 17Classes permitting at most only one instantiation. Safer, but not as performant, 18than a non-exported module level global. Difficult, but not impossible, for 19a typical end-user to exploit. Different versions tailored for different use 20cases. 21 22#### Singleton types: 23 24* **class NoValue:** singleton instance representing the absence of a value 25* **Class Sentinel:** singleton instances used as a "hidden" sentinel value 26* **class Nada:** singleton instance representing & propagating failure 27 28##### NoValue was designed as a None replacement 29 30While `None` represents "returned no values," `NoValue()` represents the absence 31of a value. Non-existing values should not be comparable to anything, even 32themselves. End-users may use both `None` and `()` as sentinel values which 33colliding with using either to represent "nothingness." 34 35--- 36 37##### Sentinel value used as a hidden implementation detailed 38 39* Here is another implementation for Sentinel: 40 * on GitHub: [taleinat/python-stdlib-sentinels](https://github.com/taleinat/python-stdlib-sentinels) 41 * on PyPI: [Project: Sentinels](https://pypi.org/project/sentinels/) 42 * see: [PEP 661](https://peps.python.org/pep-0661/) 43 44This one is quite close to mine, but enables different sentinels with different 45names, and can be pickled. These last two extra features I feel make this 46implementation overly complicated. Mind is intended to be used as a "hidden" 47implementation detail. Hidden from both end users and other library modules. 48Sometimes cast or override may be needed when over zealous typing tools get 49confused. 50 51--- 52 53##### Nada propagates failure 54 55Nada is a singleton representing & propagating failure. Failure just blissfully 56propagates down "the happy path." For almost everything you do with it, it just 57returns itself. The maintainer has not used this construct enough yet to 58determine if it is a brilliant idea or a horrible blunder. 59""" 60from __future__ import annotations 61 62__all__ = [ 'NoValue', 'Sentinel', 'Nada' ] 63 64from collections.abc import Callable, Iterator 65from typing import Any, Final, final 66class NoValue(): 67 """Singleton class representing a missing value. 68 69 * similar to `None` but 70 * while `None` represents "returned no values" 71 * `NoValue()` represents the absence of a value 72 * usage 73 * `import NoValue from grscheller.fp.err_handling` and then 74 * either use `NoValue()` directly 75 * or define `_noValue: Final[NoValue] = NoValue()` don't export it 76 * compare using `is` and `is not` 77 * not `==` or `!=` 78 * `None` means returned no values, so `None == None` makes sense 79 * if one or both values are missing, then what is there to compare? 80 81 """ 82 __slots__ = () 83 _instance: NoValue|None = None 84 85 def __new__(cls) -> NoValue: 86 if cls._instance is None: 87 cls._instance = super(NoValue, cls).__new__(cls) 88 return cls._instance 89 90 def __init__(self) -> None: 91 return 92 93 def __repr__(self) -> str: 94 return 'NoValue()' 95 96 def __eq__(self, other: object) -> bool: 97 return False 98 99@final 100class Sentinel(): 101 """Singleton class representing a sentinel value. 102 103 * intended for library code, not to be exported/shared between modules 104 * otherwise some of its intended typing guarantees may be lost 105 * useful substitute for `None` as a hidden sentinel value 106 * allows `None` to be stored in data structures 107 * allows end users to choose to use `None` or `()` as sentinel values 108 * always equals itself (unlike `noValue`) 109 * usage 110 * import Sentinel and then either 111 * use `Sentinel()` directly 112 * or define `_sentinel: Final[Sentinel] = Sentinel()` don't export it 113 * compare using either 114 * `is` and `is not` or `==` and `!=` 115 * the `Sentinel()` value always equals itself 116 * and never equals anything else 117 118 """ 119 __slots__ = () 120 _instance: Sentinel|None = None 121 _hash: int = 0 122 123 def __new__(cls) -> Sentinel: 124 if cls._instance is None: 125 cls._instance = super(Sentinel, cls).__new__(cls) 126 cls._hash = hash(((cls._instance,), cls._instance)) 127 return cls._instance 128 129 def __init__(self) -> None: 130 return 131 132 def __hash__(self) -> int: 133 return self._hash 134 135 def __repr__(self) -> str: 136 return 'Sentinel()' 137 138 def __eq__(self, other: object) -> bool: 139 if self is other: 140 return True 141 return False 142 143@final 144class Nada(): 145 """Singleton class representing & propagating failure. 146 147 * singleton `_nada: nada = Nada()` represents a non-existent value 148 * returns itself for arbitrary method calls 149 * returns itself if called as a Callable with arbitrary arguments 150 * interpreted as an empty container by standard Python functions 151 * warning: non-standard equality semantics 152 * comparison compares true only when 2 non-missing values compare true 153 * thus `a == b` means two non-missing values compare as equal 154 * usage 155 * import `Nada` and then 156 * either use `Nada()` directly 157 * or define `_nada: Final[Nada] = Nada()` don't export it 158 * start propagating failure by setting a propagating value to Nada() 159 * works best when working with expression 160 * failure may fail to propagate 161 * for a function/method with just side effects 162 * engineer Nada() to fail to trigger side effects 163 * test for failure by comparing a result to `Nada()` itself using 164 * `is` and `is not` 165 * propagate failure through a calculation using 166 * `==` and `!=` 167 * the `Nada()` value never equals itself 168 * and never equals anything else 169 170 """ 171 __slots__ = () 172 _instance: Nada|None = None 173 _hash: int = 0 174 175 sentinel: Final[Sentinel] = Sentinel() 176 177 def __new__(cls) -> Nada: 178 if cls._instance is None: 179 cls._instance = super(Nada, cls).__new__(cls) 180 cls._hash = hash((cls._instance, (cls._instance,))) 181 return cls._instance 182 183 def __iter__(self) -> Iterator[Any]: 184 return iter(()) 185 186 def __hash__(self) -> int: 187 return self._hash 188 189 def __repr__(self) -> str: 190 return 'Nada()' 191 192 def __bool__(self) -> bool: 193 return False 194 195 def __len__(self) -> int: 196 return 0 197 198 def __add__(self, right: Any) -> Nada: 199 return Nada() 200 201 def __radd__(self, left: Any) -> Nada: 202 return Nada() 203 204 def __mul__(self, right: Any) -> Nada: 205 return Nada() 206 207 def __rmul__(self, left: Any) -> Nada: 208 return Nada() 209 210 def __eq__(self, right: Any) -> bool: 211 return False 212 213 def __ne__(self, right: Any) -> bool: 214 return True 215 216 def __ge__(self, right: Any) -> bool: 217 return False 218 219 def __gt__(self, right: Any) -> bool: 220 return False 221 222 def __le__(self, right: Any) -> bool: 223 return False 224 225 def __lt__(self, right: Any) -> bool: 226 return False 227 228 def __getitem__(self, index: int|slice) -> Any: 229 return Nada() 230 231 def __setitem__(self, index: int|slice, item: Any) -> None: 232 return 233 234 def __call__(self, *args: Any, **kwargs: Any) -> Any: 235 return Nada() 236 237 def __getattr__(self, name: str) -> Callable[..., Any]: 238 def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any: 239 return Nada() 240 return method 241 242 def nada_get(self, alt: Any=sentinel) -> Any: 243 """Get an alternate value, defaults to `Nada()`.""" 244 if alt == Sentinel(): 245 return Nada() 246 else: 247 return alt
67class NoValue(): 68 """Singleton class representing a missing value. 69 70 * similar to `None` but 71 * while `None` represents "returned no values" 72 * `NoValue()` represents the absence of a value 73 * usage 74 * `import NoValue from grscheller.fp.err_handling` and then 75 * either use `NoValue()` directly 76 * or define `_noValue: Final[NoValue] = NoValue()` don't export it 77 * compare using `is` and `is not` 78 * not `==` or `!=` 79 * `None` means returned no values, so `None == None` makes sense 80 * if one or both values are missing, then what is there to compare? 81 82 """ 83 __slots__ = () 84 _instance: NoValue|None = None 85 86 def __new__(cls) -> NoValue: 87 if cls._instance is None: 88 cls._instance = super(NoValue, cls).__new__(cls) 89 return cls._instance 90 91 def __init__(self) -> None: 92 return 93 94 def __repr__(self) -> str: 95 return 'NoValue()' 96 97 def __eq__(self, other: object) -> bool: 98 return False
Singleton class representing a missing value.
- similar to
None
but- while
None
represents "returned no values" NoValue()
represents the absence of a value
- while
- usage
import NoValue from grscheller.fp.err_handling
and then- either use
NoValue()
directly - or define
_noValue: Final[NoValue] = NoValue()
don't export it
- either use
- compare using
is
andis not
- not
==
or!=
None
means returned no values, soNone == None
makes sense- if one or both values are missing, then what is there to compare?
- not
100@final 101class Sentinel(): 102 """Singleton class representing a sentinel value. 103 104 * intended for library code, not to be exported/shared between modules 105 * otherwise some of its intended typing guarantees may be lost 106 * useful substitute for `None` as a hidden sentinel value 107 * allows `None` to be stored in data structures 108 * allows end users to choose to use `None` or `()` as sentinel values 109 * always equals itself (unlike `noValue`) 110 * usage 111 * import Sentinel and then either 112 * use `Sentinel()` directly 113 * or define `_sentinel: Final[Sentinel] = Sentinel()` don't export it 114 * compare using either 115 * `is` and `is not` or `==` and `!=` 116 * the `Sentinel()` value always equals itself 117 * and never equals anything else 118 119 """ 120 __slots__ = () 121 _instance: Sentinel|None = None 122 _hash: int = 0 123 124 def __new__(cls) -> Sentinel: 125 if cls._instance is None: 126 cls._instance = super(Sentinel, cls).__new__(cls) 127 cls._hash = hash(((cls._instance,), cls._instance)) 128 return cls._instance 129 130 def __init__(self) -> None: 131 return 132 133 def __hash__(self) -> int: 134 return self._hash 135 136 def __repr__(self) -> str: 137 return 'Sentinel()' 138 139 def __eq__(self, other: object) -> bool: 140 if self is other: 141 return True 142 return False
Singleton class representing a sentinel value.
- intended for library code, not to be exported/shared between modules
- otherwise some of its intended typing guarantees may be lost
- useful substitute for
None
as a hidden sentinel value- allows
None
to be stored in data structures - allows end users to choose to use
None
or()
as sentinel values - always equals itself (unlike
noValue
)
- allows
- usage
- import Sentinel and then either
- use
Sentinel()
directly - or define
_sentinel: Final[Sentinel] = Sentinel()
don't export it
- use
- compare using either
is
andis not
or==
and!=
- the
Sentinel()
value always equals itself - and never equals anything else
- import Sentinel and then either
144@final 145class Nada(): 146 """Singleton class representing & propagating failure. 147 148 * singleton `_nada: nada = Nada()` represents a non-existent value 149 * returns itself for arbitrary method calls 150 * returns itself if called as a Callable with arbitrary arguments 151 * interpreted as an empty container by standard Python functions 152 * warning: non-standard equality semantics 153 * comparison compares true only when 2 non-missing values compare true 154 * thus `a == b` means two non-missing values compare as equal 155 * usage 156 * import `Nada` and then 157 * either use `Nada()` directly 158 * or define `_nada: Final[Nada] = Nada()` don't export it 159 * start propagating failure by setting a propagating value to Nada() 160 * works best when working with expression 161 * failure may fail to propagate 162 * for a function/method with just side effects 163 * engineer Nada() to fail to trigger side effects 164 * test for failure by comparing a result to `Nada()` itself using 165 * `is` and `is not` 166 * propagate failure through a calculation using 167 * `==` and `!=` 168 * the `Nada()` value never equals itself 169 * and never equals anything else 170 171 """ 172 __slots__ = () 173 _instance: Nada|None = None 174 _hash: int = 0 175 176 sentinel: Final[Sentinel] = Sentinel() 177 178 def __new__(cls) -> Nada: 179 if cls._instance is None: 180 cls._instance = super(Nada, cls).__new__(cls) 181 cls._hash = hash((cls._instance, (cls._instance,))) 182 return cls._instance 183 184 def __iter__(self) -> Iterator[Any]: 185 return iter(()) 186 187 def __hash__(self) -> int: 188 return self._hash 189 190 def __repr__(self) -> str: 191 return 'Nada()' 192 193 def __bool__(self) -> bool: 194 return False 195 196 def __len__(self) -> int: 197 return 0 198 199 def __add__(self, right: Any) -> Nada: 200 return Nada() 201 202 def __radd__(self, left: Any) -> Nada: 203 return Nada() 204 205 def __mul__(self, right: Any) -> Nada: 206 return Nada() 207 208 def __rmul__(self, left: Any) -> Nada: 209 return Nada() 210 211 def __eq__(self, right: Any) -> bool: 212 return False 213 214 def __ne__(self, right: Any) -> bool: 215 return True 216 217 def __ge__(self, right: Any) -> bool: 218 return False 219 220 def __gt__(self, right: Any) -> bool: 221 return False 222 223 def __le__(self, right: Any) -> bool: 224 return False 225 226 def __lt__(self, right: Any) -> bool: 227 return False 228 229 def __getitem__(self, index: int|slice) -> Any: 230 return Nada() 231 232 def __setitem__(self, index: int|slice, item: Any) -> None: 233 return 234 235 def __call__(self, *args: Any, **kwargs: Any) -> Any: 236 return Nada() 237 238 def __getattr__(self, name: str) -> Callable[..., Any]: 239 def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any: 240 return Nada() 241 return method 242 243 def nada_get(self, alt: Any=sentinel) -> Any: 244 """Get an alternate value, defaults to `Nada()`.""" 245 if alt == Sentinel(): 246 return Nada() 247 else: 248 return alt
Singleton class representing & propagating failure.
- singleton
_nada: nada = Nada()
represents a non-existent value - returns itself for arbitrary method calls
- returns itself if called as a Callable with arbitrary arguments
- interpreted as an empty container by standard Python functions
- warning: non-standard equality semantics
- comparison compares true only when 2 non-missing values compare true
- thus
a == b
means two non-missing values compare as equal
- usage
- import
Nada
and then- either use
Nada()
directly - or define
_nada: Final[Nada] = Nada()
don't export it
- either use
- start propagating failure by setting a propagating value to Nada()
- works best when working with expression
- failure may fail to propagate
- for a function/method with just side effects
- engineer Nada() to fail to trigger side effects
- test for failure by comparing a result to
Nada()
itself usingis
andis not
- propagate failure through a calculation using
==
and!=
- the
Nada()
value never equals itself - and never equals anything else
- import