I've tried to use flatMap on WriterT and it was successful.
So the problem is with my type probably but I can't find what's wrong with it.
import cats.Monad
import cats.syntax.flatMap._
object Main extends App {
type Optional[A] = A | Null
val maybeInt1: Optional[Int] = 1
val maybeInt2: Optional[Int] = null
given Monad[Optional] with {
def pure[A](x: A): Optional[A] = x
def flatMap[A, B](fa: Optional[A])(f: A => Optional[B]): Optional[B] = {
fa match {
case null => null
case a: A => f(a)
}
}
def tailRecM[A, B](a: A)(f: A => Optional[Either[A, B]]): Optional[B] = {
f(a) match {
case null => null
case Left(a1) => tailRecM(a1)(f)
case Right(b) => b
}
}
}
def f[F[_]: Monad, A, B](a: F[A], b: F[B]) = a.flatMap(_ => b)
println(Monad[Optional].flatMap(maybeInt1)(_ => maybeInt2)) //OK: null
println(f[Optional, Int, Int](maybeInt1, maybeInt2)) // OK: null
println(maybeInt1.flatMap(_ => maybeInt2)) // Compilation Error
}
The error is:
value flatMap is not a member of Main.Optional[Int].
An extension method was tried, but could not be fully constructed:
cats.syntax.flatMap.toFlatMapOps([A] =>> Any), A(given_Monad_Optional)
Your definition has several issues.
I.e. type Optional[A] = A | Null
is a type expression that will be expanded as soon as possible.
When you are using it as a result type what you actually get is
val maybeInt1: Int | Null = 1
val maybeInt2: Int | Null = null
So when the compile compiler has something like
implicit def toFlatMapOps[F[_], A](fa: F[A])(implicit F: Monad[F]): MonadOps[F, A]
imported from scala 2 library or equivalent extension in scala 3,
and finally comes to maybeOption.flatMap
,
then tries to apply former extension method,
it fails to typecheck the expression
toFlatMapOps(maybeInt1).flatMap(_ => maybeInt2)
So now you have Int | Null
as an argument since Optional
have been already expanded and need to calculate corresponding F[_]
and A
, it has many solutions such as
F[X] = Int | X , A = Null
F[X] = X | Null, A = Int
F[X] = A | Null, A = Nothing
F[X] = [X] =>> X, A = Int | Null
So scala naturally fails this attempt to guess.
Despite that scala 3 compiler can use additional information such as implicit\contextual value here, the implicit value matching Monad
with the highest priority here is
given Monad[Optional]
Now can can attempt to apply
toFlatMapOps[F = Maybe](maybeInt1 : Int | Null)
Then having F[X] = X | Null
you need to calculate the A
knowing that F[A] = Null | A
and that has many plausible solutions as well
A = Int
A = Int | Null
So even if scala wouldn't fail at the first step it'd stuck here
Add scalacOptions += "-Yexplicit-nulls"
to your sbt config and try this code
import cats.Monad
import cats.syntax.flatMap.given
object Optional:
opaque type Optional[+A] >: A | Null = A | Null
extension [A] (oa: Optional[A]) def value : A | Null = oa
given Monad[Optional] with
def pure[A](x: A): Optional[A] = x
def flatMap[A, B](fa: A | Null)(f: A => B | Null) =
if fa == null then null else f(fa)
def tailRecM[A, B](a: A)(f: A => Optional[Either[A, B]]): Optional[B] =
f(a) match
case null => null
case Left(a1) => tailRecM(a1)(f)
case Right(b) => b
type Optional[+A] = Optional.Optional[A]
@main def run =
val maybeInt1: Optional[Int] = 1
val maybeInt2: Optional[Int] = null
def f[F[_]: Monad, A, B](a: F[A], b: F[B]) = a.flatMap(_ => b)
println(Monad[Optional].flatMap(maybeInt1)(_ => maybeInt2)) //OK: null
println(f(maybeInt1, maybeInt2)) // OK: null
println(maybeInt1.flatMap(_ => maybeInt2)) // Compilation Error
Even in this fixed version Optional[A]
fails basic monadic laws
Consider this code
def orElse[F[_], A](fa: F[Optional[A]])(default: => F[A])(using F: Monad[F]): F[A] =
fa.map(_.value).flatMap(fb => if fb == null then default else F.pure(fb : A))
def filterOne(x: Int): Optional[Int] = if x == 1 then null else x - 1
println(orElse(maybeInt1.map(filterOne))(3))
The first method attempts to resolve missing values with the given calculated monadic value, the second just filter out ones. So, what do we expect to see when something like this evaluated?
orElse(maybeInt1.map(filterOne))(3)
We take non-empty maybe, then replacing the 1
with the missing place, and then immediately fixing it using provided 3
. So I would expect to see 3
but actually, we gain null
as result since null
inside the wrapped value considering as a missing branch for outer Optional
during flatMap.
This is because such naively defined type violates the left-identity law
UPDATE Regarding comment by @n-pronouns-m How this definition violates left identity law. Left identity states that
pure(a).flatMap(f) == f(a)
for all types A, B, and values a: A, f: A => Optional[B]
so lets take A = Optional[Int], B = Int, A = null, f(a) = if a == null then 3 else 2
pure(a) is still null, flatMap returns null for every in first argument, so
pure(a).flatMap(f) == null
while f(a) == 3