Re: [PHP-DEV] Proposal: AS assertions
On Fri, Mar 22, 2024 at 8:02 PM Rowan Tommins [IMSoP] wrote: > > On Fri, 22 Mar 2024, at 17:38, Claude Pache wrote: > > > Le 22 mars 2024 à 16:18, Rowan Tommins [IMSoP] a écrit > : > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else > some_other_function($expiry); > assert($optionalExpiryDateTime is ?DateTimeInterface); // cannot fail, > already asserted by the "as" > > > I think that the `is` operator is all we need; the `as` operator adds syntax > complexity for little gain. Compare: > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else > some_other_function($expiry); > > vs > > $optionalExpiryDateTime = $expiry is ?DateTimeInterface ? $expiry : > some_other_function($expiry); > > > > I agree, it doesn't add much; and that's what the draft RFC Ilija linked to > says as well. > > But the point of that particular example is that after the "is" version, you > don't actually know the type of $optionalExpiryDateTime without looking up > the return type of some_other_function() > > With the "as" version, you can see at a glance that after that line, > $optionalExpiryDateTime is *guaranteed* to be DateTimeInterface or null, > which I understood to be the intention of Robert's original proposal on this > thread. > > -- > Rowan Tommins > [IMSoP] > Indeed, "as" is to pattern matching like the fn is to function. You can live with one or the other, but having both is much more useful.
Re: [PHP-DEV] Proposal: AS assertions
On Fri, Mar 22, 2024 at 5:51 PM Rowan Tommins [IMSoP] wrote: > > On Fri, 22 Mar 2024, at 12:58, Robert Landers wrote: > > > >> $optionalExpiryDateTime = $expiry as ?DateTimeInterface else new > >> DateTimeImmutable($expiry); > > I'm not sure I can grok what this does... > > > > $optionalExpiryDateTime = ($expiry === null || $expiry instanceof > > DateTimeInterface) ? $expiry : new DateTimeImmutable($expiry) > > Trying to write it as a one-liner is going to make for ugly code - that's why > I'd love to have a new way to write it! But yes, that's the right logic. > > With the "is" operator from the Pattern Matching draft, it would be: > > $optionalExpiryDateTime = ($expiry is ?DateTimeInterface) ? $expiry : new > DateTimeImmutable($expiry); > > But with a clearer assertion that the variable will end up with the right > type in all cases: > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else > some_other_function($expiry); > assert($optionalExpiryDateTime is ?DateTimeInterface); // cannot fail, > already asserted by the "as" > > > > Maybe? What would be the usefulness of this in real life code? I've > > never written anything like it in my life. > > I already explained the scenario: the parameter is optional, so you want to > preserve nulls; but if it *is* present, you want to make sure it's the > correct type before proceeding. Another example: > // some library function that only supports strings and nulls > function bar(?string $name) { > if ( $string !== null ) ... > else ... > } > // a function you're writing that supports various alternative formats > function foo(string|Stringable|int|null $name = null) { > // we don't want to do anything special with nulls here, just pass them > along > // but we do want to convert other types to string, so that bar() doesn't > reject them > bar($name as ?string else (string)$name); > } This breaks my brain; in a good way I think. As you pointed out, people could now write this: function (int|string|null $value) { foo($value as int|null else (int)$value); } Which would pass int or null down to foo. Especially because I see something like this too often (especially with strict types): function (int|string|null $value) { foo((int) $value); } And foo() gets a 0 when $value is null and "undefined" things start happening. This isn't really possible with any of the other syntaxes I proposed. Now, if we are dealing with function returns: ($x as MyType else null)?->doSomething(); I don't hate it. It's a bit wordy, but still better than the alternative. > > To put it another way, it's no different from any other union type: at some > point, you will probably want to handle the different types separately, but > at this point in the program, either type is fine. In this case, the types > that are fine are DateTimeInterface and null; or in the example above, string > and null. > > > > $optionalExpiryDateTime = $expiry == null ? $expiry : $expiry as > > DateTimeInterface ?? new DateTimeImmutable($expiry as string ?? "now") > > If you think that's "readable" then we might as well end the conversation > here. If that was presented to me in a code review, I'd probably just write > "WTF?!" Hahaha, yeah, I wrote that before reading my own example! > > I have no idea looking at that what type I can assume for > $optionalExpiryDateTime after that line, which was surely the whole point of > using "as" in the first place? > > Regards, > -- > Rowan Tommins > [IMSoP]
Re: [PHP-DEV] Proposal: AS assertions
On Fri, 22 Mar 2024, at 17:38, Claude Pache wrote: > >> Le 22 mars 2024 à 16:18, Rowan Tommins [IMSoP] a >> écrit : >> >> $optionalExpiryDateTime = $expiry as ?DateTimeInterface else >> some_other_function($expiry); >> assert($optionalExpiryDateTime is ?DateTimeInterface); // cannot fail, >> already asserted by the "as" > > I think that the `is` operator is all we need; the `as` operator adds syntax > complexity for little gain. Compare: > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else > some_other_function($expiry); > > vs > > $optionalExpiryDateTime = $expiry is ?DateTimeInterface ? $expiry : > some_other_function($expiry); I agree, it doesn't add much; and that's what the draft RFC Ilija linked to says as well. But the point of that particular example is that after the "is" version, you don't actually know the type of $optionalExpiryDateTime without looking up the return type of some_other_function() With the "as" version, you can see at a glance that after that line, $optionalExpiryDateTime is *guaranteed* to be DateTimeInterface or null, which I understood to be the intention of Robert's original proposal on this thread. -- Rowan Tommins [IMSoP]
Re: [PHP-DEV] Proposal: AS assertions
> Le 22 mars 2024 à 16:18, Rowan Tommins [IMSoP] a écrit > : > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else > some_other_function($expiry); > assert($optionalExpiryDateTime is ?DateTimeInterface); // cannot fail, > already asserted by the "as" I think that the `is` operator is all we need; the `as` operator adds syntax complexity for little gain. Compare: $optionalExpiryDateTime = $expiry as ?DateTimeInterface else some_other_function($expiry); vs $optionalExpiryDateTime = $expiry is ?DateTimeInterface ? $expiry : some_other_function($expiry); —Claude
Re: [PHP-DEV] Re: [RFC] [Discussion] [VOTE] Rounding Integers as int
Hi Bob, and sorry for the late reply ... On 19.03.24 01:05, Bob Weinand wrote: Hey Marc, On 18.3.2024 08:53:01, Marc Bennewitz wrote: Hi Bob, On 17.03.24 14:59, Bob Weinand wrote: On 17.3.2024 13:23:04, Marc Bennewitz wrote: Hello internals, I have opened the vote for the "Rounding Integers as int" RFC: https://wiki.php.net/rfc/integer-rounding Do to Easter weekend the vote will run for two weeks and two days until Tue the 2nd of April 2024. Best regards, Marc Bennewitz Hey Marc, I've voted no; it should be just changed without any force_float parameter. Just always return int when possible (and the input was int). If users wish to have the old behaviour, they should just explicitly cast via (float). The effective BC break of that would be quite small if some things which return float today now would return int. I cannot imagine many cases where this would actually be unwanted. And as said, explicit (float) casts are always possible. I also dislike force_float, as it cannot just be added to a function in any code which shall be backwards compatible to 8.3 and older. It would just emit Uncaught Error: Unknown named parameter $force_float. Changing the return type from float to int is a non trivial quite hard to find behavior change. Imaging code like this: $x = 800; $y = 800; round($x/$y) === 1.0; This will return false instead of true especially because we teach users to use strict comparison. Such behavior change should be done in a major version. I see, here we disagree: - Strict comparison should be avoided when working with numbers. Strict comparisons are generally for strings and booleans. - There's no reason to artificially wait years here. Agree, strict comparison should be avoided when working with numbers but this detail normally does not get mentioned if an "equal vs. same" discussion comes up and there are even coding styles out there forcing users to use strict comparison everywhere like - https://github.com/slevomat/coding-standard/blob/master/SlevomatCodingStandard/Sniffs/Operators/DisallowEqualOperatorsSniff.php - https://github.com/laminas/laminas-coding-standard/blob/2.6.x/src/LaminasCodingStandard/ruleset.xml#L659 With the additional parameter it's possible to opt-in into the new behavior already in 8.4 while in PHP 9.0 the default behavior will change but previously opted in code does not need to get touched again. Just changing the behavior means waiting for PHP 9.0 without a way to opt-in in 8.4 already. If you are not interested in opting in in 8.4 already you can just ignore the additional argument as this will be the default in 9.0. I'm not interested in having an additional parameter I have to carry forward for quite some years. To mimic the previous behavior in a fully BC way it's as simple as explicitly casting the value to float. I would rather have preferred to see a static analysis solution how often round()ed results are compared strictly, assess the actual BC impact and possibly encourage tools like Rector to recognize such patterns. As of the above reason and because the rounding functions are very highly used functions I think it's obvious that such behavior change will break a lot of code out there. I'm a bit confused because normally possible BC breaks have to be avoided as much as possible and if a feature is ranked higher it still needs to smooth migration but here it seems to me now it's fine to break a lot of users code without warning and without a way to opt-in before (which was requested previously). Bob Regards, Marc Bob OpenPGP_0x3936ABF753BC88CE.asc Description: OpenPGP public key OpenPGP_signature.asc Description: OpenPGP digital signature
Re: [PHP-DEV] Proposal: AS assertions
On Fri, 22 Mar 2024, at 12:58, Robert Landers wrote: > >> $optionalExpiryDateTime = $expiry as ?DateTimeInterface else new >> DateTimeImmutable($expiry); > I'm not sure I can grok what this does... > > $optionalExpiryDateTime = ($expiry === null || $expiry instanceof > DateTimeInterface) ? $expiry : new DateTimeImmutable($expiry) Trying to write it as a one-liner is going to make for ugly code - that's why I'd love to have a new way to write it! But yes, that's the right logic. With the "is" operator from the Pattern Matching draft, it would be: $optionalExpiryDateTime = ($expiry is ?DateTimeInterface) ? $expiry : new DateTimeImmutable($expiry); But with a clearer assertion that the variable will end up with the right type in all cases: $optionalExpiryDateTime = $expiry as ?DateTimeInterface else some_other_function($expiry); assert($optionalExpiryDateTime is ?DateTimeInterface); // cannot fail, already asserted by the "as" > Maybe? What would be the usefulness of this in real life code? I've > never written anything like it in my life. I already explained the scenario: the parameter is optional, so you want to preserve nulls; but if it *is* present, you want to make sure it's the correct type before proceeding. Another example: // some library function that only supports strings and nulls function bar(?string $name) { if ( $string !== null ) ... else ... } // a function you're writing that supports various alternative formats function foo(string|Stringable|int|null $name = null) { // we don't want to do anything special with nulls here, just pass them along // but we do want to convert other types to string, so that bar() doesn't reject them bar($name as ?string else (string)$name); } To put it another way, it's no different from any other union type: at some point, you will probably want to handle the different types separately, but at this point in the program, either type is fine. In this case, the types that are fine are DateTimeInterface and null; or in the example above, string and null. > $optionalExpiryDateTime = $expiry == null ? $expiry : $expiry as > DateTimeInterface ?? new DateTimeImmutable($expiry as string ?? "now") If you think that's "readable" then we might as well end the conversation here. If that was presented to me in a code review, I'd probably just write "WTF?!" I have no idea looking at that what type I can assume for $optionalExpiryDateTime after that line, which was surely the whole point of using "as" in the first place? Regards, -- Rowan Tommins [IMSoP]
Re: [PHP-DEV] Proposal: AS assertions
On 2024-03-22 10:46, Rowan Tommins [IMSoP] wrote: On Fri, 22 Mar 2024, at 08:17, Jordi Boggiano wrote: We perhaps could make sure that as does not throw if used with `??`, or that `??` catches the type error and returns the right-hand expression instead: So to do a nullable typecast you would do: $a as int|float ?? null While this limits the impact to only expressions combining as with ?? it still has the same fundamental problem: you can't meaningfully use it with a nullable type. As a concrete example, imagine you have an optional $description parameter, and want to ensure any non-null values are converted to string, but keep null unchanged. At first sight, it looks like you could write this: $descString = $description as string|null ?? (string)$description; But this won't work - the ?? swallows the null and turns it into an empty string, which isn't what you wanted. You need some syntax that catches the TypeError, but preserves the null: $descString = $description as string|null else (string)$description; // or $descString = $description as string|null catch (string)$description; // or $descString = $description as string|null default (string)$description; I actually think there are quite a lot of scenarios where that idiom would be useful: $optionalExpiryDateTime = $expiry as ?DateTimeInterface else new DateTimeImmutable($expiry); $optionalUnixTimestamp = $time as ?int else strotime((string)$time); $optionalUnicodeName = $name as ?UnicodeString else new UnicodeString( $name ); etc Yeah I think this looks great actually, minus the confusing bits about |null which is in reality yes probably rarely useful in a "as" cast. as that throws + default to catch it Best, Jordi -- Jordi Boggiano @seldaek -https://seld.be
Re: [PHP-DEV] Proposal: AS assertions
On Fri, Mar 22, 2024 at 12:01 PM Rowan Tommins [IMSoP] wrote: > > On Fri, 22 Mar 2024, at 08:17, Jordi Boggiano wrote: > > We perhaps could make sure that as does not throw if used with `??`, or that > `??` catches the type error and returns the right-hand expression instead: > > So to do a nullable typecast you would do: > > $a as int|float ?? null > > > While this limits the impact to only expressions combining as with ?? it > still has the same fundamental problem: you can't meaningfully use it with a > nullable type. > > > As a concrete example, imagine you have an optional $description parameter, > and want to ensure any non-null values are converted to string, but keep null > unchanged. > > At first sight, it looks like you could write this: > > $descString = $description as string|null ?? (string)$description; > > But this won't work - the ?? swallows the null and turns it into an empty > string, which isn't what you wanted. You need some syntax that catches the > TypeError, but preserves the null: > > $descString = $description as string|null else (string)$description; > // or > $descString = $description as string|null catch (string)$description; > // or > $descString = $description as string|null default (string)$description; > > > I actually think there are quite a lot of scenarios where that idiom would be > useful: > > $optionalExpiryDateTime = $expiry as ?DateTimeInterface else new > DateTimeImmutable($expiry); > $optionalUnixTimestamp = $time as ?int else strotime((string)$time); > $optionalUnicodeName = $name as ?UnicodeString else new UnicodeString( $name > ); > etc > > And once you have that, you don't need anything special for the null case, > it's just: > > $nameString = $name as ?string else null; > > Regards, > -- > Rowan Tommins > [IMSoP] I'm not sure I can grok what this does... $optionalExpiryDateTime = ($expiry === null || $expiry instanceof DateTimeInterface) ? $expiry : new DateTimeImmutable($expiry) Maybe? What would be the usefulness of this in real life code? I've never written anything like it in my life. Personally, this is much more readable (assuming I got the logic right): using always null if not match, and handle the case for when $expiry isn't a string: $optionalExpiryDateTime = $expiry == null ? $expiry : $expiry as DateTimeInterface ?? new DateTimeImmutable($expiry as string ?? "now") But I can't think of why you'd want null ... null would apply to all types and have a dedicated branch, no matter what any other type is. Robert Landers Software Engineer Utrecht NL
Re: [PHP-DEV] Proposal: AS assertions
On Fri, 22 Mar 2024, at 10:05, Robert Landers wrote: > After asking an AI for some examples and usages, the most compatible > one would be C#'s. In actuality, I think it could be hugely simplified > if we simply return null instead of throwing. There'd be no special > case for |null, and it would move the decision making to the > programmer: > > $x = $a as int ?? throw new LogicException(); It might be relevant that C# has only recently introduced the concept of explicitly nullable reference types, with a complex migration process for existing code: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies So in most C# code, there isn't actually a difference between "expect a DateTime" and "expect a DateTime or null" PHP, however, strictly separates those two, and always has; so this would be surprising: $x = $a as DateTime; assert($x instanceof DateTime); // will fail if $x has defaulted to null! That's why I suggested that with an explcit default, the default would be automatically asserted as matching the specified type: $x = $a as DateTime else 'No date given'; // TypeError: string given, DateTime expected $x = $a as DateTime|string else 'No date given'; // OK $x = $a as DateTime else null; // TypeError: null given, DateTime expected $x = $a as ?DateTime else null; // OK If the statement runs without error, $x is guaranteed to be of the type (or pattern) given to the "as" operator. Regards, -- Rowan Tommins [IMSoP]
Re: [PHP-DEV] Proposal: AS assertions
On Fri, Mar 22, 2024 at 10:31 AM Jordi Boggiano wrote: > > On 2024-03-21 16:02, Robert Landers wrote: > > $a as int|float > > would be an int, float, or thrown exception. > > $a as int|float|null > > would be an int, float, or null. > > Just a suggestion here which might be more palatable to Rowan's wish for > consistency (which I can totally relate to): > > We perhaps could make sure that as does not throw if used with `??`, or that > `??` catches the type error and returns the right-hand expression instead: > > So to do a nullable typecast you would do: > > $a as int|float ?? null > > To me this reads way more intuitive what will happen, and achieves the same > in an also very concise way. > > The only catch I see is that it would also swallow errors about $a not being > defined at all. > > Best, > Jordi > > -- > Jordi Boggiano > @seldaek - https://seld.be Hey Rowan and Jordi, I did a bit of research into other languages to see how they handle "as": C#: as never throws, it either returns the type if it can be that type, or null OCaml: fails if an alternative isn't given Typescript: doesn't actually do anything, just hints the type for the compiler After asking an AI for some examples and usages, the most compatible one would be C#'s. In actuality, I think it could be hugely simplified if we simply return null instead of throwing. There'd be no special case for |null, and it would move the decision making to the programmer: $x = $a as int ?? throw new LogicException(); It also still allows for concisely making calls: $x = ($a as MyType)?->doSomething(); What do you think?
Re: [PHP-DEV] Proposal: AS assertions
On Fri, 22 Mar 2024, at 08:17, Jordi Boggiano wrote: > We perhaps could make sure that as does not throw if used with `??`, or that > `??` catches the type error and returns the right-hand expression instead: > So to do a nullable typecast you would do: > > $a as int|float ?? null > While this limits the impact to only expressions combining as with ?? it still has the same fundamental problem: you can't meaningfully use it with a nullable type. As a concrete example, imagine you have an optional $description parameter, and want to ensure any non-null values are converted to string, but keep null unchanged. At first sight, it looks like you could write this: $descString = $description as string|null ?? (string)$description; But this won't work - the ?? swallows the null and turns it into an empty string, which isn't what you wanted. You need some syntax that catches the TypeError, but preserves the null: $descString = $description as string|null else (string)$description; // or $descString = $description as string|null catch (string)$description; // or $descString = $description as string|null default (string)$description; I actually think there are quite a lot of scenarios where that idiom would be useful: $optionalExpiryDateTime = $expiry as ?DateTimeInterface else new DateTimeImmutable($expiry); $optionalUnixTimestamp = $time as ?int else strotime((string)$time); $optionalUnicodeName = $name as ?UnicodeString else new UnicodeString( $name ); etc And once you have that, you don't need anything special for the null case, it's just: $nameString = $name as ?string else null; Regards, -- Rowan Tommins [IMSoP]
Re: [PHP-DEV] Proposal: AS assertions
On 2024-03-21 16:02, Robert Landers wrote: $a as int|float would be an int, float, or thrown exception. $a as int|float|null would be an int, float, or null. Just a suggestion here which might be more palatable to Rowan's wish for consistency (which I can totally relate to): We perhaps could make sure that as does not throw if used with `??`, or that `??` catches the type error and returns the right-hand expression instead: So to do a nullable typecast you would do: $a as int|float ?? null To me this reads way more intuitive what will happen, and achieves the same in an also very concise way. The only catch I see is that it would also swallow errors about $a not being defined at all. Best, Jordi -- Jordi Boggiano @seldaek -https://seld.be
Re: [PHP-DEV] Proposal: AS assertions
On 22 March 2024 00:04:27 GMT, Robert Landers wrote: >I think that is where we are getting confused: `null` is a value (or >at least, the absence of a value). The fact that the type system >allows it to be used as though its a type (along with true and false) >is interesting, but I think it is confusing the conversation. Every value needs to belong to some type: for instance, true and false belong to the type "boolean", as returned by the gettype() function. There is a value called null, and the type it belongs to is also called "null". Unlike some languages, PHP has no concept of a typed null reference - you can't have "a null DateTime"; you can only have the one universal null, of type null. The existence of "null" in type checks is therefore necessary if you want to allow every value to pass some type check. There isn't any other type that can include the value null because the type of null is always null. That's completely different from true and false, both of which are covered by a type check for "bool". They are special cases, which aren't consistent with anything else in the type system. The "false" check was added first, as a way to express clearly the common pattern in old standard library functions of returning false on error. Then "true" was added later, for consistency. Both are newer, and far more exotic, than "null". Disallowing true and false in some type checking contexts would be fine (although mostly they're pointless, rather than harmful). Disallowing or repurposing null would mean you have an incomplete type system, because there is no other type to match a null value against. Regards, Rowan Tommins [IMSoP]