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]