25

This compiles fine:

type List a = [a]

But when I introduce a class constraint, the compiler asks for RankNTypes to be included:

type List2 a = Num a => [a]

After including that extension, it compiles fine. Why is that extension required for compiling the code ?

Edit: Why do I need the constraint in the first place ?

I was inspecting this Lens type (type RefF a b = Functor f => (b -> f b) -> (a -> f a)) from this post and found out that it actually needed RankNTypes because of the Functor constraint.

Sibi
  • 47,472
  • 16
  • 95
  • 163
  • 1
    Why do you want a constraint inside of a type alias in the first place? – Thomas M. DuBuisson Apr 08 '14 at 18:34
  • 1
    Indeed, GHC isn't smart enough to see something like `foo :: List2 a -> a -> a; foo _ a = a + 1` and correctly determine that `List a` requires a `Num` instance and lift it to the `a` after `List2` – daniel gratzer Apr 08 '14 at 18:37
  • 3
    Constraints on type synonyms are usually considered a code smell. The `RankNTypes` extension is needed because it's essentially equivalent to doing `type List2 a = forall a. Num a => [a]`. This is the same problem that plagues the famed `lens` library. It can be used for great purposes, but there is some type magic going on to make it work. – bheklilr Apr 08 '14 at 18:39
  • @ThomasM.DuBuisson Was just playing around with it. Actually this Lens type (`type RefF a b = Functor f => (b -> f b) -> (a -> f a)`) needed `RankNTypes` for exactly that reason. – Sibi Apr 08 '14 at 18:39
  • @bheklilr So is `List2` still a Rank 1 type ? – Sibi Apr 08 '14 at 18:44
  • @jozefg In that case, how does `RankNTypes` actually solves it ? – Sibi Apr 08 '14 at 18:48
  • @Sibi According to [this](http://www.haskell.org/ghc/docs/latest/html/users_guide/other-type-extensions.html#universal-quantification), it would be Rank 1, but AFAIK, enabling RankNTypes also allows you to do existential quantification, which this is. – bheklilr Apr 08 '14 at 18:49
  • @bheklilr Hm? This isn't existential quantification since we can talk about the underlying `a` directly. The existential type is `forall out. (forall a. C a => a -> out) -> out`. We can't write a function `(forall out. ....) -> a`. – daniel gratzer Apr 08 '14 at 18:52
  • This isn't Existential... I'm not exactly sure what to call it. It'd be like the lens type if the `a` variable were quantifier bound (and negative), but it's not. Instead, GHC just promotes the type bound upward: `> undefined :: List a` results in `undefined :: Num a => List a`. Perhaps it uses the RankNTypes machinery to do that, but I don't think it's a higher rank type. – J. Abrahamson Apr 08 '14 at 19:15
  • 2
    To compare some similar types, consider `type T1 b = forall a . Num a => [a] -> b` which is a "more genuine" candidate for `RankNTypes` or `data T2 = forall a . Num a => T2 [a]` which is existential. – J. Abrahamson Apr 08 '14 at 19:18
  • @jozefg My mistake, I guess I don't understand those parts of the type system as much as I thought. – bheklilr Apr 08 '14 at 19:53
  • 3
    Type synonyms don't add new meaning, `List2` requires `RankNTypes` because it is saying, wherever you see `List2 a`, insert `Num a => [a]`, and if a class constraint appears anywhere except at the left-hand side, the type has a higher rank (and there is no guarantee that `List2` will only be used in places where it would place the constraint on the left hand side). For example: `let h n = replicate n 0; h :: Int -> List2 a` is valid and equivalent to `let h n = replicate n 0; h :: Int -> (forall a . Num a => [a])`. Unless you write something like `[0,1,2] :: List2 a` which is a rank1 type. – user2407038 Apr 08 '14 at 22:30
  • @user2407038 But `let h n = replicate n 0; h :: Int -> (forall a . Num a => [a])` has the rank-1 type `h :: Num a => Int -> [a]` as the type bound is lifted. – J. Abrahamson Apr 09 '14 at 00:18
  • @J.Abrahamson woops, forgot about that. I tried a few cases now that I am at my computer; I assumed that `List a` would become `forall a . Num a => [a]` but that isn't the case: `let g x = x; g :: List b -> List a` is an error, while `let f x = x; f :: (forall b. Num b => [b]) -> (forall a . Num a => [a])` is correct. It almost seems like the `Num` constraint is always moved to the left hand side, or just completely ignored. It seems to me that class constraints on a non-existentially quantified type variable in a type synonym a misfeature. – user2407038 Apr 09 '14 at 01:22
  • 1
    @user2407038 I did the exact same thing! I want to find the type quantifier there but since the `a` carries from the left hand side of the `type` definition you just get a weird `Num` constrain that pops out of `List2` wherever it appears. Super weird... – J. Abrahamson Apr 09 '14 at 01:30
  • `List2` doesn't introduce a rank 2 type; it's not equivalent to `type ListRank2 = forall a. Num a => [a]`, which has the semantics @user2407038 was discussing. With `RankNTypes`, GHC permits `=>` to appear anywhere, but I don't think there's a semantic difference between `forall a b. (Num a => a) -> Num b => b` and `forall a b. (Num a, Num b) => a -> b`. Perhaps another question? – Christian Conkle Dec 28 '14 at 23:37

1 Answers1

23

It's not standard

In which the question is answered

The simple answer is that standard Haskell does not allow qualified type synonym declarations, i.e. a type synonym involving =>. Per the 2010 Report, the syntax for a type synonym declaration is:

type simpletype = type

where type, if you look down at Section 4.1.2, can't contain a context.

By the way, the presence of the type variable a in the context doesn't matter. With no extensions, GHC rejects

type IrrelevantConstraint a = Num Int => [a]

or, for that matter,

type QualifiedT = Num Int => String

Furthermore, even if such a type synonym were permitted, no nontrivial use of it would be standard Haskell, as manual substitution shows:

List2 a      === forall a.   Num a => [a]        -- Okay
List2 a -> b === forall a b. (Num a => [a]) -> b -- Not okay
a -> List2 b === forall a b. a -> Num b => [b]   -- Not okay

And so forth for Maybe (List2 a) etc. In each case, it's not that it's a higher-rank type in the usual sense. I've added explicit forall notation, which is also of course not standard, to emphasize that fact.

Rather, the problem is that each type is inappropriately qualified because => appears inside the type. Again, if you look at the 2010 Report sections on expression type signatures and declarations, you'll see that => is not strictly speaking part of a type, but rather a syntactically distinct thing, e.g.:

expexp :: [context =>] type

Since List2 is invalid Haskell2010, some language extension is necessary for it to work. It's not specifically documented that RankNTypes permits qualified type synonym declarations, but as you've noticed it has that effect. Why?

There's a hint in the GHC docs on RankNTypes:

The -XRankNTypes option is also required for any type with a forall or context to the right of an arrow (e.g. f :: Int -> forall a. a->a, or g :: Int -> Ord a => a -> a). Such types are technically rank 1, but are clearly not Haskell-98, and an extra flag did not seem worth the bother.

The g example is related to our List2 problem: there's no forall there, but there is a context to the right of an arrow, which is the third example I gave above. As it happens, RankNTypes enables the second example too.

A side trip through Template Haskell

In which a skippable detour is taken, Mr. Forall is discovered in an unexpected place, and ranks and contexts are pondered

I don't know whether the Template Haskell representation of a declaration is necessarily linked to the typechecker's internal representation or operation. But we find something a bit unusual: a forall where we wouldn't expect, and with no type variables:

> import Language.Haskell.TH
> :set -XTemplateHaskell
> runQ [d|type List2 a = Num a => [a]|]
[TySynD List2_2
        [PlainTV a_3]
        (ForallT []
                 [ClassP GHC.Num.Num [VarT a_3]]
                 (AppT ListT (VarT a_3)))]

-- simpler example:
> runQ [d|type T = Num Int => Int|]
[TySynD T_0
        []
        (ForallT []
                 [ClassP GHC.Num.Num [ConT GHC.Types.Int]]
                 (ConT GHC.Types.Int))]

The notable thing here is the apparently-spurious ForallT. In Template Haskell, this makes sense because ForallT is the only constructor of Type with a Cxt field, i.e. that can contain a context. If the typechecker similarly conflates forall and constraint contexts, it'd make sense that RankNTypes affected its behavior. But does it?

As implemented in GHC

In which it is discovered why RankNTypes allows List2

The precise error we get is:

Illegal polymorphic or qualified type: Num a => [a]
Perhaps you intended to use RankNTypes or Rank2Types
In the type declaration for `List2'

Searching through the GHC source reveals that this error is generated in TcValidity.hs. The entry point we're concerned with is checkValidType.

We can verify that the compiler actually enters there by compiling with -ddump-tc-trace; the last debug output before the error message is:

Starting validity check [Type constructor `List2']
checkValidType Num a => [a] :: *

Continuing in checkValidType, we see that, absent RankNTypes, the RHS of a type synonym must have rank 0. (Unfortunately the debug output doesn't specify the value of ctxt here, but it's presumably TySynCtxt.)

The note just above checkValidType define ranks in this context thus:

    basic ::= tyvar | T basic ... basic

    r2  ::= forall tvs. cxt => r2a
    r2a ::= r1 -> r2a | basic
    r1  ::= forall tvs. cxt => r0
    r0  ::= r0 -> r0 | basic

That comment squares with the Template Haskell experiment, i.e. that a rank-0 type cannot involve =>, and any type involving => must include a forall and therefore be rank 1 or 2, even if there are no type variables in the forall. In the terminology outlined in TcType, contexts only appear in sigma types.

In other words, as implemented the typechecker rejects the RHS of List2 because it interprets the RHS as being rank 1 on account of its class qualification.

The branch that leads to our error message begins here. If I understand correctly, theta represents the constraint context. The core of the first line of the do block is forAllAllowed rank, which does what it sounds like. Recall from above that the RHS of a type synonym is restricted to rank 0; since a forall is not permitted, we get an error.

This explains why RankNTypes overrides this limitation. If we trace where the parameter rank comes from, through rank0 in checkValidType and then through the preceding few lines, we find that the RankNTypes flag basically overrides the rank0 restriction. (Contrast the situation with default declarations.) And because the extra context was treated as a rank error, RankNTypes permits it.

Christian Conkle
  • 5,932
  • 1
  • 28
  • 52
  • Re. `tcSplitSigmaTy`: note that it works on `Type`, not `HsType`. `HsType` is the source language of types, and `Type` is the internal representation. In `Type`, every type variable is explicitly quantified. – Cactus Dec 29 '14 at 03:01
  • @Cactus - So here the `Type` is explicitly quantified as something like `ForAllTy (a:*) $ FunTy (TyConApp Num [a]) $ ...` and then whatever the representation for `[a]` is? (I'm looking at Note: Equality-constrained types in TypeRep.hs) Interesting. Would the RHS of my `List2'` have the same `Type` representation? – Christian Conkle Dec 29 '14 at 04:32
  • I can't try it out now, but have you tried the `-dppr-debug` flag in conjunction with `-ddump-tc-trace`? – Cactus Dec 29 '14 at 04:37