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.
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 existing
let), 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.
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
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
- 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.
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
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
Float you have some record type and some extension of the same record type.
- Ghc proposal to add Or-patterns to Haskell: https://github.com/ghc-proposals/ghc-proposals/pull/43