Pattern matching is not just sugar for an if-elseif chain, since compilers will check for exhaustiveness. This is especially important when used with sum types (which are the primary motivator for this feature):
data Thing
= Foo String
| Bar Int Double
| Baz String String
case thing of
Foo s -> putStrLn s
Bar i f -> -- ...
Baz s1 s2 -> -- ...
If you later add another variant to Thing, the compiler will check that all pattern matches against it account for the new case. (Wildcard matches alleviate the need always to handle every case.) An if-elseif chain, however, would still compile after adding the new variant, so you must track down the usages manually.
Aha, thanks for that bit, I didn't realize that. I thought Swift, which has a switch/case statement that does not have automatic fallthrough and which has to be exhaustive, was special in that regard. (Although in Swift you can in most cases just do a 'default' case too, not sure how that works with pattern matching)
This is why functional programmers get so grouchy - it's great that Swift has picked up this kind of functionality, but ML family languages have had it for 40 years now :/. Worse is when a feature gets dismissed as "incomprehensible" for years until it gets picked up by some fashionable language and then suddenly everyone's excited about it.
Pattern matching is awesome, and ML definitely gets the credit here for putting it into practice, but matching is arguably not even part of the functional paradigm. Lots of non-functional languages could greatly benefit from pattern matching and sum types.
And indeed, when you look at a language like Nim and Swift, that's exactly what happened. Rust, too, although Rust is arguably a functional language.
I personally think Rust isn't functional, but rather multi-paradigm with a strong functional influence. But if you were to add TCO and a few other features, I think Rust would be a full-blown functional language, so it may get there yet.
In any case, it's useless arguing over (should have known better than say anything), so this will be my last word on the issue.
I wasn't actually looking to argue; I was looking to understand.
And I guess it's going to come down to "whose definition of FP are we using?" If FP means "I can write in a functional style", there are many more functional languages than if FP means "I can only write in a functional style". That debate - about whose definition of FP is correct - is one that I can't bother to care very much about.
I'm still trying to understand (not argue). How is Rust like Haskell and the ML family? My impression of Rust is that it's like C++ but with clear tracking of who owns what memory.
[Edit: I see that the link from jnbiche is probably going to answer my question, though it's going to take me a while to digest it...]
It's immutable by default, with mutation only when managed (most famously a Haskell thing). It has a rich type system with ADTs and generics but without subtyping (very much the ML/Haskell tradition). It distinguishes between validation-like failures and system-level errors, handling the former with values and the latter with panics (i.e. noncaught exceptions) - Erlang is the most famous example of this style, but it's common in Haskell and ML too. It tries to push functionality into libraries rather than the core language - it seems to care about having sensible, consistent semantics rather than ad-hoc.
It really depends on what you think are the features of Haskell that typify its "family". If you think it's typified by things like type inference, sum types, and typeclasses, then Rust will feel very Haskell-y, but if you think it's typified by higher kinded types, laziness, and functional purity, then it won't feel very Haskell-y at all.
Personally, I think Haskell and C++ are the two biggest influences I feel when using Rust: it has a lot of Haskell's philosophy around data types and polymorphism, and a lot of C++'s philosophy around the life cycle of resources, and the cost of abstractions.
I'm in the "no" camp, personally, but we _did_ inherit a lot of things from OCaml. The initial compiler was written in it, a lot of the syntax and features are inspired by it, and it just _feels_ more ML than Haskell.
Rust may not be a functional language, but it draws heavy inspiration from them.
In other languages with pattern matching you often have some sort of wildcard that you can match against. Either an explicitly named variable or, commonly, an underscore (`_`) if the value is unused. Example in erlang:
foo(bar) -> baz;
foo(X) -> bang.
The X is an explicit variable that will match anything not already matched in previous patterns. It can also be used. (NB: Erlang pattern matching is the same or essentially the same for this discussion at each possible level, whether in a function declaration as above or a case statement or when receiving messages.)
Here, we don't care about the thing we've named with an underscore:
tail([_|Tail]) -> Tail.
In Erlang, `[1|[2,3]]` is equivalent to the list `[1,2,3]`. So `[H|T]` as a pattern would have `H = 1` and `T = [2,3]`. Since we don't care about the head of the list, we name it with _. Optionally, in Erlang, you can give it a fuller name:
tail([_Head|Tail]) -> Tail.
The compiler knows that _Head is supposed to be unused and won't complain (warning about unused variables).
True in Haskell, but not e.g. in Scala. In Scala, pattern matching is done using partial functions that are defined only for certain patterns. E.g. it's totally legal to write:
obj match {
case Foo(x) => ...
case Bar(y, z) => ...
// no Baz here
}
In Scala you get the compiler to check for exhaustive pattern matching if you use case classes and sealed classes. So if you want exhaustiveness checks, you can have them.
Not entirely true. You would get a warning if its not exhaustive matching for a sealed trait and you can make warnings into compiler errors with a compiler flag.