rfc:protocol_type_hinting
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
rfc:protocol_type_hinting [2013/06/25 15:55] – [Why Go-Style Interfaces?] ircmaxell | rfc:protocol_type_hinting [2013/06/27 13:17] – Add trait hinting ircmaxell | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== PHP RFC: Protocol | + | ====== PHP RFC: Structural |
- | * Version: 0.1 | + | * Version: 0.2 |
* Date: 2013-06-25 | * Date: 2013-06-25 | ||
* Author: Anthony Ferrara < | * Author: Anthony Ferrara < | ||
Line 10: | Line 10: | ||
Currently PHP uses an enforced interface style, where the argument object to a function or method must implement the respective interface. This RFC discusses inverting the dependency to allow for run-time resolving of interfaces. This is very similar to how GO implements interfaces. | Currently PHP uses an enforced interface style, where the argument object to a function or method must implement the respective interface. This RFC discusses inverting the dependency to allow for run-time resolving of interfaces. This is very similar to how GO implements interfaces. | ||
+ | In short, rather than checking the type-tree as traditional type-hinting works, this only checks to see that the **public** APIs match (that if you added " | ||
==== What Are Go-Style Interfaces? ==== | ==== What Are Go-Style Interfaces? ==== | ||
- | Go-Style interfaces (called //Protocols// by this RFC) are basically | + | Go-Style interfaces (called //Structural Typing// by this RFC) are basically interface |
==== Why Go-Style Interfaces? ==== | ==== Why Go-Style Interfaces? ==== | ||
Line 22: | Line 23: | ||
When you have cross-project dependencies. Currently, both packages must declare a dependency to a third package for the interface. A good example of this is the PSR-3 logger interface. Currently, the PSR-3 interface must be included by every project that declares a logger which you want to conform to the interface. This results in pulling in project-level dependencies even in cases where you don't need them. | When you have cross-project dependencies. Currently, both packages must declare a dependency to a third package for the interface. A good example of this is the PSR-3 logger interface. Currently, the PSR-3 interface must be included by every project that declares a logger which you want to conform to the interface. This results in pulling in project-level dependencies even in cases where you don't need them. | ||
- | Implementing | + | Implementing |
- | + | ||
- | === Duck Typing === | + | |
- | + | ||
- | Presently, to implement duck-typing, | + | |
- | + | ||
- | <file php duck_typing.php> | + | |
- | <?php | + | |
- | public function foo($logger) { | + | |
- | if (!method_exists($logger, | + | |
- | throw new Exception(' | + | |
- | } | + | |
- | $logger-> | + | |
- | } | + | |
- | ?> | + | |
- | </ | + | |
- | + | ||
- | However with protocols (go-style interfaces), | + | |
=== Decoupling Of Classes === | === Decoupling Of Classes === | ||
- | Right now, there' | + | Right now, there' |
<file php class_decoupling.php> | <file php class_decoupling.php> | ||
Line 54: | Line 38: | ||
This further strengthens the safety offered by type-hints, while decoupling the class further from the interface and classes of the hint. | This further strengthens the safety offered by type-hints, while decoupling the class further from the interface and classes of the hint. | ||
+ | |||
===== Proposal ===== | ===== Proposal ===== | ||
- | I propose adding a special syntax to the current type-hinting paradigm to allow for indicating a non-strict *instanceof* which would resolve the dependency as a protocol. | + | I propose adding a special syntax to the current type-hinting paradigm to allow for indicating a non-strict *instanceof* which would resolve the dependency as a Structural Type. |
==== Proposed Syntax ==== | ==== Proposed Syntax ==== | ||
- | The current proposed syntax is basically wrapping a traditional type hint (// | + | The current proposed syntax is basically wrapping a traditional type hint (// |
<file php duck_typing.php> | <file php duck_typing.php> | ||
Line 83: | Line 68: | ||
==== Proposed Behavior ==== | ==== Proposed Behavior ==== | ||
- | Any valid class-style identifier can be used as the "protocol | + | Any valid class-style identifier can be used as the "structure |
<file php duck_typing.php> | <file php duck_typing.php> | ||
Line 104: | Line 89: | ||
} | } | ||
class StaticLogger { | class StaticLogger { | ||
- | public static function log($message); | + | public static function log($message) |
} | } | ||
class OtherLogger { | class OtherLogger { | ||
- | public static function log($message, | + | public static function log($message, |
} | } | ||
Bar:: | Bar:: | ||
Line 115: | Line 100: | ||
?> | ?> | ||
</ | </ | ||
+ | |||
+ | ===== Use-Cases ===== | ||
+ | |||
+ | ==== Flexible Middleware ==== | ||
+ | |||
+ | Right now there' | ||
+ | |||
+ | <file php HttpKernelInterface.php> | ||
+ | <?php | ||
+ | namespace Symfony\Component\HttpKernel; | ||
+ | |||
+ | use Symfony\Component\HttpFoundation\Request; | ||
+ | |||
+ | interface HttpKernelInterface | ||
+ | { | ||
+ | const MASTER_REQUEST = 1; | ||
+ | const SUB_REQUEST = 2; | ||
+ | |||
+ | public function handle(Request $request, $type = self:: | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | Really, there isn't anything Symfony specific there. The only specific part is the //Request// class, which is quite big. | ||
+ | |||
+ | So right now, middleware has to be coupled on the // | ||
+ | |||
+ | === Reducing Coupling === | ||
+ | |||
+ | Let's imagine you're creating a firewall middleware to limit requests to a specific IP (or allow for all except a certain IP). As it stands today, you need a large chunk of Symfony to do so. | ||
+ | |||
+ | But with Structure Typing, you can create two new interfaces: | ||
+ | |||
+ | <file php RequestGetClientIp.php> | ||
+ | <?php | ||
+ | interface RequestGetClientIp { | ||
+ | public function getClientIp() | ||
+ | ?> | ||
+ | </ | ||
+ | <file php HttpKernelClientIp.php> | ||
+ | <?php | ||
+ | interface HttpKernelInterfaceForClientIp | ||
+ | { | ||
+ | const MASTER_REQUEST = 1; | ||
+ | const SUB_REQUEST = 2; | ||
+ | |||
+ | public function handle(< | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | Now, my middleware becomes: | ||
+ | |||
+ | <file php firewall.php> | ||
+ | <?php | ||
+ | class Firewall implements HttpKernelInterfaceForClientIp { | ||
+ | protected $parent = null; | ||
+ | public function __construct(< | ||
+ | $this-> | ||
+ | } | ||
+ | public function handle(< | ||
+ | if ($request-> | ||
+ | return $this-> | ||
+ | } | ||
+ | throw new Exception(' | ||
+ | } | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | The cool thing is that I'm effectively decoupled from Symfony here. If ZendFramework changed their [[https:// | ||
+ | |||
+ | ==== Standards Based Interface Declarations ==== | ||
+ | |||
+ | Currently, the [[https:// | ||
+ | |||
+ | This raises a significant issue, because it causes a triangular dependency which requires some external effort to resolve. This means that you need some sort of tool to resolve that dependency for you, or you both sides copy/paste the implementation into their tree, and must " | ||
+ | |||
+ | For example, take the [[https:// | ||
+ | |||
+ | <file php LoggerInterface.php> | ||
+ | <?php | ||
+ | namespace psr\log; | ||
+ | interface LoggerInterface | ||
+ | { | ||
+ | public function emergency($message, | ||
+ | public function alert($message, | ||
+ | public function critical($message, | ||
+ | public function error($message, | ||
+ | public function warning($message, | ||
+ | public function notice($message, | ||
+ | public function info($message, | ||
+ | public function debug($message, | ||
+ | public function log($level, $message, array $context = array()); | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | |||
+ | === Reducing Coupling === | ||
+ | |||
+ | Using Structural Typing, we can solve this triangular dependency by only requiring the interface when we need it (at the receiver) as opposed to at the producer: | ||
+ | |||
+ | We can also narrow the requirement for an acceptable logger based on our application' | ||
+ | |||
+ | <file php Psr3LogWarningAndError.php> | ||
+ | <?php | ||
+ | interface Psr3LogWarningAndError { | ||
+ | public function error($message, | ||
+ | public function warning($message, | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | <file php MyCode.php> | ||
+ | <?php | ||
+ | class MyClass { | ||
+ | protected $logger; | ||
+ | public function __construct(< | ||
+ | $this-> | ||
+ | } | ||
+ | public function doSomething($foo) { | ||
+ | if (!$foo) { | ||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | So now, our code can depend on the narrower interface that we actually need, while allowing all PSR-3 compatible implementations to pass. But it also will allow us to replace polymorphically the logger with a non-PSR-3 logger (because it doesn' | ||
+ | |||
+ | The key here is that it inverts the dependency on who gets to define what the needed units of functionality are. It allows the receiving code to define the scope of required functionality instead of the sending code. | ||
+ | |||
+ | It also solves the triangular dependency problem since the sender never needs to explicitly require the dependency. That can be left for an off-line check (or a test), reducing the amount of and need for dependency resolving tools for the purpose of common interfaces... | ||
+ | |||
+ | ==== Trait Typing ==== | ||
+ | |||
+ | Currently, traits do not allow for specifying of typing information. And this is a good thing (it is by design). | ||
+ | |||
+ | However, there are many times where we may wish to infer that the functionality presented by a trait is present in an object. An example would be Zend Framework' | ||
+ | |||
+ | <file php ProvidesEvents.php> | ||
+ | <?php | ||
+ | namespace Zend\EventManager; | ||
+ | trait ProvidesEvents { | ||
+ | protected $events; | ||
+ | public function setEventManager(EventManagerInterface $events) { /* snip */ } | ||
+ | public function getEventManager() { /* snip */ } | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | As the current system stands, classes that use the trait need to also implement a separate interface to get the message of the behavior across to the receiver that it supports events. | ||
+ | |||
+ | With Structural Type Hinting, we can instead hint against the trait directly, which would require a class with the same public API that the trait provides, // | ||
+ | |||
+ | <file php RequiresEvents.php> | ||
+ | <?php | ||
+ | function triggerEvent(< | ||
+ | $target-> | ||
+ | } | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | If the object uses the trait, it will always resolve. But it also gives other classes which implement the same public API the ability to resolve the trait. | ||
+ | |||
+ | ==== The Place For Current Interfaces ==== | ||
+ | |||
+ | Why not just get rid of current interfaces and change their behavior to Structural typing (besides the MASSIVE BC break)? | ||
+ | |||
+ | In practice there are two reasons (times) that you would use an interface: | ||
+ | |||
+ | 1. To provide typing information about a domain object (or a value object). This is where the typing actually means something specific to the application. | ||
+ | |||
+ | An example would be a User object in an application. You may want to have a UserInterface which the User class implements, because the interface actually implies that the object *belongs* to the domain. It's providing *type* information about the object. | ||
+ | |||
+ | 2. To provide functionality information about a object. This where the interface really just describes the functionality of the object. | ||
+ | |||
+ | An example would be a service which encodes a password. There' | ||
+ | |||
+ | With this new proposal, Type information would still be implemented via traditional interfaces. But capability information would use Structural Typing. | ||
+ | |||
+ | So there is very much a place for both to live side-by-side in the same language. | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
Line 148: | Line 316: | ||
==== Raising Errors ==== | ==== Raising Errors ==== | ||
- | Currently, only a // | + | Currently, only a // |
==== Syntax ==== | ==== Syntax ==== | ||
- | I considered implementing a new " | + | I considered implementing a new " |
If the //< | If the //< | ||
- | * //@Foo $foo// | + | |
- | * //%Foo $foo// | + | * //%Foo $foo// |
- | * //*Foo $foo// | + | * //*Foo $foo// |
- | * //~Foo $foo// | + | * //~Foo $foo// |
Personally, I think the //< | Personally, I think the //< | ||
Line 167: | Line 335: | ||
It's worth noting that since this is a separate branch, the current performance of normal type-hints remains uneffected. | It's worth noting that since this is a separate branch, the current performance of normal type-hints remains uneffected. | ||
- | The current Proof-Of-Concept will re-run the verification functions | + | Currently, // |
+ | |||
+ | While performance is an apparent concern, the benchmarks indicate performance at works on-par with existing type hints (and when called multiple times can be faster)... | ||
+ | |||
+ | Here are the results of the benchmarks: | ||
+ | |||
+ | <file php benchmark.php> | ||
+ | <?php | ||
+ | interface Foo { | ||
+ | public function foo(); | ||
+ | } | ||
+ | |||
+ | class Bar { | ||
+ | public function foo() {} | ||
+ | } | ||
+ | |||
+ | class Baz implements Foo { | ||
+ | public function foo() {} | ||
+ | } | ||
+ | |||
+ | function benchmark($func, | ||
+ | $s = microtime(true); | ||
+ | for ($i = 0; $i < $times; $i++) { | ||
+ | $func($arg); | ||
+ | } | ||
+ | $e = microtime(true); | ||
+ | return $e - $s; | ||
+ | } | ||
+ | $times = 1000000; | ||
+ | $interface = benchmark(function(Foo $foo) {}, $times, new Baz); | ||
+ | echo " | ||
+ | $structural = benchmark(function(< | ||
+ | echo " | ||
+ | $native = benchmark(function($foo) {}, $times, new Bar); | ||
+ | echo " | ||
+ | ?> | ||
+ | </ | ||
+ | |||
+ | === When Run Once === | ||
+ | |||
+ | When run once (with $times = 1): | ||
+ | |||
+ | '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | The margin of error for the test is approximately the same difference as between Interface and Structural. This means that the performance for a single run is about constant. | ||
+ | |||
+ | === When Run Many Times === | ||
+ | |||
+ | When run with $times = 1000000; | ||
- | === Short-Circuit on Instanceof === | + | '' |
- | We can short-circuit if //$obj instanceof hint// returns true. This is likely going to be generally false, but checking is reasonably cheap, and will push the cost of the match to the compiler. | + | '' |
- | === Cache Protocol Checks === | + | '' |
- | Once we get a valid result (either true or false), we can cache that. The outstanding question would be where to cache. On the protocol side? Or on the implementing side (if in the CE)? Or should there be an Executor Global setup for the cache (as a hash table). | + | In this case, the margin of error was less than the difference, meaning that the Structural approach is slightly more performant at runtime than the interface based approach. |
===== Unaffected PHP Functionality ===== | ===== Unaffected PHP Functionality ===== | ||
Line 194: | Line 414: | ||
* [[http:// | * [[http:// | ||
+ | * [[http:// | ||
+ | * [[http:// | ||
===== ChangeLog ===== | ===== ChangeLog ===== | ||
* 0.1 - Initial Draft | * 0.1 - Initial Draft | ||
+ | * 0.2 - Rename to Structural Typing, add benchmark results |
rfc/protocol_type_hinting.txt · Last modified: 2017/09/22 13:28 by 127.0.0.1