On 4/15/16 10:10 AM, Andrea Faulds wrote:
Hi Stas,

Stanislav Malyshev wrote:
I don't know what is complicated about "string|Stringable" or "Foo|Bar"
since it is super self-explanatory. However, I find myself checking the

It may be self-explanatory for you. It's much less self-explanatory for
somebody just starting to learn. It is also very dangerous - if it's
either Foo or Bar, can you call Foo::stuff on it or not? If it's string
or not string, can you call strlen on it? Etc., etc. It adds a lot of
cognitive load and complicates the whole picture. You may have a
specific use case where it is useful (which we have yet to see btw) but
please remember it's a language with literally millions of use cases and
users.

This is something that particularly concerns me about union types, in that they reduce type safety. If you have a union type of Foo|Bar for some variable, then the set of methods you can call on that variable is actually the intersection, not the union, of the set of methods you can call on Foo and Bar. Which, unless those two classes share some interface, is probably an empty set. So there's nothing you can actually do safely with it without doing checks within the body of the function, and if you're doing that, then why do we have a type declaration? It's only barely more useful than omitting a type declaration at all; type declarations are supposed to prevent you needing to check. On the other hand, if the two classes share some methods, then either there's an interface you can already use here, or you can create one. Either way, you don't need a union type.

There are some cases where you can't create an interface, but if that's the case, I think it is more worthwhile to look at how we can fix those cases, rather than add what amounts to a hacky workaround.

Thanks!

I think there's 2 general use cases for union types that would be "good things", which are different for & and |, and have very little... intersection. (*yaaaaaaa!*)

The OR case is for cases where the language doesn't support a unified case at all. The most obvious example here is array|Traversable. If I want "a thing I can foreach()", then PHP right now has no way of saying that syntactically. You have to type on array, or Traversable, or not type at all. array|Traversable is what you really want, because those DO have an overlap (foreach-ablility), PHP is just incapable of representing that otherwise.

A similar example would be callable|SomeInterface. An interface can specify a signature for __invoke(), which gives you documentation on the format that is expected for a callable. However, you can't strictly enforce that because then you don't allow for a function or closure that fits the same method signature. That means you have to leave it untyped. This, I argue, would be better *and* reasonably type safe:

interface MiddlewareInterface {
  function __invoke(RequestInterface $req, ResponseInterface $res);
}

function middleware_builder(callable|MiddlewareInterface $m) {
  // ...
}

As that self-documents that MiddlewareInterface is the callable signature we need, but still allows an arbitrary callable to be passed. It's not perfect (I could pass a string of a function name that doesn't have that interface and it would still explode), but it is an improvement over middleware_builder() having no type specification at all, as is the case today.

(Hat tip to Matthew O'Phinney for cueing me into this neat feature of interfaces and __invoke().)

On the flipside, the & is mostly useful for where you need multiple interfaces for something. For instance, there's the PSR-7 ResponseInterface. Drupal also has a number of interfaces for value objects to indicate their cacheability metadata, such as CacheableMetadataInterface. But that applies to more than just Responses, of course, so having it extend ResponseInterface is not good. So how can I specify that I need an object that is BOTH ResponseInterface AND CacheableMetdataInterface? That's an entirely reasonable thing to do, but currently PHP doesn't allow for it at all. Even having a custom interface that extends both of those doesn't help, because then my class needs to implement the child interface, not both parents.

Being able to type on function foo(ResponseInterface & CacheableMetadatInterface $r) seems entirely reasonable to me, and definitely more type safe than leaving it untyped and relying on documentation.

The danger zone is, as many people have noted, OR-ing interfaces together. That does undermine type safety in most cases, I'd argue. However, it's also rather obvious when it does so, because you end up with a giant if-statement inside your function. So while it's a risk, I don't think it's a subtle one, especially if well documented.

Actually, there is a use case for ORing interfaces, and that's for BC reasons. Take, for instance, Symfony 2's routing system. It started off with a RouteMatcherInterface::match(RequestContext $r) method, where RequestContext is a "junior version" of a Request. After a while it became evident that having the full for-reals Request object was useful, so after prompting from Drupal they build an alternate version that used a full Request, but overlaped in ugly ways with the original. Making RequestContext extend Request (or vice versa) was completely impractical for various reasons. However, as a BC transitional approach it would have been reasonable to simply update the interface to RouteMatcherInterface::(RequestContext|Request $r), as a signaling mechanism that the old object still worked for a while, and despite there not being a common type most of the methods *are* common between those two objects. That would have allowed that service to fully internalize the BC layer.

So while I agree there are risks to misuse of Class|Class type hints, I think the overall benefits (detailed above) outweigh the risks. Even then, hinting on A|B is better than hinting on nothing and still accepting A or B. At least it's explicit that you're doing something risky.

--
--Larry Garfield


--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to