22

Can anyone explain why these both compile happily :

data A a b = A { a :: a, b :: b }
newtype B a = B (A a (B a))
newtype C = C (A Int C)

But I cannot create a similarly recursively defined types via type synonyms?

type B a = A a (B a)
type C = A Int C

Although obviously data B a = A { a :: a, b :: B a } works just fine.

Is there any way to avoid dealing with that extra constructor X everywhere I want the type recursive? I'm mostly passing in accessor functions that pick out the b anyways, so I'm mostly okay, but if an easy circumvention mechanism exists I'd like to know about it.

Any pragmas I should be using to improve performance with the specialized data type C? Just specialize stuff?

Any clever trick for copying between A a b and A c d defining only the a -> b and c -> d mapping without copying over the record twice? I'm afraid that A's fields will change in future. Template Haskell perhaps?

Jeff Burdges
  • 4,204
  • 23
  • 46

2 Answers2

30

This has to do with Equi-recursive types versus iso-recursive types. Haskell implements recursive types using iso-recursive types, which require the programmer to tell the type-checker when type recursion is happening. The way you mark it is with a specific constructor, which a simple type-synonym doesn't allow you to have.

Equi-recursive types allow the compiler to infer where recursion is happening, but it leads to a much more complicated type-checker and in some seemingly simple cases the problem is undecidable.

If you'd like a good discussion of equi vs. iso recursive types, check out Benjamin Pierce's excellent Types and Programming Languages

Short answer: because type synonyms don't introduce constructors, and haskell needs constructors to explicitly mark recursion at the type-level, you can't use recursive type synonyms.

deontologician
  • 2,764
  • 1
  • 21
  • 33
  • Ahh, thank you for the references. If I understand correctly, a type's `forall a` really means "for all except myself." And that's needed because the programmer must tell it when recursion happens. – Jeff Burdges Jan 22 '12 at 20:55
  • 1
    No, the `forall a` has to do with polymorphism. You actually don't need a type parameter to make a recursive type. `data A = A A` is perfectly fine. The issue is that haskell has to know where to stop "drilling down" when deciding what type a term has. When it finds the constructor it knows it doesn't have to descend into that subterm when figuring out the type of the entire term. – deontologician Jan 23 '12 at 00:37
  • There isn't any `forall` in `data A = A A` though, `forall` only appears when a type remains unspecified, which requires this `newtype` trick. – Jeff Burdges Jan 23 '12 at 01:48
  • I thought you were asking about whether the `forall` had something to do with recursive types. It doesn't. I was just saying that the "newtype trick" is required whenever you have a recursive type of any kind, whether or not it is polymorphic (has unspecified types). – deontologician Jan 23 '12 at 02:03
  • I always wondered why Haskell didn't allow infinite types (it always allowed infinite data.) – PyRulez Jul 31 '14 at 14:54
  • @PyRulez I know this post is a bit old, but you might like [this exploration of what would happen if infinite types were allowed](https://mail.haskell.org/pipermail/haskell-cafe/2006-December/020074.html), which I've had bookmarked for ages. – Daniel Wagner Feb 16 '16 at 20:30
15

I will answer your first question and second questions.

The type of B is the infinite type (A a (A a (A a (A a (...)))))

The "type inference engine" could be designed to infer and handle infinite types. Unfortunately many errors (typographical or logical) by the programmer create code that fails to have the desired finite type and accidentally & unexpectedly has an infinite type. Right now the compiler rejects such code, which is nearly always what the programmer wants. Changing it to allow infinite types would create much more difficult to understand errors at compile time (at least as bad as C++ templates) and in rare cases you might make it compile and perform incorrectly at runtime.

Is there any way to avoid dealing with that extra constructor X everywhere I want the type recursive?

No. Haskell has chosen to allow recursive types only with explicit type constructors from data or newtype. These make the code more verbose but newtype should have little runtime penalty. It is a design decision.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
Chris Kuklewicz
  • 8,123
  • 22
  • 33
  • 5
    `newtype` has no runtime penalty at all on GHC. I'm not sure if this is required by the Report, but I seem to remember identical representation to the wrapped type being required. – ehird Jan 22 '12 at 18:49
  • Ahh, yes I suppose this might happen by accident if one weren't careful, but otoh `B` is simply the type of a cyclicly linked list, nothing unusual there for Haskell. Ahh, oops it appears Haskell likes my record `B` much less well than I'd thought. – Jeff Burdges Jan 22 '12 at 18:59
  • 5
    It's not quite true that `newtype Foo t = Foo t` has no runtime penalty. `map Foo` is the safe way to turn a `[t]` into a `[Foo t]`. It does sod all but it ain't cheap. – pigworker Jan 22 '12 at 19:01
  • How should a typechecker compare infinite types for equality? – pigworker Jan 22 '12 at 19:19
  • If we switch on the type families extension, are all the trees rational? I wonder if just higher-kind nested type tomfoolery would be enough to cause trouble. – pigworker Jan 22 '12 at 20:21
  • I managed to construct instances of the self recursive "infinite" types once I tried harder, i.e. got the double constructors correct. They worked just fine. – Jeff Burdges Jan 22 '12 at 20:58
  • @pigworker: Could you use `unsafeCoerce` then to have even less penalty? – Thomas Eding Jan 22 '12 at 22:57
  • 1
    @trinithis: Yes; in general, you can safely `unsafeCoerce` away any difference between a `newtype` and its representation. – ehird Jan 22 '12 at 23:03
  • @trinithis: Now that I think about it, that doesn't apply when typeclasses are involved; e.g. coerce between an unboxed array of `T` (with `sizeOf _ = 4`) and its `newtype` wrapper `S` (with `sizeOf _ = 2`) and you're in for a nasty surprise. Or GADTs: consider `data Foo a where { Bar :: Foo T; Quux :: Foo S }`. So it's not as simple as I thought. – ehird Jan 22 '12 at 23:53
  • Isn't the entire point of newtype that it doesn't add that overhead? pfft – Jeff Burdges Jan 23 '12 at 00:27
  • In some cases, libraries include GHC rewrite rules to make mapping a newtype constructor/accessor free. Also, this can sometimes be done using safe coercions. Unfortunately, the current type role mechanism forbids many valid coercions. – dfeuer Feb 16 '16 at 19:22