-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add opaque types #4028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add opaque types #4028
Conversation
Still TODO: Make implicit scope include companion objects of opaque types. [EDIT: done] |
Overall the implementation adds just 50 lines of new code (partly because the scheme to find companions has gotten simpler). Anyway, that's a lot cheaper than value classes! EDIT: As of latest count it's more like 170 lines added. Still, not terribly bad. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
I think this one is best reviewed commit by commit. |
If somemone has more tests it would be great to add them. I only picked those from the SIP and added some small tests that test various invalid code. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4028/ to see the changes. Benchmarks is based on merging with master (8d07271) |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4028/ to see the changes. Benchmarks is based on merging with master (8d07271) |
I’ll have a look at this on Tuesday, it’s great to see progress on this area. |
Just gave this a try: opaque type Fix[F[_]] = F[Fix[F]]
Any plans to support recursive opaque types? |
No. That would demand a different approach. In that case the only sane way is to demand explicit conversions between the left and right hand sides of the type. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4028/ to see the changes. Benchmarks is based on merging with master (02725c6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I left some minor comments, but the implementation looks great and simple 👍
Could you elaborate on why recursive opaque types are not supported in this implementation and how that affects existing language semantics? Our goal would be to support this in the scalac implementation.
@@ -299,6 +300,7 @@ object TastyFormat { | |||
final val DEFAULTparameterized = 30 | |||
final val STABLE = 31 | |||
final val MACRO = 32 | |||
final val OPAQUE = 33 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this deserve a version bump in the tasty format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, it should be a minor version bump.
@@ -212,8 +212,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder | |||
|
|||
// Synthetic methods that are always present do not affect the API | |||
// and can therefore be ignored. | |||
def alwaysPresent(s: Symbol) = | |||
s.isCompanionMethod || (csym.is(ModuleClass) && s.isConstructor) | |||
def alwaysPresent(s: Symbol) = csym.is(ModuleClass) && s.isConstructor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't declarations in the API extractor include companion methods? What invariant has opaque types introduced to make the previous code obsolete?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Companion methods don't exist anymore. (They were the synthetic methods that pointed to the companion class or object to the other. We now use an explicit field in ClassDenotation
for this).
mcCompanion.enteredAfter(thisPhase) | ||
classCompanion.enteredAfter(thisPhase) | ||
val modcls = modul.moduleClass | ||
modcls.registerCompanion(forClass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should modcls
be installed after this phase too?
Recursion is one of the toughest things to handle, generally. For instance, F-bounds seem useful, but have turned out the single biggest snake-pit for Scala's implementation. I can't count the number of times I wished Scala did not have them! In DOT we do have recursion via |
@jvican To elaborate from the theoretical point of view: a recursive opaque type The only hope is that you want to typecheck programs before the equality is visible, but type-directed algorithms where the equality is visible would still risk needing to deal with recursion somehow. |
Good arguments, thanks for chiming in the discussion. |
157c5fa
to
5de563b
Compare
I integrated the remaining test cases from SIP 35, except for the fixpoint one, which is not supported. The tests brought up some problems with how we treat GADT bounds, which are now addressed. Also, some of the tests did not compile at first. @jvican, it would be good to backport the fixed versions here to the SIP. |
Opaque types have very tricky interactions with the compiler's sophisticated caching strategy. The danger is that some cached information leaks between companion objects of opaque types and the outside world. 41791f7 tries to improve the situation. We also have to keep in mind that the same problems arise for all GADT constrained type variables, not just for opaque types. That means changing the opaque type spec, so that instead of GADT equalities we define (say) conversions between an opaque type and its alias gains us nothing, and in fact reduces test coverage. |
Reverting to wip status until #5254 is resolved. |
If we can't rely on GADTs to represent the equality between opaque type and its right hand side in some contexts, but not in others, we need a fallback scheme. The most straightforward one would be to make conversions available. I.e the compiler could provide in the companion object of an opaque type a pair of private conversion methods private def reveal(x: Logarithm): Double
private def inject(x: Double): Logarithm This is still too cumbersome, as one could not easily map a A more convenient solution treats
That would be straighforward to use and straightforward to implement. The only downside is that it is still a bit verbose. |
You could have an |
Can you explain that in more detail? I am not sure how to use |
so, in your example, you have: opaque type ImmutableArray[+A] = Array[A]
object ImmutableArray {
def sum[A: Numeric](a: ImmutableArray[A]): A = ...
}
opaque type Log = Double
object Log {
private implicit val reveal: Log =:= Double = ... // compiler generated, but could also be a cast of `refl`
// a product in the original space, is sum in log space, so this is a key operation
def sum(l: ImmutableArray[Log]): Log = {
val p = ImmutableArray.sum(reveal.substituteCo(l))
reveal.flip(p)
}
} |
PS: immutable array is one I'm really excited about. We can have an |
@johnynek Ah I see what you mean now. That works to some degree but I also think it can be a pain to use. I agree that immutable array is one the most exciting parts of this effort. |
@odersky probably worth mentioning that there is a less painful alternative to the substitution methods of @Blaisorblade, @sstucki, and I discussed it in a previous issue: #3844 (comment) It already works in Dotty (last I tried), and solves the problem neatly. The basic idea is to have: abstract class <:< [-A,+B] { type Ev >: A <: B } Then, if you have a def foo[S,T](xs: List[S])(implicit ev: S <:< T): List[T] = (xs: List[ev.Ev]) The same scheme can probably be used for opaque type. What'd be interesting is to see whether Dotty could leverage these evidence values automatically when they are in scope, to avoid the need for explicit ascriptions to some extent (though the general problem seems undecidable, as was discussed in a Scala’17 paper). PS: the more general/potent definition for abstract class <:<[A, B] {
type ConstrainedB >: A <: A & B // = B with constraints
type ConstrainedA >: A | B <: B // = A with constraints
} |
PPS: the above is for covariant and contravariant substitution. For substitution in invariant places, one just needs Here is a complete example (Scastie) of an opaque type implemented using my strategy: abstract class OpaqueAPI {
type S // abstract type
protected implicit val ev: S =:= String // not visible from outside
def in (xs: Set[String]): Set[S] = xs:Set[ev.Ev]
def out(xs: Set[S]): Set[String] = xs:Set[ev.Ev]
val arr0: Array[String] = Array("a")
val arr1: Array[S] = Main.foo(arr0)
}
object Main {
def foo[A,B](arr: Array[A])(implicit sub: A =:= B): Array[B] = arr:Array[sub.Ev]
val Opaque: OpaqueAPI = new OpaqueAPI {
type S = String // not visible from outside
val ev: S =:= String = implicitly
}
import Opaque._
def main(args: Array[String]): Unit = {
val s0 = Set("a")
val s1: Set[S] = in(s0)
println(s1)
val s2: Set[String] = out(s1)
println(s2)
println(s"Done.")
}
} ...where we have: abstract class <:< [-A,+B] { type Ev >: A <: B }
object <:< {
implicit def ev[A<:B,B]: A <:< B = new { type Ev = A }
}
abstract class =:= [A,B] { type Ev >: A | B <: A & B }
object =:= {
implicit def ev[A>:B<:B,B]: A =:= B = new { type Ev = A }
implicit def flip[A,B](implicit ev: A =:= B): B =:= A = new { type Ev = ev.Ev }
} |
Open opaque types as gadts in opaque companion modules.
In the new implementation, a companion object of an opaque type opaque type T = A only knows that T <: A and that A <: T. By itself that does not propagate some informations from `A` to `T`. For instance the members of A are now not the members of T.
We found a much better way to get opaque types without restrictions and without too many contortions in the compiler: #5300 |
Implements SIP 35