The scenario is we're working with a REST endpoint that gives us nice JSON objects. We're using requests, and everything works wonderfully. But one day you notice that data you though was always being pulled back isn't there for some models! You're getting index, key and attribute errors left, right and centre. What used to be obj.phones[0]
is now:
(DefaultAttr(obj, None).phones or [None])[0]
Or worse, if obj.phones[0]
could actually be None
!
And so I decided to build a simple Maybe
like group of classes. The interface in Haskell seemed really clean and simple. However there's no case
statement in Python, and not one that's based on type. so I decided to use Maybe.get(default: T) -> T
and Just._value
to get the value instead. Since Just._value
isn't implemented on either Nothing
or Maybe
, I decided to make the equality check if the LHS is an instance of the RHS, and so Just(1) == Just
would be True
.
This however isn't good enough for the JSON object, and so I subclassed Maybe
to create a MaybeNav
, that creates a MaybeNav
when you get items or attributes. And so if you don't care if obj.phones[0]
is there or None
, you can use MaybeNav(obj).phones[0].get()
. Which is much cleaner, and simpler to read.
I've added type hints to the code, so that it's a little simpler to reason with. And I've added some docstrings, I'd appreciate any way to improve these, as I don't normally use them. And so my skills with them are likely to be very poor.
Due to using f-strings, the code only works in Python 3.6. Finally any and all help is welcome.
mayby.py
from typing import TypeVar, Generic
T = TypeVar('T')
class Maybe(Generic[T]):
"""Simple Maybe abstract base class"""
def __init__(self, *args, **kwargs):
"""
Error as this should be overwridden
The code for this interface is implemented in Just and Nothing.
This is to prevent implementation errors. Such as overwritting
__getattr__ in a child class causing an infinate loop.
"""
raise TypeError("'Maybe' needs to be instansiated by child constructor")
def get(self, default: T = None) -> T:
"""Get the value in Just, or return default if Nothing"""
raise NotImplementedError("'get' should be changed in a sublcass")
class Constant(type):
def __call__(self, *args, **kwargs):
"""Simple metaclass to create a constant class"""
# Constants can't be called.
raise TypeError(f"'{self.__name__}' object is not callable")
def __repr__(self) -> str:
# Display the constant, rather than the class location in memory.
return f'{self.__name__}'
class JustBase:
def __init__(self, value: T):
"""Younger sibling class of Maybe for Just classes"""
self.__value = value
def __repr__(self) -> str:
return f'{type(self).__name__}({self._value!r})'
def __eq__(self, other: object) -> bool:
"""
Check if this is an instance of other
This makes checking if the class is a Just simpler.
As it's a common operation.
"""
return isinstance(other, type) and isinstance(self, other)
def get(self, default: T = None) -> T:
"""Get the value in Just, or return default if Nothing"""
return self._value
@property
def _value(self):
return self.__value
def build_maybes(Maybe, just_name, nothing_name):
"""Build a Just and Nothing inheriting from Maybe"""
Just = type(just_name, (JustBase, Maybe), {})
class MaybeConstant(Constant, Maybe):
def get(self, default: T = None) -> T:
"""Get the value in Just, or return default if Nothing"""
return default
Nothing = MaybeConstant(nothing_name, (object,), {})
return Just, Nothing
class MaybeNav(Maybe[T]):
"""Maybe for navigating objects"""
# __getitem__ and __getattr__ actually return MaybeNav[T].
def __getitem__(self, item: object) -> Maybe[T]:
if self == NothingNav:
return NothingNav
val = self._value
try:
val = val[item]
except Exception:
return NothingNav
else:
return JustNav(val)
def __getattr__(self, item: str) -> Maybe[T]:
obj = object()
val = getattr(self.get(obj), item, obj)
if val is obj:
return NothingNav
return JustNav(val)
Just, Nothing = build_maybes(Maybe, 'Just', 'Nothing')
JustNav, NothingNav = build_maybes(MaybeNav, 'JustNav', 'NothingNav')
# Don't delete T, so subclasses can use the same generic type
del TypeVar, Generic, Constant, JustBase
An example of using this can be:
import math
from collections import namedtuple
from typing import Iterable
from maybe import Maybe, Just, Nothing, MaybeNav, JustNav
def safe_log(number: float) -> Maybe[float]:
if number > 0:
return Just(math.log(number))
else:
return Nothing
def user_values(obj: object) -> Iterable[MaybeNav[object]]:
obj = JustNav(obj)
return [
obj.name,
obj.first_name,
obj.last_name,
obj.phones[0]
]
v = safe_log(1000)
print(v == Just, v)
v = safe_log(-1000)
print(v == Just, v)
User = namedtuple('User', 'name first_name last_name phones')
vals = user_values(User('Peilonrayz', 'Peilonrayz', None, []))
print(vals)
print([val.get(None) for val in vals])
I find maybe.py a little hard to read, mostly as I got the inheritance to happen in build_maybes
. And so the following is a class diagram of the user defined objects in maybe.py. One thing to note is the dotted arrows from Nothing
and NothingNav
to MaybeConstant
are to show they are using a metaclass, rather than box standard inheritance. The box with some classes in it is to emphasise they are created in build_maybes
.