Skip to content

Make intersection types nullable #7259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

nicolas-grekas
Copy link
Contributor

@nicolas-grekas nicolas-grekas commented Jul 19, 2021

Putting my OSS time where my mouth is :)

RFC at https://wiki.php.net/rfc/nullable_intersection_types

PHP 8.1 intersection types should be nullable IMHO. They're going to be much less interesting otherwise.

See https://externals.io/message/115479 for background and discussion.

This reuses the existing ? prefix to make intersection types compatible with null as argument, property, and return value. Updated to use X&Y|null as I feel like that's going to gather a broader consensus.

This allows writing:

class Foo
    public X&Y|null $bar;


    function setBar(X&Y|null $bar = null): X&Y|null
    {
		return $this->bar = $bar;
	}
}

The ? syntax is choosen because it is the least surprising one and because the code already handles it (eg when computing error messages, see tests.)

On the implementation side, the nullable type is handled separately from union types. That's why the patch is trivial.

Please send me more test cases if you have weird situation in mind, I'd happily add them here.

@trowski
Copy link
Member

trowski commented Jul 19, 2021

The syntax should be either ?(X&Y) or (X&Y)|null (and IMO preferably only the latter) for readability and best forward-compatibility.

@nicolas-grekas
Copy link
Contributor Author

nicolas-grekas commented Jul 19, 2021

The syntax should be either ?(X&Y)

?X&Y cannot be confused with (?X)&Y if that's what you mean, because (?X)&Y trivially means the same as X&Y. The brackets are thus not needed.

or (X&Y)|null (and IMO preferably only the latter) for readability and best forward-compatibility.

This syntax does not exist, 8.1 should not introduce it I believe. What exists is nullable types with a ? prefix. This builds on it. This is both easy to read (and people are used to it) and forward-compatible, as it doesn't collide with any of the discussed possible syntax for more complex type expressions.

We do need nullable intersection types now, not in the future.

@trowski
Copy link
Member

trowski commented Jul 19, 2021

It's not that ?X&Y is technically a problem, it's that ? was not allowed with union types due to readability concerns, so allowing it now with intersection types is not something we can quickly do before 8.1. I think the only syntax that could be allowed without an RFC would be (X&Y)|null (and I'm not sure about that, but it is the obvious and certainly forward-compatible syntax).

There are also LSP concerns with allowing null on an intersection type. I don't know what needs to be handled there as I have not given it much thought, but any implementation needs to consider and handle those cases.

@nicolas-grekas
Copy link
Contributor Author

It's not that ?X&Y is technically a problem, it's that ? was not allowed with union types due to readability concerns, so allowing it now with intersection types is not something we can quickly do before 8.1. I think the only syntax that could be allowed without an RFC would be (X&Y)|null.

I don't know why you think so, because to me, ?X&Y is the closest to what we already have, while (X&Y)|null builds on a proposal that has no technical root right now. Nobody knows if complex types are going to make it ever in the future. As such, ?X&Y is the only possible and realistic choice in the immediate future.

There are also LSP concerns with allowing null on an intersection type.

This is handled AFAIK. Please check the test cases and let me know if you have more cases in mind.

@@ -798,6 +798,7 @@ type_expr:
| '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
| union_type { $$ = $1; }
| intersection_type { $$ = $1; }
| '?' intersection_type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the nullability syntax. I find ?X&Y, while technically unambiguous due to null&Y not making sense, just isn't great for readability.

Further, we decided against ?X|Y in the union types RFC, and I think that decision should hold (even though the reasons are somewhat different.

I've got an implementation for (X&Y)|null, which is my preferred syntax at: https://gist.github.com/sgolemon/660b105e6894a6341792a479cda8f76b

However, we are much too late for 8.1 at this point and syntax is too important to rush, so this is going to have to wait for 8.2 no matter what we do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the nullability syntax. I find ?X&Y, while technically unambiguous due to null&Y not making sense, just isn't great for readability.

Further, we decided against ?X|Y in the union types RFC, and I think that decision should hold (even though the reasons are somewhat different.

Agreed.

I've got an implementation for (X&Y)|null

I'm not opposed to allowing parenthesis, but requiring them seems unnecessary - the precedence of &/| and &&/|| is known in other contexts in php (operator precedence).

If (A&B)|null was allowed, there'd be the question of whether (A)|null should be allowed, (A)|(null), etc. It seems like it should (IF allowed), though the syntax seems less readable

However, we are much too late for 8.1 at this point and syntax is too important to rush, so this is going to have to wait for 8.2 no matter what we do.

Agreed.

Copy link

@iquito iquito Jul 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can use ?X|Y being disallowed to conclude that everyone would also be against the special case of ?X&Y. At the time in the Github discussion I argued against ?(X|Y), because it added parenthesis to not be ambiguous which would otherwise not be needed, and X|Y|null was available as an alternative. In that case having a more complicated syntax only to somehow include ? would have been counterproductive, in my opinion.

Yet I very much see the value of ?X&Y - it enables optional parameters/properties, and it does not contain parenthesis. If combined union type and intersection type syntax ever makes it into PHP, it would be easy to not allow ? there, just as ? is not allowed for union types. So the syntax seems forward-compatible and geared towards a common use case in PHP.

Copy link
Member

@sgolemon sgolemon Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If (A&B)|null was allowed, there'd be the question of whether (A)|null should be allowed, (A)|(null), etc. It seems like it should (IF allowed), though the syntax seems less readable

That's a fair complaint. For me it comes down to finding a balance between providing the functionality that's being asked for quite strongly, and not over-delivering syntax that hasn't been entirely thought through. That's why my version of this patch only allows null on the right hand side, and requires parens around the intersection. It's the MOST restrictive syntax, and thereby the least likely to cause issues after we've spent more time thinking about it.

@Ocramius
Copy link
Contributor

Ocramius commented Jul 19, 2021

while (X&Y)|null builds on a proposal that has no technical root right now.

Which is why the RFC for intersection types is called "pure intersection types", and solves only that problem (intersection).

Here, we are introducing partial union + intersection semantics around null, which:

  • requires a new RFC
  • should really require explicit parentheses, as you are assuming precedence in type definition, which isn't a given.

In addition to that, @trowski makes it clear that readability is also messy. For instance, I would expect:

  • ?A&B => means (?A)&B => reduced to A&B
  • A&?B => means A&(?B) => reduced to A&B too

In both cases, the ? would be elided, leading to confusion. This is according to my naiive assumption - aware this patch uses a different precedence for ?. Instead, having a clear (A&B)|null is much simpler and leaves no doubt.

It's not a big deal to, for now:

  • hold yer horses
  • @param or @var docblock until you have well defined union semantics (for which you are welcome to start an RFC). We've done it for years, and it can wait +1 year, when/if the DSL is voted upon (can be copied from phpdocumentor/phpstan/psalm).

@trowski
Copy link
Member

trowski commented Jul 19, 2021

This is handled AFAIK. Please check the test cases and let me know if you have more cases in mind.

Ok, when I glanced at the diff I expected to see more C code changes to handle LSP, but it seems the existing code handles it nicely.

However, we are much too late for 8.1 at this point and syntax is too important to rush, so this is going to have to wait for 8.2 no matter what we do.

Agreed. Any syntax choice here is going to have repercussions, so we can't be hasty.

@sgolemon
Copy link
Member

It's not a big deal to, for now:

  • hold yer horses
  • @param or @var docblock until you have well defined union semantics (for which you are welcome to start an RFC). We've done it for years, and it can wait +1 year, when/if the DSL is voted upon (can be copied from phpdocumentor/phpstan/psalm).

☝️☝️☝️ This ☝️☝️☝️

@alcaeus
Copy link
Contributor

alcaeus commented Jul 20, 2021

It's not a big deal to, for now:

  • hold yer horses

I'll mention that using scalar types in PHP 7 was less than optimal since they weren't nullable. For most of the code I maintain (both open source and commercial) it didn't make sense to bother with typing in php 7.0, instead we waited for nullable types in 7.1.

I fear this case will be similar, especially since making the parameter nullable later will cause a bc break, while the change itself does not cause bc breaks.

If we're all aware of this and decide that limited use cases and thus limited feedback is what we're going for in favour of consistent nullability across types, there's no problem. Otherwise I would bring up the point of moving intersection types to 8.2 with nullability. This would require significant discussion, which is why I'm only doing an informal check of opinion instead of asking on the mailing list.

@nicolas-grekas
Copy link
Contributor Author

nicolas-grekas commented Jul 20, 2021

Thank you so much @sgolemon for that patch you provided in #7259 (comment)

While I personally think ?X&Y is the most appropriate, I feel like using |null would gather a broader consensus. The attached patch now contains a second commit that changes the syntax to X&Y|null (PR description also updated.) Because operator precedence is known, the brackets are not needed and I'd prefer skipping them. Type expressions are already long enough.

we are much too late for 8.1 at this point and syntax is too important to rush, so this is going to have to wait for 8.2 no matter what we do.

I would kindly ask you to reconsider this. As @alcaeus just mentioned, PHP 7.0 was blatantly missing nullable types. Let's learn from history and recognize that having intersection types not nullable right away makes them way less interesting/useful. This feels like a bug from a feature completeness pov. ExtractExactly what the feature freeze period is for. Alternatively, we could move intersection types to 8.2, but that'd be a shame given that we're so close to having them complete.

(( Apologies, I didn't mean to edit your comment, I meant to quote it and I evidently don't know how to github -- @sgolemon ))

@krakjoe
Copy link
Member

krakjoe commented Jul 20, 2021

There doesn't really look like there is consensus on this, and tagging happens this afternoon.

Since no consensus can be built in half a day, it looks like intersections will have to be deployed as they have been voted on while this is resolved.

@faizanakram99
Copy link

faizanakram99 commented Jul 20, 2021

I'll mention that using scalar types in PHP 7 was less than optimal since they weren't nullable. For most of the code I maintain (both open source and commercial) it didn't make sense to bother with typing in php 7.0, instead we waited for nullable types in 7.1.

I fear this case will be similar, especially since making the parameter nullable later will cause a bc break, while the change itself does not cause bc breaks.

Adding default null value ( = null) added nullability implicitly in 7.0 and is still quite common.

https://3v4l.org/SNctX#v7.0.0

BC breaks were avoided back then by using default null value (and later prefixing types with ? in 7.1).

@guilliamxavier
Copy link
Contributor

Adding default null value ( = null) added nullability implicitly in 7.0 and is still quite common.

https://3v4l.org/SNctX#v7.0.0

Note that this"workaround" was only possible for parameters (and with the possibly undesired side effect of making them optional), not returns (e.g. https://3v4l.org/3CrX5#v7.0.0 ), so several return type declarations couldn't be added before 7.1

@sgolemon
Copy link
Member

Because operator precedence is known, the brackets are not needed and I'd prefer skipping them. Type expressions are already long enough.

While I don't like eliding the parens, I'm not going to veto it on those grounds, but would you consider making parens optional? It wouldn't make the change to the parser massively larger...

we are much too late for 8.1 at this point and syntax is too important to rush, so this is going to have to wait for 8.2 no matter what we do.

I would kindly ask you to reconsider this.

It's not my call at the end of the day. The 8.1 RMs (@ramsey, @patrickallaert, @krakjoe) have the most sway at this point in the release. I would certainly reiterate that going slow only costs us a little time. Rushing has potentially much worse consequences.

@krakjoe
Copy link
Member

krakjoe commented Jul 20, 2021

The least disruptive course of action seems to be for this detail to go through the normal RFC process, if voting on that closes before RC, we can just merge it.

@krakjoe krakjoe added the RFC label Jul 21, 2021
@guilliamxavier
Copy link
Contributor

guilliamxavier commented Jul 21, 2021

While I don't like eliding the parens, I'm not going to veto it on those grounds, but would you consider making parens optional? It wouldn't make the change to the parser massively larger...

While at it: the current patch seems to allow X&Y|null but not null|X&Y (just saying)

Edit: OK, that's a deliberate choice

@nikic nikic added this to the PHP 8.1 milestone Jul 21, 2021
@nicolas-grekas
Copy link
Contributor Author

The RFC is declined.

I'm closing to let someone else free to take over at any time in the future.

@nicolas-grekas nicolas-grekas deleted the nullableintersection branch August 27, 2021 20:22
@nicolas-grekas nicolas-grekas restored the nullableintersection branch June 19, 2022 18:48
@nicolas-grekas nicolas-grekas deleted the nullableintersection branch June 19, 2022 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.