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

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

### comment:2 Changed 2 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 2 years ago by

Differential Rev(s): | → Phab:D841 |
---|---|

Related Tickets: | → #8028 |

Status: | new → patch |

### comment:4 Changed 2 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 2 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 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 Changed 2 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 to`data`

, except that`newtype`

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`

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

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

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 2 years ago by

Status: | patch → merge |
---|---|

Test Case: | → indexed-types/should_compile/T9840 |

### comment:11 Changed 2 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 2 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.

**Note:**See TracTickets for help on using tickets.

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.