Some thoughts about scoped conformance #1528
kyouko-taiga
started this conversation in
Language design
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
@EugeneFlesselle and I have been discussing a lot about scoped conformance and their implication on soundness lately. I thought it would be good to report some of the observations we have made so far.
Coherence
Systems supporting type classes sometimes also require coherence, which is essentially prescribes that there can be at most one instance (aka model) of a given type class (aka trait/protocol) for a given type. For example, the following program is illegal in Swift:
The compiler will complain because this program defines multiple overlapping models of
Monoid
forInt
. (Note: the actual compiler diagnostic talks about redundant conformance, but the underlying problem is relates to coherence nonetheless.)Coherence is convenient because it means that there can never be any ambiguity when one needs to select a model to satisfy the requirement of some generic function. The above program can be written in a system without coherence but then using the conformance of
Int
toMonoid
would require user intervention. For example, in Scala:Swift upholds coherence by requiring that models be defined be at the top-level. That is not sufficient, though, because two different modules
A
andB
may each define a model ofT
forP
that are separately unique but will overlap in a module that imports bothA
andB
. Rust addresses this problem with its infamous "orphan rules".In Hylo, we said that models can be defined in any scope as long as they do not overlap in that scope. In other words, we made a compromise between the restricted world of Swift and the relaxed world of Scala. While we cannot declare two instances of the
Monoid
concept at the top-level, we can use different scopes:The reason why Hylo accepts the program (possible bugs in the current implementation aside) is because the second model of
Int
forMonoid
shadows the one declared at the top-level. So while we can define multiple models, just like in Scala, we can still enjoy a form of local coherence to avoid ambiguities.This idea looks simple on the surface but things get a little thorny when we examine some edge cases.
Hidden ambiguities
There exists a dangerous combination of features that we can use to create tricky programs in Hylo, involving equality constraints and quantification. For example, consider the following program:
It should be fine to define the two models in the body of
outer
, since the type system has no reason to believe thatT
andU
are the equivalent. But theninner
constrainT
to be equal toU
and the two models become viable candidates to calla.f
. Therefore we should actually reject this program. An interesting question asks what is the exact cause of the error.Note that this problem relates to Rust's overlap rule. Rust won't let us write the following program:
In both Hylo's and Rust's cases, the two models may overlap if later we end up in a situation where
T
andU
can be unifined. In Hylo, we ended up in such a situation by requiringT
andU
to be equal. In Rust, we created a situation where bothT
andU
could be instantiated asi64
.Rust claims that the error is due to the model declaration and will refuse to compile the program even if we comment out the last two lines, that is even if we don't provide a concreate instance of ambiguity. Using the same approach in Hylo would mean rejecting
outer
even if we comment out its last two lines. There are other solutions, though:T
andU
in the signature ofouter
.inner
.a.f
, i.e., where the ambiguity actually causes a problem.I am opposed to require user-defined inequality constraints but the two other solutions sounds acceptable to me. @EugeneFlesselle has pointed out that going with the 3rd option gives us a consistent way to deal with another ambiguous situation:
If we specialize
outer
withT = Int
andU = Int
, then the two models are again equally viable to callt.f
. However, it may be reasonable to accept this program nonetheless because under separate checking, we'll have already compiledouter
so that it unconditionally selects the first model. This approach suggests that a less conservative strategy may be reasonable.Conformance escape
Consider the following program:
This program should be illegal because it must expose the local model to escape so that caller of
make
can get a value that conforms toP
. Note that the problem would not occur if the return type ofmake
wasany P
. In that case, the model would have been properly wrapped into an existential at runtime and all would be fine.There are more intricate cases of conformance escape:
In its current form, this program should probably be illegal because the return value of
outer
is implicitly capturing a local model. Capturing a local model is not necessarily harmful. Here we can still separately compile the lambda that is returned using that model and keep that detail hidden from callers ofouter
. However, the situation will get more complicated if we allow local models to capture local values:Here, the model captures
x
and therefore we cannot let the lambda escape as it would violatex
's lifetime.There are two ways we can address this problem:
P
forT
.Semantic relevance of substitution
This problem is not consequence of scoped conformance but it somehow relates to coherence nonetheless. Consider the following program:
It may be reasonable to expect that inference be able to figure out the substitution of
C
in the call totruc
inmachin
's body. That's because the type ofe
isT.Element
and so there's a relatively obvious way to deduce thatC = T
. However, the same isn't necessarily true inbidule
.If
T.Element = U.Element
, then inference should in principle be free to substitute one for the other anywhere. In fact, that's necessary to calltruc
because we need to substituteC
by eitherT.Element
orU.Element
. Either way, the type checker will then have to confirm that we aT.Element
to a parameter of vice versa.The problem is that even if
T.Element
andU.Element
are equivalent, nothing says thatT
andU
are too and so they could have different conformances toCollection
. Consequently, whether inference picksT
orU
may have an impact on the semantics oftruc
because that function may be using properties of the conformance model it obtained.Beta Was this translation helpful? Give feedback.
All reactions