Type inference in kyo vs mtl

Article that references this page: Environment Functions

Type inference is far stronger in kyo than in mtl, because we’re not doing a lot of value stacking. We only ever operate within one monad kyo.<, and that monad does compose with itself (as all monads do). It’s just monads don’t compose with each other.

Imagine we have Either[String, Int] and IO[Int], these don’t compose:

1
2
3
4
5
6
7
8
val eitherStringOrNum: Either[String, Int] = Right(4)
val ioNum: IO[Int] = IO(5)

// THIS FUNCTION FAILS TO COMPILE:
def combine = for
  x <- eitherStringOrNum
  y <- ioNum
yield x + y

To work together, we need to stack the monads with EitherT:

1
2
3
4
5
6
7
8
val eitherStringOrNum: Either[String, Int] = Right(4)
val ioNum: IO[Int] = IO(5)

// this compiles:
def combine: EitherT[IO, String, Int] = for
  x <- EitherT.fromEither(eitherStringOrNum)
  y <- EitherT.liftF(ioNum)
yield x + y

EitherT[IO, String, Int] cannot be inferred here, because EitherT.fromEither does not have enough information to know what monad it’s transforming. It can infer a String error channel and Int success channel, but nothing about that expression says that it’ll operate over IO. The next expression would, but inference cannot span across two expressions.

in kyo, we don’t run into this problem:

1
2
3
4
5
6
7
8
val numOrAbortString: Int < Abort[String] = 4
val numAsync: Int < Async = 5

// compiles!
def combine = for
  x <- numOrAbortString
  y <- numAsync
yield x + y

Int < (Abort[String] & Async) is inferred successfully. This is because numOrAbortString has a totally known type, and flatMap in kyo combines effects within the single kyo.< monad.