Re: [RFC] Nullable intersection types
| From: | Larry Garfield | Date: | Sat, 24 Jul 2021 21:31:21 +0000 |
| Subject: | Re: [RFC] Nullable intersection types | ||
| References: | 1 2 3 4 5 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-115580@lists.php.net to get a copy of this message | ||
On Sat, Jul 24, 2021, at 12:33 AM, Tobias Nyholm wrote:
> >> @Larry makes an argument to keep them:
> >>
> >>> Requiring parenthesis now leaves the option open in the future to make them
> >>> optional when doing full mixed types.
> >>
> >>
> >> I don’t understand why we should require something that is not needed simply because
> >> it would give us an option to remove it later… Could you elaborate why this is important? (Im
> >> probably missing something)
> >
> > The difference is if we make the decision to use the
> >
?X&Y syntax and we later realize it was a mistake then we are
> > stuck with it.
> >
> > OTOH if we use the (X&Y)|null syntax and later realize it is okay to also allow
> > ?X&Y PHP could later be changed to allow it.
> >
> > The later is the choice that manages future risk better.
>
> I thought Larry was discussing X & Y | null vs
> (X & Y) | null.
> I’ve dropped ?X&Y because Derick had technical arguments
> against it.
>
> The way I see it, there is no benefit in requiring the parentheses in
> (X & Y) | null. I suggest we use X & Y
> | null.
Let me expand on my previous point (though those who have restated it are essentially correct).
1) Full mixed intersection and union types is something that we want to do in the future; it may or
may not happen in 8.2, but it's something that should be considered on the informal roadmap.
2) Therefore, IF nullable intersection types are added now, they MUST be added in a way that is
future-compatible (syntactically) with full mixed types in the future, as nullable intersection
types are by definition (in PHP) a reduced-scope form of mixed types. To do otherwise would create
a needless inconsistency in the language.
3) The parsing logic for intersection types is already bonkers, and has to do some wonky shenanigans
in order to work.
4) At this time, it is unclear if it will be *technically* possible to support
Foo&Bar|Baz as a syntax. Whether it is desireable or not is subjective, but
whether it is possible at all is an unknown at this point. Given the complexity already in place,
it may not be possible, no matter how much anyone argues that it's "obvious" based on
math. If the technical answer is no, then the desireability question is entirely irrelevant.
5) Point 4 means that we simply don't know if X&Y|null will be extensible to
X&Y|Z. It may be; it may also be impossible or infeasible. Point 3 means I would be highly
skeptical of that being the case, and thus we should assume it is not.
6) That means there are a couple of possible end-states, assuming X&Y|null is implemented now:
A) X&Y|Z and (X&Y)|Z both work fine, whether Z is null or not.
B) X&Y|Z works iff Z is null; if it's not, then you must do (X&Y)|Z.
C) X&Y|null cannot remain supported once full mixed types are supported, at least not without
still-more special casing and shenanigans. That means either breaking BC, never implementing full
mixed types, or having a wonky one-off in the engine forever.
D) Some other mechanism such as the sometimes discussed type aliases provides an alternate way to
solve this problem. (Eg, you have to pre-define a complex type Foo = X&Y|Z, and then in a
function signature you can type either Foo or ?Foo.)
As of right now, *we do not know which of those will happen*. I think most would be fine with A as
an outcome, but there is very significant risk of C happening, and C is a bad outcome. Even if B
could be done with a minimum of engine wonkiness, it would still be a sub-ideal state from a
developer point of view. And D is still a very open question that may complicate the whole
situation further.
Thus, IF nullable intersection types are supported now, we're basically guessing on whether it
will be possible to support mixed types in the future without parentheses.
If we guess that it will be possible, and we're right, spiffy!
If we guess that it will be possible, and we're wrong, we're in case C above. This is the
worst outcome.
If we guess that it will not be possible, and we're right, spiffy!
If we guess that it will not be possible, and we're wrong, then people using nullable
intersection types get a bonus new feature along the way.
Of those, the only not-happily-ever-after alternative is guessing the parens will be optional and
being wrong. Everything else works out OK. Thus, we should guess that it will not be possible, as
that can only lead to a positive outcome whether we're right or wrong.
Based on the above logic, I will not be voting for this RFC if it does not include parens, as it is
too risky given the open unknowns around what will be possible in the future.
------
Separate from the technical argument, the question of how necessary it is:
The assertion that intersection types are useless without nullability is facetious and wrong. Are
they more useful with nullability? Yes, I don't think that's really debatable. But
not-useful is incorrect.
PHP 7 scalar types *were* useful. They *did* get used. Just not in as many cases as they could
have been. However, I still die a little inside every time I have a nullable argument/return. To
me, nullable always means a design flaw, even if a small one. (I still use it, but I hate that I
have to.) The overwhelming majority of my type declarations are not nullable, and so would work
perfectly fine with intersection types.
I have no data on just how many libraries skipped 7.0 as a target version, but to assert that
"everyone" did so because of the lack of nullability is simply absurd. Here's a
bunch of other perfectly good reasons why a library would have skipped 7..0 as a target, many of
which I know projects did do:
1) PHP 7's unified variable syntax was a PITA to update to in some cases, as it's hard to
test for. By the time projects really worked all that out, 7.1 was out anyway.
2) PHP 5.6 had an extra-long support lifespan, so there was less incentive to do so than usual.
3) Even today aggressively tracking new versions is unusual, based on the Composer stats. 6 years
ago it was even less common. What is super common is for projects to skip several required versions
all at once when doing larger overhauls. (That includes Symfony.)
4) Linux distributions come out less frequently than PHP versions, so they often skip over
"officially supported" PHP versions.
So yeah, to pin the entirely of PHP 7.1 seeing a larger uptick than 7.0 on nullability is an
entirely nonsensical and unsupported claim. That may well have been true for some projects, but
there is zero evidence that was the primary reason for most projects, or even a major reason at all.
I'm not against adding nullable intersection types now, but the "8.1 will be useless
otherwise" rhetoric being used to argue for it is inappropriate and unjustified.
--Larry Garfield