Motivation

Partial functions in base (especially Prelude) often cause runtime errors and is hard to locate.

(here is a document about the concept of totality and partial function, ignore this if you are familiar with them)

For example, consider the following piece of code:

import GHC.Stack

foo :: HasCallStack => [Int] -> Int
foo xs = last xs + 1

xs :: [Int]
xs = []

main :: IO ()
main = do
    print $ foo xs

In this case, the error message will tell nothing about foo, and the HasCallStack constraint is totally helpless, because the call stack is cut off by the call to last which without HasCallStack constraint.

My current workaround is define my own wrapper functions with HasCallStack constraint for some mostly used partial functions to make them traceable, and use the wrapper (the traceable version) whenever I need them.

e.g.

last' :: HasCallStack => [a] -> a
last' xs = case xs of [] -> error "abuse last"; _ -> last xs

So, IMHO, if our goal is to make errors in haskell traceable, then only providing HasCallStack mechanism is not enough, we have to provide traceable base package and prelude at the same time.

Further more, all untraceable partial functions are considered to be harmful, and should not be exported by any package. Because an improper call to an untraceable partial function will cut off the call stack, and here is a demonstration about that.

On the other hand, is it ever necessary for us to add HasCallStack for a total function? Or we can ask, is it possible that a call to a total function cause runtime error? Maybe it’s a NO, since a total function will also crash when the machine is Out Of Memory, but that is also the only situation I can find out. So I suggest that we add HasCallStack constraint only for partial functions, and IMHO this could be a good balance for better debugging experience and less runtime overhead.

Proposal

  1. add HasCallStack constraint for all partial functions in base package
  2. suggest all programmers to add HasCallStack constraint for their exported partial functions when they release a package
  3. provide a compiler option -fignore-hascallstack to toggle off the effect of HasCallStack constraint in case somebody need best performance

Other Considerations

How to get a full list of partial functions provided by the base package?

I wanted to provide a full list of partial functions exported by the base package in this post, but I find it is hard, since Haskell have no totality checking mechanism like Idris have, and there are no consistent keyword like “total” or “partial” in document, so it takes a lot of work to list all the partial functions of a package by check every item manually. Maybe we can work on this list later — when it’s turned out to be worth after some discussion.

Here is part of the list that I have tidied for several modules of the base package.

How to encourage all package contributors to obey the rule (see proposal #2)?

I don’t know, but I think there may be some other rules to obey when contributing packages. Maybe we can just add this into the list.

Obviously, the final perfect solution should be let the compiler to check the totality of functions, and automatically add HasCallStack for the ones which the compiler cannot confirm it’s totality. But this seems too far away from us, since we still doesn’t have dependent type haskell yet.

How to deal with recursive partial functions?

Since the HasCallStack constraint affects the performance (not only because of the runtime overhead, but also it’s influence on optimization strategy), It is best not to add HasCallStack on recursive functions.

In most of the cases, we can just check the input shallowly before everything start, just like how we deal with the non-recursive ones.

But in some other cases, we need to go deep to recognize the invalidity of the input. A trivial solution is just perform a deep check before everything start, but the checking phase seems expensive.

The best solution, IMHO, is to make the recursive part a total function, wrap the return value into Maybe or some similar things, and the partial version is just the total version combined fromJust. In this way, we avoid the single input checking phase and left the error awareing logic where it was before this translation.