Yes, that’s right. I’ll try to summarize:
EDIT: To clarify, the solution makes it safe to emulate Haskell’s newtype
using opaque type
, but only if no upper type bound is exposed, like in Haskell which has no subtype polymorphism at all, only parametric polymorphism.
Summary
Problem
The semantics of opaque type
is not the same as newtype
in Haskell. opaque type
is actually much more general. The main reason opaque type
doesn’t do encapsulation in the same way newtype
do is that since Scala allows you to pattern match on litterally Any
thing, you can expose the underlying type representation (intentionally or by mistake) using pattern matching.
Quoted example (minimized)
For this reason, users using the pattern of defining a constructor/unapply
method will experience some gotcha moments when opaque types don’t behave like case classes would have.*
Solution
This is solved in two steps:
- Disallow opaque types in typed patterns (like the one above).
- Restrict
Any
so that only subtypes of a new traitMatchable
can be used as the scrutinee in pattern matching.
(1) prevents conversions like String -> Name
by warning on patterns like:
(str: String) match {case n: Name => n}
Because Name
can’t appear in a typed pattern in the case
clause anymore.
(2) prevents conversions like Name -> String
by warning on patterns like:
(n: Name) match {case str: String => str}
Because Name
isn’t a subtype of Matchable
so it can’t be pattern matched on at all.
Demo: (minimized)
scala> object n :
| opaque type Name = String
| object Name :
| def apply(str: String): Name = str
| def unapply(n: Name): Some[String] = Some(n)
// defined object n
scala> import n._
scala> "hi there" match {case n: Name => n}
1 |"hi there" match {case n: Name => n}
| ^^^^^^^
| the type test for n.Name cannot be checked at runtime
val res0: String & n.Name = hi there
scala> Name("Franz Kafka") match {case str: String => str}
1 |Name("Franz Kafka") match {case str: String => str}
| ^^^^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type n.Name instead
val res1: n.Name & String = Franz Kafka
See this for more:
Note that this isn’t in master yet, and warnings won’t be turned on in 3.0.
*This is also why case classes should still be encouraged and preferred when their semantics is needed! Some kind of sugar like case opaque type
could maybe be added in the future when the semantics of opaque type
are better understood in practice and the feature has matured more. Preferably these unsafe patterns should be compiler errors rather than just warnings before such sugar is added.