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] 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]


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Robert Landers
On Thu, Mar 21, 2024 at 11:06 PM Rowan Tommins [IMSoP]
 wrote:
>
> On 21/03/2024 19:03, Robert Landers wrote:
>
> I suppose we are taking this from different viewpoints, yours appears
> to be more of a philosophical one, whereas mine is more of a practical
> one.
>
>
> My main concern is consistency; which is partly philosophical, but does have 
> practical impact - the same syntax meaning the same thing in different 
> contexts leads to less user confusion and fewer bugs.
>
> But I also think there are real use cases for "error on anything other than 
> either Foo or null" separate from "give me a null for anything other than 
> Foo".

I can't think of any example of this. In every case I've ever written
a manual typecheck, I've done something differently from null vs. the
actual type I'm checking for. In only a few instances were they ever
the same. I'd be happy to research this by looking at some older code
that has to do manual typechecks; but I have a feeling if you were to
make Foo|null only throw, it would be pointless as most people would
end up writing this anyway, making the |null completely superfluous:

if $x === null {
 /* do something for null */
}
$y = $x as Foo;

> $x = $a as null;
>
> (or any other value, such as true|false) appears to have no practical
> purpose in this particular case.
>
>
> There's plenty of possible pieces of code that have no practical purpose, but 
> that on its own isn't a good reason to make them do something different.
>
> "null" as a standalone type (rather than part of a union) is pretty much 
> always pointless, and was forbidden until PHP 8.2. It's now allowed, partly 
> because there are scenarios involving inheritance where it does actually make 
> sense (e.g. narrowing a return type from Foo|null to null); and probably also 
> because it's easier to allow it than forbid it.
>
>
> That's not really what we're talking about anyway, though; we're talking 
> about nullable types, or null in a union type, which are much more frequently 
> used.

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. It might
be worth defining the meaning of "type" and "value" as well defining
what the "as" means in each context. Is it the same? Is it different?
I think this is worth spending some time on, but I have a feeling
there'll be bigger discussion about that when pattern matching shows
up.

> Further, reading "$x =
> $a as null", as a native English speaker, appears to be the same as
> "$x = null".
>
>
> Well, that's a potential problem with the choice of syntax: "$x = $a as int" 
> could easily be mistaken for "cast $a as int", rather than "assert that $a is 
> int".
>
> If you spell out "assert that $a is null", or "assert that $a is int|null", 
> it becomes very surprising for 'hello' to do anything other than fail the 
> assertion.

$a as int is quite different from $a as null. One is a bonafide type,
the other is a value. I don't think you can mistake this and they are
very different semantics.

> As I mentioned in the beginning, I see this mostly being used when
> dealing with mixed types from built-in/library functions, where you
> have no idea what the actual type is, but when you write the code, you
> have a reasonable expectation of a set of types and you want to throw
> if it is unexpected.
>
>
> My argument is that you might have a set of expected types which includes 
> null, *and* want to throw for other, unexpected, values. If "|null" is 
> special-cased to mean "default to null", there's no way to do that.

I'm arguing that that doesn't make any sense; this isn't a
method/function signature. I invite you to try writing some fictional
code using both semantics. I'm being sincere when I say I'd love to
see an example that would use |null on the right hand side of "as" and
want to throw.

> Right now, the best way to do that is to simply
> set a function signature and pass the mixed type to the function to
> have the engine do it for you
>
>
> And if you do that, then a value of 'hello' passed to a parameter of type 
> int|null, will throw a TypeError, not give you a null.
>
> As I illustrated in my last e-mail, you can even (since PHP 8.2) have a 
> parameter of type null, and get a TypeError for any other value. That may not 
> be useful, but it's entirely logical.

Ah, yeah, I guess I could have been more clear. This is what I find
myself writing quite a lot of lately (unfortunately):

foreach ($listOfMyAttributes as $attributeReflection) {
  $instance = $attributeReflection->newInstance();
  assert($instance instanceof MyAttribute);
  $instance->someMethod();
}

as well as:

$value = genericFuncReturnsMixed();
if ($value === null) {
  /* handle null */
}
if($value instanceof MyType) {
  $value->doSomething();
} else if ($value instanceof 

Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Matthew Brown
>
> What's the advantage of a language construct over the following?
>
> ```php
> /**
>  * @template T of object
>  * @psalm-assert T $value
>  * @param class-string $type
>  */
> function as(mixed $value, string $type): mixed
> {
> if (! $value instanceof $type) { throw
> SomeKindOfException::forMismatchingRequirements($value, $type); }
>
> return $value;
> }
>
> echo as(myExpression(), MyType::class)->methodOfMyType();
> ```
>
>
A static analysis tool supporting something in docblocks should not
preclude those things being added in syntax IMO.

Hack's `as` operator is very neat, and it'd be trivial for existing SA
tools to support the same in PHP.

Here's a demo of `as` in Hack getting flagged by a SA tool:
https://hakana.dev/#XQAAgAA2AAAzHUn_qWH7EwabJzyN0tdfxv3ug6f7oZ-qScnamcl1qjUZCPmuKA3tD-KFr1f0ZPcrAXt_D1L___KsQAA%3D

This would also benefit from a `nonnull` subtype of `mixed` which could be
used as a null refinement.


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Rowan Tommins [IMSoP]

On 21/03/2024 19:03, Robert Landers wrote:

I suppose we are taking this from different viewpoints, yours appears
to be more of a philosophical one, whereas mine is more of a practical
one.



My main concern is consistency; which is partly philosophical, but does 
have practical impact - the same syntax meaning the same thing in 
different contexts leads to less user confusion and fewer bugs.


But I also think there are real use cases for "error on anything other 
than either Foo or null" separate from "give me a null for anything 
other than Foo".



$x = $a as null;

(or any other value, such as true|false) appears to have no practical
purpose in this particular case.



There's plenty of possible pieces of code that have no practical 
purpose, but that on its own isn't a good reason to make them do 
something different.


"null" as a standalone type (rather than part of a union) is pretty much 
always pointless, and was forbidden until PHP 8.2. It's now allowed, 
partly because there are scenarios involving inheritance where it does 
actually make sense (e.g. narrowing a return type from Foo|null to 
null); and probably also because it's easier to allow it than forbid it.



That's not really what we're talking about anyway, though; we're talking 
about nullable types, or null in a union type, which are much more 
frequently used.





Further, reading "$x =
$a as null", as a native English speaker, appears to be the same as
"$x = null".



Well, that's a potential problem with the choice of syntax: "$x = $a as 
int" could easily be mistaken for "cast $a as int", rather than "assert 
that $a is int".


If you spell out "assert that $a is null", or "assert that $a is 
int|null", it becomes very surprising for 'hello' to do anything other 
than fail the assertion.




As I mentioned in the beginning, I see this mostly being used when
dealing with mixed types from built-in/library functions, where you
have no idea what the actual type is, but when you write the code, you
have a reasonable expectation of a set of types and you want to throw
if it is unexpected.



My argument is that you might have a set of expected types which 
includes null, *and* want to throw for other, unexpected, values. If 
"|null" is special-cased to mean "default to null", there's no way to do 
that.




Right now, the best way to do that is to simply
set a function signature and pass the mixed type to the function to
have the engine do it for you



And if you do that, then a value of 'hello' passed to a parameter of 
type int|null, will throw a TypeError, not give you a null.


As I illustrated in my last e-mail, you can even (since PHP 8.2) have a 
parameter of type null, and get a TypeError for any other value. That 
may not be useful, but it's entirely logical.




It makes more sense, from a practical programming
point-of-view, to simply return the value given if none of the types
match.


This perhaps is a key part of our difference: when I see 
"int|bool|null", I don't see any "value given", just three built-in 
types: int, which has a range of values from PHP_INT_MIN to PHP_INT_MAX; 
bool, which has two possible values "true" and "false"; and null, which 
has a single possible value "null".


So there are 2**64 + 2 + 1 possible values that meet the constraint, and 
nothing to specify that one of those is my preferred default if given 
something unexpected.



Regards,

--
Rowan Tommins
[IMSoP]


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Robert Landers
On Thu, Mar 21, 2024 at 7:01 PM Rowan Tommins [IMSoP]
 wrote:
>
> On 21/03/2024 15:02, Robert Landers wrote:
>
> I don't think you are getting what I am saying.
>
> $a as int|float
>
> would be an int, float, or thrown exception.
>
> $a as int|float|null
>
> would be an int, float, or null.
>
>
> I get what you're saying, but I disagree that it's a good idea.
>
> If $a is 'hello', both of those statements should throw exactly the same 
> error, for exactly the same reason - the input is not compatible with the 
> type you have specified.
>
>
>
> Another way of thinking about is:
>
> $x = $a as null
>
> What do you expect $x to be?
>
>
> The same as $x inside this function:
>
> function foo(null $x) { var_dump($x); }
> foo($a);
>
> Which is null if $a is null, and a TypeError if $a is anything else: 
> https://3v4l.org/5UR5A
>
>
> Regards,
>
> --
> Rowan Tommins
> [IMSoP]

I suppose we are taking this from different viewpoints, yours appears
to be more of a philosophical one, whereas mine is more of a practical
one.

$x = $a as null;

(or any other value, such as true|false) appears to have no practical
purpose in this particular case. This is better checked with `===`, or
even in_array(). Values are easy to check in PHP and there are already
lots of great and simple ways to check a value. Further, reading "$x =
$a as null", as a native English speaker, appears to be the same as
"$x = null".

As I mentioned in the beginning, I see this mostly being used when
dealing with mixed types from built-in/library functions, where you
have no idea what the actual type is, but when you write the code, you
have a reasonable expectation of a set of types and you want to throw
if it is unexpected. Right now, the best way to do that is to simply
set a function signature and pass the mixed type to the function to
have the engine do it for you; or write out a bunch of instanceofs
when that isn't worth it. However, this is cumbersome.

I'd also like to say that I'm not strongly attached to the |null
behavior I'm proposing, but there are better ways to assert a variable
is equal to a value. It makes more sense, from a practical programming
point-of-view, to simply return the value given if none of the types
match.

Robert Landers
Software Engineer
Utrecht NL


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Rowan Tommins [IMSoP]

On 21/03/2024 15:02, Robert Landers wrote:

I don't think you are getting what I am saying.

$a as int|float

would be an int, float, or thrown exception.

$a as int|float|null

would be an int, float, or null.



I get what you're saying, but I disagree that it's a good idea.

If $a is 'hello', both of those statements should throw exactly the same 
error, for exactly the same reason - the input is not compatible with 
the type you have specified.





Another way of thinking about is:

$x = $a as null

What do you expect $x to be?



The same as $x inside this function:

function foo(null $x) { var_dump($x); }
foo($a);

Which is null if $a is null, and a TypeError if $a is anything else: 
https://3v4l.org/5UR5A



Regards,

--
Rowan Tommins
[IMSoP]


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Larry Garfield
On Thu, Mar 21, 2024, at 3:02 PM, Robert Landers wrote:

> I don't think you are getting what I am saying.
>
> $a as int|float
>
> would be an int, float, or thrown exception.
>
> $a as int|float|null
>
> would be an int, float, or null.
>
> Robert Landers
> Software Engineer
> Utrecht NL

Hi Rob.  I really do encourage you to read the RFC that Ilija linked to 
already.  What you're proposing is already mostly written (though for 
performance reasons may be rewritten soon), and the edge cases already largely 
resolved.

https://wiki.php.net/rfc/pattern-matching

--Larry Garfield


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Robert Landers
On Thu, Mar 21, 2024 at 12:45 PM Rowan Tommins [IMSoP]
 wrote:
>
> On 20/03/2024 23:05, Robert Landers wrote:
>
> In other
> words, I can't think of a case where you'd actually want a Type|null
> and you wouldn't have to check for null anyway.
>
>
> It's not about having to check for null; it's about being able to distinguish 
> between "a null value, which was one of the expected types" and "a value of 
> an unexpected type".
>
> That's a distinction which is made everywhere else in the language: parameter 
> types, return types, property types, will all throw an error if you pass a 
> Foo when a ?Bar was expected, they won't silently coerce it to null.
>
>
>
> If you think about it, in this proposal, you could use it in a match:
>
> // $a is TypeA|TypeB|null
>
> match (true) {
>   $a as ?TypeA => 'a',
>   $a as ?TypeB => 'b',
>   $a === null => 'null',
> }
>
>
> That won't work, because match performs a strict comparison, and the as 
> expression won't return a boolean true. You would have to do this:
>
> match (true) {
>   (bool)($a as ?TypeA) => 'a',
>   (bool)($a as ?TypeB) => 'b',
>   $a === null => 'null',
> }
>
> Or this:
>
> match (true) {
>   ($a as ?TypeA) !== null => 'a',
>   ($a as ?TypeB) !== null => 'b',
>   $a === null => 'null',
> }
>
>
> Neither of which is particularly readable. What you're really looking for in 
> that case is an "is" operator:
>
> match (true) {
>   $a is TypeA => 'a',
>   $a is TypeB => 'b',
>   $a === null => 'null',
> }
>
> Which in the draft pattern matching RFC Ilija linked to can be abbreviated to:
>
> match ($a) is {
>   TypeA => 'a',
>   TypeB => 'b',
>   null => 'null',
> }
>
>
> Of course, in simple cases, you can use "instanceof" in place of "is" already:
>
> match (true) {
>   $a instanceof TypeA => 'a',
>   $a instanceof TypeB => 'b',
>   $a === null => 'null',
> }
>
>
>
> Including `null` in that type
> seems to be that you would get null if no other type matches, since
> any variable can be `null`.
>
>
> I can't think of any sense in which "any variable can be null" that is not 
> true of any other type you might put in the union. We could interpret 
> Foo|false as meaning "use false as the fallback"; or Foo|int as "use zero as 
> the fallback"; but I don't think that would be sensible.
>
> In other words, the "or null on failure" part is an option to the "as" 
> expression, it's not part of the type you're checking against. If we only 
> wanted to support "null on failure", we could have a different keyword, like 
> "?as":
>
> $bar = new Bar;
> $bar as ?Foo; // Error
> $bar ?as Foo; // null (as fallback)
>
> $null = null;
> $null as ?Foo; // null (because it's an accepted value)
> $null ?as Foo; // null (as fallback)
>
> A similar suggestion was made in a previous discussion around nullable casts 
> - to distinguish between (?int)$foo as "cast to nullable int" and (int?)$foo 
> as "cast to int, with null on error".
>
>
> Note however that combining ?as with ?? is not enough to support "chosen 
> value on failure":
>
> $bar = new Bar;
> $bar ?as ?Foo ?? Foo::createDefault(); // creates default object
>
> $null = null;
> $null ?as ?Foo ?? Foo::createDefault(); // also creates default object, even 
> though null is an expected value
>
> That's why my earlier suggestion was to specify the fallback explicitly:
>
> $bar = new Bar;
> $bar as ?Foo else null; // null
> $bar as ?Foo else Foo::createDefault(); // default object
>
> $null = null;
> $nulll as ?Foo else null; // null
> $null as ?Foo else Foo::createDefault(); // also null, because it's an 
> accepted value, so the fallback is not evaluated
>
> Probably, it should then be an error if the fallback value doesn't meet the 
> constraint:
>
> $bar = new Bar;
> $bar as Foo else null; // error: fallback value null is not of type Foo
> $bar as ?Foo else 42; // error: fallback value 42 is not of type ?Foo
>
>
>
> Regards,
> --
> Rowan Tommins
> [IMSoP]

Another way of thinking about is:

$x = $a as null

What do you expect $x to be?


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Robert Landers
On Thu, Mar 21, 2024 at 12:45 PM Rowan Tommins [IMSoP]
 wrote:
>
> On 20/03/2024 23:05, Robert Landers wrote:
>
> In other
> words, I can't think of a case where you'd actually want a Type|null
> and you wouldn't have to check for null anyway.
>
>
> It's not about having to check for null; it's about being able to distinguish 
> between "a null value, which was one of the expected types" and "a value of 
> an unexpected type".
>
> That's a distinction which is made everywhere else in the language: parameter 
> types, return types, property types, will all throw an error if you pass a 
> Foo when a ?Bar was expected, they won't silently coerce it to null.
>
>
>
> If you think about it, in this proposal, you could use it in a match:
>
> // $a is TypeA|TypeB|null
>
> match (true) {
>   $a as ?TypeA => 'a',
>   $a as ?TypeB => 'b',
>   $a === null => 'null',
> }
>
>
> That won't work, because match performs a strict comparison, and the as 
> expression won't return a boolean true. You would have to do this:
>
> match (true) {
>   (bool)($a as ?TypeA) => 'a',
>   (bool)($a as ?TypeB) => 'b',
>   $a === null => 'null',
> }
>
> Or this:
>
> match (true) {
>   ($a as ?TypeA) !== null => 'a',
>   ($a as ?TypeB) !== null => 'b',
>   $a === null => 'null',
> }
>
>
> Neither of which is particularly readable. What you're really looking for in 
> that case is an "is" operator:
>
> match (true) {
>   $a is TypeA => 'a',
>   $a is TypeB => 'b',
>   $a === null => 'null',
> }
>
> Which in the draft pattern matching RFC Ilija linked to can be abbreviated to:
>
> match ($a) is {
>   TypeA => 'a',
>   TypeB => 'b',
>   null => 'null',
> }
>
>
> Of course, in simple cases, you can use "instanceof" in place of "is" already:
>
> match (true) {
>   $a instanceof TypeA => 'a',
>   $a instanceof TypeB => 'b',
>   $a === null => 'null',
> }
>
>
>
> Including `null` in that type
> seems to be that you would get null if no other type matches, since
> any variable can be `null`.
>
>
> I can't think of any sense in which "any variable can be null" that is not 
> true of any other type you might put in the union. We could interpret 
> Foo|false as meaning "use false as the fallback"; or Foo|int as "use zero as 
> the fallback"; but I don't think that would be sensible.
>
> In other words, the "or null on failure" part is an option to the "as" 
> expression, it's not part of the type you're checking against. If we only 
> wanted to support "null on failure", we could have a different keyword, like 
> "?as":
>
> $bar = new Bar;
> $bar as ?Foo; // Error
> $bar ?as Foo; // null (as fallback)
>
> $null = null;
> $null as ?Foo; // null (because it's an accepted value)
> $null ?as Foo; // null (as fallback)
>
> A similar suggestion was made in a previous discussion around nullable casts 
> - to distinguish between (?int)$foo as "cast to nullable int" and (int?)$foo 
> as "cast to int, with null on error".
>
>
> Note however that combining ?as with ?? is not enough to support "chosen 
> value on failure":
>
> $bar = new Bar;
> $bar ?as ?Foo ?? Foo::createDefault(); // creates default object
>
> $null = null;
> $null ?as ?Foo ?? Foo::createDefault(); // also creates default object, even 
> though null is an expected value
>
> That's why my earlier suggestion was to specify the fallback explicitly:
>
> $bar = new Bar;
> $bar as ?Foo else null; // null
> $bar as ?Foo else Foo::createDefault(); // default object
>
> $null = null;
> $nulll as ?Foo else null; // null
> $null as ?Foo else Foo::createDefault(); // also null, because it's an 
> accepted value, so the fallback is not evaluated
>
> Probably, it should then be an error if the fallback value doesn't meet the 
> constraint:
>
> $bar = new Bar;
> $bar as Foo else null; // error: fallback value null is not of type Foo
> $bar as ?Foo else 42; // error: fallback value 42 is not of type ?Foo
>
>
>
> Regards,
> --
> Rowan Tommins
> [IMSoP]

I don't think you are getting what I am saying.

$a as int|float

would be an int, float, or thrown exception.

$a as int|float|null

would be an int, float, or null.

Robert Landers
Software Engineer
Utrecht NL


Re: [PHP-DEV] Proposal: AS assertions

2024-03-21 Thread Rowan Tommins [IMSoP]
On 20/03/2024 23:05, Robert Landers wrote:
> In other
> words, I can't think of a case where you'd actually want a Type|null
> and you wouldn't have to check for null anyway.


It's not about having to check for null; it's about being able to distinguish 
between "a null value, which was one of the expected types" and "a value of an 
unexpected type".

That's a distinction which is made everywhere else in the language: parameter 
types, return types, property types, will all throw an error if you pass a Foo 
when a ?Bar was expected, they won't silently coerce it to null.





> If you think about it, in this proposal, you could use it in a match:
> 
> // $a is TypeA|TypeB|null
> 
> match (true) {
>   $a as ?TypeA => 'a',
>   $a as ?TypeB => 'b',
>   $a === null => 'null',
> }


That won't work, because match performs a strict comparison, and the as 
expression won't return a boolean true. You would have to do this:

match (true) {
  (bool)($a as ?TypeA) => 'a',
  (bool)($a as ?TypeB) => 'b',
  $a === null => 'null',
}
Or this:

match (true) {
  ($a as ?TypeA) !== null => 'a',
  ($a as ?TypeB) !== null => 'b',
  $a === null => 'null',
}


Neither of which is particularly readable. What you're really looking for in 
that case is an "is" operator:
match (true) {
  $a is TypeA => 'a',
  $a is TypeB => 'b',
  $a === null => 'null',
}
Which in the draft pattern matching RFC Ilija linked to can be abbreviated to:

match ($a) is {
  TypeA => 'a',
  TypeB => 'b',
  null => 'null',
}


Of course, in simple cases, you can use "instanceof" in place of "is" already:

match (true) {
  $a instanceof TypeA => 'a',
  $a instanceof TypeB => 'b',
  $a === null => 'null',
}




> Including `null` in that type
> seems to be that you would get null if no other type matches, since
> any variable can be `null`.
> 


I can't think of any sense in which "any variable can be null" that is not true 
of any other type you might put in the union. We could interpret Foo|false as 
meaning "use false as the fallback"; or Foo|int as "use zero as the fallback"; 
but I don't think that would be sensible.
In other words, the "or null on failure" part is an option to the "as" 
expression, it's not part of the type you're checking against. If we only 
wanted to support "null on failure", we could have a different keyword, like 
"?as":

$bar = new Bar;
$bar as ?Foo; // Error
$bar ?as Foo; // null (as fallback)

$null = null;
$null as ?Foo; // null (because it's an accepted value)
$null ?as Foo; // null (as fallback)

A similar suggestion was made in a previous discussion around nullable casts - 
to distinguish between (?int)$foo as "cast to nullable int" and (int?)$foo as 
"cast to int, with null on error".


Note however that combining ?as with ?? is not enough to support "chosen value 
on failure":

$bar = new Bar;
$bar ?as ?Foo ?? Foo::createDefault(); // creates default object

$null = null;
$null ?as ?Foo ?? Foo::createDefault(); // also creates default object, even 
though null is an expected value

That's why my earlier suggestion was to specify the fallback explicitly:

$bar = new Bar;
$bar as ?Foo else null; // null
$bar as ?Foo else Foo::createDefault(); // default object

$null = null;
$nulll as ?Foo else null; // null
$null as ?Foo else Foo::createDefault(); // also null, because it's an accepted 
value, so the fallback is not evaluated

Probably, it should then be an error if the fallback value doesn't meet the 
constraint:

$bar = new Bar;
$bar as Foo else null; // error: fallback value null is not of type Foo
$bar as ?Foo else 42; // error: fallback value 42 is not of type ?Foo



Regards,
-- 
Rowan Tommins
[IMSoP]

Re: [PHP-DEV] Proposal: AS assertions

2024-03-20 Thread Robert Landers
On Wed, Mar 20, 2024 at 8:30 PM Rowan Tommins [IMSoP]
 wrote:
>
>
>
> On 20 March 2024 12:51:15 GMT, Robert Landers  
> wrote:
>
> >Oh and there isn't any difference between:
> >
> >$x as ?Type
> >
> >or
> >
> >$x as Type|null
>
>
> I'm not sure if I've misunderstood your example, or you've misunderstood mine.
>
> I'm saying that this should be an error, because the value is neither an 
> instance of Foo nor null:
>
> $a = 42;
> $b = $a as Foo|null;
>
> Your earlier example implies that would make $b equal null, which feels wrong 
> to me, because it means it wouldn't match this:
>
> $a = 42;
> $b = $a as Foo|Bar;
>
> If we want a short-hand for "set to null on error" that should be separate 
> from the syntax for a nullable type.
>
>
> Regards,
> Rowan Tommins
> [IMSoP]

Interesting. I'm not sure there's a better way to say "set to null on
error" since it would be barely sensical to give a nullable type on
the right hand side anyway; so we might as well use it. In other
words, I can't think of a case where you'd actually want a Type|null
and you wouldn't have to check for null anyway.

If you think about it, in this proposal, you could use it in a match:

// $a is TypeA|TypeB|null

match (true) {
  $a as ?TypeA => 'a',
  $a as ?TypeB => 'b',
  $a === null => 'null',
}

No matter what, you're going to have to check for null if you want to
handle all cases and throwing an error when you ask for a nullable
type would render a ton of utility moot (i.e., this match statement
wouldn't work). It's probably better to say, "I want what is on the
right-hand side of `as` or an error." Including `null` in that type
seems to be that you would get null if no other type matches, since
any variable can be `null`.

Robert Landers
Software Engineer
Utrecht NL


Re: [PHP-DEV] Proposal: AS assertions

2024-03-20 Thread Rowan Tommins [IMSoP]


On 20 March 2024 12:51:15 GMT, Robert Landers  wrote:

>Oh and there isn't any difference between:
>
>$x as ?Type
>
>or
>
>$x as Type|null


I'm not sure if I've misunderstood your example, or you've misunderstood mine.

I'm saying that this should be an error, because the value is neither an 
instance of Foo nor null:

$a = 42;
$b = $a as Foo|null;

Your earlier example implies that would make $b equal null, which feels wrong 
to me, because it means it wouldn't match this:

$a = 42;
$b = $a as Foo|Bar;

If we want a short-hand for "set to null on error" that should be separate from 
the syntax for a nullable type.


Regards,
Rowan Tommins
[IMSoP]

Re: [PHP-DEV] Proposal: AS assertions

2024-03-20 Thread Robert Landers
On Wed, Mar 20, 2024 at 1:47 PM Robert Landers  wrote:
>
> On Tue, Mar 19, 2024 at 10:06 PM Rowan Tommins [IMSoP]
>  wrote:
> >
> > On 19/03/2024 16:24, Robert Landers wrote:
> >
> > $x = $attributeReflection->newInstance() as ?MyAttribute;
> > if ($x === null) // do something since the attribute isn't MyAttribute
> >
> >
> > I think reusing nullability for this would be a mistake - ideally, the 
> > right-hand side should allow any type, so "$foo as ?Foo" should mean the 
> > same as "$foo as Foo|null".
> >
> >
> > A better alternative might be to specify a default when the type didn't 
> > match:
> >
> > $x = $attributeReflection->newInstance() as ?MyAttribute else null;
> > if ($x === null) // do something since the attribute isn't MyAttribute
>
> At that point, you are just reinventing already existing things. If
> you wanted to do something like that with my proposal:
>
> $x = $attributeReflection->newInstance() as ?MyAttribute ??
> MyAttribute::createDefault();
>
> Robert Landers
> Software Engineer
> Utrecht NL

Oh and there isn't any difference between:

$x as ?Type

or

$x as Type|null

The codebase I work with prefers ? over |null, but they are the same.


Re: [PHP-DEV] Proposal: AS assertions

2024-03-20 Thread Robert Landers
On Tue, Mar 19, 2024 at 10:06 PM Rowan Tommins [IMSoP]
 wrote:
>
> On 19/03/2024 16:24, Robert Landers wrote:
>
> $x = $attributeReflection->newInstance() as ?MyAttribute;
> if ($x === null) // do something since the attribute isn't MyAttribute
>
>
> I think reusing nullability for this would be a mistake - ideally, the 
> right-hand side should allow any type, so "$foo as ?Foo" should mean the same 
> as "$foo as Foo|null".
>
>
> A better alternative might be to specify a default when the type didn't match:
>
> $x = $attributeReflection->newInstance() as ?MyAttribute else null;
> if ($x === null) // do something since the attribute isn't MyAttribute

At that point, you are just reinventing already existing things. If
you wanted to do something like that with my proposal:

$x = $attributeReflection->newInstance() as ?MyAttribute ??
MyAttribute::createDefault();

Robert Landers
Software Engineer
Utrecht NL


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Ilija Tovilo
Hi Rowan

On Tue, Mar 19, 2024 at 8:39 PM Rowan Tommins [IMSoP]
 wrote:
>
> As well pattern matching, which Ilija mentioned, another adjacent feature is 
> a richer set of casting operators. Currently, we can assert that something is 
> an int; or we can force it to be an int; but we can't easily say "make this 
> an int if safe, but throw otherwise" or "make this an int if safe, but 
> substitute null/$someValue otherwise".
>
> I've been considering how we can improve that for a while, but not settled on 
> a firm proposal - there's a lot of different versions we *could* support, so 
> choosing a minimal set is hard.

I've thought about this in the context of pattern matching a while
back. I was thinking about something like `$x is ~int`, where the
pattern match is successful iff `$x` is coercible to `int` without
loss of information. Given that patterns may be nested, `array<~int>`
could check that all elements of an array are coercible to `int`. The
same could work for literal patterns, e.g. `~5`, where `5`, `5.0` and
`'5'` are all accepted.

This can potentially be combined with the variable binding pattern,
`$var @ pattern`. The syntax looks a bit confusing at first, but it
basically makes sure that the matched value conforms to `pattern`, and
then binds it to `$var`. Hence, something like `$foo as Foo { $bar @
~int }` would 1. make sure `$foo` is an instance of `Foo`, 2. make
sure `$foo->bar` is coercible to `int`, and then assigned the coerced
value to `$bar`. (It gets more complicated, because the assignment
must be delayed until the entire pattern matches.) If the pattern
matching fails at any point, it throws.

This is just an idea, neither the `as` operator nor the `~` pattern
have been implemented. I don't know whether they are feasible.

Anyway, we're probably going off-topic. :)

Ilija


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Rowan Tommins [IMSoP]

On 19/03/2024 16:24, Robert Landers wrote:

$x = $attributeReflection->newInstance() as ?MyAttribute;
if ($x === null) // do something since the attribute isn't MyAttribute



I think reusing nullability for this would be a mistake - ideally, the 
right-hand side should allow any type, so "$foo as ?Foo" should mean the 
same as "$foo as Foo|null".



A better alternative might be to specify a default when the type didn't 
match:


$x = $attributeReflection->newInstance() as ?MyAttribute else null;
if ($x === null) // do something since the attribute isn't MyAttribute

Which then also allows you to skip the if statement completely:

$x = $attributeReflection->newInstance() as MyAttribute else 
MyAttribute::createDefault();



That then looks a lot like a limited-use version of syntax for catching 
an exception inline, which would be nice as a general feature (but I 
think maybe hard to implement?)


$x = somethingThatThrows() catch $someDefaultValue;


As well pattern matching, which Ilija mentioned, another adjacent 
feature is a richer set of casting operators. Currently, we can assert 
that something is an int; or we can force it to be an int; but we can't 
easily say "make this an int if safe, but throw otherwise" or "make this 
an int if safe, but substitute null/$someValue otherwise".


I've been considering how we can improve that for a while, but not 
settled on a firm proposal - there's a lot of different versions we 
*could* support, so choosing a minimal set is hard.



Regards,

--
Rowan Tommins
[IMSoP]


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Ilija Tovilo
Hi Marco

On Tue, Mar 19, 2024 at 7:04 PM Marco Aurélio Deleu  wrote:
>
> > On 19 Mar 2024, at 14:51, Ilija Tovilo  wrote:
> >
> > Hi Robert
> >
> >> On Tue, Mar 19, 2024 at 5:24 PM Robert Landers  
> >> wrote:
> >>
> > See https://wiki.php.net/rfc/pattern-matching#throwing_alternative. I
> > believe this idea would combine nicely with pattern matching. It has
> > many more uses there than just simple class type matching, and could
> > even be used for things like destructuring.
>
> That looks like a PHP dream. Has there been any work regarding that?

https://github.com/iluuu1994/php-src/pull/102/files

The implementation is mostly complete (it might slightly diverge from
the current specification. Bob has called for a different
implementation approach that might be more complex but potentially
easier to optimize, I'll have to play around with it. There are also
still some design decisions that we aren't completely sure about. For
now, Larry and I are just trying to get property hooks over the finish
line.

Ilija


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Marco Aurélio Deleu


Marco Deleu 

> On 19 Mar 2024, at 14:51, Ilija Tovilo  wrote:
> 
> Hi Robert
> 
>> On Tue, Mar 19, 2024 at 5:24 PM Robert Landers  
>> wrote:
>> 
>> I've been thinking about this as an RFC for awhile, but with generics
>> being far off (if at all), I'd like to propose a useful idea: reusing
>> the AS keyword in a different context.
>> 
>> Example:
>> 
>> $x = $attributeReflection->newInstance() as MyAttribute;
>> 
>> This would essentially perform the following code:
>> 
>> assert(($x = $attributeReflection->newInstance()) instanceof MyAttribute);
> 
> See https://wiki.php.net/rfc/pattern-matching#throwing_alternative. I
> believe this idea would combine nicely with pattern matching. It has
> many more uses there than just simple class type matching, and could
> even be used for things like destructuring.
> 
> Ilija

That looks like a PHP dream. Has there been any work regarding that?

Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Marco Pivetta
On Tue, 19 Mar 2024 at 17:46, Deleu  wrote:

> On Tue, Mar 19, 2024 at 1:42 PM Marco Pivetta  wrote:
>
>> One note: if what you are going for is what `azjezz/psl`, be aware that
>> exception / error tracing design needs special attention here: it's not as
>> simple as it looks!
>>
>
> I believe you answered your own question here. The proposal seems far
> simpler and reaches 100% of PHP projects as opposed to the ones that either
> opt to use psalm or opt to use azjezz/psl.
>

Eh, kinda: you'd need to check how `Psl\Type\TypeInterface` recursively
validates types and throws meaningful errors.

Having that in the engine, given its structure, is a massive BC surface
that is best kept as a `composer` dependency that can move separately.
I hardly see that working in a language-level RFC, with the speed at which
the language can do BC incompatible changes.

See
https://github.com/azjezz/psl/blob/5f0aeacb708a33d5b2d53a832736c7767a99b215/src/Psl/Type/TypeInterface.php#L21-L35
See
https://github.com/azjezz/psl/blob/5f0aeacb708a33d5b2d53a832736c7767a99b215/src/Psl/Type/Exception/CoercionException.php#L49
See
https://github.com/azjezz/psl/blob/5f0aeacb708a33d5b2d53a832736c7767a99b215/src/Psl/Type/Exception/Exception.php#L22
See
https://github.com/azjezz/psl/blob/5f0aeacb708a33d5b2d53a832736c7767a99b215/src/Psl/Type/Exception/TypeTrace.php

That stuff is all but figured out, even in userland :-)

Also worth mentioning:

https://github.com/CuyZ/Valinor/blob/37993b64a6eb04dc0aee79e03f2ddb4f86ff9c3a/src/Mapper/TreeMapper.php#L23-L25
https://github.com/CuyZ/Valinor/blob/37993b64a6eb04dc0aee79e03f2ddb4f86ff9c3a/src/Mapper/MappingError.php#L13
and the whole rabbit hole behind that

Marco Pivetta

https://mastodon.social/@ocramius

https://ocramius.github.io/


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Ilija Tovilo
Hi Robert

On Tue, Mar 19, 2024 at 5:24 PM Robert Landers  wrote:
>
> I've been thinking about this as an RFC for awhile, but with generics
> being far off (if at all), I'd like to propose a useful idea: reusing
> the AS keyword in a different context.
>
> Example:
>
> $x = $attributeReflection->newInstance() as MyAttribute;
>
> This would essentially perform the following code:
>
> assert(($x = $attributeReflection->newInstance()) instanceof MyAttribute);

See https://wiki.php.net/rfc/pattern-matching#throwing_alternative. I
believe this idea would combine nicely with pattern matching. It has
many more uses there than just simple class type matching, and could
even be used for things like destructuring.

Ilija


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Deleu
On Tue, Mar 19, 2024 at 1:42 PM Marco Pivetta  wrote:

> One note: if what you are going for is what `azjezz/psl`, be aware that
> exception / error tracing design needs special attention here: it's not as
> simple as it looks!
>

I believe you answered your own question here. The proposal seems far
simpler and reaches 100% of PHP projects as opposed to the ones that either
opt to use psalm or opt to use azjezz/psl.

-- 
Marco Deleu


Re: [PHP-DEV] Proposal: AS assertions

2024-03-19 Thread Marco Pivetta
Hey Robert,


On Tue, 19 Mar 2024 at 17:24, Robert Landers 
wrote:

> Hello internals,
>
> I've been thinking about this as an RFC for awhile, but with generics
> being far off (if at all), I'd like to propose a useful idea: reusing
> the AS keyword in a different context.
>
> Example:
>
> $x = $attributeReflection->newInstance() as MyAttribute;
>
> This would essentially perform the following code:
>
> assert(($x = $attributeReflection->newInstance()) instanceof MyAttribute);
>
> but would work even if assertions are disabled, and would provide some
> sanity when working with mixed return types, or even dealing with
> interfaces where you want to be sure you are dealing with a concrete
> type:
>
> class Query implements QueryInterface {}
>
> function getQuery(string $sql): QueryInterface {}
>
> $x = getQuery("select 1 = 1") as Query;
>
> which is more like:
>
> assert(($x = getQuery("select 1 = 1")) instanceof Query);
>
> It'd also be nice to have a non-throwing version where we simply
> specify that the type is nullable:
>
> $x = $attributeReflection->newInstance() as ?MyAttribute;
> if ($x === null) // do something since the attribute isn't MyAttribute
>
> which is more like:
>
> try {
>   assert(($x = $attributeReflection->newInstance()) instanceof
> MyAttribute);
> } catch {
>   $x = null
> }
>
> Or a more complex type:
>
> $x = $attributeReflection->newInstance() as
> PretttyAttribute|(UglyAttribute);
>
> Essentially, by using "as", you can be 100% sure that the type is the
> expected type signature, null (if the type signature includes null),
> or an error to be thrown.
>
> Note that this isn't casting from one type to another, but asserting
> that this type is the type you expect. It'd significantly help with
> static analysis, IDE code completion, etc.
>
> What do you think?
>
> Robert Landers
> Software Engineer
> Utrecht NL
>

What's the advantage of a language construct over the following?

```php
/**
 * @template T of object
 * @psalm-assert T $value
 * @param class-string $type
 */
function as(mixed $value, string $type): mixed
{
if (! $value instanceof $type) { throw
SomeKindOfException::forMismatchingRequirements($value, $type); }

return $value;
}

echo as(myExpression(), MyType::class)->methodOfMyType();
```

See https://3v4l.org/iQPok
See https://phpstan.org/r/708912d3-64e2-46f0-9f9e-467921a6489a
See https://psalm.dev/r/7f30d63865

Note that `azjezz/psl` provides a very complete toolkit around this kind of
tooling:
https://github.com/azjezz/psl/tree/5f0aeacb708a33d5b2d53a832736c7767a99b215/src/Psl/Type

One note: if what you are going for is what `azjezz/psl`, be aware that
exception / error tracing design needs special attention here: it's not as
simple as it looks!


Marco Pivetta

https://mastodon.social/@ocramius

https://ocramius.github.io/