Is it possible in Scala 3 to do pattern matching by generic type?

Viewed 398

I'm trying to migrate my mind from Java background (and a bit of Scala 2) to Scala 3 and to implement a kind of version of PartialFunction.orElse but with union type as an input parameter instead of intersection type:

trait F[A, B] extends (A => B) {
  
  def orElse[A1, B1](other: F[A1, B1]): F[A | A1, B | B1] = {
    val self = this
    
    new F[A | A1, B | B1] {
      override def apply(v: A | A1): B | B1 = {
        helper(v)
      }
    
      transparent inline def helper(v: A | A1): Any = {
        inline v match {
          case _: A => self.apply(v.asInstanceOf[A])
          case _: A1 => other.apply(v.asInstanceOf[A1])
        }
      }
    }
  }
}

But I ended up with a fact that both calls go to the first branch of "match":

 val f1 = new F[Int, String] {
    override def apply(v: Int): String = s"Int => String : $v"
  }

  val f2 = new F[String, Int] {
    override def apply(v: String): Int = v.length
  }

  val f = f1.orElse(f2)

println(f(42)) // prints "Int => String : 42"
println(f("hello")) // fails in runtime because of trying casting String to Int

So, my question is, is it even possible? If yes, what am I missing?

1 Answers

The helper may be inlined, but the entire trait isn't, and neither is orElse, so A and A1 are still erased. You should have gotten a warning from the compiler saying that A and A1 cannot be checked at runtime. To do that, you need to have a Typeable[A] and a Typeable[A1] in scope (TypeTest[A | A1, A] and a TypeTest[A | A1, A1] should've worked too, since they're more specific, but for whatever reason, the compiler doesn't use them).

I decided to go with extension methods instead of a trait because it's just simpler that way. This way, you can use it for normal functions too, as long as they have a Typeable instance in scope (I believe instances for Int, String, and other primitives/classes without type parameters or members are synthesized automatically). (Scastie)

import scala.reflect.Typeable

extension [A1: Typeable, B1](f1: A1 => B1)
  def orElse[A2: Typeable, B2](f2: A2 => B2): (A1 | A2) => (B1 | B2) =
    case v1: A1 => f1(v1)
    case v2: A2 => f2(v2)

val f1 = (v: Int) => s"Int => String : $v"
val f2 = (v: String) => v.length
val f = f1.orElse(f2)

However, explicitly checking types feels like an antipattern to me, so it would help if you could give more details about your bigger problem.

Related