Does a Maybe Monad collapses in Just or Nothing?

130 views Asked by At

I tried to implement a monad before reading about them, I just had a 'feeling' about the concept of The Monad.

I read on All about Monads that Just and Nothing are functions, not types of Monads, but I thought thought that each Maybe monad would eventually collapse into a Just or Nothing monad (or that's what I wrote) when it's bound to another value.

Is my understanding wrong? Also, in this case I made the Monad return itself via bind, but maybe I should just return the unwrapped value and that bind is already a comp.

This is what I wrote:

from abc import ABCMeta

class Monad(metaclass = ABCMeta):
    
    def __init__(self, value = None) -> None:
        self._unit = value

    @property
    def unit(self):
        return self._unit
    
    @unit.setter
    def unit(self, value):
        self._unit = value

    def __repr__(self) -> str:
        return f'{self.__class__.__name__} ( {self._unit} )'


class Maybe(Monad):
        
    def bind(self, function):
            self.unit = function(self._unit) if self.unit is not None else None
            self.__class__ = Nothing if self.unit is None else Just
            return self

class Nothing(Maybe):
    pass


class Just(Maybe):
    pass

a = Maybe(1)

b = Maybe(1)
b.bind(lambda x: x + 1).bind(lambda x: x + 1).bind(lambda x: x + 1).bind(lambda x: x + 1)

c = Maybe()
c.bind(lambda x: x + 1).bind(lambda x: x + 1).bind(lambda x: x + 1)

print(a, a.unit)
print(b, b.unit)
print(c, c.unit)

I still think this is ugly and I need to make it better... but I just want to understand if I'm correct about the concepts

1

There are 1 answers

9
Jared Smith On

You can't fully translate Monads to a dynamic language, and you can't even fully express them in most statically typed languages (you need something like HKTs, which few languages support). You also in this particular case need tagged unions.

Just and Nothing are functions

No, they aren't. They are type constructors, and there is (AFAIK) no analog in Python (even with the built in type hinting). Type constructors are similar to functions, but functions operate on values: you can take values, return values, etc. Type constructors operate on types: you can take a type as an argument, and you return a new type.

So the Maybe type is a tagged union of types: a Maybe some_type is either Just some_type or Nothing. N.B. that the type is Just some_type the type created by applying the type constructor Just to the type some_type, not Just or some_type.

Statically typed compiled languages have two "levels" if you will: the type level which exists at compile-time and the term or value level that exists at runtime. Dynamic interpreted languages like Python only have the second level, and Monads in Haskell exist at the first level, which is part of why it's so hard to understand them coming from that perspective. The water is further muddied in OOP languages, because classes exist at both levels: when we say class Foo we're defining both a runtime operation and a compile-time type (usually the type of the instances of Foo).

So what even is a Monad? I'm going to crib from this excellent explanation by Eric Lippert:

A monad is an "amplifier" of types that obeys certain rules and which has certain operations provided...what is an "amplifier of types"? By that I mean some system which lets you take a type and turn it into a more special type. What are the "certain rules"? Briefly, that there is a sensible way for functions on the underlying type to work on the amplified type such that they follow the normal rules of functional composition. What are the "operations"?

There is a "unit" operation (confusingly sometimes called the "return" operation) that takes a value from a plain type and creates the equivalent monadic value. This, in essence, provides a way to take a value of an unamplified type and turn it into a value of the amplified type. It could be implemented as a constructor in an OO language.

There is a "bind" operation that takes a monadic value and a function that can transform the value, and returns a new monadic value. Bind is the key operation that defines the semantics of the monad. It lets us transform operations on the unamplified type into operations on the amplified type, that obeys the rules of functional composition mentioned before.

Note that when he says "Monadic value" he's talking about a value that has the type of the Monad.

So it's going to be difficult to translate all this to Python (or Ruby, or Javascript, or...). If you've ever programmed in any statically typed compiled language you're going to get more mileage there in trying to understand Monads, even if that language's type system isn't powerful enough to actually express Monads (e.g. Java, C#, Typescript).

Edit

Based on comments, clarifying Monads in other statically typed compiled languages: you absolutely can create a Monad in a lot of statically typed languages. For example, Maybe in Typescript:

class Just<T> {
  constructor (public readonly value: T) {}
}

const Nothing = Symbol('Nothing');

type Maybe<T> = Just<T> | typeof Nothing

function bind<T, U>(f: (x: U) => T, y: Maybe<U>): Maybe<T> {
  if (y === Nothing) return Nothing;
  return new Just(f(y.value));
}

There's the Maybe monad in Typescript. The problem is that Typescript's type system (AFAIK) isn't powerful enough to abstract out that pattern that says "this generic type implements this contract for all types which may themselves be generic". My Maybe example is a Monad, but Typescript doesn't let me describe it as such or enforce it via the type system, it's on the programmer to make sure that every monad they create in TS upholds the contract rather than having the compiler check it.

So what might the Maybe Monad look like in Python? Probably something like this:

from typing import Callable, TypeVar, Generic, Union

T = TypeVar('T')
U = TypeVar('U')

class Just(Generic[T]):
    def __init__(self, value):
        self.value = value


class Nothing():
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            cls.instance = super(Nothing, cls).__new__(cls)
        
        return cls.instance


Maybe = Union[Just[T], Nothing]

def bind(f: Callable[[U], T], x: Maybe[U]) -> Maybe[T]:
    if isinstance(x, Nothing):
        return x
    else:
        return Just(f(x.value))


def add_one(n: int) -> int:
    return n + 1

bind(add_one, Just(1))   # Just(2)
bind(add_one, Nothing()) # Nothing
bind(add_one, 'hello')   # type error

Here the unit operation is calling Just, the bind operation is a function, Nothing is a Singleton value and the type from the class (I can't figure out how to do a unique uninhabited type in Python). That's as close as I could get, but you hopefully get the idea. Writing the function composition tests to ensure it upholds the Monad laws is left as an exercise to the reader and totally not omitted because it's Saturday morning and I want to go play video games.