grscheller.untyped.nothing

Singleton object representing a missing value

  • this module was an attempt to give Python a "bottom" type
    • a true bottom type has no instantiated values
    • nothing: Nothing is instantiated as a singleton
  • types like ~T|None and ~T|() are a poor man's Maybe Monad
    • both None and () make for lousy "bottom" types
    • both accept few methods
      • None has no length
      • () at least is iterable
    • both must constantly be checked for when returned from functions
    • it is Pythonic for developers to use None and () as sentinels
  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 object representing a missing value
 16
 17* this module was an attempt to give Python a "bottom" type
 18  * a true bottom type has no instantiated values
 19  * nothing: Nothing is instantiated as a singleton
 20* types like `~T|None` and `~T|()` are a poor man's Maybe Monad
 21  * both `None` and `()` make for lousy "bottom" types
 22  * both accept few methods
 23    * `None` has no length
 24    * `()` at least is iterable
 25  * both must constantly be checked for when returned from functions
 26  * it is Pythonic for developers to use `None` and `()` as sentinels
 27"""
 28
 29from __future__ import annotations
 30
 31__all__ = [ 'Nothing', 'nothing' ]
 32
 33from typing import Any, Callable, Final, Iterator
 34
 35_Nothing_Nada = tuple[None, tuple[None, tuple[None, tuple[None, None]]]]
 36_nothing_nada: _Nothing_Nada = None, (None, (None, (None, None)))
 37
 38class Nothing():
 39    """
 40    ##### Singleton semantically represents a missing value.
 41
 42    * singleton nothing: Nothing = Nothing() represents a non-existent value
 43    * makes for a better "bottom type" than either `None` or `()`
 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      * for comparison operators return `False` to "stop an action"
 48        * behaves like IEEE Float NAN's with comparison operators
 49        * avoid using else clauses when comparing ~T|Nothing values
 50        * when on left:
 51          * always returns `False`
 52        * when on right:
 53          * relies on the "std convention" of returning `False` if types differ
 54          * avoid using `~T|Nothing` values on right side of comparisons
 55            * especially for numerical comparisons
 56    """
 57    __slots__ = ()
 58
 59    def __new__(cls) -> Nothing:
 60        if not hasattr(cls, 'instance'):
 61            cls.instance = super(Nothing, cls).__new__(cls)
 62        return cls.instance
 63
 64    def __iter__(self) -> Iterator[Any]:
 65        return iter(())
 66
 67    def __repr__(self) -> str:
 68        return 'nothing'
 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) -> Any:
 77        return Nothing()
 78
 79    def __radd__(self, left: Any) -> Any:
 80        return Nothing()
 81
 82    def __mul__(self, right: Any) -> Any:
 83        return Nothing()
 84
 85    def __rmul__(self, left: Any) -> Any:
 86        return Nothing()
 87
 88    def __ge__(self, right: Any) -> bool:
 89        return False
 90
 91    def __gt__(self, right: Any) -> bool:
 92        return False
 93
 94    def __le__(self, right: Any) -> bool:
 95        return False
 96
 97    def __lt__(self, right: Any) -> bool:
 98        return False
 99
100    def __eq__(self, right: Any) -> bool:
101        return False
102
103    def __ne__(self, right: Any) -> bool:
104        return False
105
106    def __getitem__(self, index: int|slice) -> Any:
107        return Nothing()
108
109    def __setitem__(self, index: int|slice, item: Any) -> None:
110        return
111
112    def __call__(*args: Any, **kwargs: Any) -> Any:
113        return Nothing()
114
115 #  def __getattr__(self, name: str) -> Callable[[Any], Any]:
116 #      """Comment out for doc generation, pdoc gags on this method."""
117 #      def method(*args: Any, **kwargs: Any) -> Any:
118 #          return Nothing()
119 #      return method
120
121    def get(self, alt: Any=_nothing_nada) -> Any:
122        """
123        ##### Get an alternate value, defaults to Nothing().
124        """
125        if alt == _nothing_nada:
126            return Nothing()
127        else:
128            return alt
129
130nothing = Nothing()
class Nothing:
 39class Nothing():
 40    """
 41    ##### Singleton semantically represents a missing value.
 42
 43    * singleton nothing: Nothing = Nothing() represents a non-existent value
 44    * makes for a better "bottom type" than either `None` or `()`
 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      * for comparison operators return `False` to "stop an action"
 49        * behaves like IEEE Float NAN's with comparison operators
 50        * avoid using else clauses when comparing ~T|Nothing values
 51        * when on left:
 52          * always returns `False`
 53        * when on right:
 54          * relies on the "std convention" of returning `False` if types differ
 55          * avoid using `~T|Nothing` values on right side of comparisons
 56            * especially for numerical comparisons
 57    """
 58    __slots__ = ()
 59
 60    def __new__(cls) -> Nothing:
 61        if not hasattr(cls, 'instance'):
 62            cls.instance = super(Nothing, cls).__new__(cls)
 63        return cls.instance
 64
 65    def __iter__(self) -> Iterator[Any]:
 66        return iter(())
 67
 68    def __repr__(self) -> str:
 69        return 'nothing'
 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) -> Any:
 78        return Nothing()
 79
 80    def __radd__(self, left: Any) -> Any:
 81        return Nothing()
 82
 83    def __mul__(self, right: Any) -> Any:
 84        return Nothing()
 85
 86    def __rmul__(self, left: Any) -> Any:
 87        return Nothing()
 88
 89    def __ge__(self, right: Any) -> bool:
 90        return False
 91
 92    def __gt__(self, right: Any) -> bool:
 93        return False
 94
 95    def __le__(self, right: Any) -> bool:
 96        return False
 97
 98    def __lt__(self, right: Any) -> bool:
 99        return False
100
101    def __eq__(self, right: Any) -> bool:
102        return False
103
104    def __ne__(self, right: Any) -> bool:
105        return False
106
107    def __getitem__(self, index: int|slice) -> Any:
108        return Nothing()
109
110    def __setitem__(self, index: int|slice, item: Any) -> None:
111        return
112
113    def __call__(*args: Any, **kwargs: Any) -> Any:
114        return Nothing()
115
116 #  def __getattr__(self, name: str) -> Callable[[Any], Any]:
117 #      """Comment out for doc generation, pdoc gags on this method."""
118 #      def method(*args: Any, **kwargs: Any) -> Any:
119 #          return Nothing()
120 #      return method
121
122    def get(self, alt: Any=_nothing_nada) -> Any:
123        """
124        ##### Get an alternate value, defaults to Nothing().
125        """
126        if alt == _nothing_nada:
127            return Nothing()
128        else:
129            return alt
Singleton semantically represents a missing value.
  • singleton nothing: Nothing = Nothing() represents a non-existent value
  • makes for a better "bottom type" than either None or ()
    • 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
    • for comparison operators return False to "stop an action"
      • behaves like IEEE Float NAN's with comparison operators
      • avoid using else clauses when comparing ~T|Nothing values
      • when on left:
        • always returns False
      • when on right:
        • relies on the "std convention" of returning False if types differ
        • avoid using ~T|Nothing values on right side of comparisons
          • especially for numerical comparisons
def get(self, alt: Any = (None, (None, (None, (None, None))))) -> Any:
122    def get(self, alt: Any=_nothing_nada) -> Any:
123        """
124        ##### Get an alternate value, defaults to Nothing().
125        """
126        if alt == _nothing_nada:
127            return Nothing()
128        else:
129            return alt
Get an alternate value, defaults to Nothing().
instance = nothing
nothing = nothing