Should exhaustiveness testing be on by default?

Claus Reinke claus.reinke at talk21.com
Mon May 18 07:06:51 EDT 2009


> I'm not sure I'd want -Wall on by default (though being -Wall clean is
> very good). But exhaustive pattern checking might well help out a lot of
> people coming from untyped backgrounds.
> 
>    http://ocaml.janestreet.com/?q=node/64
> 
> Ron's also wondering why exhaustive pattern checking isn't on ?
> 
> Anyone know why it isn't the default?

The answer to the latter is probably just that it is imprecise. Don't
read on if that's all you wanted to hear;-)

But before anyone goes suggesting to change the defaults, consider 
that it would be solving the wrong problem

- exhaustiveness of Haskell matching is not decidable, so you end up
    with an approximation; if you want to enforce this particular
    approximation, you end up with "defensive programming"

- defensive programming may look good in blame-driven development
    methods, but is actually bad for fault-tolerance (google for "Erlang
    defensive programming")

- if you are serious about correctness, rather than the appearance of
    correctness, there are other approaches:

    - non-defensive programming with external recovery management
        is the "industry-standard" (well, in Erlang industries, at least;-);

        the idea being that trying to handle all cases everywhere makes
        for unreadable code that doesn't address the problem, while the
        combination of checking cases on entry/handling the expected
        case locally/handling unexpected cases non-locally has been
        demonstrated to work in high-reliability scenarios.

    - checking for non-crashing rather than syntactic coverage is slightly
        better, but will likely only demonstrate that there are cases where
        you can't put in a useful answer locally; 

        you can add local information to the error before raising it (which
        would still be reported as a crash), but trying to handle a case 
        locally (just to avoid the local crash) when you don't have the 
        information to do so is just going to hide bugs (which is not what 
        a coding style or warning should encourage); 

        the way forward, if you believe Erlangers, is to embrace localised
        crashes (google for Erlang motto: "let it crash";-), and have the
        infrastructure in place to automatically add useful information to
        crash messages [1], to programatically deal with crashes (preferably 
        in a separate process on an independent machine), and to factor
        out the recovery code from the business-as-usual code (which is
        why Erlang programs with good fault-tolerance characteristics
        are often shorter than non-Erlang programs without). 

        The added benefit is that when a crash happens, in spite of serious
        efforts spent on proving/testing/model checking, the system doesn't 
        just go "oops!".

    - if you want coverage to have value semantically, you need to 
        restrict the expressiveness of matching. In Haskell/OCaml, that
        would probably mean extensible variants/records, or possibly 
        GADTs, (and no guards), so that you get a type error when 
        *using* a value that is not covered (instead of a syntax error 
        when not covering a value that may not ever be used).

        In particular, if you really care about such things, you should
        check for the good cases once, then convert to types that do
        not have the bad cases. That still doesn't help you with the entry
        check, where the best option for the unexpected case might still
        be to raise an exception, but such a typing strategy makes 
        exhaustiveness checking slightly more useful.

In management brief: enforcing exhaustiveness testing without any 
other measures just ensures that your team members will never tell
you about things they do not know how to handle, so the first sign
of trouble you see is when everything comes down. Establishing a
non-local framework for dealing with local non-exhaustiveness and
encouraging team members to raise alarms when they see trouble
gives your team a chance to notice and recover from issues before
they become critical.

Perhaps syntactic exhaustiveness testing should be renamed to
certified-everything-swept-under-the-rug testing?-)

Note: this is no excuse for using things like 'head' unguardedly in
a large code base! Just that the solution is not to add an empty
case to 'head', but to ensure that 'head' is never called with an
empty list, so "exhaustiveness everywhere" is the wrong target. 
Adding an empty case to 'head' is to raise an alert with useful 
info, working around current weaknesses in the error-reporting 
infrastructure [1], after we've already run into an unhandled 
unexpected case.

If we get a "non-exhaustive" warning, it might mean a variety
of things (you might want to document things like 3/4 in the
types):

1 nothing (error in exhaustiveness checking)
2 nothing (approximation in exhaustiveness checking)
3 nothing (missing cases are proven never to arrive here)
4 nothing (exceptional cases crash here, are dealt with properly
    elsewhere)
5 wrong type used (you shouldn't even try to make this match
    exhaustive, you should use a type that encodes only the
    cases you handle)

beside the naive expectation

- missing case (need to cover the case)

These warnings need to be used with care, and with a 
conscious choice of strategy/framework, not by default. If
you have a good strategy/framework in place, switch them
on, even make them errors, but doing so by default would
only encourage bad coding practices.

Just another opinion (should I've put it on a blog instead?-)
Claus

"Did you want to talk about the weather or were you just
 making chit-chat?" Weather man, Groundhog Day

[1] http://hackage.haskell.org/trac/ghc/wiki/ExplicitCallStack




More information about the Glasgow-haskell-users mailing list