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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With