grscheller.untyped.nada

Singleton Representing a Non-existing Value

An attempt to give Python a "bottom" type.

  • nada is an attempt to give Python a "bottom" type

While a true bottom type has no instances, nada is a singleton. Python's evolving typing system seems to reject the concept of a true bottom type.

  • types like None and () make for lousy bottoms
    • they take few methods (much less EVERY method)
    • None has no length and not indexable, () is at least iterable
    • returned values must be constantly checked for
      • preventing one from blissfully go down the "happy path"
    • None and () are commonly used as sentinel values
      • hindering both as being interpreted as "nothingness"

The nada object makes for a better bottom like singleton object than either None and () do.

  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"""### Singleton Representing a Non-existing Value
 16
 17An attempt to give Python a "bottom" type.
 18
 19* **nada** is an attempt to give Python a "bottom" type
 20
 21While a true bottom type has no instances, `nada` is a singleton. Python's
 22evolving typing system seems to reject the concept of a true bottom type.
 23
 24* types like `None` and `()` make for lousy bottoms
 25  * they take few methods (much less EVERY method)
 26  * `None` has no length and not indexable, `()` is at least iterable
 27  * returned values must be constantly checked for
 28    * preventing one from blissfully go down the "happy path"
 29  * `None` and `()` are commonly used as sentinel values
 30    * hindering both as being interpreted as "nothingness"
 31
 32The `nada` object makes for a better bottom like singleton object than
 33either `None` and `()` do.
 34"""
 35
 36from __future__ import annotations
 37from typing import Any, Callable, Final, Iterator
 38
 39__all__ = [ 'Nada', 'nada' ]
 40
 41class _Sentinel():
 42    def __repr__(self) -> str:
 43        return '_Sentinel()'
 44
 45_sentinel: Final[_Sentinel] = _Sentinel()
 46
 47class Nada():
 48    """#### Singleton representing a missing value.
 49
 50    WARNING: OBSOLETED - use grscheller.experimental.nada instead
 51
 52    * singleton nada: nada = Nada() represents a non-existent value
 53    * returns itself for arbitrary method calls
 54    * returns itself if called as a Callable with arbitrary arguments
 55    * interpreted as an empty container by standard Python functions
 56    * warning: non-standard equality semantics
 57      * comparison compares true only when 2 non-missing values compare true
 58        * when compared to itself behaves somewhat like IEEE Float NAN's
 59        * `nada is nada` is true
 60        * `nada == nada` is false
 61        * `nada != nada` is true
 62      * thus a == b means two non-missing values compare as equal
 63    * warning: does not handle named arguments
 64
 65    """
 66    __slots__ = ()
 67
 68    def __new__(cls) -> Nada:
 69        if not hasattr(cls, 'instance'):
 70            cls.instance = super(Nada, cls).__new__(cls)
 71            cls._hash = hash((_sentinel, (_sentinel,)))
 72        return cls.instance
 73
 74    def __iter__(self) -> Iterator[Any]:
 75        return iter(())
 76
 77    def __hash__(self) -> int:
 78        return self._hash
 79
 80    def __repr__(self) -> str:
 81        return 'nada'
 82
 83    def __bool__(self) -> bool:
 84        return False
 85
 86    def __len__(self) -> int:
 87        return 0
 88
 89    def __add__(self, right: Any) -> Nada:
 90        return Nada()
 91
 92    def __radd__(self, left: Any) -> Nada:
 93        return Nada()
 94
 95    def __mul__(self, right: Any) -> Nada:
 96        return Nada()
 97
 98    def __rmul__(self, left: Any) -> Nada:
 99        return Nada()
100
101    def __eq__(self, right: Any) -> bool:
102        return False
103
104    def __ne__(self, right: Any) -> bool:
105        return True
106
107    def __ge__(self, right: Any) -> bool:
108        return False
109
110    def __gt__(self, right: Any) -> bool:
111        return False
112
113    def __le__(self, right: Any) -> bool:
114        return False
115
116    def __lt__(self, right: Any) -> bool:
117        return False
118
119    def __getitem__(self, index: int|slice) -> Any:
120        return Nada()
121
122    def __setitem__(self, index: int|slice, item: Any) -> None:
123        return
124
125    def __call__(*args: Any, **kwargs: Any) -> Any:
126        return Nada()
127
128 #  def __getattr__(self, name: str) -> Callable[[Any], Any]:
129 #      """Comment out for doc generation, pdoc gags on this method."""
130 #      def method(*args: Any, **kwargs: Any) -> Any:
131 #          return Nada()
132 #      return method
133
134    def nada_get(self, alt: Any=_sentinel) -> Any:
135        """
136        Get an alternate value, defaults to Nada().
137
138        """
139        if alt == _sentinel:
140            return Nada()
141        else:
142            return alt
143
144nada = Nada()
class Nada:
 48class Nada():
 49    """#### Singleton representing a missing value.
 50
 51    WARNING: OBSOLETED - use grscheller.experimental.nada instead
 52
 53    * singleton nada: nada = Nada() represents a non-existent value
 54    * returns itself for arbitrary method calls
 55    * returns itself if called as a Callable with arbitrary arguments
 56    * interpreted as an empty container by standard Python functions
 57    * warning: non-standard equality semantics
 58      * comparison compares true only when 2 non-missing values compare true
 59        * when compared to itself behaves somewhat like IEEE Float NAN's
 60        * `nada is nada` is true
 61        * `nada == nada` is false
 62        * `nada != nada` is true
 63      * thus a == b means two non-missing values compare as equal
 64    * warning: does not handle named arguments
 65
 66    """
 67    __slots__ = ()
 68
 69    def __new__(cls) -> Nada:
 70        if not hasattr(cls, 'instance'):
 71            cls.instance = super(Nada, cls).__new__(cls)
 72            cls._hash = hash((_sentinel, (_sentinel,)))
 73        return cls.instance
 74
 75    def __iter__(self) -> Iterator[Any]:
 76        return iter(())
 77
 78    def __hash__(self) -> int:
 79        return self._hash
 80
 81    def __repr__(self) -> str:
 82        return 'nada'
 83
 84    def __bool__(self) -> bool:
 85        return False
 86
 87    def __len__(self) -> int:
 88        return 0
 89
 90    def __add__(self, right: Any) -> Nada:
 91        return Nada()
 92
 93    def __radd__(self, left: Any) -> Nada:
 94        return Nada()
 95
 96    def __mul__(self, right: Any) -> Nada:
 97        return Nada()
 98
 99    def __rmul__(self, left: Any) -> Nada:
100        return Nada()
101
102    def __eq__(self, right: Any) -> bool:
103        return False
104
105    def __ne__(self, right: Any) -> bool:
106        return True
107
108    def __ge__(self, right: Any) -> bool:
109        return False
110
111    def __gt__(self, right: Any) -> bool:
112        return False
113
114    def __le__(self, right: Any) -> bool:
115        return False
116
117    def __lt__(self, right: Any) -> bool:
118        return False
119
120    def __getitem__(self, index: int|slice) -> Any:
121        return Nada()
122
123    def __setitem__(self, index: int|slice, item: Any) -> None:
124        return
125
126    def __call__(*args: Any, **kwargs: Any) -> Any:
127        return Nada()
128
129 #  def __getattr__(self, name: str) -> Callable[[Any], Any]:
130 #      """Comment out for doc generation, pdoc gags on this method."""
131 #      def method(*args: Any, **kwargs: Any) -> Any:
132 #          return Nada()
133 #      return method
134
135    def nada_get(self, alt: Any=_sentinel) -> Any:
136        """
137        Get an alternate value, defaults to Nada().
138
139        """
140        if alt == _sentinel:
141            return Nada()
142        else:
143            return alt

Singleton representing a missing value.

WARNING: OBSOLETED - use grscheller.experimental.nada instead

  • 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
      • when compared to itself behaves somewhat like IEEE Float NAN's
      • nada is nada is true
      • nada == nada is false
      • nada != nada is true
    • thus a == b means two non-missing values compare as equal
  • warning: does not handle named arguments
def nada_get(self, alt: Any = _Sentinel()) -> Any:
135    def nada_get(self, alt: Any=_sentinel) -> Any:
136        """
137        Get an alternate value, defaults to Nada().
138
139        """
140        if alt == _sentinel:
141            return Nada()
142        else:
143            return alt

Get an alternate value, defaults to Nada().

instance = nada
nada = nada