4

I was thinking about how I could save myself from undefinition, and one idea I had was to enumerate all possible sources of partiality. At least I would know what of to beware. I found three yet:

  1. Incomplete pattern matches or guards.
  2. Recursion. (Optionally excluding structural recursion on algebraic types.)
  3. If a function is unsafe, any use of that function infects the user code. (Should I be saying "partiality is transitive"?)

I have heard of other ways to obtain a logical contradiction, for instance by using negative types, but I am not sure if anything of that sort applies to Haskell. There are many logical paradoxes out there, and some of them can be encoded in Haskell, but may it be true that any logical paradox requires the use of recursion, and is therefore covered by the point 2 above?

For instance, if it were proven that a Haskell expression free of recursion can always be evaluated to normal form, then the three points I give would be a complete list. I fuzzily remember seeing something like a proof of this in one of Simon Peyton Jones's books, but that was written like 30 years ago, so even if I remember correctly and it used to apply to a prototype Haskell back then, it may be false today, seeing how many a language extension we have. Possibly some of them enable other ways to undefine a program?

And then, if it were so easy to detect expressions that cannot be partial, why do we not do that? How easier would life be!

Ignat Insarov
  • 4,660
  • 18
  • 37
  • The reason you can't detect it is you left off the most general way for a function to be partial: by depending on the return value of *another* partial function. – chepner Oct 12 '19 at 14:19
  • @chepner That would be point 3, my sir! – Ignat Insarov Oct 12 '19 at 14:21
  • OK, I read "unsafe" in a different fashion. But that's something you can determine lexically. In any case, this question is simply too broad for Stack Overflow. – chepner Oct 12 '19 at 14:23
  • @chepner Why, is it? _"if your question could be answered by an entire book, or has many valid answers (but no way to determine which - if any - are correct), then it is probably too broad for our format"_ — which of this do you believe applies? – Ignat Insarov Oct 12 '19 at 14:28
  • @chepner I can see how the last paragraph contains a question that is not technical. But consider that it is rhetorical. – Ignat Insarov Oct 12 '19 at 14:36
  • Suppose I add a fourth condition and claim that is all you need to worry about. How would know whether that answer is correct or not? – chepner Oct 12 '19 at 14:40
  • @chepner If it looks reasonable, I will believe it. I know there are excellent programming language theorists around here. If you could add a reference to a complete proof, that is even better. – Ignat Insarov Oct 12 '19 at 14:43
  • @chepner Also notice how, for instance, Idris makes a solid effort to distinguish total functions. Of course it [cannot detect all](http://blog.vmchale.com/article/secret-santa), but it does show that the question not unlike mine here may have a definitive answer. – Ignat Insarov Oct 12 '19 at 14:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/200758/discussion-between-ignat-insarov-and-chepner). – Ignat Insarov Oct 12 '19 at 14:55

1 Answers1

4

This is a partial answer (pun intended), where I'll only list a few arguably non obvious ways one can achieve non termination.

First, I'll confirm that negative-recursive types can indeed cause non termination. Indeed, it is known that allowing a recursive type such as

data R a = R (R a -> a) 

allows one to define fix, and obtain non termination from there.

{-# LANGUAGE ScopedTypeVariables  #-}
{-# OPTIONS -Wall #-}

data R a = R (R a -> a)

selfApply :: R a -> a
selfApply t@(R x) = x t

-- Church's fixed point combinator Y
-- fix f = (\x. f (x x))(\x. f (x x))
fix :: forall a. (a -> a) -> a
fix f = selfApply (R (\x -> f (selfApply x)))

Total languages like Coq or Agda prohibit this by requiring recursive types to use only strictly-positive recursion.

Another potential source of non-termination is that Haskell allows Type :: Type. As far as I can see, that makes it possible to encode System U in Haskell, where Girard's paradox can be used to cause a logical inconsistency, constructing a term of type Void. That term (as far as I understand) would be non terminating.

Girard's paradox is unfortunately rather complex to fully describe, and I have not completely studied it yet. I only know it is related to the so-called hypergame, a game where the first move is to choose a finite game to play. A finite game is one which causes every match to terminate after finitely many moves. The next moves after that would correspond to a match according to the chosen finite game at step one. Here's the paradox: since the chosen game must be finite, no matter what it is, the whole hypergame match will always terminate after a finite amount of moves. This makes hypergame itself a finite game, making the infinite sequence of moves "I choose hypergame, I choose hypergame, ..." a valid play, in turn proving that hypergame is not finite.

Apparently, this argument can be encoded in a rich enough pure type system like System U, and Type :: Type allows to embed the same argument.

chi
  • 111,837
  • 3
  • 133
  • 218
  • Is there any hope for a _more defined_ answer at all? – Ignat Insarov Oct 12 '19 at 16:00
  • @IgnatInsarov The fact is that it's not trivial to design a total language taking Haskell and trying to remove stuff until it becomes total, so I am unable to provide a full recipe to follow. Maybe you could instead look at Coq/Agda, which are already total, and try to find a common part between them and Haskell, since this "intersection" would of course be total. – chi Oct 12 '19 at 17:01
  • @chi thanks to your description I managed to [encode girard's paradox](https://github.com/luqui/experiments/blob/master/hypergame.agda) (maybe) in agda, which had previously eluded me. Still not quite sure what is going on with the recbuilder idioms (poked my way through the dark on that one). Not sure how you would massage this into haskell just yet, if that's possible at all. – luqui Oct 12 '19 at 18:12
  • @luqui To be honest, I'm also unsure about how to embed that into actual Haskell. IIRC, the GHC docs simply says "we should be careful with TypeInType since that makes the logic unsound, but it is already unsound because of a billion ways to achieve non termination, so TypeInType is not much worse". I am not knowledgeable enough to say whether your code can be seen as a version of Girard's paradox, but it looks very interesting. It's amazing that `builder` is accepted by the termination checker, since it's very non-trivial! – chi Oct 12 '19 at 21:41
  • @chi I think for us FPers it's nontrivial because of the way we think of what a function is. But if you look at `M -> Game` as an `M`-ary product of `Game`s it is just basic structural recursion. .. but we digress – luqui Oct 12 '19 at 21:50
  • @luqui That's a good intuitive argument. Of course, inside `hypergame` one of the components of that `Game`-ary product is `hypergame` itself, which breaks a bit the "basic structural induction" intuition ;-) I guess that's because we are modelling a paradox after all. – chi Oct 12 '19 at 21:55