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.
HasCallStack
constraint for all partial functions in base packageHasCallStack
constraint for their exported partial functions when they release a package-fignore-hascallstack
to toggle off the effect of HasCallStack
constraint in case somebody need best performanceI 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.
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.
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.