Given a heavily abstracted method as follows:
def someMethod(): IO[Outcome[IO, Throwable, ?]] =
val a = IO(42)
val b = IO("hello")
Outcome.Succeeded(a)
where IO
and Outcome
are types from cats-effect, and ?
is a placeholder for either String
or Int
, we can write:
def someMethod(): IO[Outcome[IO, Throwable, ? <: Int | String]]
I’m not sure why we need the upper bound, instead of simply Int | String
. If we don’t use the upper bound, we get a compilation error.
Found: cats.effect.IO[cats.effect.kernel.Outcome[cats.effect.IO, Throwable, Int]]
Required: cats.effect.IO[cats.effect.kernel.Outcome[cats.effect.IO, Throwable, Int | String]]
Probably because of this:
The compiler assigns a union type to an expression only if such a type is explicitly given.
So, IO(42)
is treated as IO[Int]
and not IO[Int | String]
. Using the upper bound means any type that fits inside the union Int | String
, including Int
, String
, or the union itself, so, the types check out.
Further reduction of someMethod
type signature is possible using the OutcomeIO
alias:,
type OutcomeIO[A] = Outcome[IO, Throwable, A]
def someMethod[T <: Int | String](): IO[OutcomeIO[T]]
But this doesn’t compile.
Found: cats.effect.IO[cats.effect.kernel.Outcome[cats.effect.IO, Throwable, Int]]
Required: cats.effect.IO[cats.effect.kernel.Outcome[cats.effect.IO, Throwable, T]]
where: T is a type in method someMethod with bounds <: Int | String
Both the compilation errors are counterintuitive, and seems like something the compiler should be able to figure out. Your thoughts?