### Monads in Python, 2

Today I worked with monads again. In the process I wrote an implementation of the Maybe monad in Python, using the same ABC I wrote in my former post. I also came across another interesting topic, but I'll get to that in a bit; below is my implementation of Maybe.
#! /usr/bin/env python3.6
# -*- coding: utf-8 -*-

from typing import Any
from types import FunctionType as Function

def __init__(self, value: Any =None, just: bool =True) -> None:
self.value = value
self.just = just

def __str__(self) -> str:
if self.just:
return f'Just({self.value})'
else:
return 'Nothing'

def __repr__(self) -> str:
return self.__str__()

def bind(self, f: Function) -> 'Maybe':
""" Action upon value in Maybe context that returns a new Maybe context
"""
if self.just:
return f(self.value)
else:
return self

def __eq__(self, other: 'Maybe') -> bool:
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False

@classmethod
def unit(cls, value: Any) -> 'Maybe':
return cls(value)

class Just(Maybe):
""" A successful Maybe monad """
def __init__(self, value: Any) -> None:
super(Just, self).__init__(value)

class Nothing(Maybe):
""" A failed Maybe monad """
def __init__(self) -> None:
super(Nothing, self).__init__(just=False)

def isJust(arg: Any) -> bool:
return type(arg) is Just

def isNothing(arg: Any) -> bool:
return type(arg) is Nothing

I included definitions for Just and Nothing, as well as the library functions isJust and isNothing mentioned on the Haskell Wiki page for Maybe (c.f. scabl).

A simple example of using this type includes handling ZeroDivisionErrors, as in the following function.
def division(num: int, den: int) -> Maybe:
""" breaks if j == 0 (ZeroDivisionError) """
if den == 0:
return Nothing()
else:
return Just(num // den)

Now we can act upon a value in the Maybe context.
from functools import partial

print(f'** {division(0, 1) >> partial(division, den=1)}')
print(f'** {division(1, 0) >> partial(division, den=1)}')
print(f'** {division(1, 1) >> partial(division, den=0) >> partial(division, den=1)}')

# ** Just(0)
# ** Nothing
# ** Nothing

We see that attempting $$\frac{1}{0}$$ results in Nothing, whereas $$\frac{0}{1}$$ is Just(0), as we'd expect. Moreover, further attempts to divide a value in the Nothing context results in Nothing, even though it may be a valid denominator for division. This is a result of the bind method's definition, i.e.
...
if self.just:
return f(self.value)
else:
return self
...

In words, if self.just is False, then we get the same instance of Nothing back. As opposed to exceptions and try-except blocks, we use if-then statements.

We can also check the monad axioms I defined in my last post, to make sure we're dealing with a monad.
check_monad_axioms(Maybe, 15, lambda d: Maybe(d * 2), partial(division, den=0.0))

# ** All tests passed for Maybe


I also tried to come up with an interesting use case of this monad. In the following block of code I've written a decorator that terminates a function with signal if it takes too long to return a value.
import signal
from functools import wraps
from typing import Optional, Callable

def signaling_limiter(time: int) -> Function:
def _outer_f(f: Function) -> Function:
@wraps(f)
def _inner_f(*args, **kwargs) -> Maybe:

res: Optional[Maybe] = None

class SignalFunctionEndError(Exception):
pass

def handler(*args) -> None:
# sets res to Nothing if time's up
nonlocal res
res = Nothing()
raise SignalFunctionEndError

try:
# If this finishes, handler is never called
signal.signal(signal.SIGALRM, handler)
signal.alarm(time)

res = f(*args, **kwargs)

except SignalFunctionEndError:
pass

finally:
signal.alarm(0)

return res

return _inner_f
return _outer_f

def infinity() -> int:
while True:
pass

# This return statement is never, ever reached
return 1

@signaling_limiter(time=1)
def timeout(comp: Callable) -> Maybe:
if not callable(comp):
return Nothing()
else:
return Just(comp())

In the event that it does (or we don't pass it an argument with the right attributes), we get Nothing.
print(f'** {Maybe(infinity) >> timeout}')

# ** Nothing

On the other hand, changing pass to break in the infinite while-loop, we get
# ** Just(1)

as we'd expect, since the return statement is allowed to complete.

Now for that interesting topic' I mentioned at the outset. I'm not exactly happy with the decorator above because it uses, again, the try-except block, which means we used exception handling. However, attempting to run f (or, in this case, infinity`) in a thread led to a rabbit hole of issues I wouldn't want to occur in real software. If it's one thing that's been a terrible nuisance as long as I've been using Python, it's the GIL and threading.