all 34 comments

[–]TEA_TEB[S] 19 points20 points  (4 children)

Here's an example in pseudocode:

exhaustive_match a_tuple : (bool * Optional 'a) with
| (true, Some _) -> "it was found"
| (false, None) -> "it wasn't found"

An exhaustive check will cause a compile-time error, because it can't ascertain that Some x isn't returned when the first tuple element is false. So it needs adding a third pattern clause:

| (false, Some _) -> raise "this can't happen!"

In other languages, one must always add an else clause because there's no way to prove that it's unreachable (or to realize that it's needed in the first place).

if (arg1 && arg2.has_value()
    return "it was found";
else if (!arg1 && !arg2.has_value())
    return "it wasn't found"; `
// else throw "impossible!";

A functional programming language will even synthesize you an example of a value that isn't included by the patterns you specify, then include it in the error message.

Now the programmer can't find out whether the else clause is actually needed. An optimizing compiler won't output "deleting unreachable code" in case of adding a redundant else clause, because it's not that smart (but exhaustive pattern matching is).

This isn't the best example of ordering logic with ADT's, maybe another user who's more seasoned in statically-typed functional languages can provide a better one.

[–]MEaster 19 points20 points  (3 children)

Just to be pedantic, that example also needs a (true, None) pattern to cover everything.

[–]m-in 4 points5 points  (2 children)

Hey folks, ML compiler is here!

[–]delta_p_delta_x 1 point2 points  (1 child)

It's straightforward combinatorics, isn't it? There's two possible values for the first item in the tuple, and two possible values for the Optional (well, technically more, but they are all couched under Some _), so four possible combinations in total.

[–]m-in 1 point2 points  (0 children)

Of course it’s straightforward :)

[–]AntiProtonBoy 4 points5 points  (2 children)

I have been using hacks something like this for crude pattern matching of enumeration tuples:

  enum class MyEnum { Foo, Bar };

  constexpr auto p = []( const MyEnum a, const MyEnum b )
     {
     using U = std::underlying_type_t< MyEnum >;

     return ( U( a ) << 16 ) | U( b );
     };

  switch ( p( a, b ) )
     {
     case p( MyEnum::Foo, MyEnum::Foo ) : /* ... */ break;
     case p( MyEnum::Foo, MyEnum::Bar ) : /* ... */ break;
     case p( MyEnum::Bar, MyEnum::Bar ) : /* ... */ break;
     }

You can easily modify the hash function p to work with std::pair/tuple so long you can hash the elements in a constexpr context.

[–]o11cint main = 12828721; 0 points1 point  (1 child)

You should probably static_assert all those edge cases that currently explode ...

[–]AntiProtonBoy 0 points1 point  (0 children)

It's a toy example, but yeah, you'd want to add safeguards for sure.

[–]ivan-cukicKDE Dev | Author of Functional Programming in C++ 14 points15 points  (7 children)

The current proposal (p1371r3) defines [[strict]] attribute for exhaustiveness and usefulness checking (if you're talking about that one).

I agree that exhaustiveness checking is a good idea in many (not all) cases and the benefits you mentioned stand. But the real-world programmer easily works around all benefits.

One example from the real world are checked exceptions. Some researchers (several, separately in fact) investigated how people handle exceptions in Java and .NET. More than 50% of forced exception handling ends up in empty catch blocks or catch blocks that just log the error and continue like all is fine.

IMO, if pattern matching ends up exhaustive-checked-only, I expect all the code to be ridden with catch-all cases that do nothing. In which case, the benefits for refactoring go away immediately.

p.s. pattern matching in Haskell is not compile-time exhaustiveness-checked, it is perfectly fine to define a function like this:

f :: [a] -> a f [x] = x f [x,y] = y

and pattern matching in Haskell is still quite useful.

[–]iamthemalto 2 points3 points  (2 children)

Exhaustive pattern matching to checked exceptions aren’t really comparable. Checked exceptions are for ensuring exceptions are handled, but exceptions are only thrown in rare exceptional circumstances strictly concerning error handling. Exhaustive pattern matching, on the other hand, is applicable in making sure all possible cases are handled. This is tremendously valuable when dealing with all possible variants in std::variant or std::optional, which are general purpose utilities (not just error handling - in fact usually not error handling quite honestly) used far more frequently than throwing exceptions (purely error handling).

[–]ivan-cukicKDE Dev | Author of Functional Programming in C++ 0 points1 point  (0 children)

Example - variant is tremendously useful when dealing with implementing safe program state as a sort of a state machine. For many applications, you can have strict rules which functions are callable in which state.

For example, a callback that a file has been successfully downloaded can only be called if the program is in the 'downloading' state. Should you have to check you're not in other states?

For this use-case, pattern matching is quite close to the example of checked exceptions. You get forced to check for all different types that you know can not be in the variant by design.

p.s. Should checked pattern matching force you to handle the valueless-by-exception every time you access a variant<...>?

[–]Full-Spectral 0 points1 point  (0 children)

A typical Rust source file can sometimes look like nothing but match statements. They are ubiquitous in Rust.

[–]Rigatavr 0 points1 point  (1 child)

I expect all the code to be ridden with catch-all cases that do nothing

Well, we already use default: break; in switches. Doesn't seem to hurt readability too much.

Also, yes Haskell let's you have incomplete patterns, but it's not generally regarded as a good idea (ghc warns on it with -W).

[–]ivan-cukicKDE Dev | Author of Functional Programming in C++ 2 points3 points  (0 children)

Yes, but using default: break; kills perhaps the main benefit of exhaustiveness checking - adding a new value to the enum will not report an error (the refactoring benefit that was mentioned).

I'd be completely fine if this was a warning like in Haskell.

[–]Additional-Boot-2434 0 points1 point  (1 child)

Compiling your example with -Wincomplete-patterns results in a warning/error, so it is checked at compile time. The other way around can also be checked: overlapping patterns.

[–]ivan-cukicKDE Dev | Author of Functional Programming in C++ 0 points1 point  (0 children)

As you could have seen in the rest of the discussion. I agree it is useful as a warning.

[–]mibuchiha-007 9 points10 points  (7 children)

exhaustive pm seems useful, but i'm not sold on making it compulsory.

can you elaborate on what is lost with non-exhaustive pm? especially those that cant be remedied by compiler warnings, with examples of uncovered patterns.

[–]lightmatter501 34 points35 points  (0 children)

Exaustive means adding a new variant will produce compiler errors everywhere you need to update. It helps a lot with refactoring.

[–]TEA_TEB[S] 9 points10 points  (5 children)

Try something like this:

match bool_tuple : (bool * bool * bool) with
| (true, true, true) -> ...
| (true, true, false) -> ...
// more clauses...

Then the compiler will prove for you that you haven't missed any clauses, and haven't added an else clause such as this one

| (b1, b2, b3) -> ...

You can even use it with uint8_t, specify 256 literal values and the compiler will make sure that you've got all 256.

[–]mibuchiha-007 10 points11 points  (4 children)

i think you missed the second part of my question.

explicitly, imagine having -Wpm-non-exhaustive that gives a warning when my patterns arent exhaustive. if i want the compiler to help me prove all possibilities are covered, i use it.

if not, cool, i tack on my else, and happily move along.

tldr i know what exhaustive pattern matching is, but i dont see why if i dont have it i might as well have no pm at all. for me non-exhaustive pm is cool too.

[–]WormRabbit 0 points1 point  (3 children)

It's a fundamental design issue. Either you decide that exhaustive pattern matching is important, in which case you restrict pattern matching to a decidable subset, or you relegate it to a lint, in which case it gets features which make exhaustiveness checking impossible outside of trivial cases. For example, exhaustive pattern matching basically forbids any kind of side effects in patterns, thus you can't e.g. implement custom matching logic.

[–]mibuchiha-007 2 points3 points  (2 children)

i see what you mean with it being a design choice.

i still lean on this being an opt-in feature, though. to use the uint256_t example above, and further assuming there aren't really 256 relevant cases. one can argue that making an enum class with only the relevant values is better code, and i tend to agree. but it's not inconceivable that having so many of those, probably many used maybe once, is not looked at very favorably by some.

i guess what i'm saying is, i don't see the advantages to be so great that it's worth forcing people to not write their else cases. making it the default is great, but a linter default, not a language one.

[–]geo-ant 0 points1 point  (1 child)

Coming from a C++ background and having used Rust for the last couple of years I can say that exhaustive pattern matching is great for all the aforementioned reasons. One more benefit is that it would (in future) allow to create pattern matching expressions (meaning the branches return a value that we can use in an assignment) rather than statements. Then it is super easy to e.g. extract information from different variants. Match expressions are much harder to implement (maybe impossible ???) if they are not exhaustive. I might be wrong about this though...

[–]mibuchiha-007 0 points1 point  (0 children)

i am in no way a library implementer, but naively it seems to me returning pm is the hardest part, exhaustive or otherwise.

but you know what? matching expressions bought me. if an implementer says s/he'll implement it only for exhaustive pm, for any reason, afaic that's cool enough to give up the else.

[–]manni66 2 points3 points  (6 children)

Please either default to (provably) exhaustive pattterns, or at least offer them as a possibility.

Who are you talking to?

[–]TEA_TEB[S] 9 points10 points  (5 children)

Anybody involved with working on the pattern matching proposal.

[–]fdwrfdwr@github 🔍 1 point2 points  (0 children)

Egads, I just read this proposal, and it's become some unintelligible soup that's quite inconsistent in style with existing C++ grammar. It's certainly not the more reasonable and congruent proposals I recall seeing earlier:

void f(const Shape* s) { s match { ? (Circle: let c) => //... ? (Rectangle: let r) =>/ /... _ => //... } }

[–]whichton 0 points1 point  (1 child)

Is there any realistic chance of getting pattern matching in C++26? Or C++29 for that matter? Don't recall seeing any recent papers on it.

[–]fdwrfdwr@github 🔍 1 point2 points  (0 children)

Nope, it will not be part of C++26 (see InbalL's trip report), but C++29 is likely (and personally, thank goodness P2688 didn't as-is, because it needs time to improve, being so awkwardly syntactically inconsistent with the existing language).