Proposal: add HasCallStack for all partial functions in base

LuoChen luochen1990 at gmail.com
Sun Jun 16 12:30:20 UTC 2019


Motivation

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

(here <https://gist.github.com/luochen1990/6fcbb0fa4c0c7caa1b242188eb586d1f>
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
<https://gist.github.com/luochen1990/94179a2492ff7d1ca45153645f1bb449>.

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 <https://gist.github.com/luochen1990/5b7a0844701413486da595b9b997f3c2>
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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/libraries/attachments/20190616/ecbd9e9d/attachment.html>


More information about the Libraries mailing list