Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Haskell have bottom (infinite recursions)?

There are languages other than Haskell, such as Coq, which banned bottom, or undefined, or infinite recursive definitions like

bot :: forall a. a
bot = bot

The benefit of not having bottom is simple : all programs terminate. The compiler guarantees that there are no infinite loops, no infinite recursions.

There is also a less obvious benefit : the logic of the language (given by the Curry-Howard correspondence) is consistent, it cannot prove a contradiction. So the same language can write both programs and proofs that the programs are correct. But that's maybe off-topic here.

The protection against infinite recursions is also simple : force each recursive definition to have arguments (here bot has none) and force recursive calls to be decreasing on one of those arguments. Here decreasing is in the sense of algebraic data types, seen as finite trees of contructors and values. Coq's compiler checks that the decreasing argument is an ADT (data in Haskell) and that the recursive calls are done on subtrees of the argument, typically via a case of, not on other trees coming from somewhere else.

Now the cost of this language constraint : we lose Turing-completeness (because we cannot solve the halting problem). That means there are terminating functions, possible to code in Haskell using general recursions, that would become refused by the compiler. In practice however, the magnitude of Coq's library shows that those exotic functions are rarely needed. Does someone even know one of them ?

There are cases where infinite loops make sense :

  • Interactive programs, where the user issues commands by clicking or typing on the keyboard, usually run forever. They wait for a command, process it and then wait for the next command. Until the end of time, or more seriously until the user issues the quit command.
  • Likewise, instead of processing an infinite stream of user commands, process an infinite stream of data. Such as continuous queries on a database.

Those cases are rather specific and might be treated by new language primitives. Haskell introduced IO to trace unsafe interactions. Why not declare the possibility of infinite loops in the signature of functions ? Or split a complex program into a DSMS that calls Haskell functions for pure computations ?

EDIT

Here is an algorithm example, that might clarify what changes if we switch to total programming. Euclid's algorithm for computing the GCD of 2 numbers, first in plain recursive Haskell

euclid_gcd :: Int -> Int -> Int
euclid_gcd m n = if n <= 0 then m else euclid_gcd n (m `mod` n)

Two things can be proven concerning this function : that it terminates, and that it does compute the GCD of m and n. In a language accepting proof scripts, we would give the compiler a proof that (m mod n) < n, so that it concludes the recursion is decreasing on its second argument, and therefore terminates.

In Haskell I doubt we can do that, so we can try to rewrite this algorithm in a structural recursive form, that the compiler can easily check. That means a recursive call must be done on the predecessor of some argument. Here m mod n won't do, so it looks like we are stuck. But as with tail recursion, we can add new arguments. If we find a bound on the number of recursive calls, we are done. The bound does not have to be precise, it just needs to be above the actual number of recursive calls. Such a bound argument is usually called variant in the literature, I personally call it fuel. We force the recursion to stop with an error value when it runs out of fuel. Here we can take the successor of any of the 2 numbers :

euclid_gcd_term :: Int -> Int -> Int
euclid_gcd_term m n = euclid_gcd_rec m n (n+1)
  where
    euclid_gcd_rec :: Int -> Int -> Int -> Int
    euclid_gcd_rec m n fuel =
      if fuel <= 0 then 0
      else if n <= 0 then m else euclid_gcd_rec n (m `mod` n) (fuel-1)

Here the termination proof somewhat leaks into the implementation, making it slightly harder too read. And the implementation makes useless computations on the fuel argument, which could slow down a bit, though in this case I hope Haskell's compiler will make it negligible. Coq has an extraction mechanism, that erases the proof part of such mixes of proofs and program, to produce OCaml or Haskell code.

As for euclid_gcd we would then need to prove that euclid_gcd_term does compute the GCD of n and m. That includes proving Euclid's algorithm terminates in less than n+1 steps.

euclid_gcd_term is obviously more work than euclid_gcd and arguably less fun. On the other hand, once the habit is taken, I find it rewarding intellectually to know bounds for my algorithms. And when I cannot find such bounds, it usually means I don't understand my algorithms. Which also usually means they are bugged. We cannot force all developers to use total programming for all programs, but wouldn't a compiling option in Haskell to do it on demand be nice?

like image 478
V. Semeria Avatar asked Aug 11 '18 14:08

V. Semeria


1 Answers

I can't give you a comprehensive answer, but I've spent some time working in Agda over the past year, and here are some drawbacks of totality that I've seen.

Basically, when writing a program in Haskell, there are some bits of information that I have, but that I do not explicitly share with the compiler. If this information is necessary for the program to terminate without errors, then Agda forces me to make this information explicit.

Consider Haskell's Data.Map.! operator that lets you lookup an element in a map by its key. If you pass a key that is not in the map, it will throw an exception. The Agda counterpart of this operator would need to take not only the key, but also a proof that the key is in the map. This has some drawbacks:

  1. Someone would have to come up with a type that lets us express "map m contains key k", and prove lemmas about how this type interacts with insertion and deletion.
  2. Any changes to the definitions of insert and delete will likely invalidate the proofs of these lemmas.
  3. When I use this map, I have to keep track of all the membership proofs explicitly, passing them around and keeping them up to date. This is both a syntactic and a mental burden.
  4. If I care about performance, I need to make sure that all these proofs are erased at runtime.

Alternatively, I could use Maybe or Either to explicitly pass these errors around. Often this is the right thing to do, but it makes it less clear when I anticipate an error happening, and when I've simply not gone through the trouble of showing that an error is impossible. This approach also doesn't work as well with interactive debuggers: I can easily break on an exception, but not so easily on the construction of a Nothing.

I've been focusing on errors in the above, but the same things hold for non-termination.

This isn't to say that total languages are useless—as you say, they have many benefits. So far, I just wouldn't say that those benefits obviously outweigh these drawbacks for all applications.

like image 63
Anton Golov Avatar answered Nov 10 '22 10:11

Anton Golov