On Thu, Mar 21, 2024 at 11:06 PM Rowan Tommins [IMSoP]
<imsop....@rwec.co.uk> 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 MyOtherType) {
  $value->doSomethingDifferent();
}

This would be better written as

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

or

$value = genericFuncReturnsMixed();
if ($value === null) {
  /* handle null */
}
($value as MyType|null)?->doSomething();
($value as MyOtherType|null)?->doSomethingDifferent();

Not having an "escape hatch" would mean writing something like:

$value = genericFuncReturnsMixed();
if ($value === null) {
  /* handle null */
}
try {
  ($value as MyType)->doSomething();
} catch {
}

try {
  ($value as MyOtherType|null)->doSomethingDifferent();
} catch {
}

which will probably trip up a bunch of lint rules for having an empty
catch ... but that is beside the point.


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

I agree this is the key point, and goes back to what I was saying
about null not actually being a type, but a value. If any value could
be a type, I think I would feel differently, but I'd still argue that
null should be a special case for the nice syntax you get from that.

>
> Regards,
>
> --
> Rowan Tommins
> [IMSoP]

Reply via email to