I'm currently going through Scala with Cats and enjoying it a lot.
In Exercise 4.1.2 (Getting Func-y) we are tasked to implement map
using the methods pure
and flatMap
only.
The code for reference:
import scala.language.higherKinds
trait Monad[F[_]] {
def pure[A](a: A): F[A]
def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
def map[A, B](value: F[A])(func: A => B): F[B] =
???
}
My thinking was as follows:
For the first parameter of flatmap
we just pass value
.
For the second parameter, we have the function func: A => B
and a method pure
, which, after eta expansion, has the form pure: A => F[A]
. Therefore, we compose pure
with func
, i.e. we pass pure _ compose func
, where pure _
eta expands pure
.
map
would then look like this:
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)(pure _ compose func)
However, this didn't work and produced the following error:
[error] 7:35: type mismatch;
[error] A => B|scala.Nothing
[error] flatMap(value)(pure _ compose func)
[error] ^
[info] A => B <: ? => Nothing?
[info] false
[error] 6:30: parameter value func in method map is never used
[error] def map[A, B](value: F[A])(func: A => B): F[B] =
[error] ^
[error] two errors found
[error] (Compile / compileIncremental) Compilation failed
To my surprise, using func andThen pure
for the second parameter does compile.
map
defined like this compiles:
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)(func andThen pure)
I'm surprised because I assumed that for two functions f: A => B
and g: B => C
, the following would hold:
f andThen g
is equivalent to a => g(f(a))
and
g compose f
is equivalent to a => g(f(a))
,
therefore f andThen g
and g compose f
are equivalent.
What's the reason that this doesn't hold in general and in this example?
I used scala version 2.13.8 above.
I thought, maybe the problem relates to the eta expansion of the pure
method. I found several questions relating to compose
and andThen
methods as well as eta expansion, but they didn't answer my question.
I found a scala 3 reference entry about Automatic Eta Expansion, so I tried my luck in scala 3.2.1:
trait Monad[F[_]]:
def pure[A](a: A): F[A]
def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)(pure compose func)
Notice, how we don't need the underscore after pure
anymore (it will be deprecated at some point in the future) because of the automatic eta expansion mentioned in the scala 3 reference article above.
However, this also throws an error:
-- [E007] Type Mismatch Error: --------------------------------------------------------
7 | flatMap(value)(pure compose func)
| ^^^^^^^^^^^^^^^^^
| Found: A => F[Any]
| Required: A => F[B]
|
| where: F is a type in trait Monad with bounds <: [_] =>> Any
(In the where clause, I don't understand what kind of upper bound [_] =>> Any
should be.)
But, again, map
using func andThen pure
compiles.
Based on the error message, I now believe that the compiler can somehow not infer the correct type because... of the type parameters? I don't know.
Sometimes eta-expansion is sometimes confusing for the compiler and you have to clarify things yourself.
In Scala 2 I managed to make your code compile with:
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)((pure[B](_)).compose(func))
because method _
doesn't always work and method(_)
is more in your face (you still have to define [B]
to avoid inferring it to something silly like Any
or Nothing
).
In Scala 3 it works slightly better:
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)((pure[B] _).compose(func))
or even
def map[A, B](value: F[A])(func: A => B): F[B] =
flatMap(value)(pure[B] compose func)
works - you only have to provide [B]
explicitly so that it wont attempt to combine Nothing => F[Nothing]
with A => B
. Currently it cannot be avoided as compiler:
pure
before calling compose
on eta-expanded methodNothing
.compose
which takes A => B
value? => Nothing
so this particular problem comes from compiler not inferring whole expression at once but doing it in chunks.