Extra: Do You Want to Build a Monad?
This article is inspired by (The Best Introduction to Monad)[https://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/#], but is adapted to the OO-context with Java.
Let's say we have:
1 2 3 4 5 6 7 | double sin(double x) { return Math.sin(x); } double cube(double x) { return x*x*x; } |
We can easily chain the methods together:
1 2 | sin(cube(5.0)); cube(sin(5.0)); |
But, what if we need to print something while doing this operation?
We can easily do:
1 2 3 4 5 6 7 8 9 | double sin(double x) { System.out.println("called sin"); return Math.sin(x); } double cube(double x) { System.out.println("called cubed"); return x*x*x; } |
But that has side effects, so it violates the spirit of functional programming. We should concat the logs into a string (just like what you did in Lab 4). So we need a class that encapsulates the variable with its log.
1 2 3 4 5 6 7 8 9 | class DoubleString { Double x; String log; DoubleString(double x, String log) { this.x = x; this.log = log; } } |
Now, we can write the methods as:
1 2 3 4 5 6 7 | DoubleString sinAndLog(double x) { return new DoubleString(sin(x), "called sin"); } DoubleString cubeAndLog(double x) { return new DoubleString(cube(x), "called cube"); } |
In a way, we are writing methods that take in a value (double x
) and add some context to it (the log). We wrap both the value and its context in a box (the DoubleString
).
But these new functions do not compose anymore. We cannot do sinAndLog(cubeAndLog(x))
We need methods that takes in a DoubleString
and return a DoubleString
1 2 3 4 5 6 7 | DoubleString sinAndLog(DoubleString ds) { return new DoubleString(sin(ds.x), ds.log + "called sin"); } DoubleString cubeAndLog(DoubleString ds) { return new DoubleString(cube(ds.x), ds.log + "called cube"); } |
Great, now we can write the methods to compose them:
1 | sinAndLog(cubeAndLog(new DoubleString(5.0,""))); |
or in the OO-way
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class DoubleString { Double x; String log; DoubleString(double x, String log) { this.x = x; this.log = log; } DoubleString sinAndLog() { return new DoubleString(sin(this.x), log + "called sin"); } DoubleString cubeAndLog() { return new DoubleString(cube(this.x), log + "called cube"); } } |
In the OO-way, we chain the methods together.
1 | new DoubleString(5.0, "").sinAndLog().cubeAndLog(); |
Making It A Monad
Now, here is where I jump to creating a monad. I do not want to convert all my methods that takes in double
and returns DoubleString
into something that takes in DoubleString
and returns DoubleString
. Yet, I want to be able to compose them and chain them together. So I write a general method that allows that, and that is our flatMap
method:
1 2 3 4 | DoubleString flatMap(Function<Double, DoubleString> mapper) { DoubleString ds = mapper.apply(x); return new DoubleString(ds.x, this.log + "\n" + ds.log); } |
We can now use flatMap
to chain different operations together.
1 2 3 | DoubleString ds = new DoubleString(5.0, "") .flatMap(x -> sinAndLog(x)) .flatMap(x -> cubeAndLog(x)); |
Now DoubleString
is a monad!
Going back to our "wrap a value in a box with context" explanation. If we have two such wrappers, how do we wrap twice? We have to (i) wrap it one time, (ii) unwrap to get the new value and new context, and wrap it again.
The two lines in flatMap
corresponds to (i) and (ii) respectively.
Line ds = mapper.apply(x);
wraps it once; The next line unwraps the value and the context (ds.x
and dx.log
) and wraps it again (
new DoubleString(..)
) with the next context (this.log + "\n" + ds.log
).
Here is our monad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class DoubleString { Double x; String log; DoubleString(double x, String log) { this.x = x; this.log = log; } DoubleString flatMap(Function<Double, DoubleString> mapper) { DoubleString ds = mapper.apply(x); return new DoubleString(ds.x, this.log + "\n" + ds.log); } public String toString() { return x + "\n" + log; } } |
Making a Generic Monad that Logs
We can make DoubleString
a generic class that logs what happen to a variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Logger<T> { T x; String log; Logger(T x, String log) { this.x = x; this.log = log; } <R> Logger<R> flatMap(Function<? super T, ? extends Logger<? extends R>> mapper) { Logger<R> ds = mapper.apply(x); return new Logger<>(ds.x, this.log + "\n" + ds.log); } public String toString() { return x + "\n" + log; } } |
Functor
Can we do this with a functor? Note that a functor has a map
method with type Function<T,R>
. A map
method for DoubleString
would looks like Function<Double,Double>
. So it cannot do what the monad does. A functor can only change the value inside the box, but it cannot rewrap it with an updated context.
Wait, What is a Monad Again?
I hope the example above helps explain what is a monad -- it is a value wrapped in a box with context, and it allows us to compose wrappers (wrap multuple times), operate on its value and update the context if needed.