📜 ⬆️ ⬇️

For those who want the weird: monads in Python

Good day!

Recently, having begun to study Haskell, I tried several times to get close to monads, but Nikik could not, what is called, grab a thread (perhaps, the lack of basic knowledge). Helped a wonderful book Learn you a Haskell for great Good .
I read, imbued, I decided to convey to colleagues / friends. We develop in Python, it would seem, there is no need to delve into “all this functionalism”, at least further filter / map / reduce. But the expansion of horizons is certainly useful, so I decided to implement a couple of monads in Python, so that it does not turn out to be completely unpythonic. Of course, I am not the first and I am not the last, there were and there are several implementations of monads based on Python, but all the implementations that I encountered are either completely unpythonic or difficult for people to understand far from the concept itself. I had to reinvent my own bicycle, which, however, allows us to grasp the essence ...


I will note right away, I myself, so far only at the very beginning of the study of the main foundation: category theory, therefore this implementation is, most likely, rather naive. I hope for constructive criticism (and destructive is useful).
')
I followed the path of monads in Haskell, in which historically monads appeared before applicative functors. I also (perhaps, so far) did not implement functors / applicative functors. But you can try if the topic seems interesting.

First I implemented the Maybe monad, my favorite. I sometimes really want Python to have some value that a function could return as an unsuccessful result. None is not suitable for such a task in the general case - this is quite a result.
Many will answer: "in Python, there are exceptions." I agree, but sometimes the wrapping in try / except of a simple expression, such as 100 / x, seems to me somewhat cumbersome. This suggests the option of wrapping the result of functions into something like a tuple (Bool, result), where the first member is a sign of unsuccessful execution. This is where the monad value is obtained in the context of the Maybe monad. Such behavior of functions is sometimes convenient in itself, but I want more - I want a binding (>> =). Binding allows you to implement sequential calculations through a sequence of actions, each of which can fail. In this case, each element of the sequence does not follow the result of the previous one - the binding operation itself, if the previous step fails, skips all subsequent ones, as they have no meaning. All this resulted in this class:

class _MaybeMonad(object): def __init__(self, just=None, nothing=False): self._just = just self._nothing = nothing def __rshift__(self, fn): if not self._nothing: res = fn(self._just) assert isinstance(res, _MaybeMonad), ( "In Maybe context function result must be Maybe") self._just = res._just self._nothing = res._nothing return self @property def result(self): return (self._nothing, self._just) 


When instantiating, the constructor takes either a value that falls into context (via just = x), or a sign of an unsuccessful result (via nothing = True). In this case, nothing has priority, which is logical.
The result from the monad value (an instance of this class) can be obtained through the result property in the form of the tuple already described above.

It is more convenient to use this class after a couple of cuts:

 nothing = lambda: _MaybeMonad(nothing=True) just = lambda val: _MaybeMonad(just=val) 

the first creates an unsuccessful result, the second - a successful one with a value

To package the initial value for a sequence of calculations, I made an alias:

 returnM = just 


Now you can write simple functions that return a monadic result:

 def divM(value, divider): '''  .   . "" ''' if divider: return just(value // divider) return nothing() div100by = lambda x: divM(100, x) # ,    def sqrtM(value): if value >= 0: return just(math.sqrt(value)) return nothing() 


Binding allows you to do such things:
 do = returnM(4) >> div100by >> sqrtM # 4 -  error, result = do.result 


This sequence of actions normally digests a negative value, instead of a parameter (on which math.sqrt would sprinkle); the division operation will normally accept 0 as a divisor and return nothing (), which will be the result of the whole expression.
Thus exceptions remain for extreme cases.

You can and should add the following function:
 lift = lambda fn: lambda val: just(fn(val)) 


It looks scary, but it's easy to use:
lift (str) simply "pulls" the usual function str () into context, i.e. the result returned by the normal function will be packed into a monad value.

Now, as a final touch, we add a currying partial application (thanks, corrected ):

 curried = lambda fn, *cargs: lambda *args: fn(*(cargs + args)) 


Finally a more comprehensive example:

 def calc(x): do = ( returnM(x) >> curried(maybe_div, 100) >> lift(lambda x: x+75) >> lift(int) ) #   failed, result = do.result if failed: print "  ((" else: print ": %s" % result calc(0) #   100 / 0 calc(4) 


It turned out not quite pythonic, especially lambda on lambda, but this is just as fixable.
I also wanted to write about the implementation of the List monad, which I also like very much, but it seemed like a bit too much for one article. There will be interest - there will be an article.

Source: https://habr.com/ru/post/138546/


All Articles