Opened 2 years ago

Closed 23 months ago

Last modified 19 months ago

#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: 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:


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 (13)

comment:1 Changed 2 years ago by simonpj

But you also want them to be non injective? Otherwise you could say

data Plus a b

Yes, feel free to offer a patch. Don't forget the user manual.

Last edited 2 years ago by simonpj (previous) (diff)

comment:2 Changed 2 years ago by goldfire

I'm in support of this, too. But I won't have time to implement before the feature freeze...

comment:3 Changed 2 years ago by adamgundry

Differential Rev(s): Phab:D841
Status: newpatch

comment:4 Changed 2 years ago by goldfire

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.


comment:5 Changed 2 years ago by adamgundry

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 imply u ~ 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 in reply to:  5 Changed 2 years ago by goldfire

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 to data, except that newtypes 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 where a and b 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 a type family, but with even more limited behavior.
  • class: Like data, but the result kind must be Constraint.

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 like Symbol, but I want to be able to hook into GHC's module system to disambiguate identically-named inhabitants. I wanted this for units, 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 2 years ago by adamgundry

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, closed type 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 2 years ago by goldfire

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-UnitExps 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:9 Changed 2 years ago by Adam Gundry <adam@…>

In 4efa421327cf127ebefde59b2eece693e37dc3c6/ghc:

Permit empty closed type families

Fixes #9840 and #10306, and includes an alternative resolution to #8028.
This permits empty closed type families, and documents them in the user
guide. It updates the Haddock submodule to support the API change.

Test Plan: Added `indexed-types/should_compile/T9840` and updated
`indexed-types/should_fail/ClosedFam4` and `th/T8028`.

Reviewers: austin, simonpj, goldfire

Reviewed By: goldfire

Subscribers: bgamari, jstolarek, thomie, goldfire

Differential Revision:

GHC Trac Issues: #9840, #10306

comment:10 Changed 2 years ago by adamgundry

Status: patchmerge
Test Case: indexed-types/should_compile/T9840

comment:11 Changed 23 months ago by thoughtpolice

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 23 months ago by adamgundry

Milestone: 7.12.1
Resolution: fixed
Status: mergeclosed

On reflection, I agree, let's leave this in HEAD but not merge into 7.10.

comment:13 Changed 19 months ago by thoughtpolice


Milestone renamed

Note: See TracTickets for help on using tickets.