grscheller.fp.nada

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

Singleton semantically represents 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
  • comparison ops compare 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
def get(self, alt: Any = (None, (None, (None, ())))) -> Any:
124    def get(self, alt: Any=_sentinel) -> Any:
125        """
126        ##### Get an alternate value, defaults to Nada().
127        """
128        if alt == _sentinel:
129            return Nada()
130        else:
131            return alt
Get an alternate value, defaults to Nada().
instance = nada