Trying to Make Sense of Monads

I'm not really a functional programming person, so when I read that "For a monad m, a value of type m a represents having access to a value of type a within the context of the monad." (C. A. McCann) as on the Wikipedia page, I can confidently say that I understood what a monad was exactly as much before and after that explanation. The only thing this led me to conclude was that a monad must be some really simple thing if so many of the explanations I've found of it are barely a step beyond "it is what it is".

After digging around, though, I think I've made some sense of things. In order to compute some output (a result), we need some input (arguments or a state) and some algorithm to feed those inputs through (a function). So, we have something like 

    algorithm: input -> output

or

    function: state -> result

Easy enough, right? However, we usually think of the function as something permanent, something that might get put into a library or module, while the input and output are transient. This is more often than not true, but it really doesn't have anything to do with the reality that in order to compute an output, we definitely need both a function and an input to come together (to bind). So, what if the function was the transient thing, the input was relatively persistent, and we could choose how to apply (bind) the function to it?

    class State:
        def __init__(self, *args, **kwargs):
            self.args, self.kwargs = args, kwargs
        def __call__(self, func: callable):
            return func(*self.args, **self.kwargs)

So now we have this sort of backwards situation, where we hold onto the state and bring in functions as needed:

    >>> state = State((1, 2, 3))
   
>>> state(sum)
    6
    >>> list(state(reversed))
    [3, 2, 1]

As far as I can tell, State is a just monad, with __call__ being the rule as to how to bind the state to a function, in this case by just applying the function. It is of course true that the result of the binding is itself a state that could potentially be bound to a function, so maybe the full binding operation should return a State object, too. Either way, that seems like a pedantic detail that isn't of any fundamental importance. What's more interesting is that we can change the binding rules:

    class Nothing(State):
        def __call__(self, func: callable):
            return self

This is called a nothing monad, I think. So now, if we define a function like

    def add_ints(*args):
        if any(not isinstance(arg, int) for arg in args):
            return Nothing()
        return State(sum(args))

we can do stuff like

    >>> State(-1, -2, -3)(add_ints)(abs)
    6
    >>> State(-1, -2, 'not an int')(add_ints)(abs)
    <__main__.Nothing at 0x7f754b12a700>

where we managed to get abs, a built-in function we definitely haven't modified, to correctly behave on the sum of some integers and be bypassed when something went wrong because one of the inputs wasn't an integer. Without this approach, we'd have to resort to wrapping abs or raising exceptions. Instead we did, well, nothing.

And the other kinds of monads just seem to have to do with how the binding is done, so we might write

    class Monad:
        def __init__(self, *args, **kwargs):
            self.args, self.kwargs = args, kwargs

    class Just(Monad):
        def __call__(self, func):
            return func(*self.args, **self.kwargs)

    class Nothing(Monad):
        def __call__(self, func):
            return self

    class Map(Monad):
        def __init__(self, *args):
            super().__init__(*args)
        def __call__(self, func):
            return map(func, self.args)

and whatever else catches our fancy. So really, all the monad is the bit that captures some state (or value) and allows it to be used in the context of the monad, which is what that quote at the start says.

Comments