'switch-expression' and the 'type guard' unary operator demo

php.internals

Kosit Supanyo

6 years ago
Hi Internals I've just finished an implementation of 'switch-expression' that have been discussed recently. So I would like to present my ideas here. The basic syntax of switch-expression in this implementation is: $result = switch ($expr) { case $cond1 => $result1, case $cond2 => $result2, case $cond3 => $result3, default => $default_result, }; Like function declaration and function expression in JavaScript, if `switch` appears as first token at statement level it will be recognized as statement but if `switch` is in expression context it will be switch-expression. switch ($expr) { case $cond1 => $result1, case $cond2 => $result2, case $cond3 => $result3, default => $default_result, }; // Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW) But this is OK. !switch ($expr) { case $cond1 => $result1, case $cond2 => $result2, case $cond3 => $result3, default => $default_result, }; // semicolon is still required because it is an expression You can also use comma to associate multiple conditions with single result expression. $result = switch ($expr) { case $cond1 => $result1, case $cond2 => $result2, case $cond3, $cond4, $cond5 => $result3, default => $default_result, }; Which is equivalent to: $result = switch ($expr) { case $cond1 => $result1, case $cond2 => $result2, case $cond3 => $result3, case $cond4 => $result3, case $cond5 => $result3, default => $default_result, }; If there's no default value or switch-expression will just give NULL. $x = 'c'; $v = switch ($x) { case 'a' => 1, case 'b' => 2, }; var_dump($v); // NULL Empty switch-expression is also allowed. $v = switch ($x) { }; var_dump($v); // NULL You can omit parenthesized expression which is shortcut to `switch (true)`. This change applies to switch statement as well. $v = switch { case $x >= 0 && $x <= 100 => 1, case $x >= 100 && $x <= 200 => 2, default => 3, }; switch { case $x >= 0 && $x <= 100: doSomething1(); break; case $x >= 100 && $x <= 200: doSomething2(); break; default: doNothing(); break; } You can also use `return` and `throw` in result expression. I recalled some languages have this feature (but I've forgotten what language). This feature can be very handy and useful in many use cases. $x = 'd'; $v = switch ($x) { case 'a' => 1, case 'b' => 2, case 'c' => return true, default => throw new Exception("'$x' is not supported"), }; Additional feature in the demo patch is the 'type guard' unary operator which is an operator that will perform type check on given value and throw `TypeError` when type mismatch occurred, otherwise return the value as is. It has the same precedence as `new`. $a = 'This is a string'; $v = <int>$a; // TypeError: Value is expected to be int, string given Just like type hints, can accept nullable types. $v = <?int>switch ($x) { case 'a' => $result1, case 'b' => $result2, }; Also works with variable references. $x = &<int>$a; $y = &<callable>$functions[$name]; $arr = [&<Countable>$obj]; This operator can also be used to simulate typed variables. The demo is here: https://github.com/webdevxp/php-src P.S. This is just a bunch of ideas with concrete demo, not intended to be pull request nor proposal because I think I'm still new here and inexperienced in many other ways. Cheers

Mike Schinkel

6 years ago
> On Oct 19, 2019, at 12:40 PM, Kosit Supanyo <webdevxp.com@gmail.com> wrote: > > Hi Internals > > I've just finished an implementation of 'switch-expression' that have been > discussed recently. So I would like to present my ideas here.
This is great to see that you proactively implemented this.
> The basic syntax of switch-expression in this implementation is: > > $result = switch ($expr) { > case $cond1 => $result1, > case $cond2 => $result2, > case $cond3 => $result3, > default => $default_result, > };
However, why the need to deviate with the existing syntax, making refactoring between switch statements and switch expressions more work that it needs to be. Is there some reason why keeping colons and semlicolons would not work? Given your syntax manually refactoring existing switches to use expressions, or refactoring back from expressions to statements will require more and tedious keystrokes than if you just kept colons and semicolons. Is there a strong reason to change to using fat arrows and colons?
> You can omit parenthesized expression which is shortcut to `switch (true)`. > This change applies to switch statement as well. > > $v = switch { > case $x >= 0 && $x <= 100 => 1, > case $x >= 100 && $x <= 200 => 2, > default => 3, > }; > > switch { > case $x >= 0 && $x <= 100: > doSomething1(); > break; > case $x >= 100 && $x <= 200: > doSomething2(); > break; > default: > doNothing(); > break; > }
I do very much like this innovation. Would love to see this work for switch statements too.
> You can also use `return` and `throw` in result expression. I recalled some > languages have this feature (but I've forgotten what language). This > feature can be very handy and useful in many use cases. > > $x = 'd'; > $v = switch ($x) { > case 'a' => 1, > case 'b' => 2, > case 'c' => return true, > default => throw new Exception("'$x' is not supported"), > };
What about break?
> Additional feature in the demo patch is the 'type guard' unary operator > which is an operator that will perform type check on given value and throw > `TypeError` when type mismatch occurred, otherwise return the value as is. > It has the same precedence as `new`. > > $a = 'This is a string'; > $v = <int>$a; // TypeError: Value is expected to be int, string given > > Just like type hints, can accept nullable types. > > $v = <?int>switch ($x) { > case 'a' => $result1, > case 'b' => $result2, > };
Nice! -Mike

Kosit Supanyo

6 years ago
Hi Mike Is there a strong reason to change to using fat arrows and colons? The reason is statement/expression semantic. The reason for choosing comma over semicolon is because in PHP semicolons are used only as statement separator while commas are used as expression separator within brackets and item separator of some statements that can take numbers of item such as `echo` or variable/property declarations. Since switch-expression is a list of expression pairs, commas should be used to separate its items. And the reason for choosing fat arrow over colon is still the same as above, expression semantic. In PHP fat arrows (double arrows) were used only to separate key and value until 7.4 where their role has extended (arrow functions), while colons are used in three places labels, ternary operator and return types. Since cases in switch statements are labels, colons are approprate. But as I stated above, switch-expression is a list of expression pairs so it should use syntax like array rather than its statement counterpart. If PHP was using colon as key/value separator then colons should be used here. And here are examples of languages that use arrow-like syntax: [C#] var result = bank.Status switch { BankBranchStatus.Open => true, BankBranchStatus.Closed => false, BankBranchStatus.VIPCustomersOnly => isVip }; [Rust] let message = match x { 0 | 1 => "not many", 2 ..= 9 => "a few", _ => "lots" }; [Kotlin] var result = when (number) { 0 -> "Invalid number" 1, 2 -> "Number too low" 3 -> "Number correct" 4 -> "Number too high, but acceptable" else -> "Number too high" } What about break?
>
Let's see this example. for ($i = 0; $i < 100; $i++) { $x = getX($i); doSomething( $i, $x, switch ($x) { case 0 => 'ZERO', case 1 => 'ONE', case 2 => 'TWO', case 3 => 'THREE', default => break, }, ); } This shoud break `switch` or `for`? And if it breaks `switch` NULL should be returned but this will be the same as omitting `default`. And if it breaks `for`, call to `doSomething()` will be incomplete because a call frame was already pushed to VM stack. As I know ongoing function calls can only be aborted by exceptions (and `return` should not be allowed either, I need to fix that) so expressions must give something or throw. Cheers On Sun, Oct 20, 2019 at 6:52 AM Mike Schinkel <mike@newclarity.net> wrote:

Mike Schinkel

6 years ago
Hi Kosit, Thank you for your reply.
> On Oct 20, 2019, at 3:54 AM, Kosit Supanyo <webdevxp.com@gmail.com> wrote: > > Hi Mike > > Is there a strong reason to change to using fat arrows and colons? > > The reason is statement/expression semantic.
Okay, so you chose commas and fat arrows because of perceived consistency. I can buy that. However, I think by being consistent with one set of PHP syntax you ended up being inconsistent with another set; the brace-enclosed block. In a brace-enclosed block statements are separated by semi-colons, not commas. And given your use of `case` we could argue that the `case`s are sub-statements of the switch. So I think both of our arguments as suspect here you are actually introducing a new syntactical structure with this switch expression that has no current analogue in PHP. It is unlike anything else that comes before it, and so I think it would be a mistake to be consistent in some ways but not in others. Better to introduce rules that are specific to this structure that to justify the rules based on only partial consistency. Basically you are introducing the following syntax where the entirety is an expression: <expression-designating-keyword> [(<selector-expression>)] { <brace-enclosed-expression> } And we should explicitly consider what are the best rules to use for this rather than allow the rules to be defined based on consistency with something that is not like this new structure and thus potentially paint us into a corner in the future simple because we did not evaluate it fully. The question I think we should consider here is, what makes for a valid <brace-enclosed-expression>? Is it a single expression, or can be be multiple expressions with a delimiter where the last expression produces the resultant value? Or can it even be multiple statements, the last of which produces the resultant value? We can limit to single expressions, but that seems, well, limiting. If we instead leave open the option for multiple statements — which a semi-colon would not conflict with — then we leave open the future option to support multiple statements in a block-enclosed expression. But if we choose a comma we can never go down that path. Given this, I think using comma paints us into a corner that using semi-colon would not. As for fat arrows vs. colons, that too feels like misapplication of consistency. A `case` is more like a ternary operator than an array initializer, and is much more like a switch statement. I fear that use of a fat arrow will also paint us into a corner, but I cannot think of a specific example yet, it is just a fear based on my gut feeling. Using a colon would likely not paint us into a corner because it has already been used in effectively exactly the same context, e.g. the `case` statement for a `switch`. So the question is, do we want to be consistent with an expression but inconsistent with brace-enclosed blocks, or do we want to recognize that this is a new type of element and design the best rules we can for this new type of element? We can either limit a brace-enclosed expression to containing only simple expressions — which as I said seems too limiting — or we can enable the future potential for multi-line statements in a block-enclosed expression. And that is my 200 cents on the matter. :-)
>> What about break? > > Let's see this example. > > for ($i = 0; $i < 100; $i++) { > $x = getX($i); > doSomething( > $i, > $x, > switch ($x) { > case 0 => 'ZERO', > case 1 => 'ONE', > case 2 => 'TWO', > case 3 => 'THREE', > default => break, > }, > ); > } > > This should break `switch` or `for`? And if it breaks `switch` NULL should be returned but this will be the same as omitting `default`. And if it breaks `for`, call to `doSomething()` will be incomplete because a call frame was already pushed to VM stack. As I know ongoing function calls can only be aborted by exceptions (and `return` should not be allowed either, I need to fix that) so expressions must give something or throw.
As I envision, it should break `for`. And since the fact the call frame is already pushed does not stop a `throw` from working I do not see why it should stop a `break` from working. And if for some reason that is too complicated, I do not see any reason why it could not throw an error in that context, because the refactored solution is trivial, and I would say, preferred for its readability anyway: for ($i = 0; $i < 100; $i++) { $x = getX($i); $y = switch ($x) { case 0 => 'ZERO', case 1 => 'ONE', case 2 => 'TWO', case 3 => 'THREE', default => break, }, doSomething( $I, $x, $y ); } BTW, when I code I use `break` wherever possible instead of `throw`[1-6], and only ever use `throw` and `try...catch` when I am forced to. So supporting throw but not break here would require an additional comparison after the `switch` expression for people who like me avoid `throw`, which is not ideal. -Mike [1] http://www.lighterra.com/papers/exceptionsharmful/ <http://www.lighterra.com/papers/exceptionsharmful/> [2] https://sidburn.github.io/blog/2016/03/25/exceptions-are-evil <https://sidburn.github.io/blog/2016/03/25/exceptions-are-evil> [3] http://xahlee.info/comp/why_i_hate_exceptions.html <http://xahlee.info/comp/why_i_hate_exceptions.html> [4] https://www.joelonsoftware.com/2003/10/13/13/ <https://www.joelonsoftware.com/2003/10/13/13/> [5] https://mortoray.com/2012/04/02/everything-wrong-with-exceptions/ <https://mortoray.com/2012/04/02/everything-wrong-with-exceptions/> [6] https://www.atlassian.com/blog/archives/exceptions_are_bad <https://www.atlassian.com/blog/archives/exceptions_are_bad>

Kosit Supanyo

6 years ago
Hi Mike In a brace-enclosed block statements are separated by semi-colons, not
> commas.
Yes because they are statements, that's the point which I've stated many times. And if your argument is that there's no place in PHP that uses commas within curly braces, you're wrong. PHP already has one. use X\Y\Z\{ A, B, C, }; The reason to choose what to be a separator is not about the container but rather its items and semantics associated with them. And there's many languages out there using commas to separate expressions within curly braces if you did do some researches you might not think they are inconsistent at all. In fact I'd already gave you some examples in previous reply (C#, Rust). We can either limit a brace-enclosed expression to containing only simple
> expressions — which as I said seems too limiting — or we can enable the > future *potential *for multi-line statements in a block-enclosed > expression.
No one is limiting anything. I'm working on block expressions too. One of them is `eval` block which would be like: $z = eval { $x = doSomething1(); $y = doSomething2(); if ($x >= 0 && $y >= 0) { give $x + $y; } give null; // can be omitted }; If you think that anything inside curly braces must be separated by semicolon, you are the one that limit yourself. As I envision, it should break `for`. And since the fact the call frame is
> already pushed does not stop a `throw` from working I do not see why it > should stop a `break` from working. >
That's because `throw` do different thing at the engine level not just jumping things like `break`, `continue` or `goto`. BTW, when I code I use `break` wherever possible instead of `throw`[1-6],
> and only ever use `throw` and `try...catch` when I am forced to. So > supporting throw but not break here would require an additional comparison > after the `switch` expression for people who like me avoid `throw`, which > is not ideal.
If `break` is allowed in switch-expressions and its behavior is to break outer loop, don't you think it could be an another inconsistency or confusion? Because I'm 100% sure when people see `break` within `switch` they'll all think it should break `switch` not something else. Regards On Mon, Oct 21, 2019 at 4:12 AM Mike Schinkel <mike@newclarity.net> wrote:

Mike Schinkel

6 years ago
> On Oct 20, 2019, at 7:08 PM, Kosit Supanyo <webdevxp.com@gmail.com> wrote: > > Yes because they are statements, that's the point which I've stated many times.
And I am suggesting that a block-enclose expression should not be limited to expressions only, but instead be able to use statements in the future. Using commas ensures we can never go that route.
> And if your argument is that there's no place in PHP that uses commas within curly braces, you're wrong. PHP already has one. > > use X\Y\Z\{ > A, > B, > C, > };
You have a point. But that use-case is not a block, it is an obscure short-hand syntax for which nothing else in PHP is similar. I would not expect we should use it as a model unless it were for a very similiar thing.
> if you did do some researches
Seriously?
> you might not think they are inconsistent at all. In fact I'd already gave you some examples in previous reply (C#, Rust).
I did not state it before because I did not want to spend time on things I felt were irrelevant but the fact another language uses syntax does not make it appropriate for PHP. What makes it appropriate for PHP is "feel", consistency, and how sigils are already being used, and how they might be used in the future. But I am not saying you do not look to other language for inspiration. What I am saying is you cannot look to other languages for validation, at least not in a vacuum.
> No one is limiting anything. ... If you think that anything inside curly braces must be separated by semicolon,
Ok, so let's look at a scenario. Will we be able to do this in the future, if we decide to allow multiple expressions to result in a final value? $y = switch ($x) { case 1 => $arr = get_array(), array_pop($arr), case -1 => $arr = get_array(), array_unshift($arr), default => null, }; What if in the future PHP implements variadic syntax for assigning to arrays, e.g. $var = 1,2,3 where $var could satisfy a $variadic parameter set of parameters w/o requiring `...`? If PHP embraces that — note GoLang has something similar — then will comma syntax allow for it with hacky use of parentheses? $y = switch ($x) { case 1 => $var = 1,2,3, $var[1], case -1 => $var = 9,8,7, $var[1], default => null, };
> That's because `throw` do different thing at the engine level not just jumping things like `break`, `continue` or `goto`.
Are you saying it is literally impossible given the current engine, or just that it has not been done with `break`, `continue` or `goto`?
> If `break` is allowed in switch-expressions and its behavior is to break outer loop, don't you think it could be an another inconsistency or confusion? Because I'm 100% sure when people see `break` within `switch` they'll all think it should break `switch` not something else.
No. I would assume the other, so I have to assume I would not be the only one.
> you are the one that limit yourself.
Your response makes me think you are responding in anger? I am not arguing in anger myself, so I hope that I am not causing you to reply in anger? I am merely asking posing technical challenges in hopes to ensure that PHP is the best it can be. I assume we all want that? -MIke P.S. And I do not mean my input is needed for PHP to be the best it can be but only that having different opinions debated in a reasoned manner tends to make for a better outcome.

Kosit Supanyo

6 years ago
Hi Mike
> Ok, so let's look at a scenario. Will we be able to do this in the future, > if we decide to allow multiple expressions to result in a final value? > $y = switch ($x) { > case 1 => $arr = get_array(), array_pop($arr), > case -1 => $arr = get_array(), array_unshift($arr), > default => null, > };
As I told you in previous reply, I'm working on block-expressions as well. So if PHP has block-expressions you can do the same this way: $y = switch ($x) { case 1 => eval { $arr = get_array(); give array_pop($arr); }, case -1 => eval { $arr = get_array(); give array_shift($arr); }, default => null, }; Which is obviously cleaner and more powerful. Are you saying it is literally impossible given the current engine, or just
> that it has not been done with `break`, `continue` or `goto`?
Yes, it is impossible to break out of an ongoing function call without exceptions. Your response makes me think you are responding in anger? I am not arguing
> in anger myself, so I hope that I am not causing you to reply in anger? I > am merely asking posing technical challenges in hopes to ensure that PHP is > the best it can be. I assume we all want that?
No, I was not angry. Just a little bit tired. I appologize making you feel that way. Regards

Mike Schinkel

6 years ago
> On Oct 20, 2019, at 11:11 PM, Kosit Supanyo <webdevxp.com@gmail.com> wrote: > Ok, so let's look at a scenario. Will we be able to do this in the future, if we decide to allow multiple expressions to result in a final value? > $y = switch ($x) { > case 1 => $arr = get_array(), array_pop($arr), > case -1 => $arr = get_array(), array_unshift($arr), > default => null, > }; > > As I told you in previous reply, I'm working on block-expressions as well. So if PHP has block-expressions you can do the same this way: > > $y = switch ($x) { > case 1 => eval { > $arr = get_array(); > give array_pop($arr); > }, > case -1 => eval { > $arr = get_array(); > give array_shift($arr); > }, > default => null, > };
I know you said you were working on block expressions, but I did not view that as the best solution to the question I asked although I see that is how you envision it.
> Which is obviously cleaner and more powerful.
That is arguable. I find the solution you proposed above that uses `eval()` to be very "busy" visually, and needlessly complex/ You could instead simply assume that case statements supports multiple lines with the last providing the value, and thus not requiring the use of an explicit `eval()`. But to do that would require semi-colon line terminators, I think. Note I also think `eval()` would also be a nice to have, but what would be even nicer is if the inline switch did not require use of `eval()` to support multiple lines per case. #fwiw -Mike

Kosit Supanyo

6 years ago
Hi Mike I think your hypothetical PHP syntax won't happen. Not because it's bad idea but because PHP just simply can't. I understand that you came up with that idea from Golang background but the reason Golang can do that because in Golang, assignments are statement not expression. Even `++` or `--` are statement in Golang. While assignments in PHP are expressions so they can appear in any expression contexts. And it will conflict with other places as Rowan said in previous reply. You could instead simply assume that case statements supports multiple
> lines with the last providing the value, and thus not requiring the use of > an explicit `eval()`. >
I'm not fond of implicit things. And I think my proposed solution is more useful because it can return value anywhere not just at the end. You might think it is complex or visually busy but others may feel it is more comfortable if they are able to write well structured and and cleaner codes by this way. Regards On Tue, Oct 22, 2019 at 3:42 AM Mike Schinkel <mike@newclarity.net> wrote:

Rowan Collins

6 years ago
On Mon, 21 Oct 2019 at 15:13, Mike Schinkel <mike@newclarity.net> wrote:
> Ok, so let's look at a scenario. Will we be able to do this in the future, > if we decide to allow multiple expressions to result in a final value? > $y = switch ($x) { > case 1 => $arr = get_array(), array_pop($arr), > case -1 => $arr = get_array(), array_unshift($arr), > default => null, > }; >
Unlike some languages, not every statement in PHP is a valid expression (e.g. you cannot write "$foo = echo $bar"), so regardless of whether we use commas or semi-colons, the right-hand-side in those examples would have to be some new construct, so we could choose whatever syntax we wanted for it. We'd probably want something other than bare commas anyway, to make it more widely usable, e.g. in array declarations. If what you want is a full *statement* block on the right hand side, then you'll need to use the current switch statement anyway.
> What if in the future PHP implements variadic syntax for assigning to > arrays, e.g. $var = 1,2,3 where $var could satisfy a $variadic parameter > set of parameters w/o requiring `...`? If PHP embraces that — note GoLang > has something similar — then will comma syntax allow for it with hacky use > of parentheses? > $y = switch ($x) { > case 1 => $var = 1,2,3, $var[1], > case -1 => $var = 9,8,7, $var[1], > default => null, > };
I'm not really clear what feature you're suggesting here, but I'm pretty sure it conflicts with existing uses of commas, so shouldn't constrain us from using them elsewhere. foo(1,2,3); foo($var = 1,2,3); $a = [0, $var = 1,2,3]; // etc Regards,
-- Rowan Tommins [IMSoP]

Mike Schinkel

6 years ago
> On Oct 21, 2019, at 11:12 AM, Rowan Tommins <rowan.collins@gmail.com> wrote: >> What if in the future PHP implements variadic syntax for assigning to >> arrays, e.g. $var = 1,2,3 where $var could satisfy a $variadic parameter >> set of parameters w/o requiring `...`? If PHP embraces that — note GoLang >> has something similar — then will comma syntax allow for it with hacky use >> of parentheses? >> $y = switch ($x) { >> case 1 => $var = 1,2,3, $var[1], >> case -1 => $var = 9,8,7, $var[1], >> default => null, >> }; > > I'm not really clear what feature you're suggesting here, but I'm pretty > sure it conflicts with existing uses of commas, so shouldn't constrain us > from using them elsewhere. > > foo(1,2,3); > foo($var = 1,2,3); > $a = [0, $var = 1,2,3]; > // etc
You may well be right. But to follow up to clarify what I was thinking consider the following (hypothetical) PHP syntax. Note that the return value has two comma separated values, and the assignment can accept those multiple values into two comma separated variables in the assignment: function read_file(string $filepath):string,PHP\Error { $content = file_get_contents( $filepath ); $error = false === $content ? new PHP\Error( "failed to open '%s'", $filepath ) : null; return $content, $error; } $content, $err = read_file( __DIR__ . '/myfile.txt' ); if (!is_null( $err ) ) { error_log($err->message); } Now yes I know we can do this with arrays and list(), but I have never seen anyone do this in the wild and hence I think it is not idiomatic PHP. And I hypothesize they do not do this because it is ugly to read and write and because PHP will throw a runtime error (vs. load-time error) if you have fewer values returned than you assign: function read_file( string $filepath ): array { $content = file_get_contents( $filepath ); $error = false === $content ? new My\Error( "failed to open '%s'", $filepath ) : null; return array( $content, $error ); } list( $content, $err ) = read_file( __DIR__ . '/myfile.txt' ); if ( ! is_null( $err ) ) { error_log( $err->message ); } With the first example PHP could type check at load time vs. having to wait for coverage of this code at runtime, and `:array` type hint in the latter is not as good as the `:string, PHP\Error` type hint of the former. I am probably having unreasonable expectations to believe we will ever see this type of thing in PHP. Still I hold out hold and want to register an objection if I see new syntax that would make this syntax impossible in the future. #fwiw -Mike

Rowan Collins

6 years ago
On 21/10/2019 21:18, Mike Schinkel wrote:
> > But to follow up to clarify what I was thinking consider the > following/ (hypothetical) /PHP syntax. Note that the return value > has two comma separated values, and the assignment can accept > those multiple values into two comma separated variables in the > assignment: > > > function read_file(string $filepath):string,PHP\Error { > $content = file_get_contents( $filepath ); $error = false === > $content ? new PHP\Error( "failed to open '%s'", $filepath ) : > null; return $content, $error; }$content, $err= read_file( > __DIR__ . '/myfile.txt' ); if (!is_null( $err ) ) { > error_log($err->message); } >
Ah, I see. Yes, multiple return values would be nice sometimes. Note that you can get a lot closer to this in current versions of PHP than your example, because you can use the [...] syntax for both creating and destructuring arrays:         function read_file(string $filepath):array {            $content = file_get_contents( $filepath );            $error = false === $content               ? new PHP\Error( "failed to open '%s'", $filepath )               : null;            return [ $content, $error ];         }         [ $content, $err ] = read_file( __DIR__ . '/myfile.txt' );         if (!is_null( $err ) ) {            error_log($err->message);         }
> With the first example PHP could type check at load time vs. > having to wait for coverage of this code at runtime, and `:array` > type hint in the latter is not as good as the `:string, PHP\Error` > type hint of the former. >
The ability to typehint multiple return values like that would be indeed be useful, but I'm not sure it would need to reserve that syntax. One way it could be achieved is using a "tuple" declaration, like in Hack: https://docs.hhvm.com/hack/built-in-types/tuples In general, grouping things with commas and no parentheses is always going to *look* ambiguous IMO, even if you can limit where it's used to not be *technically* ambiguous. Regards,
-- Rowan Tommins (né Collins) [IMSoP]

Rowan Collins

6 years ago
Hi Kosit, There's some really interesting ideas in here, thanks for sharing them. On 19/10/2019 17:40, Kosit Supanyo wrote:
> Like function declaration and function expression in JavaScript, if > `switch` appears as first token at statement level it will be recognized as > statement but if `switch` is in expression context it will be > switch-expression. > > switch ($expr) { > case $cond1 => $result1, > case $cond2 => $result2, > case $cond3 => $result3, > default => $default_result, > }; > // Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW) > > But this is OK. > > !switch ($expr) { > case $cond1 => $result1, > case $cond2 => $result2, > case $cond3 => $result3, > default => $default_result, > }; // semicolon is still required because it is an expression
This feels like an odd restriction to me, and one that as far as I'm aware PHP doesn't have anywhere else. For instance, it might be considered bad style, but it's possible to use a ternary operator as an abbreviated if statement: isset($_GET['logout']) ? $session->logout() : $session->extend(); Was this restriction added to make the implementation easier, or because you thought it was a useful feature?
> You can omit parenthesized expression which is shortcut to `switch (true)`. > This change applies to switch statement as well. > > $v = switch { > case $x >= 0 && $x <= 100 => 1, > case $x >= 100 && $x <= 200 => 2, > default => 3, > }; > > switch { > case $x >= 0 && $x <= 100: > doSomething1(); > break; > case $x >= 100 && $x <= 200: > doSomething2(); > break; > default: > doNothing(); > break; > }
This is an interesting idea, given that switch(true) is the documented way to do this right now. However, I've always felt switch(true) was rather limited in its advantage over if-elseif. I would prefer a syntax that reduced the boilerplate for: * Different comparisons applied to the same variable/expression, e.g. match($user->getScore()) { case <0 => foo(), case >100 => bar(), default => baz() } * Different values compared against the same expression using the same operator, e.g. match( $exception instanceOf ) { case FooException => handleFoo(), case BarException => handleBar() }
> You can also use `return` and `throw` in result expression. I recalled some > languages have this feature (but I've forgotten what language). This > feature can be very handy and useful in many use cases. > > $x = 'd'; > $v = switch ($x) { > case 'a' => 1, > case 'b' => 2, > case 'c' => return true, > default => throw new Exception("'$x' is not supported"), > };
This seems confusing to me, because it mixes statements and expressions in a rather unusual way. The "return" case in particular seems ambiguous - if $v is visible outside the function, will it have a value assigned to it by the "return" branch, and if so what would that value be? I can imagine more use cases for the "throw" version, and it's reasonably obvious how it would behave, so it might be reasonable to have that one special case.
> Additional feature in the demo patch is the 'type guard' unary operator > which is an operator that will perform type check on given value and throw > `TypeError` when type mismatch occurred, otherwise return the value as is. > It has the same precedence as `new`. > > $a = 'This is a string'; > $v = <int>$a; // TypeError: Value is expected to be int, string given
This is a very interesting feature, although I think what would be even more useful would be syntax to check if something _can_ be cast, i.e. the same check you have here, but as an operator evaluating to boolean. That way, you could write code like this: if ( <int>$_GET['id'] ) {    $user = getUser($_GET['id']): } else {    echo "Invalid ID provided"; } Which would be equivalent (given a type hint on getUser() and no strict_types declaration) to this, but without needing to use exceptions as flow control: try {    getUser($_GET['id']); } catch ( TypeError $e ) {    echo "Invalid ID provided"; } Regards,
-- Rowan Tommins (né Collins) [IMSoP]

Kosit Supanyo

6 years ago
Hi Rowan Thank you for your reply. Was this restriction added to make the implementation easier, or because
> you thought it was a useful feature? >
This is not a restriction but a result of implementation workarounds. Let's see: switch ($x) { case 1: echo "ONE\n"; break; case 2: echo "TWO\n"; break; } vs switch ($x) { case 1 => print "ONE\n", case 2 => print "TWO\n", }; You can recognize the difference of those by looking for `=>` right? But the parser generator (bison) cannot do that in LR mode (it maybe can in GLR mode but I'm sure that would be unacceptable due to performance losses/more memory usage). So workarounds have to be used. I did it by simply return different tokens from the lexer base on previous token before `switch`. https://github.com/webdevxp/php-src/blob/switch-expression/Zend/zend_language_scanner.l#L1460 https://github.com/webdevxp/php-src/blob/switch-expression/Zend/zend_language_scanner.l#L1247 And use that different tokens to define rules in the parser definition so it won't have any shift/reduce conflicts. https://github.com/webdevxp/php-src/blob/switch-expression/Zend/zend_language_parser.y#L443 https://github.com/webdevxp/php-src/blob/switch-expression/Zend/zend_language_parser.y#L1107 * Different comparisons applied to the same variable/expression, e.g.
> match($user->getScore()) { case <0 => foo(), case >100 => bar(), default > => baz() }
I'd thought about that feature too. But since I also introduced type guard operator which uses `<` token it would have parser conflicts and simple workarounds cannot apply. $v = switch ($x) { case < <int>$y => 1, // shift/reduce conflict here }; But if type guard was removed or changed to use another token like `(:int)` that feature would be easy to implement because currently `ZEND_CASE` opcode has `extended_value` unused so we can use it to store operator data. Cheers
> >
On Sun, Oct 20, 2019 at 9:15 PM Rowan Tommins <rowan.collins@gmail.com> wrote:

Rowan Collins

6 years ago
Hi Kosit, On Sun, 20 Oct 2019 at 16:20, Kosit Supanyo <webdevxp.com@gmail.com> wrote:
> You can recognize the difference of those by looking for `=>` right? But > the parser generator (bison) cannot do that in LR mode (it maybe can in GLR > mode but I'm sure that would be unacceptable due to performance losses/more > memory usage). So workarounds have to be used. I did it by simply return > different tokens from the lexer base on previous token before `switch`. >> >>
Thanks for clarifying, I thought it might be something like that.
> * Different comparisons applied to the same variable/expression, e.g. >> match($user->getScore()) { case <0 => foo(), case >100 => bar(), default >> => baz() } > > > I'd thought about that feature too. But since I also introduced type guard > operator which uses `<` token it would have parser conflicts and simple > workarounds cannot apply. >
I'm not sure <foo> is the right syntax for type guards, but it's possible we'd want a different syntax for new switch/match functionality anyway. For instance, using a placeholder for the value being tested, like $$ (a common choice in other languages is _, but that's a valid and commonly used function name): match ( $user->getScore() ) { $$ < 0, $$ > 100 => foo(), $$ < 10 => bar(), default => baz() } You could also allow the placeholder in the opening clause to define the same operation for each case: match ( $user->getScore() < $$ ) { 0, default => foo(), 10 => bar(), 100 => baz() } Regards,
-- Rowan Tommins [IMSoP]

Kosit Supanyo

6 years ago
Hi Rowan I'm not sure <foo> is the right syntax for type guards, but it's possible
> we'd want a different syntax for new switch/match functionality anyway.
I've decided to change the syntax to `(: foo)`. It may look weird at frist but it is able explain why I think this syntax is more appropriate. Because PHP already uses `:` to specify return type of functions so `(: foo)` syntax can be seen as a way to specifying return type of expressions. And it can be put into the same visual category of cast operators. $a = (int)$b; // filter $c = (: int)$d; // guard This syntax choice may not satisfy everyone's taste but at least it is explainable. So now there's 2 choices `<foo>` and `(: foo)`. And more choices are welcome from anyone who think type guard should be a new feature. For instance, using a placeholder for the value being tested, like $$ (a
> common > choice in other languages is _, but that's a valid and commonly used > function name):
About that, I'm working on a different approach which I would like to show you after I finished the next demo :) Regards On Mon, Oct 21, 2019 at 8:52 PM Rowan Tommins <rowan.collins@gmail.com> wrote:

Mike Schinkel

6 years ago
> On Oct 20, 2019, at 7:59 AM, Rowan Tommins <rowan.collins@gmail.com> wrote: > On 19/10/2019 17:40, Kosit Supanyo wrote: >> Like function declaration and function expression in JavaScript, if >> `switch` appears as first token at statement level it will be recognized as >> statement but if `switch` is in expression context it will be >> switch-expression. >> >> switch ($expr) { >> case $cond1 => $result1, >> case $cond2 => $result2, >> case $cond3 => $result3, >> default => $default_result, >> }; >> // Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW) >> >> But this is OK. >> >> !switch ($expr) { >> case $cond1 => $result1, >> case $cond2 => $result2, >> case $cond3 => $result3, >> default => $default_result, >> }; // semicolon is still required because it is an expression > > > This feels like an odd restriction to me, and one that as far as I'm aware PHP doesn't have anywhere else. For instance, it might be considered bad style, but it's possible to use a ternary operator as an abbreviated if statement: > > isset($_GET['logout']) ? $session->logout() : $session->extend(); > > Was this restriction added to make the implementation easier, or because you thought it was a useful feature?
What restriction are you referring to? The forced `default` or the semi-colon being required? If `default`, I concur.
> I've always felt switch(true) was rather limited in its advantage over if-elseif.
I would much prefer to use a switch for multiple, mutually exclusive cases no matter what how complex the expression is because with a switch the cases expressions line up vertically. With `if` and `else if` it is harder to vertically eyeball the different mutually exclusive cases. With that, I would even argue that we deprecate `else`. Except I know that would be a battle so I wouldn't actually suggest it. :-)
> I would prefer a syntax that reduced the boilerplate for: > > * Different comparisons applied to the same variable/expression, e.g. match($user->getScore()) { case <0 => foo(), case >100 => bar(), default => baz() } > * Different values compared against the same expression using the same operator, e.g. match( $exception instanceOf ) { case FooException => handleFoo(), case BarException => handleBar() }
However, I would very much like to see both of those options, for switch expressions and switch statements. It is "syntax sugar," but IMO[1] programming languages evolve best when they recognize commonly used-patterns and then apply syntax sugar to make those patterns more obvious, less error prone and easier to reason about. Further, your introduction of a `match` keyword here is intriguing, and we could probably leverage it for other use cases. Maybe as an alternate to switch statements that do not require `break`s?
>> $x = 'd'; >> $v = switch ($x) { >> case 'a' => 1, >> case 'b' => 2, >> case 'c' => return true, >> default => throw new Exception("'$x' is not supported"), >> }; > > The "return" case in particular seems ambiguous - if $v is visible outside the function, will it have a value assigned to it by the "return" branch, and if so what would that value be?
It should have its prior value, since the expression did not complete. Alternately we could choose to always assign it null on return or break. But how often will we have this use-case? Only for global variables, which hopefully will eventually become extinct in userland code.
> This is a very interesting feature, although I think what would be even more useful would be syntax to check if something _can_ be cast, i.e. the same check you have here, but as an operator evaluating to boolean. > > That way, you could write code like this: > > if ( <int>$_GET['id'] ) { > $user = getUser($_GET['id']): > } > else { > echo "Invalid ID provided"; > } > > Which would be equivalent (given a type hint on getUser() and no strict_types declaration) to this, but without needing to use exceptions as flow control: > > try { > getUser($_GET['id']); > } > catch ( TypeError $e ) { > echo "Invalid ID provided"; > }
+1 to use for can-cast checking. In GoLang this is called a type assertion and is thus similar: if id,ok = get['id'].(string); ok { $user = get['id'].(string); } else { echo "Invalid ID provided"; } It would be super nice for PHP to have an equivalent. That said, we should almost be able to do both, and just let PHP throw an error if there is a problem. This would be useful if you know 100% that the code won't fail because of the code that came before it: if ( is_numeric($_GET['id'] ?? 0 ) ) { $id = intvar($_GET['id'] ?? 0); $user = getUser(<int>$id): } -Mike [1] https://mikeschinkel.me/2019/in-defense-of-syntactic-sugar/ <https://mikeschinkel.me/2019/in-defense-of-syntactic-sugar/>

Rowan Collins

6 years ago
On Sun, 20 Oct 2019 at 22:35, Mike Schinkel <mike@newclarity.net> wrote:
> What restriction are you referring to? The forced `default` or the > semi-colon being required? > >
Neither; I was referring to not being able to put a switch expression as a statement on its own, which Kosit has explained is a limitation of the parser.
> I would much prefer to use a switch for multiple, mutually exclusive cases > no matter what how complex the expression is because with a switch the > cases expressions line up vertically. With `if` and `else if` it is harder > to vertically eyeball the different mutually exclusive cases. >
I've never particularly been bothered by it, but wouldn't adding four spaces in your "if" statement make it line up with all your "elseif" statements?
> The "return" case in particular seems ambiguous - if $v is visible outside > the function, will it have a value assigned to it by the "return" branch, > and if so what would that value be? > > > It should have its prior value, since the expression did not complete. > > Alternately we could choose to always assign it null on return or break. > > But how often will we have this use-case? Only for global variables, > which hopefully will eventually become extinct in userland code. > >
Not just global variables, no; it could be assigning an object property ($this->foo = switch($x){ case 1 => return 1; }) for instance. It could also be in the argument for a function call ( doSomething(switch($x){ case 1 => return 1; }) ) in which case presumably the function would not be called at all? Overall, having a "return" in the middle of an expression would be very confusing, I think.
> That said, we should almost be able to do *both*, and just let PHP throw > an error if there is a problem. This would be useful if you know 100% that > the code won't fail because of the code that came before it: > > if ( is_numeric($_GET['id'] ?? 0 ) ) { > $id = intvar($_GET['id'] ?? 0); > > $user = getUser(<int>$id): > > } > >
I'm not sure what the point of this example is. You've just cast to int, so the assertion can never fail, and if getUser has a type hint, it's about to make that assertion anyway. Regards,
-- Rowan Tommins [IMSoP]

Mike Schinkel

6 years ago
> On Oct 21, 2019, at 4:51 AM, Rowan Tommins <rowan.collins@gmail.com> wrote: > > On Sun, 20 Oct 2019 at 22:35, Mike Schinkel <mike@newclarity.net> wrote: >> I would much prefer to use a switch for multiple, mutually exclusive cases >> no matter what how complex the expression is because with a switch the >> cases expressions line up vertically. With `if` and `else if` it is harder >> to vertically eyeball the different mutually exclusive cases. > > I've never particularly been bothered by it, but wouldn't adding four > spaces in your "if" statement make it line up with all your "elseif" > statements?
Possibly, but then again I am: 1) Mostly reading other people's code who will not have formatted their IFs that way, and 2.) Using a code reformatter to standardize code format via my IDE and there is no option for that. As for #1, I know I cannot enforce switch usage outside my own team, but at least for own team our standard says "use switch for multiple expressions" when possible so at least it would provide we can have consistency across our team.
>> But how often will we have this use-case? Only for global variables, >> which hopefully will eventually become extinct in userland code. > > Not just global variables, no;...
Fair point.
>> That said, we should almost be able to do *both*, and just let PHP throw >> an error if there is a problem. This would be useful if you know 100% that >> the code won't fail because of the code that came before it: >> >> if ( is_numeric($_GET['id'] ?? 0 ) ) { >> $id = intvar($_GET['id'] ?? 0); >> $user = getUser(<int>$id): >> } > > I'm not sure what the point of this example is. You've just cast to int, so > the assertion can never fail, and if getUser has a type hint, it's about to > make that assertion anyway.
Yeah, I know. I could not come up with a great example on the spot. I'll have to go off and do some more coding because that's when I always come across the good examples. Which is usually not the same time I need the good example. :-( -Mike