Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guards to match patterns in GDScript #4775

Closed
vnen opened this issue Jun 29, 2022 · 41 comments · Fixed by godotengine/godot#80085
Closed

Add guards to match patterns in GDScript #4775

vnen opened this issue Jun 29, 2022 · 41 comments · Fixed by godotengine/godot#80085
Milestone

Comments

@vnen
Copy link
Member

vnen commented Jun 29, 2022

Describe the project you are working on

The GDScript implementation.

Describe the problem or limitation you are having in your project

The GDScript implementation.When using a match statement, there's no natural way to implement guards as it's done in other languages with this functionality. The close we can get is using an if statement inside a block together with the continue keyword, like this:

match arr:
	[var x, var y]:
		if x >= y:
			continue
		print(" x is less than y")
	[var x, var y]:
		if x <= y:
			continue
		print(" x is more than y")
			

This is awkward because the "guards" in this case have the opposite conditions that you want to capture. It needs more mental effort to understand what is happening.

Another case is using ranges instead of absolute values when matching numbers:

match number:
	var n:
		if n != 0:
			continue
		print("number is zero")
	var n:
		if n >= 5:
			continue
		print("number is less than 5")

Which is better expressed as series of if blocks:

if number == 0:
	print("number is zero")
elif number < 5:
	print("number is less than 5")

But loses the more obvious approach with match that makes it all about the same number, since an elif can have any arbitrary expression.

There's also the issue that the continue keyword is grabbed by the match statement, so you can't use it to go to the next iteration of a loop inside it.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Using match guards we are able to take advantage of the match syntax while still being able to use arbitrary conditional expressions to further restrict each pattern arm. This without the need of having awkward if statements with negations inside the block.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Edit: Added -> token for disambiguation.

Following a syntax similar to what Python and other languages do, we could add an if keyword after the match pattern, which could have any expression as the condition. This would come after the -> token to avoid mixing up with the ternary expression in the pattern (while arbitrary expression aren't valid patterns, they still work if evaluated at compile time, including the ternary):

match arr:
	[var x, var y] -> if x < y:
		print(" x is less than y")
	[var x, var y] -> if x > y:
		print(" x is more than y")

Which makes the blocks much more straightforward to understand.

For the example of ranges:

match number:
	var n -> if n == 0:
		print("number is zero")
	var n -> if n < 5:
		print("number is less than 5")

Note that condition for the guard can use any arbitrary expression and use identifiers in the scope.

func foo(x, y)
match x:
	1 -> if y > 5:
		bar()
	2 -> if y < 5:
		baz()
	1, 2: # Will match if the guards fail
		qux()

Guards are only evaluated after the pattern matches. If there are multiple patterns, the condition applies to all of them. That is, if any of pattern matches, then the guard expression will be evaluated.

match arr:
	[1, 2, ..], [2, 1, ..] -> if len(arr) == 4:
		# Matches [1, 2, x, x] and [2, 1, x, x].
		print(arr)
	_:
		return

This proposal also includes removing the use of continue inside match. This will allow user to use the keyword to go to the next iteration of the loop, even when inside match.

If this enhancement will not be used often, can it be worked around with a few lines of script?

As mentioned before, it can be worked around but it's cumbersome and makes for a confusing use of the continue keyword. Many users don't know about this usage and trip when trying to use the keyword for the loop inside a match statement.

Is there a reason why this should be core and not an add-on in the asset library?

This is an integral part of GDScript, it cannot be implemented as an add-on.

@mieldepoche
Copy link

This proposal also includes removing the use of continue inside match. This will allow user to use the keyword to go to the next iteration of the loop, even when inside match.

I suppose this means no more matching multiple patterns in one match?

@vnen
Copy link
Member Author

vnen commented Jun 29, 2022

I suppose this means no more matching multiple patterns in one match?

No, multiple patterns would still work. Check the sample code right above the phrase you quoted.

@mieldepoche
Copy link

No, multiple patterns would still work. Check the sample code right above the phrase you quoted.

I was reffering to matching several cases in the match block at once (basically what the continue keyword offered):

var damage_data := {amount=50, fire_duration=1, fire_amount=10}

match damage_data:
	{"amount": var amount, ..}:
		# take raw damage
		continue
	{"fire_amount": var fire, "fire_duration": var duration, ..}:
		# setup fire damage
		continue

@Calinou
Copy link
Member

Calinou commented Jun 29, 2022

Related to #160 (which is about the use of continue as fallthrough).

@vnen
Copy link
Member Author

vnen commented Jun 29, 2022

I was reffering to matching several cases in the match block at once (basically what the continue keyword offered):

Ah, yes, this wouldn't work anymore. But is there any reason to use continue without any condition? At worst you can simply use multiple match blocks:

var damage_data := {amount=50, fire_duration=1, fire_amount=10}

match damage_data:
	{"amount": var amount, ..}:
		# take raw damage
match damage_data:
	{"fire_amount": var fire, "fire_duration": var duration, ..}:
		# setup fire damage

Unless you have very complex logic to trigger the continue. In such case I think it's probably difficult to read and splitting the match statements and moving the condition outside may still be better.

@Janders1800
Copy link

Wouldn't be easier to make match use another keyword instead of continue, like cascade, skip, fall, etc.?
That way you get the best of both worlds.

@GreenMoonMoon
Copy link

GreenMoonMoon commented Jun 29, 2022

Wouldn't be easier to make match use another keyword instead of continue, like cascade, skip, fall, etc.?
That way you get the best of both worlds.

It seems using continue to match multiple condition is an uncommon issue that can be worked around with few line of scripts if the proposal was implemented while using the keyword to iterate on the loop should be the common usage.
Breaking down a multi-match into a series of match or a mix of iss and matchs would be more readable as it's expected in other languages that a match case is exclusionary

@vnen
Copy link
Member Author

vnen commented Jun 29, 2022

Wouldn't be easier to make match use another keyword instead of continue, like cascade, skip, fall, etc.? That way you get the best of both worlds.

The point here is that using continue is quite awkward when you want actual pattern guards, it's not only about reusing the keyword. It's also about making it more similar to other languages, which helps learning GDScript in general.

Using a different keyword is difficult because it would be harder for users to discover it and many of the suggestion I see either doesn't convey what it actually does (like fallthrough) or uses a common word that's likely to be used as an identifier (like skip). Rather than changing the keyword, I believe it would be better to have something that enables continue to act on outer scopes (which would also be useful for e.g. acting on nested loops). But that's a different proposal and if added it wouldn't solve all the issues I pointed out here.

@dalexeev
Copy link
Member

I like it, but I'm not sure about the syntax, it looks like a ternary operator without else branch.

match x:
    1 if y > 5:
        bar()
    2 if y < 5:
        baz()
    1, 2: # Will match if the guards fail
        qux()

Perhaps some separator or other keyword is needed instead of if.

@dalexeev
Copy link
Member

Do I understand correctly that now only one match branch can be executed? And the following code

match x:
    1, 2:
        print("a")
        continue
    1:
        print("b1")
    2:
        print("b2")

must now be:

match x:
    1:
        print("a")
        print("b1")
    2:
        print("a")
        print("b2")

The part that is common to several branches should now be duplicated or be moved to a separate match or if?

@vnen
Copy link
Member Author

vnen commented Jan 25, 2023

[...]
The part that is common to several branches should now be duplicated or be moved to a separate match or if?

Yes, that's the one drawback of this. However, I don't believe continue is used that often in match and having multiple patterns executing can easily become difficult to read. It's not equivalent to regular switch because the fallthrough always run the next case but with match it can run any of the following branches (or none at all).

@krazy-j
Copy link

krazy-j commented Jan 30, 2023

Overall, I like this change, although I must point out that I find the ability to use continue to run multiple branches extremely useful, and I've used it many times. It's especially useful when unpacking values from a dictionary where values can be omitted. For example (slightly modified from one of my projects)

match data:
    {"pos": var pos, ..}:
        position = pos
        continue
    {"size": var size, ..}:
        if shape is RectangleShape2D:
            shape.extents = size / 2
        continue
    {"enabled": true, ..}:
        enable()
        continue
    # ...and more...

Would now have to each be a separate match statement (which defeats the purpose of match, in my opinion), or

if data.has("pos"):
    position = data.pos
if data.has("size") and shape is RectangleShape2D:
    shape.extents = data.size / 2
if data.has("enabled") and data.enabled is bool and data.enabled:
    enable()
# ...and more...

...which requires checking to see if a key exists on the Dictionary, then getting the value. Although, looking at it now, perhaps that's exactly how I should be doing it. Overall, I think this is a good change, I just worry that the very convenient ability to easily check many Dictionary values will be gone.

@vnen
Copy link
Member Author

vnen commented Jul 31, 2023

Added the -> token to the proposal to avoid mixing up with the ternary as @dalexeev pointed out (only noticed it now, sorry).

@dalexeev
Copy link
Member

dalexeev commented Jul 31, 2023

  1. -> is already used as a return type hint delimiter.
  2. The arrow usually means implication or that the left side is the cause and the right side is the effect. And we have the opposite situation, first the right condition is checked, and then the left pattern.
  3. Also, in my opinion, the separator mixing characters and words looks strange: -> if.

I was thinking about when keyword or similar. Compare:

match x:
    0 -> if y == 0:
        print("y is zero")
    0 -> if y < 5:
        print("y is less than 5")
match x:
    0 :: if y == 0:
        print("y is zero")
    0 :: if y < 5:
        print("y is less than 5")
match x:
    0 when y == 0:
        print("y is zero")
    0 when y < 5:
        print("y is less than 5")

@vnen
Copy link
Member Author

vnen commented Jul 31, 2023

  • -> is already used as a return type hint delimiter.

Same tokens are used in different contexts, it's not exclusive to this one. The context makes it very obvious this isn't a return type. I prefer using something already in the language than introducing yet more tokens and keywords.

  • The arrow usually means implication or that the left side is the cause and the right side is the effect. And we have the opposite situation, first the right condition is checked, and then the left pattern.

That's not correct, the right condition is checked only after the pattern matches. Not that it matters much because this an and condition, so order isn't relevant. So the arrow meaning "this after that" is not really a problem IMO.

  • Also, in my opinion, the separator mixing characters and words looks strange: -> if.

That's very subjective. Also, I see the separator as -> and the if only identifies this is a condition. Technically we can remove the if but then I (subjectively) feel that something is missing:

match x:
    0 -> y == 0:
        print("y is zero")
    0 -> y < 5:
        print("y is less than 5")

I was thinking about when keyword or similar.
[...]

match x:
    0 when y == 0:
        print("y is zero")
    0 when y < 5:
        print("y is less than 5")

I think introducing a new keyword just makes things harder. It's one more name to remember, while if is already commonplace in terms of checking for conditions. I would prefer just using if if the separator isn't wanted (it makes parsing a bit harder but it's technically possible, the missing else is enough to tell it ins't a ternary).

@vonagam
Copy link

vonagam commented Aug 1, 2023

I didn't get why -> was needed. It does not look ergonomic. Python has the same syntax for ternary and has if for guards, without arrows. Ternary is not a valid pattern, so there should be no problem on that front.

@dalexeev
Copy link
Member

dalexeev commented Aug 2, 2023

I asked for opinions in a local Godot group, and got confirmation that some people also associate -> co with implication and functions/lambdas/predicates.

Also, I'm concerned that the guard separator consists of two tokens. In my opinion, when it is a single token, its role is more clear (separate the guard expression from the pattern list).

func _ready() -> void:
    var x := 1
    var y := 2
    var a := true
    var b := false
    match x:
        1 if true else 2 -> if a if y == 2 else b:
#       ^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^
#       const expression guard guard expression
#       pattern          sep.
            print("Branch 0")
        _:
            print("Branch 1")

The problem with the two-token separator is that it is not solid. There is a space, many people will read it like this:

  1 if true else 2 -> if a if y == 2 else b:
# ^^^^^^^^^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^
# something left      something right

Yes, this example seems far-fetched, but it demonstrates that both pattern and guard can include a ternary operator. Now, the pattern expression must be constant (unless it's an identifier or an attribute chain). [code 1] [code 2] But that's reason enough, in my opinion. Plus, we may allow arbitrary non-constant expressions (like function calls) in the future, as far as I remember there is an issue about this.

Compare:

  1 if true else 2 when a if y == 2 else b:
# ^^^^^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^
# const expression g.   guard expression
# pattern          sep.

Alternatively, we could use while if we want to avoid introducing new tokens. But it doesn't seem like a good idea to me, since it might make associations with while loop, but there is no loop here.

  1 if true else 2 while a if y == 2 else b:
# ^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^
# const expression guard guard expression
# pattern          sep.

@vonagam
Copy link

vonagam commented Aug 2, 2023

Personally I find python style ternary atrocious, so yeah, if you pile those nasty things it does not look good. Even your example with when seems not really good (better than -> if though).

Even a simple ternary expression as a pattern without a guard seems strange to me:

match value:
    1 if true else 2:
        print("hm")

But, again, python has that ternary syntax and they were ok with marking guard with if. Most likely because it is rare to for ternaries to be there. And if you have to use them for some reason and need more readability just add parentheses and things will look clearer. So I don't think that because of rarer case of ternary the syntax for more common usages should suffer.

@vnen
Copy link
Member Author

vnen commented Aug 3, 2023

I didn't get why -> was needed. It does not look ergonomic. Python has the same syntax for ternary and has if for guards, without arrows. Ternary is not a valid pattern, so there should be no problem on that front.

Patterns can be constant expressions, which can include the ternary if. There's even a test case for it already. Disallowing this now would be breaking compatibility (which honestly I don't mind that much, is very rare someone actually need this).

In Python patterns are strictly literals, never expressions, and they introduced guards right away with match, so they could go a different route. You can have a ternary in the guard itself, but that is past the point of ambiguity.

The problem with the two-token separator is that it is not solid. There is a space, many people will read it like this:

  1 if true else 2 -> if a if y == 2 else b:
# ^^^^^^^^^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^
# something left      something right

[...]
Compare:

  1 if true else 2 when a if y == 2 else b:
# ^^^^^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^
# const expression g.   guard expression
# pattern          sep.

It is meant to be read as two sides (pattern and guard) so I don't understand what is the actual problem. I feel like that space is actually better for reading. I can see the -> token much easier than I can see a keyword like when.

Perhaps not the -> token itself but another one and perhaps without the extra if, but I do think that adding a keyword for this is detrimental IMO. If we're using a keyword, let's just use if.


I do think @vonagam gave a good idea: force using parenthesis if you really need the ternary inside the pattern:

match x:
	1 if true: # This is a guard
		pass
	1 if true else 2: # This is not allowed
		pass
	(1 if true else 2): # This is fine
		pass
	(1 if true else 2) if true: # Ternary pattern with guard
		pass

This way the parser can disambiguate the ternary from the guard, while still allowing some way of making a ternary pattern.

Now, this can still allow some atrocious writing:

match x:
	(true if true else false) if true if true else false:
		pass

But we shouldn't measure by the worst possible case (though the parenthesis actually help in this one). This is already a "problem" with the regular if statement:

if x if true else y:

That does break compatibility, but at least provide an alternative and one that also works on the current version so there's no need for a version switch.

@vonagam
Copy link

vonagam commented Aug 3, 2023

Ah, so those things are ok:

print(1 if true if false else false else 2)
print(1 if false else 2 if false else 3)

Yeah, I see how this will complicate parsing those cases, even if there is only one valid interpretation of a case it still can be problematic to write parser logic for that. (I mean that I think it is possible to do without breaking change, but it might be complicated, maybe too much...)

An alternative to -> if and when can be and if - no new keywords, but reads better in my opinion than arrow one.

@vnen
Copy link
Member Author

vnen commented Aug 3, 2023

An alternative to -> if and when can be and if - no new keywords, but reads better in my opinion than arrow one.

What about using | instead?

match x:
	0 | if y < 0:
		print(n)

@vonagam
Copy link

vonagam commented Aug 4, 2023

Well, | by itself stands for bitwise OR, while in this case it is and condition. So it should be & if you go for shorter symbol. By even then it is mix of symbol and word operators which is meh, in my opinion.

If you go for shorter - just do and.

match value:
 [var x] and x > 0:
   print(x)

There should be no problem with constants, since the point here is that right side is non-constant expression. Is there any downside that I do not see? Edit: I think I found some downside, so can pass on simple and.

@dalexeev
Copy link
Member

dalexeev commented Aug 4, 2023

What about using | instead?

To me it looks better than an arrow. It looks like:

https://en.wikipedia.org/wiki/Vertical_bar:

set-builder notation: { x | x < 2 }, read "the set of x such that x is less than two". Often, a colon ':' is used instead of a vertical bar

The vertical bar is used for list comprehensions in some functional languages, e.g. Haskell and Erlang. Compare set-builder notation.

But I'm not sure if our parser can handle it as easily as with ->, because | is bitwise OR operator and after that the parser probably expects the next operand. And vonagam is right that there is some chance that users will read this as OR (while it's AND), not as guard separator.

@adamscott
Copy link
Member

I for one prefer the when keyword. It's not a if statement per se, so when makes sense. And it makes it possible to read the line in simple English, which is in line with the design of the language.

@Calinou
Copy link
Member

Calinou commented Aug 14, 2023

I for one prefer the when keyword. It's not a if statement per se, so when makes sense. And it makes it possible to read the line in simple English, which is in line with the design of the language.

I've mentioned when as a possible candidate for compile-time if statements (like in Nim), so it would conflict with that use case.

@adamscott
Copy link
Member

I've mentioned when as a possible candidate for compile-time if statements (like in Nim), so it would conflict with that use case.

I don't think it's a "hard" conflict. Another keyword could be used for compile-time if statements too.

@nlupugla
Copy link

I think it's important that whatever syntax we choose also reads well when used in conjunction with the proposals for a type narrowing syntax. For example.

match x:
    var y: int: y > 0:
        # code treating x as a positive int named y

or

match x:
    var y: int if y > 0:
        # code treating x as a positive int named y

or

match x:
    var y: int when y > 0:
        # code treating x as a positive int named y

@Xananax
Copy link

Xananax commented Aug 14, 2023

To summarize the suggestions for syntax we have until now:

  1. M -> C
  2. M -> if C
  3. M | C
  4. M if C
  5. M when C
  6. M and C
  7. M and if C

I think I got them all?

It's very difficult and somewhat unproductive to discuss such things from a rational point of view, because all of it is subjective. I'm not going to attempt having very objective reasons. I will however provide my 2c:

->, | would be my favorite (they look like languages I like) but I think that are not very good contenders. If there is a will for GDScript to be consistent, then this breaks the consistency of using keywords instead of symbols that is used elsewhere.

For the same reasons of consistency, I would remove anything requiring a sequence of tokens, like and if or -> if.

That leaves

  1. M if C
  2. M when C
  3. M and C

All three are valid, for my money, I don't feel either is a big departure from the language, or too difficult to teach.

Out of these, if is my favorite, as it is concise, does not introduce a new keyword in the lexicon, and expresses the intent in what I think is the least-ambiguous way for beginners.

I do understand the issues with ternaries, and I agree there's a problem there. I already see myself explaining 3 times a day on Discord that "the if used in match is not the same as the if somewhere else). I still think it is the best tradeoff overall.

Would it be possible to treat an if as a ternary or as a guard, depending on the presence of else? If not, then enforcing parenthesis would be best indeed.

Second favorite is when, which I think also explains very well the intent, disambiguates from if. It introduces a new word, but that's ok. For its usage as comptime instruction, I would suggest that should be an annotation @comptime or keyword comptime, something of the sort.

@dalexeev
Copy link
Member

I think I got them all?

If there is a will for GDScript to be consistent, then this breaks the consistency of using keywords instead of symbols that is used elsewhere.

I also suggested ::, but I agree that we prefer keywords over symbols. Also we can use while as it is not currently used inside expressions (and so it should be easy from the technical side, unlike if). But I don't think this is a good idea, because it might be associated with while loop. I don't think our key argument should be "don't introduce new keywords as long as we can use existing ones", because in some cases existing keywords may don't fit well semantically.

Second favorite is when, which I think also explains very well the intent, disambiguates from if. It introduces a new word, but that's ok. For its usage as comptime instruction, I would suggest that should be an annotation @comptime or keyword comptime, something of the sort.

I think that we could reuse existing keywords in this case. For example, static if has already been suggested. Also a good alternative is const if, because const in GDScript is the equivalent of constexpr in C++.

@vonagam
Copy link

vonagam commented Aug 14, 2023

That leaves

  1. M if C
  2. M when C
  3. M and C

Good summary. Can remove and though because it's behavior will change depending on whenever M and C are constants or not - (value == M) && C vs value == (M && C).

Would it be possible to treat an if as a ternary or as a guard, depending on the presence of else?

Yes, it should be. There whole thing about finding alternative syntax to simple if happens because "it is possibly confusing", not because "it is breaking". I do not agree with that. Using simple if is consistent with python (which most of the things in gdscript syntax come from, like ternary), not hard to implement and does not require any breaking changes. Ternaries are rare in a match case and there is already an optional way to increase readability for any confusing expression - parentheses.

I would understand the search for alternatives if there was an ambiguous (for parser) statement with ternary. But there is no such example, right?

@dalexeev
Copy link
Member

Using simple if is consistent with python (which most of the things in gdscript syntax come from, like ternary), not hard to implement and does not require any breaking changes.

I don't think the comparison with Python is correct. Python doesn't support ternary patterns, but we don't have to copy this constraint, our match can be more feature rich (and it's already partially done). Yes, GDScript is similar in some ways to Python, but I think that there are much more differences between these languages. We can take Python experience into account when making decisions, but we shouldn't depend on or rely on it.

Other languages mostly have C-style ternary operator, so there can be no conflict with if.

I would understand the search for alternatives if there was an ambiguous (for parser) statement with ternary. But there is no such example, right?

See GDScript progress report: Writing a new parser - Less lookahead:

Quote

The previous parser relied on the tokenizer ability to buffer tokens to ask for a few the next (and sometimes the previous) tokens in order to decide what the code means. The new tokenizer lacks this ability on purpose to make sure we can use the bare minimum of a lookahead to parse the code.

Minimizing lookahead means the code is easier to parse. If we were to introduce a new syntax for a feature, now we have to make sure it does not increase the complexity of the parser by demanding too much tokens at once.

The new parser only stores the current and previous token (or the current and the next if you look it in another way). It means that GDScript only needs one token of lookahead to be fully parsed. This takes us to a simpler language design and implementation.

Either we 1. restrict expressions in the context of match patterns so that the first if is treated as a pattern guard separator, or 2. we need a more complex parser with more lookahead to decide later (by absence of else) if if was a ternary operator or a pattern guard separator.

  1. I think this is not good, not only because it formally breaks compatibility, but also because it is a less clear design. Even if this is a rare case, I see no reason to forbid it and make expressions in the context of patterns something different than in other contexts.
  2. In my opinion, it's not worth the complication. This is not only more difficult for the parser, but also less human readable. I would prefer either Option 1 or a new token (when or something else) if we want to support the ternary operator in patterns.

@vonagam
Copy link

vonagam commented Aug 15, 2023

I don't think the comparison with Python is correct. Python doesn't support ternary patterns, but we don't have to copy this constraint, our match can be more feature rich (and it's already partially done).

Either we 1. restrict expressions in the context of match patterns so that the first if is treated as a pattern guard separator

I mean, yes, we don't have to copy that constraint. That's what I am saying, is there anything that blocks supporting combination of ternaries and guards? If yes, an example would be beneficial.

  1. we need a more complex parser with more lookahead to decide later (by absence of else) if if was a ternary operator or a pattern guard separator.

At this position in parser you would just add check call (if we are in a case). And if there is no else return a GuardNode instead of TernaryOpNode. (There are other small changes, but that's the relevant part for parsing.) Is it complicates some other things too much?

@dalexeev
Copy link
Member

At this position in parser you would just add check call (if we are in a case). And if there is no else return a GuardNode instead of TernaryOpNode. (There are other small changes, but that's the relevant part for parsing.) Is it complicates some other things too much?

Well, it's probably not as difficult to implement as I thought (although it still requires context and should only work for the top-level "ternary", not for nested ternaries). With another token, such hacks are not needed.

My main concern is not technical, but semantic. It seems wrong to me to use the same token in different meanings:

I do understand the issues with ternaries, and I agree there's a problem there. I already see myself explaining 3 times a day on Discord that "the if used in match is not the same as the if somewhere else).

@vonagam
Copy link

vonagam commented Aug 15, 2023

Python uses it. A lot of people come from there and expect similar syntax. Other languages use if too - surface level googling showed that Rust and Scala use if, I assume they are not only ones.

So unless there is something that blocks using if on technical level I don't see why should we diverge from Python here and introduce a breaking change when we have pretty acceptable syntax already with which this proposal has started.

@dalexeev
Copy link
Member

Python uses it.

Python doesn't support ternary patterns, but we don't have to copy this constraint, our match can be more feature rich (and it's already partially done).

A lot of people come from there and expect similar syntax.

We don't have to repeat Python, it's okay that there are differences. For example, we don't have a case. I expect users to read the documentation or the changelog and not try to copy code from another language. All languages are slightly different.

I emphasize that the goal is not to do "different from Python". It's just that Python initially chose a more limited option, while we can (and already partially do) support pattern expressions more universally. And in this case, the if token is not very suitable, in my opinion.

Plus maybe even with the limitation it's not a good idea in Python. We shouldn't blindly copy it as "this is how it's done in Python". Python can have bad solutions too, and we should discuss any solution, whether it's good specifically in GDScript, before copying it from Python.

Other languages use if too - surface level googling showed that Rust and Scala use if, I assume they are not only ones.

Other languages mostly have C-style ternary operator, so there can be no conflict with if.

@vonagam
Copy link

vonagam commented Aug 15, 2023

There are two different arguments here that I've already addressed:

  1. if is somehow technically complicated in terms of working with ternaries and limits us somehow. For that I've asked for an example of limitation.
  2. if is confusing for some reason and when is better. Personally I do not agree, both if and when work in my opinion but when will be a breaking change. Also this proposal has only thumbs ups and it was/is about if, so people here do not seem to find it confusing.

@Xananax
Copy link

Xananax commented Aug 15, 2023

I don't see an issue for forbidding ternaries in match either. Is there some real-life example of a ternary expression in match? I can't think of one. There might be something obvious I'm missing, but seems to me like a feature that isn't used or obvious.

@dalexeev
Copy link
Member

dalexeev commented Aug 15, 2023

  1. if is somehow technically complicated in terms of working with ternaries and limits us somehow. For that I've asked for an example of limitation.

I think operator precedence and parsing order for equal precedence may prevent us from implementing Option 2 (allow ternary operators).

The as operator has lower precedence than the ternary operator (we can ignore assignments), while the match pattern operator should have the lowest precedence.

Also, if you are proposing to allow both ternary patterns and ternary pattern guards, then we must consider all 4 cases:

p1 if g1
(p1 if p2 else p3) if g1
p1 if (g1 if g2 else g3)
(p1 if p2 else p3) if (g1 if g2 else g3)

Without parentheses:

p1 if g1
p1 if p2 else p3 if g1
p1 if g1 if g2 else g3
p1 if p2 else p3 if g1 if g2 else g3

Since you said that parentheses are only for readability and should not affect the result, I tried to check how the parser will consume tokens. To do this, I added fake operands.

    print(pc if gc else fk)
    print(pa if pc else pb if gc else fk)
    print(pc if ga if gc else gb else fk)
    print(pa if pc else pb if ga if gc else gb else fk)
|   |   print( (pc) IF (gc) ELSE (fk) )
|   |   print( (pa) IF (pc) ELSE ((pb) IF (gc) ELSE (fk)) )
|   |   print( (pc) IF ((ga) IF (gc) ELSE (gb)) ELSE (fk) )
|   |   print( (pa) IF (pc) ELSE ((pb) IF ((ga) IF (gc) ELSE (gb)) ELSE (fk)) )

It looks like the parser is grouping the if/else in an order that would be difficult for us to process. So I think Option 2 is hard to implement. Option 1 is much more practical: completely disable the ternary operator in patterns and make the first if start a pattern guard.

For comparison, there is no ambiguity with when:

p1 when g1
p1 if p2 else p3 when g1
p1 when g1 if g2 else g3
p1 if p2 else p3 when g1 if g2 else g3

Is there some real-life example of a ternary expression in match?

I'll try to find an example later. Yes, this is rare case, but I see no reason to forbid it.

@vonagam
Copy link

vonagam commented Aug 15, 2023

in an order that would be difficult for us to process

Hm... that's a valid thing. That order will complicate things. Might be possible still, but more complex than worth it. Ok, I switch to when as preferable option.

@nlupugla
Copy link

nlupugla commented Aug 15, 2023

I like when also, but if we are worried that when might be useful for something else in the future, we could use only if or onlyif as an alternative. Perhaps also and if, andif, & if, &if, etc...

@krazy-j
Copy link

krazy-j commented Aug 17, 2023

It would be ideal to have something that uses an existing keyword, can't be ambiguous, and makes sense. I thought of reusing the keyword match.

match x:
    var y match y > 0:
        print("x is greater than 0")

It's just an idea, but it seems to fit the requirements better than other keywords. match definitely isn't valid in an expression (unlike if or and), so there is no conflict. It also isn't confusing (unlike while) as it is checking that the value matches the condition. if also works, but needing to check for a matching else seems a bit complicated and less readable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.