-
Notifications
You must be signed in to change notification settings - Fork 37
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
Consider allowing promotion and singling of all rank-1 types #542
Comments
I'd like to hear more about your thoughts on promotion and defunctionalization symbols. It doesn't surprise me that singling is possible. But I worry about promotion. In particular, what's our current stance on promoting/singling code with type applications? If we support that, then we're pretty constrained in how to proceed. But if we don't support that today, then we have more freedom. Of the two singling approaches you propose, I prefer the second. This would be incompatible with current usage if someone uses visible type application to (redundantly) choose singleton arguments. But singletons hasn't, I think, stressed backward compatibility, so I'm not terribly bothered by this. Regardless of the approach, there is a non-trivial amount of work here. (It's not all that much, but more than an hour!) Is there good motivation for doing this? (Just seeing how far |
Our current stance is that As that section of the
Indeed, I also prefer the second approach for the same reasons that you list.
"Good" is subjective, but I am very keen to add support for visible dependent quantification in terms to Additionally, I am definitely curious to see how far |
All sounds good to me. Fire when ready. |
I've been working on this issue for a while, and in the process of doing so, I've gained a better understanding of the code that governs how type variable binders are singled. This new insight has led to a number of simplifications to the code (see #545, #547, #551, and #550, as well as the respective fixes in #548, #549, #552, and #553). In this regard, I consider this issue to have been quite lucrative. Unfortunately, I think I will have to admit defeat on the end goal of actually implementing the plan described in #542 (comment) — at least for now. The problem is that there is no way to uniformly implement the plan in all forms of type signatures. There are two places where the plan falls apart:
For now, I think I'm simply going to park this. My WIP implementation can be found in the |
Ugh. Nice write up. I agree with your conclusion. |
Currently,
singletons-th
adopts a hard-line stance regarding quantifiers in type signatures. Namely, they must all appear at the top level in this order:With the additional stipulations that:
forall
s are permitted. A type likeforall a -> a
(with a visibleforall
) is not allowed.forall
s and contexts are not permitted inctx
, thearg
s, theres
, or the kinds of thetvb
s.In
singletons
parlance, this sort of type is referred to as a vanilla type. Vanilla types are a subset of rank-1 types, where quantifiers are allowed to appear contravariantly at a depth of at most 1.¹singletons-th
does not support all rank-1 types, as it will currently reject this one:In #401, I originally came to the conclusion that we should not support anything more sophisticated than vanilla types. Recently, however, I'm reconsidering that stance. One thing that is making me reconsider my stance is the advent of visible
forall
s in types, where you can write things like this:This is an exciting step towards full dependent types. Moreover, it would be interesting to support this feature in
singletons
. On the surface, it appears simple to support. Here is roughly how the example above would look when promoted:And singled:
Note that when promoted
(type a)
simply becomesa
, and(type a)
isn't changed at all when singled. The most interesting difference when singled is the need to have a separate, invisibleforall (x :: a).
after the visibleforall a ->
. (More on this in a bit.)As cool as this idea is, it is not possible within
singletons-th
's current restriction that all types must be vanilla. What if we relaxed this restriction to allow all rank-1 types, however? Although I decided against this in #401, all of the problematic examples that I came up with are actually rank-2 types, not rank-1. As it turns out, each of the issues that I identified in #401 are manageable when the scope is limited to rank-1 types:Defunctionalization symbols
You definitely can't make defunctionalization symbols for rank-2 or higher of types, as you would need to define
Apply
instances with polytypes as arguments. This just isn't possible in GHC, even with the advent of Quick Look impredicativity.With rank-1 types, we can avoid this problem by applying the workaround described in Wrinkle 2: Non-vanilla kinds in
Note [Defunctionalization game plan]
. The trick is that we can still generate defunctionalization symbols for things with non-vanilla types, but we may have to make some sacrifices:forall
s, the defunctionalization symbols would put all of theforall
s up front to avoid the impredicativity issues described above.forall
s, the defunctionalization symbols would turn something likeforall a -> a
intoforall a. Type ~> a
.For many use cases, these sacrifices are acceptable, as you can still profitably use the defunctionalization symbols even with the slightly different treatment of
forall
s.In singled definitions
Singling rank-2 or higher definitions would be tricky, as the examples in #401 demonstrate. Singling rank-1 definitions, on the other hand, would be much more tractable. In today's
singletons-th
, the algorithm works roughly like so: given a (vanilla) type signature of the form:The singled type signature would become (roughly):
Where
F
andsF
are the promoted and singled versions off
, respectively. Note that we introduce additionals
type variables to instantiateSing
with. For lack of a better name, I'll call these "Sing
type variables".To handle all rank-1 types, this algorithm would need to be generalized slightly to be able to handle nested
forall
s and visibleforall
s. Here are two possible algorithms for making this work. (Feel free to skip reading the algorithm descriptions if you don't care about the ugly details.)Algorithm 1: The nearest telescope algorithm
Sing
type variable in an invisibleforall
telescope directly after the nearestforall
telescope (starting from the::
) in which all of the variables in theSing
type variable's kind have been bound by preceding telescopes.Sing
type variable telescope appears directly after another invisibleforall
telescope, combine the two telescopes into one.Here are some examples of how this would work:
A vanilla type signature
We need to quantify two
Sing
type variables, one for each of the arguments preceding a function arrow. The kinds of theseSing
type variables area
andb
, and the nearestforall
telescope that we could pick after whicha
andb
would be bound isforall a b.
. As a result, we will quantify theSing
type variables directly after this telescope:Then, we would combine the
Sing
type variable telescope into the preceding one, which also uses invisibleforall
s:This is exactly the same type signature that
sConst
would receive in today'ssingletons-th
.An example with nested, invisible
forall
sThis time, we need to quantify four
Sing
type variables. The first and thirdSing
type variables have kinda
, and the nearestforall
telescope that we could pick after whicha
would be bound isforall a.
. The kind of the secondSing
type variable isb
, and the kind of the fourthSing
type variable is(a, b)
. Because both of these kinds mentionb
, the nearestforall
telescope that we could pick after whichb
would be bound isforall b.
These constraints give rise to the following type signature:Finally, we combine adjacent invisible
forall
telescopes to get:An example with a visible
forall
The type of
slurmpVis
is identical toslurmp
except that theb
is quantified visibly. As a result, theforall b ->
andforall (s2 :: b) (s4 :: (a, b)).
telescopes would not be combined, resulting in a final singled type signature of:Algorithm 2: The nearest function argument algorithm
Algorithm 1 (the nearest telescope algorithm) is designed to maintain backwards compatibility with
singletons-th
's existing treatment offorall
s in singled type signatures. However, it is not obvious that Algorithm 1 produces the nicest type signatures for things with nestedforall
s. For instance, in the type ofsSlurmp
above, we quantifieds3
befores2
, which feels somewhat awkward.Algorithm 2 (The nearest function argument algorithm) is an attempt to address this awkwardness. It only has one rule:
Sing
type variables_i
directly before the function argumentSing s_i
.That's it. There's no mention of telescopes in this algorithm, which makes it conceptually simpler. On the other hand, it does not preserve the existing behavior of how
singletons-th
quantifies type variables.Here are the same examples from above, but using Algorithm 2:
A vanilla type signature
would be singled to:
An example with nested, invisible
forall
swould be singled to:
An example with a visible
forall
would be singled to:
Either algorithm should suffice, but we will have to think carefully about the tradeoffs involved.
tl;dr It is quite possible to promote and single all rank-1 type signatures, even those with visible
forall
s. The downside is that we may have to sacrifice the fidelity of the defunctionalization symbols involved a little. I think this is an acceptable price to pay, but others may feel differently. What are your thoughts?¹For a more formal definition of what a rank-1 type is, see Section 3.1 of Practical type inference for arbitrary-rank types.
The text was updated successfully, but these errors were encountered: