Search code examples
scalaimplicit-conversionscala-3union-types

Implicit conversion between a Union Type and Either in Scala 3


I'm trying to develop an implicit converter from type Error | A to type Either[Error, A] in Scala 3. The code of the converter is the following:

object MyConversions:
  given unionTypeToEither[Error, A]: Conversion[Error | A, Either[Error, A]] with
    def apply(value: Error | A): Either[Error, A] = value match
      case error: Error => Left(error)
      case a: A         => Right(a)

The converter should work with the following code:

import in.rcard.raise4s.MyConversions.unionTypeToEither

val actual: String | Int = 42
val actualEither: Either[String, Int] = actual

The compiler gives me the following error tho:

[error] 90 |    val actualEither: Either[String, Int] = actual
[error]    |                                            ^^^^^^
[error]    |                                       Found:    (actual : String | Int)
[error]    |                                       Required: Either[String, Int]
[error]    |
[error]    | longer explanation available when compiling with `-explain`
[error] one error found

Anyone can help me?


Solution

  • The issue comes from the difference in semantics between Either[A, B] and A | B.

    A | B is the same as B | A, any A is the same as A | Nothing - while combining 2 types (with |) is always unambiguous, decomposition is always ambiguous. You cannot do this without e.g. writing some macro with custom logic and additional assumptions.

    In my experience if you try to use inference to e.g. split A | B by passing it to something like:

    [X, Y](value: X | Y): Either[X, Y] = ...
    

    what you'll actually get is Either[A | B, Nothing] or something slimilarly useless.

    As someone with some context about your types, you are probably assuming that:

    • A and B have the lowest upper bound of Any/AnyRef/AnyVal
    • there is no A <:< B nor B <:< A relationship
    • there is nothing lost through type erasure and so they can be safely compared in runtime

    but as a very general rule if all the code knows about the types is that they are <: Any then the compiler has nothing to work with, and probably nobody even tried to handle the type inference to make it somehow work in a smart way.

    Assuming that you have some deterministic way of deciding which of the components goes first and which last (that is: what would be left and what would be right in A | B, since it's the same as B | A), you'd still have to write that decomposition logic yourself, probably by writing a macro and accepting that some cases couldn't be handled by it.