#9840 closed feature request (fixed)
Permit empty closed type families
Reported by: | adamgundry | Owned by: | |
---|---|---|---|
Priority: | normal | Milestone: | 8.0.1 |
Component: | Compiler (Type checker) | Version: | 7.8.3 |
Keywords: | TypeCheckerPlugins | Cc: | goldfire |
Operating System: | Unknown/Multiple | Architecture: | Unknown/Multiple |
Type of failure: | None/Unknown | Test Case: | indexed-types/should_compile/T9840 |
Blocked By: | Blocking: | ||
Related Tickets: | #8028 | Differential Rev(s): | Phab:D841 |
Wiki Page: |
Description
At the moment, closed type families without any equations fail with a parse error. In addition, they cannot be created by TH (see #8028). Would it be possible to permit these instead?
My use case is in my typechecker plugin for units of measure, where I want to add new type-level operators without any equational theory (because it will be supplied by the plugin) and without users having the ability to introduce their own type family instances.
Change History (14)
comment:2 Changed 4 years ago by
I'm in support of this, too. But I won't have time to implement before the feature freeze...
comment:3 Changed 3 years ago by
Differential Rev(s): | → Phab:D841 |
---|---|
Related Tickets: | → #8028 |
Status: | new → patch |
comment:4 Changed 3 years ago by
I'm afraid something smells a little off here.
I agree that we should permit empty closed type families -- that's not a problem. But I'm not convinced that empty CTFs are the solution to Adam's problem.
What Adam really wants here is the ability to create new type-level constants at arbitrary kind. An empty CTF fits the bill, but there's something strange about it. Despite my early objections, I've come to agree with Lennart's viewpoint in #9636 that a CTF that is known to be unable to reduce is a strange beast that should probably be disallowed.
Here is a simple proposal:
type constant Unit :: * -- kind annotation required; types of kind * are promoted to kinds -- this is essentially indistinguishable from `data Unit` type constant Meter :: Unit -- Here, we can define a new constant of a non-* kind. This is very useful! type constant Second :: Unit -- We can even pattern-match on these constants: type family UnitToString (x :: Unit) :: Symbol where UnitToString Meter = "m" UnitToString Second = "m" type constant Weird :: Nat -> Bool -- This is OK to declare. But we *cannot* pattern-match on it, because the return -- kind is closed.
This last example may be controversial, but I think it's OK. The others are less controversial in my opinion. What this essentially does is allow for open datakinds.
Once these constants are declared, then a plugin can define an equational theory over them.
Unfortunately, this is a much bigger change than just allowing empty CTFs. But it somehow smells better to me.
Thoughts?
comment:5 follow-up: 6 Changed 3 years ago by
What Adam really wants here is the ability to create new type-level constants at arbitrary kind.
Is it? Open datakinds would be nice, but Symbol
and *
already give us quite a lot of wriggle room.
My real problem is more in creating type-level function symbols without any equational theory, which (with Phab:D841 at least) goes like this:
data Unit type family Mul (u :: Unit) (v :: Unit) :: Unit where
In particular, to avoid conflict with plugin-generated axioms, Mul
- must be saturated;
- is not injective, so
Mul u v ~ Mul v u
does not implyu ~ v
;
- cannot be used in patterns;
- cannot be given any instances by the user (this is the only condition not satisfied by an open type family).
I don't quite understand how your proposal would work for this case. What does it mean to declare Weird
as a constant in a closed kind? Note that giving Mul
kind Unit -> Unit -> Unit
is no good, because that means it is injective (at least unless we add another flavour of type-level function space). In any case, adding a whole new declaration form feels like rather more work than my needs justify.
Phab:D841 is a relatively simple change (deleting 7 lines of code, in its original incarnation before distinguishing abstract/empty CTFs) that is consistent with the way type families currently work. No matter how much we might wish it otherwise, type families are very different from term-level functions.
I'm happy to see a fix for #9636, but preferably in a backwards-compatible way.
comment:6 Changed 3 years ago by
Replying to adamgundry:
What Adam really wants here is the ability to create new type-level constants at arbitrary kind.
Is it?
Yes. :) But you don't know it yet.
After posting this morning, I thought more about this. The more I thought about it, the more I liked it.
The key idea (for me) is that currently Haskell has many different forms of type-level symbol declaration (data
, newtype
, type family
, data family
, type
, class
, maybe more?), but perhaps these can all be rolled into one, more flexible type of declaration. (I'm not necessarily proposing to expose this to users as such, but to think of it this way.) At the moment, I'm concerned only with how these type-level symbol declarations behave in types, completely forgetting about any term-level behavior. I'm also forgetting about FC for the moment, concentrating only on Haskell.
data
: Declares generative, injective constants whose result kind must be*
. Can be pattern-matched. When considered as a kind, the set of inhabitants is closed and always known.newtype
: Identical todata
, except thatnewtype
s are neither generative nor injective under representational equality.type family
: Not generative, not injective. Accordingly, must appear always saturated. (GHC's type variable application operation in types -- that is,a b
wherea
andb
are type variables -- assumes that the function is generative & injective. Anything that isn't must always be saturated.) These have a limited reduction behavior known to GHC. Closed type families have a fixed reduction behavior; open type families' reduction behavior can be extended. Cannot be pattern-matched against.data family
: Generative & injective. When considered as a kind (in the future with kind equalities) the set of inhabitants is open, but each extension of this set requires parameters to be apart from previous sets. Can be pattern-matched against.type
: Like atype family
, but with even more limited behavior.class
: Likedata
, but the result kind must beConstraint
.
Phab:D202 extends this collection with injective type families, which are like type families, but allow injectivity in some subset of its parameters.
I have wanted the following:
- A generative, injective type constant, whose result kind is
*
. When considered as a kind, the set of inhabitants is open. Can be pattern-matched against.
This is like
*
, but still distinguished from*
. One may argue that this is likeSymbol
, but I want to be able to hook into GHC's module system to disambiguate identically-named inhabitants. I wanted this forunits
, where I settle for declaring my extensible set of units in kind*
. This leads to worse error messages than would be possible if I could declare a new, open kind.
- A generative, injective type constant that can not be pattern-matched against.
This is the right place for
Any
.
Adam seems to want:
- A non-generative, non-injective type-level symbol with reduction behavior unknown to GHC. This cannot be pattern-matched against.
The reduction behavior is implemented in a plugin. This type of declaration is also the correct characterization of the type-lits operators.
So it seems that there are several characteristics each type-level declaration varies in:
- Generativity (possibly different choices for nominal vs. representational equality)
- Injectivity (possibly different choice for nominal vs. representational equality, and per argument)
- Matchability (if something is matchable, it had better be generative and injective, too!)
- Open/closed
In addition, GHC blesses type families (and type synonyms) with a limited form of reduction.
I'm not totally sure where all of this goes, but somehow classifying this menagerie seems meaningful. It also helps to identify missing spots and to understand what is going on better. Perhaps using empty closed type families for Adam's needs and for type-lits is OK, but it isn't really what we want, according to these classifications: these type-level symbols have an unknown reduction behavior, in contrast to empty closed type families, which have a known (vacuous) reduction behavior.
comment:7 Changed 3 years ago by
Thanks, Richard, it's good to try to simplify and bring order to these; I hadn't realised what a can of worms I was opening with this feature request!
In the interests of simplicity, is it actually necessary to distinguish "matchability" from generativity+injectivity? In particular, if Any
remains non-generative (as at present) then I think the distinction collapses. The only case this matters is if we needed to conclude f ~ Any : forall k . k
from f @k0 ~ Any @k0 : k0
; but do we?
I think it's perhaps also helpful to divide things up along the lines of where their equational theory comes from (considering only nominal roles for now):
- trivial equational theory (
data
,newtype
,class
) - equational theory specified at declaration site (
type
, closedtype family
) - special equational theory either built-in or provided via plugin (type lits operators, what I want)
- open equational theory that can be extended anywhere (open
type family
,data family
)
(The data family
case is slightly strange, because the equational theory can be extended anywhere, but only in a very limited way.)
comment:8 Changed 3 years ago by
You're right that in the current state of affairs, generativity+injectivity imply matchability. But (until Phab:D202 lands) generativity also implies injectivity and vice versa. What characteristics to track separately is one of the challenges here...
Suppose units-of-measure annotations were represented by a type headed UnitExp
. UnitExp
would not be injective, because it would take as arguments information about the different units and their powers, and this information has a non-trivial equational theory. However, UnitExp
would, assumedly, be generative, in that no non-UnitExp
s would ever be the same as a UnitExp
. I don't know exactly what this means to the issue above, but it's an example of something that's generative but not injective.
Yes, I completely agree about distinguishing based on equational theory. And the data family
case is quite interesting, I think, because it shows how these concepts can be combined in unusual ways: data families are generative+injective, but yet have a non-trivial and extensible equational theory. If they didn't already exist, I would probably say this point in the space didn't exist! So there's something to be learned from them.
Also, in case I haven't made this clear: these musings probably don't belong in this ticket, as I don't think these ideas should hold anything up about empty closed type families. But I'm hoping this leads to a slightly cleaner solution down the road.
comment:10 Changed 3 years ago by
Status: | patch → merge |
---|---|
Test Case: | → indexed-types/should_compile/T9840 |
comment:11 Changed 3 years ago by
So, this one visibly changes the API and requires a Haddock update, so I'm pretty inclined to leave it as is and not merge. Adam, is this one critical, or can it be worked around/backported reasonably?
comment:12 Changed 3 years ago by
Milestone: | → 7.12.1 |
---|---|
Resolution: | → fixed |
Status: | merge → closed |
On reflection, I agree, let's leave this in HEAD but not merge into 7.10.
comment:14 Changed 2 months ago by
Keywords: | TypeCheckerPlugins added |
---|
But you also want them to be non injective? Otherwise you could say
Yes, feel free to offer a patch. Don't forget the user manual.