9

When expressing infinite types in Haskell:

f x = x x -- This doesn't type check

One can use a newtype to do it:

newtype Inf = Inf { runInf :: Inf -> * }

f x = x (Inf x)

Is there a newtype equivalent for kinds that allows one to express infinite kinds?

I already found that I can use type families to get something similar:

{-# LANGUAGE TypeFamilies #-}
data Inf (x :: * -> *) = Inf
type family RunInf x :: * -> *
type instance RunInf (Inf x) = x

But I'm not satisfied with this solution - unlike the types equivalent, Inf doesn't create a new kind (Inf x has the kind *), so there's less kind safety.

Is there a more elegant solution to this problem?

2
  • 1
    Valid types are finite. They may be self-referential though. In order to create an self-referential kind, you need a way to reference a kind, presumably by name. I suspect DataKinds would be helpful here. Commented Dec 18, 2018 at 18:22
  • @n.m.: Yes this is now the accepted answer
    – yairchu
    Commented Dec 19, 2018 at 10:01

3 Answers 3

6

Responding to:

Like recursion-schemes, I want a way to construct ASTs, except I want my ASTs to be able to refer to each other - that is a term can contain a type (for a lambda parameter), a type can contain a row-type in it and vice-versa. I'd like the ASTs to be defined with an external fix-point (so one can have "pure" expressions or ones annotated with types after type inference), but I also want these fix-points to be able to contain other types of fix-points (just like terms can contain terms of different types). I don't see how Fix helps me there

I have a method, which maybe is near what you are asking for, that I have been experimenting with. It seems to be quite powerful, though the abstractions around this construction need some development. The key is that there is a kind Label which indicates from where the recursion will continue.

{-# LANGUAGE DataKinds #-}

import Data.Kind (Type)

data Label = Label ((Label -> Type) -> Type)
type L = 'Label

L is just a shortcut to construct labels.

Open-fixed-point definitions are of kind (Label -> Type) -> Type, that is, they take a "label interpretation (type) function" and give back a type. I called these "shape functors", and abstractly refer to them with the letter h. The simplest shape functor is one that does not recurse:

newtype LiteralF a f = Literal a  -- does not depend on the interpretation f
type Literal a = L (LiteralF a)

Now we can make a little expression grammar as an example:

data Expr f
    = Lit (f (Literal Integer))
    | Let (f (L Defn)) (f (L Expr))
    | Var (f (L String))
    | Add (f (L Expr)) (f (L Expr))

data Defn f
    = Defn (f (Literal String)) (f (L Expr))

Notice how we pass labels to f, which is in turn responsible for closing off the recursion. If we just want a normal expression tree, we can use Tree:

data Tree :: Label -> Type where
    Tree :: h Tree -> Tree (L h)

Then a Tree (L Expr) is isomorphic to the normal expression tree you would expect. But this also allows us to, e.g., annotate the tree with a label-dependent annotation at each level of the tree:

data Annotated :: (Label -> Type) -> Label -> Type where
    Annotated :: ann (L h) -> h (Annotated ann) -> Annotated ann (L h)

In the repo ann is indexed by a shape functor rather than a label, but this seems cleaner to me now. There are a lot of little decisions like this to be made, and I have yet to find the most convenient pattern. The best abstractions to use around shape functors still needs exploration and development.

There are many other possibilities, many of which I have not explored. If you end up using it I would love to hear about your use case.

5
  • 2
    Yeah this is probably exactly what I'm looking for! For re-writing Lamdu's type inference and working on its language, I'm in the progress of making an AST library, and I found out I need the same flexibility for the fix-points (currently called "Knots" in the library) as I want for the ASTs, as evident in the non-ideal interfaces in AST.Class.ZipMatch, for example. I've started experiments based on type-families but if this approach works it indeed looks a lot cleaner! I'll update here :)
    – yairchu
    Commented Dec 16, 2018 at 22:02
  • 2
    Looks like your use of GADTs replaces my use of type-families, and the Labels I want seem to be data Label = Label (Label -> Type). I'll give it a try and update.
    – yairchu
    Commented Dec 16, 2018 at 23:48
  • Updating that I used data kinds like you suggested, combined with a type-family rather than GADTs, and this now exists in github.com/lamdu/syntax-tree . In my formulation ASTs (Expr, DefN) are "Knots" just like Annotated and the same "Children" type-class applies for both. I'm quite satisfied with this solution. For example it really simplified my ZipMatch class, and it should also enable the type-inference to share resolved unification terms (which I couldn't do when the UTerm knot couldn't have pure trees as children).
    – yairchu
    Commented Dec 17, 2018 at 17:54
  • Would you like me to edit your answer to add the gist of it on top (that the solution is data kinds combined with either GADTs or type families) and then accept it, or do you prefer to edit it yourself, or do you prefer that I add a new answer?
    – yairchu
    Commented Dec 17, 2018 at 17:55
  • 1
    @yairchu in this case it seems like a new answer is appropriate, but do what you think is best. I'm glad it's working well for you.
    – luqui
    Commented Dec 17, 2018 at 23:19
2

With data-kinds, we can use a regular newtype:

import Data.Kind (Type)

newtype Inf = Inf (Inf -> Type)

And promote it (with ') to create new kinds to represent loops:

{-# LANGUAGE DataKinds #-}

type F x = x ('Inf x)

For a type to unpack its 'Inf argument we need a type-family:

{-# LANGUAGE TypeFamilies #-}

type family RunInf (x :: Inf) :: Inf -> Type
type instance RunInf ('Inf x) = x

Now we can express infinite kinds with a new kind for them, so no kind-safety is lost.

  • Thanks to @luqui for pointing out the DataKinds part in his answer!
1

I think you're looking for Fix which is defined as

data Fix f = Fix (f (Fix f))

Fix gives you the fixpoint of the type f. I'm not sure what you're trying to do but such infinite types are generally used when you use recursion scehemes (patterns of recursion that you can use) see recursion-schemes package by Edward Kmett or with free monads which among other things allow you to construct ASTs in a monadic style.

3
  • Unfortunately Fix is not what I'm looking for. I'm attempting to make something more general than recursion-schemes (at least in some ways) and I really need a type to get something of its own kind (or a representative of it) rather than itself recursively applied.
    – yairchu
    Commented Dec 16, 2018 at 19:54
  • 3
    @yairchu Are you sure that Fix + DataKinds is not what you want? In particular, try :k 'Fix in ghci and see 'Fix :: forall (f :: * -> *). f (Fix f) -> Fix f -- you get a new kind, not *. Commented Dec 16, 2018 at 20:10
  • 1
    @DanielWagner: Like recursion-schemes, I want a way to construct ASTs, except I want my ASTs to be able to refer to each other - that is a term can contain a type (for a lambda parameter), a type can contain a row-type in it and vice-versa. I'd like the ASTs to be defined with an external fix-point (so one can have "pure" expressions or ones annotated with types after type inference), but I also want these fix-points to be able to contain other types of fix-points (just like terms can contain terms of different types). I don't see how Fix helps me there
    – yairchu
    Commented Dec 16, 2018 at 21:11

Not the answer you're looking for? Browse other questions tagged or ask your own question.