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]

Reply via email to