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 :
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?
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:
m
contains key k
", and prove lemmas about how this type interacts with insertion and deletion.insert
and delete
will likely invalidate the proofs of these lemmas.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With