[RFC] Union Types v2 - Externals
externals.io/message/106844Posted Sep 4, 2019, 8:46 AM UTC
Posted Sep 4, 2019, 8:46 AM UTC
It's also great that this RFC is discussed on GitHub. I believe it'll result in a more on-topic and relevant discussion: https://github.com/php/php-rfcs/pull/1
Discussion is not limited to the collaborators right now though, so it's probably going to derail pretty soon.
I don't think it will derail at all. Similar workflow works for Symfony PRs/RFCs for many years and it's very healthy.
You can kick the bad guys. No problem
You can moderate GitHub comments, you can't do that on a mailing list ;)
[deleted]
It’s possible, but plenty of other languages manage their RFC process this way and seem to get by fine.
This, so much. Mailing lists are a thing of the past, now we have much be tools at our disposal.
I believe it'll result in a more on-topic and relevant discussion:
Slowly turning into typephp (typescript). I like it tho.
pthypescript (pt as in pterodactyl, thy as in thyme)
We need it turning into typephp faster!
Isn't this less strict than only allow one type though?
It id a good thing. And I like the proposals. I love typescript. Suprisingly, I designed my own language (https://nuua.io) with a similar syntax before learning it.
Not necessarily. It just gives you more options. For instance you could type hint Chronos\Chronos|Carbon\Carbon, which is more specific than \DateTimeInterface.
I really hope this one will pass !
Hell yes, please. And when we add intersections and generics, we gonna finally be able to say we have a type system worth something.
Intersection types, parametrized types, aliased types (typedefs), and perhaps even general value types.
The reason I say parameterized instead of generics is that what I really want is the type information at runtime. And since Java generics delete that info...
That way you can do something like
class Foo<T> {
public function whatever(T $value) {
if (T instanceof Observable) {
$value->subscribe($this);
}
// Other stuff
}
}
Also, I'd really LOVE to see "structural" typing come in. Where you can say "Any object that has these methods with these signatures, even if they don't declare the interface". Something I proposed a few years ago: https://wiki.php.net/rfc/protocol_type_hinting which is very similar to Go's type system...
Where did that RFC go?
Someone suggested that Psalm support a similar syntax earlier this year.
It went nowhere. There was a decent amount of support for it, but several key contributors didn't see "the value" and hence I pulled it.
It comes up every now and then, and a few major frameworks have asked for it. I think it will just take someone picking it up again and running with it to get it in...
I'm aware of generics, but don't think I've heard of the term "intersection" before in PHP context. Can you briefly elaborate or point me in the right direction? :)
It's basically the opposite of a union type. A union type like Foo | Bar is "Foo or Bar", while an intersection type like Foo & Bar would be "Foo and Bar".
That makes a lot of sense! Thanks for coming up with the shortest explanation while still getting the point across :)
Let's say you have two interfaces, `Countable` and `ArrayAccess` for example. Both provide different behavior.
Let's say your method wants to accept classes that implement both interfaces. Today, you have two choices to enforce that behavior: 1) Do manual if statements to check if the object implements both, 2) Create a new interface that extends both, and check for that interface.
Intersection types let you say "this parameter must implement *all* of these interfaces".
function foo((Countable&ArrayAccess)|array $arrayLike) {
for ($i = 0; $i < count($arrayLike); $i++) {
echo $arrayLike[$i];
}
}
I definitely like the sound of this! I can think of multiple places I could use this functionality!
Pardon my ignorance, but what other languages has this kind of feature? I've never really come across it in my few professional years.
Also, wouldn't this be the same as implementing your own interface that extends the 2-or-more interfaces you're requesting? I'm aware that this means extra code = extra maintenance, but depending on the use case having comprised interfaces might improve readability. E.g.:
interface CountableAndArrayAccess extends \Countable, \ArrayAccess
{
[...]
}
I'm aware my above suggestion is a step away from the generics concept, and the amount of comprised interfaces might scale exponentially. Consider it me playing the devil's advocate. But as soon as you specify more than 2 interfaces, the function signature might start to look cluttered in extreme cases. Compare:
function foo((Countable&ArrayAccess&ThirdClass&FourthClass)|array $arrayLike) {
for ($i = 0; $i < count($arrayLike); $i++) {
echo $arrayLike[$i];
}
}
... To:
interface Comprised extends \Countable, \ArrayAccess, ThirdClass, FourthClass
{
[...]
}
function foo(Comprised|array $arrayLike) {
for ($i = 0; $i < count($arrayLike); $i++) {
echo $arrayLike[$i];
}
}
That totally works if you own the implementing classes. But what happens if it's a library that you're taking in, or a library that you're building where you don't have control of the code being written. Sure, you could make an empty child class just to implement your interfaces:
class LibWithInterface extends Lib implements Comprised {}
But that gets really messy quickly, and as you pointed out the number of combinations explodes quickly. Especially if you follow [ISP](https://en.wikipedia.org/wiki/Interface_segregation_principle). It's not that it isn't *possible*, but that it's messy and a pain in the rear end for anything non-trivial.
As far as the signature getting messy, that's where type aliases come into play. For example, something like this could be supported:
use (Countable&ArrayAccess&ThirdClass&FourthClass)|array as MyType;
function foo(MyType $bar) {...}
Or even:
use Countable&ArrayAccess as ArrayLike;
use ArrayLike&ThirdClass as MyWhatever;
use MyWhatever&FourthClass as MyType;
function foo(MyType $bar) {...}
As far as other languages that support this:
There are others that are more obscure, but they are the popular ones. Incidentally, they are also under discussion for [C#](https://github.com/dotnet/csharplang/issues/399)
Check out my article on this topic: https://medium.com/@ondrejmirtes/union-types-vs-intersection-types-fd44a8eacbb
Let's say you have two interfaces, A and B. A union of them means "I want something that implements A or implements B". An intersection means "I want something that implements both A and B". The syntax for these is respectively A|B, and A&B. You can also combine them:
A | B & C
"It should either implement A, or B and C."
Thanks for the explanation.
Your example kinda showcases the complexity in this though, as without any parentheses, it can be hard to determine the actual intention. E.g., does A | B & C mean either:
I'm thinking order of operations (for lack of better word). As long as there's a common definition, there' no problem of course, other than people not reading the docs.
There's only way this will run. The order of the operations is the same as with boolean expressions:
A || B && C
I.e. intersections have higher priority than unions. This is the same in all languages, because it makes most sense from practical PoV.
Also, once we have both unions and intersections, we'll have support for parentheses, which means you can override the default order:
(A | B) & C
Good stuff!
Currently in userland code when a parameter for a function can be one of multiple types, or the return value from a function can be one of multiple types, there can be no type information supplied
I'm not a fan of this. In my opinion parameter type and return value type overloading muddies the interface and makes them more difficult to understand. At a distance I now need to understand why it may accept multiple types or return multiple types. This makes function composition more difficult. In general, just because a mistake made long ago shows a feature possible in the language doesn't mean it's still a good idea.
Most PHP functions tend to return either their result if they succeed, or false if they don't.
For example, the return type of strpos is technically int|false.
The |false bit comes because in PHP land, false is technically its own type.
You also need to understand why multiple types can be used without this RFC - the same code needs to add the possible types as comments, which can be out-of-date, or maybe it is only visible in the implementation. This RFC does not say it is good practice to return/pass many different types, it just makes it possible to make it explicit if it is unavoidable.
And it can be hard to avoid multiple types in some instances. int|float is a good example - maybe the function does not care, but float would maybe change its type, and int might change its value. Or maybe two interfaces are acceptable to a function, because both implement a method signature - it can happen, and making it explicit can help to make it more reliable.
I understand the current pain point. I worked in PHP for many years. Thank you though for highlighting it. In my opinion this isn't a clean or consistent solution. It feels like another hack to cover for a previous one. Wouldn't the correct solution be to examine the interface and introduce a new abstraction? In this case I wouldn't complicate the behavior of the language to understand a poorly designed interface. I'd use a new type of number. With this new abstraction it won't matter if you accept an int or a float.
If you are creating a library/component you don't really want to create your own simple types (making a library more difficult to use and difficult to integrate into other components), and union types is probably a bigger feature for reusable libraries than for actual application code, because making something reusable can mean allowing multiple types or multiple interfaces, or something like array|ArrayIterator, or string|callable, etc.
In my application code I also don't need to union types in 95% of cases, but for components or even just for helper functions etc. this can be huge, and it can also be a first step towards refactoring in "old" code. Having a union type starts to restrict the types, so you can maybe reduce it to one type later, or to one interface or class.
without this i cannot live anymore.
Yet, you are still alive! :-p
damn this is good
Good to see this back in action. Much needed!
Just a small note that public function param2(A|B|string $param) should be accepted for extended class typing.
Also, didn't see much on handling null types? What would be the resolution on that?
You can use the nullable type with the union using the ?(A|B) syntax.
https://github.com/nikic/php-rfcs/blob/union-types/rfcs/0000-union-types-v2.md#nullable-union-types
I am glad though that there are many comments against that syntax - it seems terribly unreadable to me, instead of just doing A|B|null. It almost looks like a mini-function with the parentheses and would also confuse the context (like as a return type, where having additional parentheses makes it less clear that it is a return type). I even like the ?SomeInterface syntax, yet really dislike this proposal to put it in union types.
Yes please sweet jeebus
[deleted]
I hope the move away from using pidgeons to send Kipu to using Github issues will be a permanent one. It's much easier to trach the discusion here, on a fragment-by-fragment basis and overall.
Alright bois, we buildin' actual programming language!
Can anyone explain what this is?
I'm not sure at what level you're not understanding so I'll be thorough even if you may already know some of it
An RFC is a Request For Comments. For PHP, it's a proposal for a language change in a future version of PHP. RFCs after comment can be voted on by maintainers, and if they pass, will be implemented for future release.
This particular one is for adding support for union types. Currently, PHP only allows a single type as a typehint. For example,
function myFunction(string $foo);
But about a case where we'd like to receive either a string or an array? We'd have to drop the typehint entirely.
function myFunction($foo);
Doing this has the downside of not knowing what type $foo is, and allowing the passing of types we don't want, like an integer
myFunction(100);
Union types allows us to specify both and maintain type safety
function myFunction(string|array $foo);
Now we can pass a string or an array, but not a random object or (if strict types is enabled - see comment from u/akeniscool) integer, and our IDE will know that $foo is definitely either a string or array and can suggest based on it.
Excellent explanation. Just a follow up, because we're programmers and enjoy being technically correct. :)
Now we can pass a string or an array, but not an integer or random object, and our IDE will know that
$foois definitely either a string or array and can suggest based on it.
This is accurate if strict types is enabled. With strict types disabled, PHP will attempt to coerce the integer into a matching type if possible. In this case, it would try to turn the integer into a string.
This RFC has a specific order of types it will attempt to coerce into when the type doesn't match exactly and is possibly ambiguous.
The extra explanation is always great, but I was mostly unaware of what was meant by a union type and that explains it perfectly.
i like it... makes me want generics more and more
or at least typed arrays
function getItems(): Item[]
I like this feature,it can help write better php code.
I don't understand this RFC. This will completely render type hinting useless. Why is everyone so enthusiastic?
Why do you think it would do that?
Ofcourse you don't have to use it, but if a method can accept a string|int|float why bother with static typing at all?
Because the method does indeed accept such type.
A simplistic example based on a possible state machine:
interface GamePhase {
// ...
}
class DrawCard implements GamePhase {}
class Untap implements GamePhase {}
class Attack implements GamePhase {}
class DiscardCard implements GamePhase {}
class End implements GamePhase {}
class Game {
public function drawCard() : Attack|DiscardCard|End { /* ... */ }
}
I think a method should only return one type to begin with(or null)
In that case wouldn't drawCard just return GamePhase? Or am I missing a subtlety with interfaces, or your exclusion of DrawCard and Untap?
Static typing =/= one type at a time.
Interfaces already allow you to do more-or-less the same thing (albeit with more ceremony and less flexibility). Union types are a very useful way to existing dynamically-typed APIs. And aside from that, they also allow you to more safely specify APIs that do accept different types, which plenty of people like to design.
From the example, why not number and numeric types?
Another great RFC by Nikita!