2

Consider the following program (in Haskell, but could be any HM-inferred language):

x = []
y = x!!0

Using HM (or by running the compiler), we infer:

x :: forall t. [t]
y :: forall a. a

I understand how this happens, playing by the usual generalization/instantiation rules, but I'm not sure it's desirable to have something like forall a. a.

One question is: since we have an out-of-bounds access here, one can rule out the program as a valid example. Instead, can we say the universal type we inferred is a sign of something wrong in the program? If yes, can we use this "fact" to deliberately fail checking of invalid programs in other cases as well?

The next program gets even stranger types:

c = []
d = (c!!0) + (1 :: Int)

Inferred types:

c :: forall t. [t]
d :: Int

...although d was drawn from c!

Can we augment HM to do a better job here without ruling out valid programs?

EDIT: I suspected this is an artifact of using partial functions (!!0 in this case). But see:

c = []
d = case c of [] -> 0; (x:_) -> x + (1 :: Int)

There are now no partial functions in use. And yet, c :: forall t. [t] and d :: Int.

sinelaw
  • 16,205
  • 3
  • 49
  • 80
  • 1
    The reason you get `Int` for `d` is because `(+ (1 :: Int)) :: Int -> Int`, so `c`'s type is instantiated accordingly. – Cactus Dec 21 '14 at 08:35
  • @Cactus, I realize how it happens. But did it make sense to infer these types? Can we augment HM to do a better job here without ruling out valid programs? – sinelaw Dec 21 '14 at 09:15
  • 2
    I don't see at all why you don't like the type of d. It just specialises the type of c, which is itself perfectly validly polymorphic. – AndrewC Dec 21 '14 at 09:39
  • @AndrewC, I see why it's correct when we don't use any partial functions (since `c = []`, we will always be using the pattern `[]` and will have to explicitly come up with an `Int`, no magic stuff here). But in the presence of partial functions, you can derive any type whatsoever regardless of what will happen in runtime (it will be a runtime error). Would be nicer to fail! – sinelaw Dec 21 '14 at 09:43
  • Also, `y = error "Prelude.(!!): empty list"` or similar, and it's a good/handy thing that errors have universally quantified type. – AndrewC Dec 21 '14 at 09:44
  • 1
    @AndrewC in other words, I wrote a fancy implementation for `error`! – sinelaw Dec 21 '14 at 09:45
  • Are you sure you are not confusing type inference with type checking? What type do you want `!!` to have? – n. m. could be an AI Dec 21 '14 at 09:45
  • 1
    OK, I admit I'm a big fan of compile time errors over runtime errors, so I'll concede that. I worry slightly that we shouldn't expect the compiler to solve the halting problem so that when it comes down to it, dependent types are what you need here. – AndrewC Dec 21 '14 at 09:46
  • 1
    How would dependent types protect you from partial functions? If you have decided to lie in the type by using a silent exception mechanism, no matter how powerful your TT is, you'll never recover from it. – gallais Dec 21 '14 at 09:59

2 Answers2

5

The Hindley-Milner type of a term doesn't depend on the value of its subterms, only on their types. A HM type-checker will never evaluate expressions, only type-check them, so it sees your x as just "a list of a", not as an "empty list of a", as a human does when informally type-checking your program.

There are type systems that would flag your program as type-incorrect, e.g. dependent types, but those don't have type inference without explicit type declarations, which is one of the luxuries Haskell/ML programmers enjoy, thanks to HM.

Using an extension to HM (GADTs) Haskell can define a type for "safe lists"

data Empty
data NonEmpty

data SafeList a b where
  Nil :: SafeList a Empty
  Cons:: a -> SafeList a b -> SafeList a NonEmpty

(!!) :: SafeList a NonEmpty -> Int -> a
-- etc

This would make Nil!!0 a type error.

Hans Lub
  • 5,513
  • 1
  • 23
  • 43
  • Thanks! Do you know if we can augment HM itself to prevent this without going overboard with dependent typing? What about disallowing top-level fully universal types (`forall a. a`)? – sinelaw Dec 21 '14 at 09:19
  • I don't think this answers the question. Whether `c` is the empty list or not, as long as it's something of type `forall a. [a]`, you still get `x !! 0 :: forall a. a`, which is an impossible type. I think this is because `!!` itself has an impossible type, `forall a. [a] -> Int -> a`. – kini Dec 21 '14 at 09:19
  • @KeshavKini, I also suspect that this is an artifact of partial functions (`!!0` is better known as `head`) – sinelaw Dec 21 '14 at 09:21
  • @KeshavKini see updated question - you can get the same result without using a partial function like `!!` – sinelaw Dec 21 '14 at 09:27
  • @sinelaw: `_|_` (bottom) has type `forall a. a`. A total Haskell (without [fixpoints](http://en.wikibooks.org/wiki/Haskell/Fix_and_recursion)) would not have any term with type `forall a.a` but it would be incredibly useless :-) – Hans Lub Dec 21 '14 at 09:39
  • @HansLub so partial functions are the culprit here? – sinelaw Dec 21 '14 at 09:44
  • @sinelaw I was referring to your comment about `forall a. a`. The `c` and `d` examples don't seem wrong or bad to me at all. `d` may be "drawn from" `c` but that doesn't necessarily mean anything about its type... and this becomes even more obvious in the new `d` you formulated in your edit to the question. You might just as well ask why `d = case c of [] -> 0; (x:_) -> 1` is of type `Int`. What's strange about that? – kini Dec 21 '14 at 09:49
  • @sinelaw: The fixpoint combinator is the culprit. Without it, Haskell would be strongly normalizing: every computation would yield a value, and all functions would be total. Boring! – Hans Lub Dec 21 '14 at 09:52
  • @HansLub I don't think a total Haskell would be *that* useless. You could have some sort of controlled recursion with a termination checker, like in Coq. Throw in a distinction between recursion and corecursion and you'll get even more. But then it won't really be Haskell anymore... – kini Dec 21 '14 at 09:52
  • @KeshavKini you're obviously right when `(x:_) -> 1`. You're also right but less obviously, when it's `(x:_) -> x + (1::Int)` which suggests that we take a list of any type and expect its element `x` to have type `Int`. However this is ok because the list happens to be empty. – sinelaw Dec 21 '14 at 09:53
  • HansLub and @KeshavKini: Thank you - I'll go learn more about total languages now. – sinelaw Dec 21 '14 at 09:58
  • It's not because the list happens to be empty, it's because the list is of type `forall a. [a]`, and `[Int]` is a specialization of that type. Don't think about the value, think about the type -- the same thing occurs with the non-empty, in fact infinite, list, `repeat undefined`, which is also of type `forall a. [a]`. – kini Dec 21 '14 at 11:03
  • @KeshavKini, I'm aware of the instantiation that's happening, and of course `repeat undefined` (and other constructions using `forall a. a` as elements) would get that type too, but I'm more interested in preventing accidental bugs (such as trivial empty lists combined with unchecked list access) where I'd like the type inference & checking to warn me about stupid code. It doesn't seem likely that `forall a. [a]` would be the programmer's intention. – sinelaw Dec 21 '14 at 13:09
  • 1
    That makes sense. BTW, note that `repeat undefined` doesn't have elements of type `forall a. a` -- if it did, then `repeat defined` would be of type `[forall a. a]`, which requires the ImpredicativeTypes language extension to represent in Haskell. What it does have is elements of type `a`, where `a` is bound by a `forall` outside the list constructor. – kini Dec 21 '14 at 17:45
3

I’m not sure it's desirable to have something like forall a. a.

It isn’t desirable. By parametricity, the only thing that an expression of such a type can do when you evaluate it is to fail to halt, either by throwing an exception or by looping infinitely. This is what Haskellers mean when we talk about computations producing “bottom” (⊥).

If you’re thinking of what extension to HM would rule out such types, you could disallow any type which, when interpreted as a logical formula, is not a tautology. Such functions would be guaranteed to raise errors for some inputs.

So x :: forall a. [a] would be okay, because for any type a, we can indeed construct a value of type [a]—an empty list! But, for example, head :: forall a. [a] -> a would not be okay, because it’s not true that we can always get a value of type a from a value of type [a]—since the list might be empty.

However, this becomes less useful the more concrete your types are. You would get basically no guarantees about functions of type Int -> Int, for example.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • Thanks. See the second example I added at the end of my question. From only `arr :: forall a. [a]` you then instantiate a usage of `arr` to have type `[b]`, which you can then unify with whatever list type successfully. For example, with `f [] = 0; f (x:_) = x + (1:: Int)` you'll get `f arr :: Int`. – sinelaw Dec 21 '14 at 09:40
  • @sinelaw: Okay. What’s the problem with that? You defined `f [] = 0 :: Int`, so if you `let arr = [] in f arr`, then of course you should expect to get `0 :: Int`. – Jon Purdy Dec 21 '14 at 12:25
  • You're right, there's nothing wrong with that. The confusing thing is when using partial functions (such as `head`/`!!`) where the function doesn't check the list's contents before trying to extract an `Int` from it. You give it a polymorphic `forall a. [a]` and it will get an `Int` out of it! – sinelaw Dec 21 '14 at 12:39
  • @sinelaw: I guess the important thing to remember is that the type `A -> B` doesn’t mean “If you give me an `A`, I’ll give you a `B`”. It means “If you give me an `A`, I won’t give you something that’s not a `B`”. – Jon Purdy Dec 21 '14 at 21:07