gvanrossum / patma

Pattern Matching
1.02k stars 64 forks source link

Consider one-off match syntax #21

Open gvanrossum opened 4 years ago

gvanrossum commented 4 years ago

In @ilevkivskyi's proposal there's an interesting shorthand for a match with one case only:

if match <target> as <pattern> [and <guard>]:
    <block>

This would be a shorthand for (using Ivan's syntax):

match <target>:
case <pattern> [if <guard>]:
    <block>
else:
    pass

This is nice and compact. (And it is not a special case of an if statement -- it's a special case of a match statement.)

Question: Should it allow an else clause? (Ivan's PEP doesn't, but it makes some sense). I could imagine allowing

if match <target> as <pattern> [and <guard>]:
    <block>
else:
    <other-block>
ilevkivskyi commented 4 years ago

Should it allow an else clause?

I am not sure about this. On one hand a user would be surprised if it is not allowed, on the other hand this is a slippery slope towards elif match (that I don't really like).

Another important thing here is that my initial motivation was to have one-off match as an opposite to exhaustive match. Because if exception is raised on no match, and one wants to check just a single match, then it is tedious to write the redundant second catch-all pass arm. If we don't raise an exception, then

if match <target> as <pattern> [and <guard>]:
    <block>

is not much better than

match <target>:
as <pattern> [if <guard>]:
    <block>

So there is some dependency between these two decisions IMO.

gvanrossum commented 4 years ago

That's really well thought through. Let's wait until we've reached agreement (again) on whether to raise if no case matches (#5).

Tobias-Kohn commented 4 years ago

Even though I like the idea of a one-off match, I am not too happy about the use of if. As you pointed out here, it suggests a closeness to if, elif and else. So, I would expect that I could do something like:

if isinstance(x, int):
    ...
elif match x as (a, b):
    ...
else:
    ...

I think my own proposal went rather along the lines of using match <target> as <pattern>:, thereby resuing the match keyword, but merging it with the first case—a bit similar to if as statement vs. if as expression.

But as Ivan has said, it might not even be necessary or worthwhile to include a one-off syntax. I am not even certain that we should have it, irrespective of whether the full match statement throws an exception.

gvanrossum commented 4 years ago

I think it would be rather unfortunate for the one-off form to pass by default if the general form raises by default -- it would probably confuse people.

Ivan made the same observation: "So probably whether to raise an exception or not may depend on whether we include if match syntax."

viridia commented 4 years ago

Well, the one-off match syntax - with the no-exception fallback case - closely matches my experience with use cases in other languages that support matching. Also, although C++ has no 'match' keyword, this pattern of code is extremely common in C++17 and later:

if (auto str = dynamic_cast<StringNode>(astNode)) {
  out << str.value;
}

...which is effectively equivalent to a one-off match. In such cases, throwing an error in the 'else' case is rare to non-existent, because the one-off form presumes that you are picking out a single possibility out of many, whereas the regular match statement is presumed to be exhaustive. In fact, in C++ if you wanted to throw an exception, you wouldn't use an if at all, you'd say something like:

// Throws an exception if astNode is not of type StringNode
auto str = try_cast<StringNode>(astNode);
out << str.value;

I agree with Ivan that the two decisions are related. The arguments for throwing an exception for the fall-through case seems strong, but that also increases the motivation for the one-off case being the exception.

One other suggestion: why not just drop the 'if'? Use exactly the proposed syntax otherwise:

match <target> as <pattern> [and <guard>]:
    <block>

The fact that as is on the same line signals that it's a one-off, and prevents users from confusing it with an if-statement.

viridia commented 4 years ago

Thinking about this some more, there's another reason why the one-off match statement shouldn't throw an exception if the match fails: because if it did throw an exception, you wouldn't actually need a new Python statement to implement it!

Since the code following the suite cannot execute unless the suite executes, there's no reason to have an indented block. All you need is something like:

x = ensureInstanceOf(y, Type) # Throws if not isinstance(y, Type), otherwise returns y
# Now do something with x

(This example doesn't support destructuring, but one could envision other specialized wrappers that do.)

gvanrossum commented 4 years ago

Okay, I'm keeping an open mind (although examples from C++ don't really sway me, sorry -- the languages and theirs users are just too different to apply guidance from one to the other).

Everybody seems to agree that the one-off syntax should not throw if the pattern does not match. (And your argument that if it were to throw we wouldn't need the indented block is a good one.)

Does it follow from this that the multi-case syntax should throw if no pattern matches? Or is it rather the other way around, that since the multi-case syntax should throw, we need a one-off syntax that doesn't throw? Or what?

Also, if one form throws and the other form doesn't, maybe they shouldn't look too similar, and if match is better for that reason? (This is from Josh Bloch's API design talk I believe -- "similar things should have similar APIs, and therefore different things should have different APIs.")

viridia commented 4 years ago

To answer your questions: restating what I said before, I think that the 'full' syntax and the 'one-off' syntax represent similar but fundamentally (and qualitatively) distinct use cases. The former is used to handle the entire suite of potential possibilities, whereas the latter is used to select a single possibility out of many, ignoring all the others. One is comprehensive - and should support reasoning about its comprehensiveness - while the other focuses narrowly, and is focused on brevity and convenience.

gvanrossum commented 4 years ago

Then again I wonder if this is just a bee in the bonnet of static type zealots? The first time I encountered someone who cared about exhaustiveness checking it was in C++ code, and they were also more generally into trying to get the compiler to check runtime invariants. The code base was eventually abandoned.

We could have a convention where people who want exhaustiveness checking just add case _: assert False and mypy can special-case that to produce a static error if not all cases are covered. That's not so different from asking people to add type annotations when they want type checking. :-)

viridia commented 4 years ago

Well, a lot of static type checkers in TypeScript support exhaustiveness checking for switch statements if the input type is either an enum or a union of singleton types (eg. 'red' | 'green' | 'blue'). I've gotten used to this.

(I say 'a lot of' because static type checking has a lot of strictness options in the TypeScript world, and different projects choose different levels of strictness. However, bigger, more established companies like to set policies and guidelines for engineers to follow, and that includes tightening the screws on static checking and linting.)

gvanrossum commented 4 years ago

Clearly pattern matching comes from a functional, static checking world. But we have to compromise to make it feel sufficiently Pythonic. We clearly don't all fall on the same side of this issue, so for now let's agree to disagree.

Tobias-Kohn commented 4 years ago

@viridia's reasoning why the one-off case should not throw an exception sound very convincing to me.

However, I wonder if we really want to include the one-off case in this PEP, or rather could postpone it. After all, it is quite orthogonal to the full match-statement in that neither strictly requires the other, and it could easily be seen as an extension (or shorthand) for the more general case.

viridia commented 4 years ago

There is something to be said for limiting the scope of the PEP as much as possible and then extending it later. Once we are able to survey common uses of the full syntax, we may find many potential optimizations and shorthands that we did not anticipate. "The street finds its own uses for things."

This sort of mirrors the evolution of format strings, which went through several evolutionary stages before it got to it's current form.

We will need to write up a 'rejected, but only for now' section on this.

gvanrossum commented 4 years ago

Note: @ilevkivskyi relented on raising when no case matches, and remarked that then we won't need the one-off syntax. Hence I think we're in agreement that we don't need this right now.

(@viridia: you're the master of labels now -- do you think it makes sense to have a separate label for "rejected for now"?)

viridia commented 4 years ago

We can use the postponed label for this.