Skip to content

Instantly share code, notes, and snippets.

@buggymcbugfix
Last active October 11, 2017 15:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buggymcbugfix/ee9b054b42bbc2ed06992a323b7dfbd8 to your computer and use it in GitHub Desktop.
Save buggymcbugfix/ee9b054b42bbc2ed06992a323b7dfbd8 to your computer and use it in GitHub Desktop.
Catch-all considered harmful?

Catch-all considered harmful?

I have been thinking about a potential source of bugs from catch-all pattern matches and would like to know your thoughts.

Motivation

Totality is usually a desirable property of a function and the catch-all can conveniently buy us totality. But at what price?

I have been indoctrinated that rigour goes above convenience (think along the lines of: "Once we indulge in the impurities of I/O, there is no redemption.")

I would like to evaluate the trade-offs between convenience for the programmer and a potential source of bugs.

My questions to the community—

  1. Are there real world examples of bugs caused by catch-alls?
  2. Do you think that a language extension that disallows catch-alls (and annotations to opt back in at pattern match sites or type declaration) could be useful for certain code bases?
  3. If this is a potential problem, then can you think of any better solutions a compiler could provide (i.e. that don't rely on an IDE / structured editing) other than disallowing catch-alls?

Feel free to chip in with your 2p (or 2¢), but please only if you have any concrete experience (or compelling theoretical evidence).

Example

Consider the sum type:

data Answer = No | Yes

and the function:

foo : Answer -> String
foo Yes = "Woo-hoo!"
foo _   = "Bother."

Say we need to extend our sum type:

data Answer = No | Perhaps | Yes

However, we forget to handle the new case appropriately in foo. The compiler is happy, but at runtime foo Perhaps would evaluate to "Bother."—with potentially catastrophic consequences.

(Please imagine this happening in a large codebase with several contributors, no single one of whom knows the entire codebase.)

@shlevy
Copy link

shlevy commented Oct 3, 2017

Catch-alls that either a) are simply part of sane denotational semantics or b) just give a slightly improved error message are fine. Otherwise, just document your function appropriately or, where possible, use stronger types.

As an example of a, consider an AST with many different forms of literals (int, float, string, etc.) and a function that wants to count how many applications there are in a given term. For that function, a single catch-all to catch all of the AST constructors that don't have subterms seems fine to me (though, admittedly, riskier than just listing them all out)

@freeman42x
Copy link

I think one reason that makes catch all's bad is the fact that the compiler won't guarantee that you will have updated all the places which might require changing when new variants are added.

So I would recommend to not use catch all's unless finding good reasons to tilt the balance in certain use cases.

@buggymcbugfix
Copy link
Author

Idea: disallow catch-all as default and opt-out of the restriction via an (arguably ugly) annotation (or vice-versa):

{-# ANN type Answer <opt-out flag> #-}
data Answer = No | Yes

@damncabbage
Copy link

I'd say they're useful in some particular instances, where you want a function that is only applied to one of the constructors (I think this is a... Prism(?) in Lens terminology).

In particular, if you're proposing a pragma (c.f. previous {-# ANN ... #-} comment), I think it's arguably something you want at the pattern-matching site, rather than where the type is declared. If only because the aforementioned functions are going to be once-offs, but the rest of the time I'd want the safeties turned on.

@buggymcbugfix
Copy link
Author

Good point, @damncabbage

@libeako
Copy link

libeako commented Oct 3, 2017

The problem would be solved by dropping pattern matching as a language feature [or just avoiding to use it] and replacing it by compiler-auto-generated eliminators for each sum type. The eliminator functions would take the continuations for each case of the sum type as parameter. One can not make the mistake to not provide all arguments for a function. As a bonus : the code could naturally be more point-free. Point-free is impossible with the current 'case _ of _' syntax.

@jbgi
Copy link

jbgi commented Oct 3, 2017

@libeako: makeCata is what you are thinking of I believe. It is indeed a good replacement for exhaustive pattern matching. And for case where you don't want to be exhaustive there is Prisms and friends.

@buggymcbugfix
Copy link
Author

Chris Allen added on the Haskell Cafe mailing list:

We made it a policy at a previous company using Haskell to not use
catch-all patterns whenever possible because it meant adding a new
value to a sum type could mean silent problems. We had one bad
experience with that, did the five-whys thing, never did it again.

This mostly applied to the data types we made to represent domain
specific information. Less true for stuff like Int, naturally.

Addendum: it's more a smell when you aren't unconditionally ignoring
an sum type argument - it's when you're special casing some behavior
for specific constructors and then not doing so for others that I
would tend to reject it on code review.

@willtim
Copy link

willtim commented Oct 6, 2017

Haskell needs or-patterns like OCaml, to explicitly handle common cases without writing a catch-all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment