Embrace Type :: Type¶
GHC 8.0 included a major change to GHC’s type system: the Type :: Type axiom.
Though casual users were protected from this by hiding its features behind the
-XTypeInType extension, all programs written in GHC 8+ have the axiom behind
the scenes. In order to preserve backward compatibility, various legacy features
were left unchanged. For example, with -XDataKinds but not -XTypeInType,
GADTs could not be used in types.
This proposal suggests to remove the backward-compatibility features, embracing
Type :: Type. Specifically:
Incorporate the features currently in
-XTypeInTypeinto the-XPolyKindsand-XDataKindsextensions.Deprecate the
-XTypeInTypeextension (as it would be a synonym for-XPolyKinds -XDataKinds).Introduce a new extension
-XStarIsTypeto control how to parse*in code and whether to print it in error messages.
Motivation¶
This is a simplification over the status quo, with two closely related extensions (
-XPolyKindsand-XTypeInType) and an arbitrary, historical distinction between them.GHC’s ability to parse
*has a significant cost. It’s the only symbolic identifier that’s handled like an alphanumeric one. (Note thatT * Intnormally looks like an application of a binary operator*toTandInt, but with the kind*, it’sTapplied to*andInt.) Because*is sometimes indeed a binary operator in types (seeGHC.TypeLits), we can disambiguate the infix from the prefix case only in the renamer. This means that the parser has to treat a type-level expressionA B * C * Eessentially as a list of types, only to be rejigged by the renamer. (This rejigging is independent of the fixity-rejigging the renamer also has to do, so there’s no shared cost here.) GHC also has to handle both*and its unicode variant identically, adding to this cost.The new approach to handling
*makes it obvious in the parser whether*is infix or not, vastly simplifying matters. Also, by using an extension to specify the handling of*, it is straightforward also to have the same extension control output in error messages.If we plan to remove
*from the language at some point, we should start updating error messages sooner than later.In truth, GHC always has
Type :: Type, whether you say-XTypeInTypeor no. Thus, the real extension name should be-XPolyKinds, because it’s kind polymorphism that the user wants, not the always-trueType :: Type.The reason for a distinction between the extensions was because
-XTypeInTypestarted out as rather buggy and experimental, whereas-XPolyKindsand-XDataKindshad settled down by GHC 8. There was the possibility that-XTypeInTypewould allow you to shoot the gorillas (my suggestion for an update of “launch the rockets”; the latter seems just a bit too poignant these days) while-XPolyKinds -XDataKindswouldn’t. That possibility has not come to fruition (happily), and so the distinction isn’t really paying its way. Note that what we’re doing here is very much like the merger between-XRankNTypesand-XRank2Types.The one difference between
-XPolyKindsand-XTypeInTypethat’s worth preserving is that the former allows easy access to the kind*. The-XStarIsTypeextension is meant to preserve this difference.
Proposed Change Specification¶
Make
-XPolyKindsincoporate the new features (other than expanded promotion of types) of-XTypeInType. By scanning through GHC’s source code, I was able to find the places where GHC currently distinguishes between these extensions (labeled for easy reference):The meaning of CUSK is slightly different between
-XPolyKindsand-XTypeInType. See the second bullet of the CUSK section of the manual. The-XTypeInTypebehavior would be retained. Migrating from the-XPolyKindsbehavior simply requires adding an explicitforall kin some cases.Type and kind vars can be freely mixed in
-XTypeInTypecode. This change is fully backward compatible; no migration would be necessary.Various constructs can appear in kinds with
-XTypeInType, but not without. These include promoted lists,forall, among others. This change is fully backward compatible; no migration would be necessary.
With
-XPolyKinds, kind variables are assumed to have kindBOX(which has becomeType).-XTypeInType, on the other hand, makes no assumption about the kind of a kind variable. This is a generalization over current behavior, which can potentially lead to trouble. I am unable to come up with an example, though.Kind-indexed GADTs are allowed with
-XTypeInType. These would now be allowed with-XGADTs -XPolyKindsonly. This change is fully backward compatible; no migration would be necessary.
-XDataKindswould now promote GADTs and GADT constructors. This change is fully backward compatible; no migration would be necessary.Two releases after this proposal is implemented, deprecate
-XTypeInType.Introduce a new language extension
-XStarIsType, with the following behavior:-XStarIsTypeis on by default.When
-XStarIsTypeis on, any occurrence of the symbol*in a type is treated as the kind of types with values. It is parsed similarly to alphanumeric identifiers, never as a binary operator.When
-XStarIsTypeis on, a user can use a binary operator*only with a qualifying module name. For example,8 ~ (4 GHC.TypeLits.* 2), or8 ~ (4 L.* 2)if we haveimport GHC.TypeLits as L.When
-XStarIsTypeis not on, the pretty-printer will printTypeinstead of*in error messages.Without
-XStarIsType, there is no way to use the symbol*to refer to the kind of types with values. UseType(which can be imported fromData.Kind) instead. The symbol*will refer to any type-level binary operator*in scope, according to the normal scoping rules. (If-XTypeOperatorsis not in effect, use of*in a type will be an error.)
The
-XStarIsTypeidea is due to David Feuer, @treeowl.
Effect and Interactions¶
One way to understand the changes to
*is this:Currently, GHC follows this process to determine what a
*in a type-level context means:If
-XTypeInTypeis in effect:If the use of
*refers toData.Kind.*, then parse it as an alphanumeric identifier; it meansType.If
*refers to some other type, it is a binary operator.
If
-XTypeInTypeis not in effect:If the use of
*is in a context that is syntactically understood to be a kind,*is parsed as an alphanumeric identifier and meansType.Otherwise, it is a binary operator.
Under this proposal, this is all simplified to this:
If
-XStarIsTypeis in effect,*is parsed as an alphanumeric identifier and meansType.Otherwise,
*is a binary operator.
Much simpler!
Note that the design of this proposal conforms to the three-release policy, in that users will not need to use CPP to avoid warnings. (In particular, note that
import Data.Kindis always a fine thing to do, even without-XTypeInType.)This proposal paves the way for future proposals relating to type-level features. Specifically, implementing this will make it possible to treat kind-variable scoping the same way we do type-variable scoping, as proposed in #103.
Migration path: For most users, no migration will be necessary. The exception will be those programs which define or use
*as a type family. Currently working code like:{-# LANGUAGE KindSignatures, DataKinds, TypeOperators #-} import Data.Proxy import GHC.TypeLits mult :: forall (m :: Nat) (n :: Nat). Proxy m -> Proxy n -> Proxy (m * n) mult _ _ = Proxy
will need to explicitly enable
-XNoStarIsType. See Have TypeOperators imply NoStarIsType for an alternative.
Costs and Drawbacks¶
This is a simplification to the implementation and description of GHC. Hooray!
This will effectively create two different versions of
-XPolyKindsand-XDataKinds, which could be problematic for users who want tooling to choose compilers based on extension names. Is this a problem in practice? I don’t know. Even without this change,-XPolyKindsevolved significantly during the GHC 7 releases, as do various other extensions, so users already have to resort to measures other that just looking at extensions when choosing a compiler version.Modules that use
*both as a binary operator and as the kind of types with values will have to be updated to useTypeinstead, as imported fromData.Kind. This change is backward compatible to GHC 8.0. (Alternatively, they could use-XStarIsTypeand fully-qualify their uses of the binary operator*.)
Alternatives¶
Come up with a new extension name that encompasses both
-XTypeInTypeand-XPolyKinds. All three would be synonymous.Live with the status quo, with quite a bit of code in GHC to support it.
Do not support fully-qualified uses of the binary operator
*when-XStarIsTypeis in effect. Under this alternative, users would have no workaround to access the binary operator*with-XStarIsType.Introduce a new extension
-XTypeColonOperators, which allows only those type-level operators that begin with a:, conveniently working withData.Type.EqualityandGHC.Generics. This new extension would not disable-XStarIsType, as the two don’t conflict.I personally do not think this addition is worth it, but it was suggested on the pull request.
Report
Typein error messages, regardless of whether-XStarIsTypeis enabled. An advantage here is that I thinkTypeis easier to understand than*: just about everyone whom I’ve taught about kinds gets very confused about the name*, thinking that*is some kind of universal kind that encompasses all other kinds. (Indeed, I thought this, too, once upon a time.)This alternative has two noteworthy drawbacks:
There are gobs of resources that use
*. These would all go out of date.The Haskell Reports mention
*by name. If error messages printTypeinstead of*, we’ll be further from the behavior that the Report authors intended at the time. However, as the Reports do not specify error message text, this change does not bring us further from formal compliance to the letter of the Report. It would bring us further from the spirit of the Report.
Currently, and in this proposal, both
*and its unicode variant ★ are treated identically. One way to have our cake and eat it too is to follow the plan above for*but force ★ to always lex as an alphanumeric identifier (the way*-as-Typelexes now). That way, folks who are really wedded to using a star can still do so. This would not be backwards compatible, because anyone who uses ★ as a type-level infix operator would have to change the name of their operator; there would be no way to use ★ infix (without backquotes, as usual).Introduce a new way of writing
Type:type. That is, the keywordtypewould be the kind of types with values. We could sayclass Monad (m :: type -> type). This has the advantage that clients do not need to import anything, as we could maketypealways in scope (as it is a keyword). Furthermore, existing tools already apply syntax highlighting totype, which I think is reasonable. Disadvantages include the fact thattypewill look like a type variable without syntax highlighting enabled, this is a new change to an area that has already undergone some disruptive changes, and it has been mentioned previously and rejected. But I still like it, so I’m mentioning it here.EDIT: After further discussion, I’m less enamored of this idea, for two reasons:
This
typewould simply be a type synonym forTYPE LiftedRep, as it is today. It’s quite strange to have a keyword be an ordinary type synonym.@nomeata pointed out (offline discussion) that currently,
typeis used in export lists to denote a namespace. It’s quite possible that its use as a namespace selector might grow in the future, and usingtypeto meanTypewould preclude this.
I find both arguments compelling independently, and so I withdraw support for this alternative. Nevertheless, I’m keeping it in the proposal in case someone wants to argue in support of it.
Have TypeOperators imply NoStarIsType¶
One alternative is to have an additional 4.f step in the Proposed Change Specification section:
For two releases,
-XTypeOperatorswill imply-XNoStarIsType, to provide a migration path for code that uses the binary operator*. (After two releases, this code can include-XNoStarIsTypeexplicitly without going against the three-release policy.) Users can re-enable-XStarIsTypeafter-XTypeOperatorsis enabled if they wish.
This alternative is problematic. It’s intended to help migration, but
implementation evidence shows it causes more trouble. If the code being
migrated only enables -XTypeOperators, then 4.f will indeed be helpful, but
there is a lot of code which also enables -XKindSignatures in which
-XNoStarInType changes the semantics. For example, this code from the
hashable library
newtype Tagged (s :: * -> *) = Tagged {unTagged :: Int}
would fail to compile with the following error:
Data/Hashable/Generic.hs:116:22: error:
Operator applied to too few arguments: *
With NoStarIsType (implied by TypeOperators), ‘*’ is treated as a regular type operator.
Did you mean to use ‘Type’ from Data.Kind instead?
|
116 | newtype Tagged (s :: * -> *) = Tagged {unTagged :: Int}
|
Many other libraries in the wild simultaneously enable -XTypeOperators and use
* as a kind, including servant, aeson, cereal, cassava, and others, so
including 4.f would break all of these libraries. While not having 4.f also
results in some breakage, far fewer libraries in the wild break without 4.f
than with 4.f, so this was ultimately decided against.
If -XTypeOperators were to imply -XNoStarIsType, then any code which
uses * as kind instead of a binary operator would have to migrate somehow.
Two migration options are:
Declare
-XStarIsType. If they ever use*as a binary operator, those uses would have to be qualified with a module prefix.Import
TypefromData.Kindand change uses of*toType. If they already have aTypein scope, they may have to use qualified imports, etc.
Unresolved questions¶
Is this the right deprecation schedule? Is it moving too fast?
What is the educational impact of this proposal? Specifically,
-XPolyKindsis now bigger and harder to learn. On the other hand, the previous implementation of-XPolyKindshas some restrictions that may not have been obvious to users.What do we want the long-term future of
*to be? I favor removing it after a long time (> 5 years). But deciding now what we want to have in the distant future can influence decisions made in the meantime. One particular decision: should-Wcompatwarn on uses of*asType? Relatedly, should there be a plan to deprecate-XStarIsType?Regardless of the long-term future of
*, is the migration path described around-XStarIsTypethe best possible path? Notably, the current migration path will cause breakage in-XTypeOperatorscode that uses*as a kind, requiring users to change all uses of*toTypewhen upgrading GHC. David Feuer has expressed unease at the migration path detailed here, but his counter-suggestion remains unclear to me. I am not without unease myself, but I don’t see a better way.
Summary of discussion¶
Much (just about all, really) of the discussion surrounds the future of *. I’ve made
my case in the comments for eventually deprecating and removing it, though I’ve been
convinced by the -XStarIsType plan (which grew out of the discussion) that supporting
* into perpetuity isn’t so terrible, and that we should plan to keep it around for
years more. One vocal participant, @AntC2, has strenuously objected to any move toward
removing *, but their points have not been echoed by anyone else in the discussion.
In particular, @AntC2 is worried about rotting of educational resources, something I was
perhaps too glib about in earlier versions of this proposal. I expect the committee will
carry on this debate, and I’m happy to submit to the view of the committee on this matter.
Other discussion concerns the details of the migration path and the -XStarIsType aspect
of this proposal, briefly summarized in the last unresolved question, above.
Implementation Plan¶
I or a close collaborator volunteers to implement.