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    """
 49    #### Singleton representing a missing value.
 50
 51    * singleton nada: nada = Nada() represents a non-existent value
 52    * returns itself for arbitrary method calls
 53    * returns itself if called as a Callable with arbitrary arguments
 54    * interpreted as an empty container by standard Python functions
 55    * warning: non-standard equality semantics
 56      * comparison compares true only when 2 non-missing values compare true
 57        * when compared to itself behaves somewhat like IEEE Float NAN's
 58        * `nada is nada` is true
 59        * `nada == nada` is false
 60        * `nada != nada` is true
 61      * thus a == b means two non-missing values compare as equal
 62    * warning: does not handle named arguments
 63
 64    """
 65    __slots__ = ()
 66
 67    def __new__(cls) -> Nada:
 68        if not hasattr(cls, 'instance'):
 69            cls.instance = super(Nada, cls).__new__(cls)
 70            cls._hash = hash((_sentinel, (_sentinel,)))
 71        return cls.instance
 72
 73    def __iter__(self) -> Iterator[Any]:
 74        return iter(())
 75
 76    def __hash__(self) -> int:
 77        return self._hash
 78
 79    def __repr__(self) -> str:
 80        return 'nada'
 81
 82    def __bool__(self) -> bool:
 83        return False
 84
 85    def __len__(self) -> int:
 86        return 0
 87
 88    def __add__(self, right: Any) -> Nada:
 89        return Nada()
 90
 91    def __radd__(self, left: Any) -> Nada:
 92        return Nada()
 93
 94    def __mul__(self, right: Any) -> Nada:
 95        return Nada()
 96
 97    def __rmul__(self, left: Any) -> Nada:
 98        return Nada()
 99
100    def __eq__(self, right: Any) -> bool:
101        return False
102
103    def __ne__(self, right: Any) -> bool:
104        return True
105
106    def __ge__(self, right: Any) -> bool:
107        return False
108
109    def __gt__(self, right: Any) -> bool:
110        return False
111
112    def __le__(self, right: Any) -> bool:
113        return False
114
115    def __lt__(self, right: Any) -> bool:
116        return False
117
118    def __getitem__(self, index: int|slice) -> Any:
119        return Nada()
120
121    def __setitem__(self, index: int|slice, item: Any) -> None:
122        return
123
124    def __call__(*args: Any, **kwargs: Any) -> Any:
125        return Nada()
126
127 #  def __getattr__(self, name: str) -> Callable[[Any], Any]:
128 #      """Comment out for doc generation, pdoc gags on this method."""
129 #      def method(*args: Any, **kwargs: Any) -> Any:
130 #          return Nada()
131 #      return method
132
133    def nada_get(self, alt: Any=_sentinel) -> Any:
134        """
135        Get an alternate value, defaults to Nada().
136
137        """
138        if alt == _sentinel:
139            return Nada()
140        else:
141            return alt
142
143nada = Nada()
class Nada:
 48class Nada():
 49    """
 50    #### Singleton representing a missing value.
 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

Singleton representing a missing value.

  • 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:
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

Get an alternate value, defaults to Nada().

instance = nada
nada = nada