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

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
class NoValue:
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
  • usage
    • import NoValue from grscheller.fp.err_handling and then
      • either use NoValue() directly
      • or define _noValue: Final[NoValue] = NoValue() don't export it
    • compare using is and is not
      • not == or !=
      • None means returned no values, so None == None makes sense
      • if one or both values are missing, then what is there to compare?
@final
class Sentinel:
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)
  • usage
    • import Sentinel and then either
      • use Sentinel() directly
      • or define _sentinel: Final[Sentinel] = Sentinel() don't export it
    • compare using either
      • is and is not or == and !=
      • the Sentinel() value always equals itself
      • and never equals anything else
@final
class Nada:
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
    • 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 using
      • is and is not
    • propagate failure through a calculation using
      • == and !=
      • the Nada() value never equals itself
      • and never equals anything else
sentinel: Final[Sentinel] = Sentinel()
def nada_get(self, alt: Any = Sentinel()) -> Any:
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

Get an alternate value, defaults to Nada().