Better implicit resolution in pattern matches

I put up a PR for this feature request. I’m not sure if this qualifies as a feature request or a bug fix, but in any case it’s probably a big enough change to merit discussion. I’m happy to do it on the PR itself, but @smarter recommended that I start discussion here since it’s been quiet.

To give a little background: my team is trying to implement something like trait objects in Rust: boxes that wrap an instance of some typeclass T. We would like to be able to unbox using pattern matching:

trait Typeclass[Self]
object Typeclass {
    class Box private (t: Any)

    object Box {
        def apply[T: Typeclass](t: T) = new Type(t)
        def unapply[T: Typeclass](t: Type): Option[T] = t.t.asInstanceOf[T]
    }
  }

case object Impl1
given Typeclass[Impl1]

case class Impl2(a: Int, b: String)
given Typeclass[Impl2]

(myBox: Typeclass.Box)  match {
  case Typeclass.Box(impl1: Impl1) => ...
  case Typeclass.Box(Impl2(a, b)) => ...
}

We can implement Box by wrapping an Any, but then the compiler won’t be smart enough to figure out that case Typeclass.Box(_: String) shouldn’t compile. This is arguably a niche use case, but I have always mentally modeled pattern matches as sugar for a series of calls to unapply, so it doesn’t seem like a new language feature to expect that x match { case Foo(y: Bar) => ...} would act similarly to Foo.unapply(x).map { (y: Bar) => ...}.

What’s Typeclass.Box in your example?

Sorry, it was in the linked feature request (as .Type) but I should have copied it over. I’ve updated the original post.

Thanks for the clarification.

I don’t understand what you need the Box for. Why not just use Typeclass[?] objects and pattern-match on them directly?

The asInstanceOf call in unapply is very icky and unsafe.

The asInstanceOf call is wrong (sorry) but I can’t edit the post – it should be Some(t.t.asInstanceOf[T]) but if you want to feel more safe you could add a def self(Typeclass.Box): Self to Typeclass if you like.

I’m not sure what you mean by “use Typeclass[?]” objects? Do you mean have Impl1 and Impl2 extend Typeclass? If so, that would not be using a typeclass pattern, which is the precise point of the exercise? In particular, that would mean you couldn’t wrap up a particular Impl in a Typeclass object unless you were the one who wrote Impl.

But in any case, the point of the PR is not to enable this specific case, just to more generally notice that there is no good reason that I can think of to prevent type inference from using bottom-up information in a pattern match. This is just one motivating example. Would it help if I came up with another?

Yes that’d be a better idea. The new asInstanceOf is still wrong, and an obvious anti-pattern. What if someone calls Typeclass.Box.unapply(Typeclass.Box(Impl1))(new Typeclass[Nothing]).get or similar?

Sorry, I read the example too fast. Looking again, what you’re doing does not make much sense to me. Type class instances are not global, unique properties of types, unlike in Rust. I suspect that once you encode things properly at the type-level, probably using GADT-style programming, you won’t encounter the problem anymore.

Probably yes. If the only given example is kind of bogus, there’s no reason to even consider the feature.

Type class instances are not global, unique properties of types, unlike in Rust. I suspect that once you encode things properly at the type-level, probably using GADT-style programming, you won’t encounter the problem anymore.

I don’t understand. GADT-style programming is simply different from typeclass programming – GADTs don’t address the expression problem. I understand that Rust/Haskell/Swift help enforce the singleton property for typeclass instances because typeclasses are built into the language and Scala does not, but Scala very explicitly advertises that typeclasses can be simulated using implicits and traits. The abstraction is imperfect, as you have noted, and an adversary can cause headaches. That does not make the example “bogus.”

As another example: suppose I wanted to be able match on object and it’s string form simultaneously:

  case WithRendering(Foo("bar", _), "a foo constructed with bar and baz") => ...
}

I could write

object WithRendering {
  def unapply[T: Show](matchee: Any): Some[(T, String)] = Some((matchee.asInstanceOf[T], summon[Show[T]].show(matchee.asInstanceOf[T])))
}

trait Show[Self] {
  def show(self: Self): String
}
                                                             
case class Foo(x: String, y: String)
given Show[Foo] with {
  def show(self: Foo): String = s"Foo constructed with ${self.x} and ${self.y}"
}

case class Bar(x: Int, y: String)
given Show[Bar] with {
  def show(self: Bar): String = s"Bar constructed with ${self.x} and ${self.y}"
}

(new Foo("bar", "baz"): Any) match {
  case WithRendering(Foo("bar", _), "Foo constructed with bar and baz") => println("did it")
  case _ => println("failed")
}

But this fails with an ambiguous implicit. I can double check, but I’m pretty sure the PR I put up will make it work.

Yes, I know what they are. But you can use them together. Type class instances can also benefit from GADT reasoning when you pattern-match on them, for example. I don’t know if that would apply to your use cases, I was just throwing the idea out there, in case you wanted to look for a more “typeful” solution.

That’s not what I noted – I don’t think Scala’s support for type classes is imperfect. It’s just a different design philosophy, with different tradeoffs. Having first-class instances enables many patterns that are simply impossible to express directly (or at all) in Haskell and Rust. This is by design.

Still a bad example. Still an unsafe use of an unchecked cast:

WithRendering.unapply[Foo](1) // java.lang.ClassCastException: java.lang.Integer cannot be cast to Foo

I think uses of asInstanceOf are a code smell unless you’re in well-encapsulated, performance-sensitive portions of your code, where you know they’ll never fail. So any example that relies on them is bogus in my book, especially if it’s meant to justify a new type system feature.

Yes, I know what they are. But you can use them together. Type class instances can also benefit from GADT reasoning when you pattern-match on them, for example. I don’t know if that would apply to your use cases, I was just throwing the idea out there, in case you wanted to look for a more “typeful” solution.

If there is a way of writing a case match where each case must correspond to an instance of a typeclass, I’m happy to hear it! Any way using GADTs that I understand does not solve the expression problem.

Still a bad example. Still an unsafe use of an unchecked cast:

Yes, erasure is very annoying. Obviously it would be nice to check that the matchee really is a T (even though in doesn’t matter for case matches because that will be guaranteed at runtime – it only matters if your friendly neighborhood adversary wants to call unapply directly). You can of course ask for a T: ClassTag, but that has its own headaches.

For completeness, here is another example, where we want to match on an arbitrary tuple if the two elements are equal according to a custom function. This does not work if there are two given instances, but does the right thing if there is only 1 (because there is no ambiguous implicit). The PR would address this case I believe.

import scala.reflect.ClassTag

trait MyEq[Self: ClassTag] {
  def compare(a: Self, b: Self): Boolean
}

object MyEq {
  def unapply[T: MyEq: ClassTag](x: Tuple2[_, _]): Option[(T, T)] = x match {
    case (x: T, y: T) if summon[MyEq[T]].compare(x, y) => Some((x, y))
    case _ => None
  }
}

 given MyEq[Int] with {
   def compare(a: Int, b: Int): Boolean = a == b
 }

given MyEq[String] with {
  def compare(a: String, b: String): Boolean = a.toLowerCase ==  b.toLowerCase
}

(("A", "a"): Tuple2[_, _]) match {
  case MyEq(a: String, b) => println("equal")
  case _ => println("failed")
} // prints equal
(("A", 1): Tuple2[_, _]) match {
  case MyEq(a: String, b) => println("equal")
  case _ => println("failed")
} // prints failed

println(MyEq.unapply[String]((1, 1))) // prints None

For posterity: here is a discussion of how the analog is possible (though tricky) in Haskell Pattern matching against typeclass instances in Haskell function - Stack Overflow.