Skip to content

Instantly share code, notes, and snippets.

@danieldietrich
Last active January 15, 2018 15:03
Show Gist options
  • Save danieldietrich/71be006b355d6fbc0584 to your computer and use it in GitHub Desktop.
Save danieldietrich/71be006b355d6fbc0584 to your computer and use it in GitHub Desktop.
Monads in Javaslang

Monad Laws

Let

  • A, B, C be types
  • unit: A -> Monad<A> a constructor
  • f: A -> Monad<B>, g: B -> Monad<C> functions
  • a be an object of type A
  • m be an object of type Monad<A>

Then all instances of the Monad interface should obey the three control laws:

  • Left identity: unit(a).flatMap(f) ≡ f a
  • Right identity: m.flatMap(unit) ≡ m
  • Associativity: m.flatMap(f).flatMap(g) ≡ m.flatMap(x -> f.apply(x).flatMap(g))

Monads in Javaslang

Strictly, given a Monad type Monad, the signature of the flatMap method should look like this in Java. For the sake of simplicity we omitted other interface methods like Functor.map.

interface Monad<A> {
  <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

Example:

interface Try<A> extends Monad<A> {
  <B> Try<B> flatMap(Function<? super A, ? extends Try<? extends B>> f);
}

Because of the lack of higher-kinded types we cannot define the Try interface as shown above. It does not compile because we changed the function return type from ? extends Monad to ? extends Try. To circumvent this limitation we relaxed the Monad interface a bit:

interface Monad<A> extends Iterable<A> {
  <B> Monad<B> flatMap(Function<? super A, ? extends Iterable<? extends B>> f);
}

We are now able to define Try like this:

interface Try<A> extends Monad<A> {
  <B> Try<B> flatMap(Function<? super A, ? extends Iterable<? extends B>> f);
}

The Monad implementations are still satisfying the (adapted) Monad laws.

Examples:

// = Some(0.1)
Option.of(10).flatMap(i -> Try.of(() -> 1.0 / i));

// = List(-1, 1)
List.ofAll(-2, 0, 2).flatMap(i -> Try.of(() -> 2 / i));

// = TreeSet(1, 2, 3, 4)
TreeSet.ofAll(3, 2, 1).flatMap(i -> List.ofAll(i, i + 1));

// = Failure(java.lang.ArithmeticException: / by zero)
Monad.lift((Integer a, Integer b) -> a / b)
     .apply(Try.success(20), List.ofAll(0, 4, 1, 2, 3));

// = Success(5)
Monad.lift((Integer a, Integer b) -> a / b)
     .apply(Try.success(20), List.ofAll(4, 1, 2, 3));

// = List(5, 20, 10, 6)
Monad.lift((Integer a, Integer b) -> b / a)
     .apply(List.ofAll(0, 4, 1, 2, 3), Try.success(20));

Scala does not compile such monadic operations.

Example 1: Using a for-comprehension

// error: type mismatch;
//  found   : List[Int]
//  required: scala.util.Try[?]
//        for (a <- Try(20); b <- List(4, 1, 2, 3)) yield { a / b }
//                             ^
for (a <- Try(20); b <- List(4, 1, 2, 3)) yield a / b

Example 2: Using the equivalent flatMap/map API

// error: type mismatch;
//  found   : List[Int]
//  required: scala.util.Try[?]
//        Try(20).flatMap(a => List(4, 1, 2 ,3).map(b => a / b))
//                                                 ^
Try(20).flatMap(a => List(4, 1, 2, 3).map(b => a / b))
@ggalmazor
Copy link

When you say:

interface Monad<A> {
  <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

Why does f's return type have to be Monad<? extends B>? Is it for expressing covariance? Shouldn't be Monad<B> enough in Java? The way you wrote it it seems that an instance of exactly B wouldn't pass type checks... (only objects extending B allowed) Am I wrong? (probably :/ )

@danieldietrich
Copy link
Author

I asked the same question here. @szarnekow assembled some examples which describe the difference.

@danieldietrich
Copy link
Author

Just one word about this example:

// = Success(5)
Monad.lift((Integer a, Integer b) -> a / b)
     .apply(Try.success(20), List.ofAll(4, 1, 2, 3));

One might expect that the result should be something like a Success(Seq(5, 20, 10, 6)).

But this is not what the Try.flatMap method does. Try is a single-valued type, List is a multi-valued type. Try.flatMap takes just the first element of the list, if present.

We can't use Monad.lift to express it, it is more like this:

Function<Integer, Integer> divide = (a, b) -> a / b;
Function<Monad<Integer>, Monad<Integer>> divideM = (mb, ma) -> mb.sequence( ma.map(a -> mb.map(b -> f.apply(b, a))) );

// = Success(Seq(5, 20))
divideM.apply(Try.success(20), List.of(4, 1));

However, the sequence method is static, so this will not work. (Note: sequence currently isn't implemented for all Javaslang types, but it will be there soon.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment