Instantly share code, notes, and snippets.

@vimuel /catch-all.md
Last active Oct 11, 2017

Embed
What would you like to do?
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

This comment has been minimized.

Show comment
Hide comment
@shlevy

shlevy 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)

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)

@razvan-panda

This comment has been minimized.

Show comment
Hide comment
@razvan-panda

razvan-panda Oct 3, 2017

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.

razvan-panda commented Oct 3, 2017

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.

@vimuel

This comment has been minimized.

Show comment
Hide comment
@vimuel

vimuel Oct 3, 2017

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
Owner

vimuel commented Oct 3, 2017

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

This comment has been minimized.

Show comment
Hide comment
@damncabbage

damncabbage Oct 3, 2017

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.

damncabbage commented Oct 3, 2017

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.

@vimuel

This comment has been minimized.

Show comment
Hide comment
@vimuel
Owner

vimuel commented Oct 3, 2017

Good point, @damncabbage

@libeako

This comment has been minimized.

Show comment
Hide comment
@libeako

libeako 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.

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

This comment has been minimized.

Show comment
Hide comment
@jbgi

jbgi 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.

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.

@vimuel

This comment has been minimized.

Show comment
Hide comment
@vimuel

vimuel Oct 3, 2017

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.

Owner

vimuel commented Oct 3, 2017

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

This comment has been minimized.

Show comment
Hide comment
@willtim

willtim Oct 6, 2017

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

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