The interesting question is how to prove the equivalence (and what that equivalence actually means) under some required restrictions.
The required restrictions are that your function's code must be "fully parametric": use only type parameters and never any specific types; no run-time identification of type parameters; no reflection; and of course no side effects. Of course you can make an example in Scala (and in Haskell, etc.) where the functions do not satisfy the law because they identify types at run time. But it's a case that almost never comes up in practice.
What you are missing for your proof is a naturality law of compose
. Without assuming that law, it is not true that flatMap(unit(y))(f) == f(y)
.
Naturality laws will hold automatically whenever the code is "fully parametric", i.e., restricted in the way I just described. But this is a complicated subject that currently has no good tutorials available. For instance, the "Theorems for free" paper is certainly not suitable as a tutorial for a practicing Scala or Haskell programmer trying to understand how to prove - or even how to write - a naturality law for compose
.
So, let us just assume that the following additional "left naturality" law holds for compose
:
for all f : A => B, g: B => M[C], h: C => M[D]:
compose(f andThen g, h) = f andThen compose(g, h)
Now we have:
flatMap(unit(y))(f)
== compose(_: Unit => unit(y), f)(())
== compose( (_ => y) andThen unit, f)(()) // Now use the left naturality law of `compose`.
== ((_ => y) andThen compose(unit, f))(())
== ((_ => y) andThen f)(())
== ((_ => f(y))(())
== f(y)
The other laws of flatMap
can be also derived from the laws of compose
only by assuming a naturality law of compose
.
The book "Functional programming in Scala" never mentions this additional assumption - neither in the chapter on monads nor in the chapter on applicatives, where a similar equivalence holds between "ap" and "zip" but only when certain naturality laws are assumed.
For the flatMap
and compose
, the authors of "FPiS" show you the proof of laws only in the direction where no additional assumptions are needed: namely, when compose
is defined via flatMap
. So, I guess, the authors never tried themselves proving those laws in the other direction.
The equivalence between compose
and flatMap
is explained in detail (but using a special, better adapted notation) in Section 10.2.6 of my book "The Science of Functional Programming". LyX/LaTeX source: https://github.com/winitzki/sofp pdf: https://leanpub.com/sofp
In my book, I go through a large number of derivations where one function type is equivalent to another. In each case, I find that there are extra naturality laws that one has to assume. Intuitively, this makes sense, because how else can a function flatMap
with two type parameters be equivalent to a function compose
with three type parameters? Only when compose
is additionally constrained by a law that can arbitrarily modify one of its type parameters and so cancels the additional freedom of having one more type parameter.
The easiest example of "equivalence under naturality" is the equivalence of pure[A]: A => M[A]
and u: M[Unit]
. You can define pure(x) = u.map(_: Unit => x)
and u = pure(())
but this does not already prove an equivalence between the types of these functions.
Indeed, they cannot be simply equivalent: pure[A]
has a type parameter A
but u
has no type parameters.
So, in fact, they are equivalent only we assume a naturality law for pure
:
for all x: A, f: A => B:
pure(x).map(f) = pure(f(x))
Only with this additional law, one can prove that defining pure
via u
and back gives indeed an isomorphism in both directions.