javamonads

Why do we need to check all three monadic laws when testing if an Object is a Monad?


From what I understand, the three monadic laws are as follows (I come from a Java background instead of Haskell so pardon my syntax):

Monad.of(x).flatMap(y -> f(y)) = f(x)
monad.flatMap(y -> Monad.of(y)) = monad
monad.flatMap(x -> f(x)).flatMap(x -> g(x)) = monad.flatMap(x -> f(x).flatMap(x -> g(x)))

I have read that to show that an Object is a Monad, I will have to show that it satisfies all three monadic laws. This seems to imply that all three laws are necessary, and neither implies the other. However, I am having difficulty thinking of an examples where an Object can violate each one of the monadic laws without violating the other two.

I am also unable to find examples online. (I have found some people say Java Optional fails the Left Identity Law, but it uses null which is of any type and some may argue that it is not a proper value for Optional.) Is it possible if I could see some of such examples? Thanks in advance!

Edit 1: Recently, I took an exam and in the exam there was an instance of an example of an object that violates the Right Identity Law and satisfies the other two laws. Here it is:

class Counter<T> {
  private final T val;
  private final int count;
  
  private Counter(T val, int count) {
    this.val = val;
    this.count = count;
  }
  
  public static <T> Counter<T> of(T val) {
    return new Counter<>(val, 1);
  }
  
  public <R> Counter<R> map(Function<T, R> fn) {
    return new Counter<>(fn.apply(this.val), this.count + 1);
  }
  
  public <R> Counter<R> flatMap(Function<T, Counter<R>> fn) {
    Counter<R> tmp = fn.apply(this.val);
    return new Counter<>(tmp.val, tmp.count);
  }
  
  @Override
  public boolean equals(Object obj) {
    if (this == obj) { return true; }
    if (!(obj instanceof Counter<?>)) { return false; }
    Counter<?> ctx = (Counter<?>) obj;
    return this.val.equals(ctx.val) && this.count == ctx.count;
  }
}

Solution

  • One of the original motivations for introducing the Optional type to Java was to provide an alternative to the null value for expressing the concept of an absent value. Using Optionals the absent value would be an `Optional.empty()' value. Given this, it does not generally make sense to use Optionals in conjunction with null values (aside from immediately converting null values to Optionals). Despite this some people insist on combining the two, which then leads to some unexpected results.

    In particular, if one chooses to interpret the Optional type as a monad, and elects to make the Optional.ofNullable act as the monadic unit/return and the Optional.flatMap as the monadic bind, then the monad laws won't hold.

    The Left Identity Law re-expressed under this proposed monad for the Optional type is as follows:

    Optional.ofNullable(x).flatMap(f) = f.apply(x)
    

    (where f is a function from T to Optional<T> for some type T)

    I.e. the effect of taking a value, lifting it into an Optional and then flatMaping a function f over it, is the same as simply applying the function to the value.

    If x is null` then we can take the left-hand side and reduce it by substituting in the function definitions for ofNullable and flatMap:

    Optional.ofNullable(x).flatMap(f)
        => Optional.empty().flatMap(f)
        => Optional.empty()
    

    (where => means reduces to, or is equivalent to)

    However the function f when applied directly to x (i.e. null) doesn't necessarily have to return Optional.empty(). A simple example is the following function:

    Optional<String> stringify(Object obj) {
        if (obj == null) {
            return Optional.of("NULL");
        } else {
            return Optional.of(obj.toString());
        }
    

    If we take f in the RHS of the Left Identity Law to be the stringify function, and we apply it to null, then we get an Optional value that wraps the string "NULL", i.e. it doesn't match the LHS of the law, therefore the law doesn't hold.

    This issue goes away if, instead of using Optional.ofNullable as the monadic unit, you use Optional.of, as the latter function doesn't allow null values.

    Moving onto the Counter code that you provided - it's a little bit strange.

    Firstly, the implementation of flatMap is creating a redundant copy of the tmp object. Since this has no real effect that method can simply be rewritten as follows:

    public <R> Counter<R> flatMap(Function<T, Counter<R>> fn) {
        return fn.apply(this.val);
    }
    

    Secondly, since the context is monads we only need concern ourselves with 1) the fields that comprise the class, 2) it's of method and 3) the flatMap method, as these are the only elements that have any relevance to the monadic behaviour. Those methods never change the count field, so we can simply ignore that. So, we can effectively rewrite the class as follows:

    class Counter<T> {
        private final T val;
    
        private Counter(T val) {
            this.val = val;
        }
    
        public static <T> Counter<T> of(T val) {
            return new Counter<>(val);
        }
    
        public <R> Counter<R> flatMap(Function<T, Counter<R>> fn) {
            return fn.apply(this.val);
        }
    }
    

    This is in fact the Identity monad, for which the 3 laws definitely do hold.

    However, if we go back to the original code and look at the map method, then we can see it increments the count field every time map is called. All Monad type are also Functors, and Functors need to have a method (usually called map) that also adheres to a rule, one that effectively says that chaining two functions over a functor value by calling map twice, is the same as combining the functions and applying the combined result by calling map once. The map implementation in the Counter class will break that rule because the count field depends on how many times map is called.

    This arguably means the class can't be a monad, assuming you interpret the map method as the Functor map function. This is an assumption, and I would expect any question regarding that code to make this explicit, otherwise the question is missing key information required to be able to answer it.

    I assume the author couldn't think of a better example of a monad that breaks one rule and not the others, so they simply implemented the Identity monad in Java, and then added a bugged implementation of map. If you really want to learn about monads then I would suggest you find someone a better source of information.