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
values used as hidden implementation details
- Here is another implementation for Sentinel:``
- on GitHub: taleinat/python-stdlib-sentinels
- on PyPI: Project: Sentinels
- see: PEP 661
Initially this one was somewhat close to mine and also enabled pickling. Subsequently it was "enhanced" to allow sentinel values to be subclassed. My implementation is substantially more simple, python implementation independent, less Gang-of-Four OOP, and more Pythonic.
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-2025 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` values used as hidden implementation details 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 44Initially this one was somewhat close to mine and also enabled pickling. 45Subsequently it was "enhanced" to allow sentinel values to be subclassed. My 46implementation is substantially more simple, python implementation independent, 47less Gang-of-Four OOP, and more Pythonic. 48 49--- 50 51##### `Nada` propagates failure 52 53Nada is a singleton representing & propagating failure. Failure just blissfully 54propagates down "the happy path." For almost everything you do with it, it just 55returns itself. The maintainer has not used this construct enough yet to 56determine if it is a brilliant idea or a horrible blunder. 57""" 58from __future__ import annotations 59 60__all__ = [ 'NoValue', 'Sentinel', 'Nada' ] 61 62from collections.abc import Callable, Iterator 63from typing import Any, Final, final 64 65class NoValue(): 66 """Singleton class representing a missing value. 67 68 * similar to `None` but 69 * while `None` represents "returned no values" 70 * `NoValue()` represents the absence of a value 71 * usage 72 * `import NoValue from grscheller.fp.err_handling` and then 73 * either use `NoValue()` directly 74 * or define `_noValue: Final[NoValue] = NoValue()` don't export it 75 * compare using `is` and `is not` 76 * not `==` or `!=` 77 * `None` means returned no values, so `None == None` makes sense 78 * if one or both values are missing, then what is there to compare? 79 80 """ 81 __slots__ = () 82 _instance: NoValue|None = None 83 84 def __new__(cls) -> NoValue: 85 if cls._instance is None: 86 cls._instance = super(NoValue, cls).__new__(cls) 87 return cls._instance 88 89 def __init__(self) -> None: 90 return 91 92 def __repr__(self) -> str: 93 return 'NoValue()' 94 95 def __eq__(self, other: object) -> bool: 96 return False 97 98@final 99class Sentinel(): 100 """Singleton classes representing a sentinel values. 101 102 * intended for library code, not to be exported/shared between modules 103 * otherwise some of its intended typing guarantees may be lost 104 * useful substitute for `None` as a hidden sentinel value 105 * allows `None` to be stored in data structures 106 * allows end users to choose to use `None` or `()` as sentinel values 107 * always equals itself (unlike `noValue`) 108 * usage 109 * import Sentinel and then either 110 * define `_my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')` 111 * or use `Sentinel('my_sentinel')` directly 112 * compare using either 113 * `is` and `is not` or `==` and `!=` 114 * the `Sentinel()` value always equals itself 115 * and never equals anything else, especially other sentinel values 116 117 """ 118 __slots__ = '_sentinel_name', 119 _instances: dict[str, Sentinel] = {} 120 121 def __new__(cls, sentinel_name: str) -> Sentinel: 122 if sentinel_name not in cls._instances: 123 cls._instances[sentinel_name] = super(Sentinel, cls).__new__(cls) 124 return cls._instances[sentinel_name] 125 126 def __init__(self, sentinel_name: str) -> None: 127 self._sentinel_name = sentinel_name 128 return 129 130 def __repr__(self) -> str: 131 return "Sentinel('" + self._sentinel_name + "')" 132 133@final 134class Nada(): 135 """Singleton class representing & propagating failure. 136 137 * singleton `_nada: nada = Nada()` represents a non-existent value 138 * returns itself for arbitrary method calls 139 * returns itself if called as a Callable with arbitrary arguments 140 * interpreted as an empty container by standard Python functions 141 * warning: non-standard equality semantics 142 * comparison compares true only when 2 non-missing values compare true 143 * thus `a == b` means two non-missing values compare as equal 144 * usage 145 * import `Nada` and then 146 * either use `Nada()` directly 147 * or define `_nada: Final[Nada] = Nada()` don't export it 148 * start propagating failure by setting a propagating value to Nada() 149 * works best when working with expression 150 * failure may fail to propagate 151 * for a function/method with just side effects 152 * engineer Nada() to fail to trigger side effects 153 * test for failure by comparing a result to `Nada()` itself using 154 * `is` and `is not` 155 * propagate failure through a calculation using 156 * `==` and `!=` 157 * the `Nada()` value never equals itself 158 * and never equals anything else 159 160 """ 161 __slots__ = () 162 _instance: Nada|None = None 163 _hash: int = 0 164 165 sentinel: Final[Sentinel] = Sentinel('Nada') 166 167 def __new__(cls) -> Nada: 168 if cls._instance is None: 169 cls._instance = super(Nada, cls).__new__(cls) 170 cls._hash = hash((cls._instance, (cls._instance,))) 171 return cls._instance 172 173 def __iter__(self) -> Iterator[Any]: 174 return iter(()) 175 176 def __hash__(self) -> int: 177 return self._hash 178 179 def __repr__(self) -> str: 180 return 'Nada()' 181 182 def __bool__(self) -> bool: 183 return False 184 185 def __len__(self) -> int: 186 return 0 187 188 def __add__(self, right: Any) -> Nada: 189 return Nada() 190 191 def __radd__(self, left: Any) -> Nada: 192 return Nada() 193 194 def __mul__(self, right: Any) -> Nada: 195 return Nada() 196 197 def __rmul__(self, left: Any) -> Nada: 198 return Nada() 199 200 def __eq__(self, right: Any) -> bool: 201 return False 202 203 def __ne__(self, right: Any) -> bool: 204 return True 205 206 def __ge__(self, right: Any) -> bool: 207 return False 208 209 def __gt__(self, right: Any) -> bool: 210 return False 211 212 def __le__(self, right: Any) -> bool: 213 return False 214 215 def __lt__(self, right: Any) -> bool: 216 return False 217 218 def __getitem__(self, index: int|slice) -> Any: 219 return Nada() 220 221 def __setitem__(self, index: int|slice, item: Any) -> None: 222 return 223 224 def __call__(self, *args: Any, **kwargs: Any) -> Any: 225 return Nada() 226 227 def __getattr__(self, name: str) -> Callable[..., Any]: 228 def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any: 229 return Nada() 230 return method 231 232 def nada_get(self, alt: Any=sentinel) -> Any: 233 """Get an alternate value, defaults to `Nada()`.""" 234 if alt == Sentinel('Nada'): 235 return Nada() 236 else: 237 return alt
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
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
99@final 100class Sentinel(): 101 """Singleton classes representing a sentinel values. 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 * define `_my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')` 112 * or use `Sentinel('my_sentinel')` directly 113 * compare using either 114 * `is` and `is not` or `==` and `!=` 115 * the `Sentinel()` value always equals itself 116 * and never equals anything else, especially other sentinel values 117 118 """ 119 __slots__ = '_sentinel_name', 120 _instances: dict[str, Sentinel] = {} 121 122 def __new__(cls, sentinel_name: str) -> Sentinel: 123 if sentinel_name not in cls._instances: 124 cls._instances[sentinel_name] = super(Sentinel, cls).__new__(cls) 125 return cls._instances[sentinel_name] 126 127 def __init__(self, sentinel_name: str) -> None: 128 self._sentinel_name = sentinel_name 129 return 130 131 def __repr__(self) -> str: 132 return "Sentinel('" + self._sentinel_name + "')"
Singleton classes representing a sentinel values.
- 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
- define
_my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')
- or use
Sentinel('my_sentinel')
directly
- define
- compare using either
is
andis not
or==
and!=
- the
Sentinel()
value always equals itself - and never equals anything else, especially other sentinel values
- import Sentinel and then either
134@final 135class Nada(): 136 """Singleton class representing & propagating failure. 137 138 * singleton `_nada: nada = Nada()` represents a non-existent value 139 * returns itself for arbitrary method calls 140 * returns itself if called as a Callable with arbitrary arguments 141 * interpreted as an empty container by standard Python functions 142 * warning: non-standard equality semantics 143 * comparison compares true only when 2 non-missing values compare true 144 * thus `a == b` means two non-missing values compare as equal 145 * usage 146 * import `Nada` and then 147 * either use `Nada()` directly 148 * or define `_nada: Final[Nada] = Nada()` don't export it 149 * start propagating failure by setting a propagating value to Nada() 150 * works best when working with expression 151 * failure may fail to propagate 152 * for a function/method with just side effects 153 * engineer Nada() to fail to trigger side effects 154 * test for failure by comparing a result to `Nada()` itself using 155 * `is` and `is not` 156 * propagate failure through a calculation using 157 * `==` and `!=` 158 * the `Nada()` value never equals itself 159 * and never equals anything else 160 161 """ 162 __slots__ = () 163 _instance: Nada|None = None 164 _hash: int = 0 165 166 sentinel: Final[Sentinel] = Sentinel('Nada') 167 168 def __new__(cls) -> Nada: 169 if cls._instance is None: 170 cls._instance = super(Nada, cls).__new__(cls) 171 cls._hash = hash((cls._instance, (cls._instance,))) 172 return cls._instance 173 174 def __iter__(self) -> Iterator[Any]: 175 return iter(()) 176 177 def __hash__(self) -> int: 178 return self._hash 179 180 def __repr__(self) -> str: 181 return 'Nada()' 182 183 def __bool__(self) -> bool: 184 return False 185 186 def __len__(self) -> int: 187 return 0 188 189 def __add__(self, right: Any) -> Nada: 190 return Nada() 191 192 def __radd__(self, left: Any) -> Nada: 193 return Nada() 194 195 def __mul__(self, right: Any) -> Nada: 196 return Nada() 197 198 def __rmul__(self, left: Any) -> Nada: 199 return Nada() 200 201 def __eq__(self, right: Any) -> bool: 202 return False 203 204 def __ne__(self, right: Any) -> bool: 205 return True 206 207 def __ge__(self, right: Any) -> bool: 208 return False 209 210 def __gt__(self, right: Any) -> bool: 211 return False 212 213 def __le__(self, right: Any) -> bool: 214 return False 215 216 def __lt__(self, right: Any) -> bool: 217 return False 218 219 def __getitem__(self, index: int|slice) -> Any: 220 return Nada() 221 222 def __setitem__(self, index: int|slice, item: Any) -> None: 223 return 224 225 def __call__(self, *args: Any, **kwargs: Any) -> Any: 226 return Nada() 227 228 def __getattr__(self, name: str) -> Callable[..., Any]: 229 def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any: 230 return Nada() 231 return method 232 233 def nada_get(self, alt: Any=sentinel) -> Any: 234 """Get an alternate value, defaults to `Nada()`.""" 235 if alt == Sentinel('Nada'): 236 return Nada() 237 else: 238 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