Synthesize constructor for opaque types

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 Anything, 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:

  1. Disallow opaque types in typed patterns (like the one above).
  2. Restrict Any so that only subtypes of a new trait Matchable 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.

1 Like