### 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

A simple example of using this type includes handling

We can also check the monad axioms I defined in my last post, to make sure we're dealing with a monad.

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

Now for that `interesting topic' I mentioned at the outset. I'm not exactly happy with the decorator above because it uses, again, the

`Maybe`

.#! /usr/bin/env python3.6 # -*- coding: utf-8 -*- from typing import Any, Callable as Function from monads1 import Monad class Maybe(Monad): 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 NothingI 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

`ZeroDivisionError`

s, 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 # ** NothingWe 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}') # ** NothingOn 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.
## Comments

## Post a Comment