Re: [PHP-DEV] Proposal: AS assertions

2024-03-22 Thread Robert Landers
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

2024-03-22 Thread Robert Landers
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

2024-03-22 Thread Rowan Tommins [IMSoP]
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

2024-03-22 Thread Claude Pache

> 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

2024-03-22 Thread Marc Bennewitz

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

2024-03-22 Thread Rowan Tommins [IMSoP]
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

2024-03-22 Thread Jordi Boggiano

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

2024-03-22 Thread Robert Landers
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

2024-03-22 Thread Rowan Tommins [IMSoP]
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

2024-03-22 Thread Robert Landers
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

2024-03-22 Thread Rowan Tommins [IMSoP]
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

2024-03-22 Thread Jordi Boggiano

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

2024-03-22 Thread Rowan Tommins [IMSoP]



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]