Or patterns
I’d like to suggest introducing or
patterns. I’ll explain the simplest case of this and try to give some justification for inclusion in the language, then work up to more complicated examples. The simplest case is to simply allow multiple cases before the ->
in a case expression. Something like this:
isEmpty : Maybe String -> Bool
isEmpty mString =
case mString of
Nothing
Just "" ->
True
Just _ ->
False
Ocaml is an example language that includes this functionality.
Motivation
Allowing the use of these patterns has the following advantages:
- It’s a bit easier to see that two (or more) cases produce the same answer.
- Some code duplication removal. If multiple cases have the same answer, then currently you have to copy that answer. If the answer is a complicated expression you can use a
let
(or just another definition within an existinglet
), however doing so produces names at a scope larger than you would really want, and may involve unnecessary computation (I don’t know if the compiler is smart enough to move such expressions around). In particular the name that is introduced must be available in all cases, even though you are only using it in a subset of them. - Speculative: I would hope it would reduce the instances of
_ ->
because it’s easier to list the remaining cases, thus meaning that updates to a custom type are more likely to produce a compiler error where appropriate. - More speculative: It’s possible that allowing this can improve the compilation of pattern matching. See for example: http://pauillac.inria.fr/~maranget/papers/opat/
The two patterns in the or
pattern above do not introduce any new variables. But they could, and when that happens of course both patterns must introduce the same names at the same types.
In theory or
patterns could be restricted to the top level but they could also be sub-patterns, Such as:
containsEmptyName : Tree -> Bool
containsEmptyName tree =
case tree of
Node (Nothing | Just "") _ _ ->
True
Node (Just _) left right ->
case containsEmptyName left of
True ->
True
False ->
containsEmptyName right
Leaf _ ->
False
Here the or
pattern is nested within a constructor pattern. I’ve had to invent syntax for it and used |
to separate the two patterns. I’m not suggesting that this is indeed the appropriate syntax.
Small example, consider testing an Http.Error for whether or not it is probably network related:
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus Int
| BadBody String
Currently you have to write this as:
isNetworkRelated : Error -> Bool
isNetworkRelated error =
case error of
Timeout ->
True
NetworkError ->
True
BadUrl _ ->
False
BadStatus _ ->
False
BadBody _ ->
False
The lazy might do this following, meaning you won’t get warned if a new constructor is added to the Error type:
isNetworkRelated : Error -> Bool
isNetworkRelated error =
case error of
Timeout ->
True
NetworkError ->
True
_ ->
False
Under the proposal to add or-patterns you can do this:
isNetworkRelated : Error -> Bool
isNetworkRelated error =
case error of
Timeout
NetworkError ->
True
BadUrl _
BadStatus _
BadBody _ ->
False
It’s arguable, but to my mind the latter is clearer than the lazy version even if we forget about being warned when a constructor is added to the Error
type.
Disadvantages
- It increases the complexity of the language, I would say fairly modestly.
- This would likely lead to developers asking for ‘guards’ on cases a la Haskell, or ‘not’ patterns.
- It might be easier to mistakenly include a pattern in a group? Well it almost certainly would be easier, I’m not sure whether it represents a major problem.
- Speculative: Might produce error messages that are difficult to understand.
Not a disadvantage but a counter-argument to advantage number 2: This doesn’t solve all instances of this problem because sometimes multiple cases share an intermediate value but not the entire answer.
Other considerations
A potential corner case is related with Elm’s special type-variables
type Number = MyInt Int | MyFloat Float
...
case n of
MyInt x
MyFloat x ->
x + 1
I think in this case the compiler should reject the or-pattern as defining the same variable with different types, the fact that the expression is valid for both is neither here-nor-there as you could, for example get the same thing by using identity
or always
when the two different types don’t even share a class. Note that in any case the result of the expression is a different type anyway, though it’s isn’t hard to come up with an expression that would have exhibit this problem but produce the same type.
Extensible record types may also represent something of a corner-case. Consider the same example but instead of Int
and Float
you have some record type and some extension of the same record type.
Relevant link
- Ghc proposal to add Or-patterns to Haskell: https://github.com/ghc-proposals/ghc-proposals/pull/43