Combine Match-Types and Either - "A match type could not be fully reduced"

I’m using Scala 3.3.0 and I want to capture that the type of one field depends on the value of another field, which seems to be possible using match types.

type B[X <: Boolean] = X match {
  case true => Option[String]
  case false => String
}
class C(val b: Boolean, val c: B[b.type])

As expected, when instantiating C, if b is true, then c must be an Option[String] and otherwise just a String.

val c1 = C(true, None)
val c2 = C(true, Some("Foo"))
val c3 = C(false, "Bar")
// val c4 = C(false, Some("Foo"))       Does not compile as expected
// val c5 = C(true, "Foo")				Does not compile as expected

On top of that I implemented a function that, given a Boolean and an Option[String], returns a C like follows:

def bWitness(b: Boolean, s: Option[String]): B[b.type] = b match {
  case _: true => s
  case _: false => s.getOrElse("")
}
def buildC(myBool: Boolean, maybeString: Option[String]): C = C(myBool, bWitness(myBool, maybeString))

Now, in case that b is false and s is None, I want to return an error and not just the empty string, so I would like to wrap everything in an Either

case class Error(msg: String)
def bWitnessEither(b: Boolean, s: Option[String]): Either[Error, B[b.type]] = b match {
  case _: true => Right(s)
  case _: false => s.map(Right(_)).getOrElse(Left(Error("Not value present.")))
}

Here, however, the compiler complains that it could not fully reduce the match type. Can anybody explain to me, what the reason for this is? What do I need to change in order for the code to compile? Is it possible at all?

Surprisingly (at least for me), if I just return Left the code compiles, or for the following implementation, the compiler only complains about the first case. What is the explanation?

def bWitnessEither2(b: Boolean, s: Option[String]): Either[Error, B[b.type]] = b match {
  case _: true => Right(s)						// The compiler seems to only complain in this case
  case _: false => Left(Error("foo")) 
}

You can find the code here: Scastie - An interactive playground for Scala.

The problem here is that this dependent typing only works when the result type is a match type, so wrapping it in Either breaks the special case. Instead define a new match type:

case class Error(msg: String)
type EitherB[X <: Boolean] = X match {
  case true => Either[Error, Option[String]]
  case false => Either[Error, String]
}
def bWitnessEither(b: Boolean, s: Option[String]): EitherB[b.type] = b match {
  case _: true => Right(s)
  case _: false => s.map(Right(_)).getOrElse(Left(Error("Not value present.")))
}

… (note the use of the match type as the return type):
This special mode of typing for match expressions is only used when the following conditions are met:

  1. The match expression patterns do not have guards
  2. The match expression scrutinee’s type is a subtype of the match type scrutinee’s type
  3. The match expression and the match type have the same number of cases
  4. The match expression patterns are all Typed Patterns, and these types are =:= to their corresponding type patterns in the match type
4 Likes