rfc:callable-types

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Next revisionBoth sides next revision
rfc:callable-types [2015/11/11 15:07] – expand RFC a little more marciorfc:callable-types [2016/05/24 05:57] – param default values nikita2206
Line 1: Line 1:
-====== PHP RFC: Callable Types ====== +====== PHP RFC: Callable Prototypes ====== 
-  * Version: 0.0+  * Version: 1.0
   * Date: 2015-08-27   * Date: 2015-08-27
   * Authors: Nikita Nefedov <inefedor@gmail.com>, Márcio Almada <marcio3w@gmail.com>   * Authors: Nikita Nefedov <inefedor@gmail.com>, Márcio Almada <marcio3w@gmail.com>
-  * Status: Draft+  * Status: Voting
   * First Published at: http://wiki.php.net/rfc/callable-types   * First Published at: http://wiki.php.net/rfc/callable-types
  
Line 9: Line 9:
  
 This RFC proposes an evolution of the **callable** type, scoped to argument lists. This should allow more detailed declarations This RFC proposes an evolution of the **callable** type, scoped to argument lists. This should allow more detailed declarations
-of **callable** "typehints" - including their **arity**, **argument types** and **return type**.+of **callable** type declarations - including their **arity**, **argument types** and **return type**.
  
 Here is one basic comparison between a simple **callable** and a detailed **callable prototype** declaration: Here is one basic comparison between a simple **callable** and a detailed **callable prototype** declaration:
Line 43: Line 43:
 }); });
 // >>> // >>>
-// TypeError: Argument 3 passed to reduce() must be callable(int, int):int, callable($a, $b, $c) given...+// TypeError: Argument 3 passed to reduce() must be compliant with callable(int, int):int, incompatible callable($a, $b, $c) given...
 </code> </code>
  
-This concept is also present in other languages, commonly referred as **function prototypes**, **function types** or **function interfaces**.+This concept is also present in other languages, commonly referred as **function prototypes**, **function types**.
  
 > NOTE: This RFC is **not** related to "generics". > NOTE: This RFC is **not** related to "generics".
Line 59: Line 59:
  
 The very first example of this RFC already illustrates well why a detailed "Type Error" offers better debuggability when compared to The very first example of this RFC already illustrates well why a detailed "Type Error" offers better debuggability when compared to
-a trail of warnings and notices. In case warnings and notices get converted to exceptions, the scenario get'even less friendly+a trail of warnings and notices. In case warnings and notices get converted to exceptions, the scenario gets even less friendly
 as problems would be revealed one by one. as problems would be revealed one by one.
  
Line 66: Line 66:
 While callable types can offer more debug friendlier messages, there are other factor that could favor earlier While callable types can offer more debug friendlier messages, there are other factor that could favor earlier
 failures approach. The objective of a callable type is to know when a callable is safe to execute before executing it. failures approach. The objective of a callable type is to know when a callable is safe to execute before executing it.
-Specifying a more constrained callable type allows a given routine to fail before the next step of some important operation+Specifying a more constrained callable type allows a given routine to fail before the next step of some important operation.
- +
-<code php> +
-// example of a rejected callable that shows early failure +
-</code>+
  
 This certainly can be achieved without callable types: perhaps using reflection or manual type checking of return values This certainly can be achieved without callable types: perhaps using reflection or manual type checking of return values
Line 77: Line 73:
 === Self Documented Code === === Self Documented Code ===
  
-It's common to see callback based libraries doing their best to declare the callable signatures on docblocks, hoping that+It's common to see callback based libraries doing their best to declare the callable signatures on doc comments, hoping that
 the consumers will be able to figure out what to pass around: the consumers will be able to figure out what to pass around:
  
 <code php> <code php>
 +/**
 + * @param  array                  $array
 + * @param  callable($key, $value) $callback
 + */
 function array_walk(array $array, callable $callback) { function array_walk(array $array, callable $callback) {
     // ...     // ...
Line 96: Line 96:
 === Empower Anonymous Functions === === Empower Anonymous Functions ===
  
-Currently the only possible way to formally specify the type information of a callable is using classes:+Currently the only possible way to formally specify the type information of a callable is by using classes:
  
 <code php> <code php>
Line 120: Line 120:
 </code> </code>
  
-Unfortunately, this solution completely excludes anonymous functions. But with a more specific signature, +Unfortunately, this solution completely excludes anonymous functions as they can't implement any interface. But with a more 
-callable types could work as an interface over **__invoke**:+specific signature, callable types could work as requirements over the `<nowiki>__invoke</nowiki>` method of callables:
  
 <code php> <code php>
Line 138: Line 138:
 </code> </code>
  
-==== Arity ====+==== Nested callables ====
  
-=> Add case by case + examples with less required argsmore required argsoptional args...+Nested callables can be used with no imposed limit on the nesting level. 
 +<code php> 
 +function foo(callable(callable(int)) $cb) { 
 +    $cb(function (int $i) { 
 +        var_dump($i); 
 +    }); 
 +
 + 
 +foo(function (callable(int) $intPrinter) { 
 +    $intPrinter(123); 
 +}); 
 +</code> 
 + 
 +There's currently no way to reference ''callable'' signature from within itselfmeaning there's no way to make recursive signatures like below: 
 +<code php> 
 +function bar(callable(int $number): callable(int $number): callable(int $number): parent_callable $recursiveCb) { // this wouldn't work currently 
 +
 +</code> 
 + 
 +To add to thatnested callables can get pretty unreadable quickly, both of these problems would be best solved by a typedef feature of some kind, added to PHP later.
  
 ==== Variance and Signature Validation ==== ==== Variance and Signature Validation ====
  
-Variance is supported and adheres to LSP. This means that whenever function of type ''F'' is expected, any function that takes equal or more general input than ''F'' and gives equal or narrower output than ''F'', can be considered of type ''F''. Classes in argument/return type of a callable typehint are a subject to variance, primitives are not.+Variance is supported and adheres to LSP. This means that whenever function of type ''F'' is expected, any function that takes equal or more general input than ''F'' and gives equal or narrower output than ''F'', can be considered of type ''F''. Classes in argument/return type of a callable type declarations are a subject to variance, primitives are not.
  
 Examples: Examples:
Line 155: Line 174:
  
 foo(function (A $a) {}); // there's no variance in this case, A can be substituted by A foo(function (A $a) {}); // there's no variance in this case, A can be substituted by A
-foo(function (B $b) {}); // Uncaught TypeError: Argument 1 passed to foo() must be callable of compliant signature: callable(A), callable(B $b) given+foo(function (B $b) {}); // Uncaught TypeError: Argument 1 passed to foo() must be compliant with callable(A), incompatible callable(B $b) given
 bar(function (A $a) {}); // callable(A) > callable(B) - we can substitute callable(B) with callable(A) because the latter has a wider input than the latter bar(function (A $a) {}); // callable(A) > callable(B) - we can substitute callable(B) with callable(A) because the latter has a wider input than the latter
 </code> </code>
Line 161: Line 180:
 The same rules apply to return type of a callable: The same rules apply to return type of a callable:
 <code php> <code php>
-function foo(callable: A $cb) { }+function bar(callable() $cb) { } 
 +bar(function (): A { return new A; }); // it is valid to pass a function with declared return type as a parameter of `callable(..)` 
 + 
 +function foo(callable(): A $cb) { }
  
 foo(function (): A { return new A; }); // A == A foo(function (): A { return new A; }); // A == A
-foo(function (): B { return new B; }); // B < A this closure will return narrower type than what is expected by "foo", which means it can be a substitute for callable: A+foo(function (): B { return new B; }); // B < A this closure will return narrower type than what is expected by "foo", which means it can be a substitute for callable(): A
 </code> </code>
  
Line 173: Line 195:
 </code> </code>
  
-Optional arguments count just like any other arguments:+Optional parameters count just like any other parameters if they are typed:
 <code php> <code php>
 function foo(callable() $cb) { } function foo(callable() $cb) { }
Line 183: Line 205:
 // And callable(A $a) < callable(), so the call to foo() will fail here // And callable(A $a) < callable(), so the call to foo() will fail here
 </code> </code>
 +
 +Otherwise they can pass type check boundaries even if they are not defined in the callable type:
 +<code php>
 +function foo(callable() $cb) { }
 +foo(function ($a = 123) { }); // valid as it won't have a problem explained above
 +</code>
 +The same goes for variadics as they are a special kind of optional parameters.
  
 When callable type is nested (when you have ''callable(callable(A))'') variance has to be inversed with each nesting level. So if we have ''callable(A) > callable(B)'' then ''callable(callable(A)) < callable(callable(B))''. When callable type is nested (when you have ''callable(callable(A))'') variance has to be inversed with each nesting level. So if we have ''callable(A) > callable(B)'' then ''callable(callable(A)) < callable(callable(B))''.
 +
 +==== References in callables ====
 +
 +Reference parameters are supported: no variance is applied to the fact whether parameter is referential or not.
 +
 +Example:
 +<code php>
 +function foo(callable(&$byref) $cb) { }
 +
 +foo(function (&$bar) { }); // valid
 +foo(function ($bar) { }); // TypeError: Argument 1 passed to foo() must be compliant with callable(&$byref), incompatible callable($bar) given
 +
 +function bar(callable($byval) $cb) { }
 +
 +bar(function (&$bar) { }); // TypeError: Argument 1 passed to bar() must be compliant with callable($byval), incompatible callable(&$bar) given
 +</code>
 +
 +Functions returning a reference are compatible with functions returning a value for caller, hence both are interchangeable:
 +<code php>
 +function foo(callable(): A $cb) { }
 +
 +foo(function (): A { return new A; });
 +foo(function &(): A { static $a; $a = $a ?: new A; return $a; }); // both would pass the boundaries of a type check
 +</code>
 +
 +There's no way to declare that you expect a callable that returns reference due to syntax limitations.
 +
 +==== Parameters with default values ====
 +
 +It's not possible to declare default value of a parameter in a callable prototype. Because currently PHP doesn't consider parameter's default values in signature validation (their invariance is not enforced in overridden methods in classes).
  
 ==== Syntax Choices ==== ==== Syntax Choices ====
  
-The syntax is similar to the one already used to declare prototypes on interfaces and abstract classes member, and should +The proposed syntax is similar to what we have on interfaces and abstract methods, and will look meaningful 
-look meaningful to anyone who already knows how to declare a PHP interface. There are only two minor distinctions:+to anyone who already knows how to declare a PHP interface. There are only two minor distinctions:
  
 While declaring a callable type, it's possible to omit the argument names from argument lists when a given argument has type information. While declaring a callable type, it's possible to omit the argument names from argument lists when a given argument has type information.
Line 202: Line 261:
 </code> </code>
  
-The other distinction is that empty argument lists can be omitted: +It's already common to see analogous syntax inside doc comments even though there are no regnant conventions:
- +
-<code php> +
-// the declarations below are synonyms: +
- +
-function foo(callable():string $callback) {} +
- +
-function foo(callable:string $callback) {} +
-</code> +
- +
-It's already common to see analogous syntax inside docblocks even though there are no regnant conventions, like:+
  
 <code php> <code php>
Line 241: Line 290:
 } }
 </code> </code>
 +
 +==== Why Not Add Callable Types Through Interfaces ====
 +
 +During off list discussions, it was proposed to add callable types to PHP by hacking the interface system:
 +
 +<code php>
 +interface FooCallback extends Callable {
 +    function __invoke(int $i, string $str, FooClass $foo) : ReturnType;
 +}
 +</code>
 +
 +The RFC authors rejected the idea because of the many design problems this would cause. For example, the following situation would completely
 +**break anonymous functions support**:
 +
 +<code php>
 +interface FooCallback extends Callable {
 +    function someExtraMethod();
 +    function __invoke(int $i, string $str, FooClass $foo) : ReturnType;
 +}
 +</code>
 +
 +In order to amend this design issue, we would have to add many weird checks to PHP interfaces just to accommodate something that conceptually
 +(at least for PHP) doesn't pertain to interfaces:
 +
 +    - interfaces extending Closure (or abstract classes implementing any interface extending closure) would have to be forbidden to declare constants or any extra method other than invoke.
 +    - interfaces extending any interface that extends Closure (or abstract classes that...) would need the same checks as above.
 +    - in case an interface has only invoke all rules to determine compatibility would change.
 +
 +The obvious conclusion is that extending the behavior of `callable` should **not** require deep changes (or any change at all) on
 +the current interface system. The fact that objects can become callables by having an `<nowiki>__invoke</nowiki>` method is just a detail.
 +
 +As a side note, any comparison with callable types and interfaces on this RFC is for didactic purpose.
  
 ==== When To Use Return Types On Callable Types ==== ==== When To Use Return Types On Callable Types ====
  
 It should be noted that, while perfectly valid, adding return types to callable types may not be as useful as it seems at a first sight. It should be noted that, while perfectly valid, adding return types to callable types may not be as useful as it seems at a first sight.
-Perhaps this bit is only valuable if the returned value of the callback is really going to be used by the receiver, otherwise the recomendation+Perhaps this bit is only valuable if the returned value of the callback is really going to be used by the receiver, otherwise the recommendation
 is to simply skip it. is to simply skip it.
  
-It's also notable that some types like 'void' should never be used as callable return types. They simply impose unnecessary restrictions to callables +It's also notable that some types like 'void' should never be used as callable return type. They simply impose unnecessary restrictions 
-in any imaginable use case. Eg:+to callables in any imaginable use case. E.g:
  
-<php code>+<code php>
 function foo(callable($a, $b):void $callback) { function foo(callable($a, $b):void $callback) {
   //...   //...
 +}
 +</code>
 +
 +===== Reflection =====
 +
 +There are no BC-breaking changes in Reflection.
 +
 +Here are the changes needed for reflection:
 +
 +`ReflectionParameter::getType()` can now return instance of `ReflectionCallableType` which extends `ReflectionType`:
 +
 +<code php>
 +class ReflectionCallableType extends ReflectionType
 +{
 +    /**
 +     * Tells whether it's just a `callable` hint or if it has a prototype `callable(something): something`
 +     */
 +    public function hasPrototype(): bool;
 +
 +    /**
 +     * Returns a number of parameters required by a callable prototype
 +     */
 +    public function getArity(): bool;
 +
 +    /**
 +     * Returns an array of ReflectionCallableParameter instances
 +     */
 +    public function getParameters(): array;
 +
 +    /**
 +     * Tells whether the prototype has return type defined
 +     */
 +    public function hasReturnType(): bool;
 +
 +    /**
 +     * Returns return type of the callable prototype
 +     */
 +    public function getReturnType(): ReflectionType;
 +
 +    /**
 +     * Tells whether $value has compatible callable prototype. This is the easiest way
 +     * to implement runtime-error-free compatibility checks at this point...
 +     * Later we could implement it in the form of `instanceof`
 +     */
 +    public function isA($value);
 +}
 +
 +class ReflectionCallableParameter
 +{
 +    /**
 +     * Tells whether this callable parameter has a type
 +     */
 +    public function hasType(): bool;
 +
 +    /**
 +     * Returns a type of this callable parameter
 +     */
 +    public function getType(): ReflectionType;
 +
 +    /**
 +     * Tells whether callable parameter is named or not (e.g. callable(Foo $foo) vs callable(Foo))
 +     */
 +    public function hasName(): bool;
 +
 +    /**
 +     * Returns name of the callable parameter
 +     */
 +    public function getName(): string;
 +
 +    /**
 +     * Whether this is by-val or by-ref parameter
 +     */
 +    public function isPassedByReference(): bool;
 +    
 +    /**
 +     * Whether this is a variadic parameter
 +     */
 +    public function isVariadic(): bool;
 } }
 </code> </code>
Line 273: Line 432:
 ==== To Opcache ==== ==== To Opcache ====
  
-=> ...+Currently, the patch works with opcacheAny possible further break should be easily fixable anyway.
  
 ===== Unaffected PHP Functionality ===== ===== Unaffected PHP Functionality =====
Line 285: Line 444:
  
 Named callable types were deliberately left out the current proposal. The rationale behind this decision is that Named callable types were deliberately left out the current proposal. The rationale behind this decision is that
-most callable types are very transient, so the proposed inlined syntax will solve 90% of the use cases. This bit could be +most callable types are very transient, so the proposed inlined syntax will solve 90% of the use cases.
-added through another RFC.+
  
-Another reason is that depending on how the [Union Types](https://wiki.php.net/rfc/union_typesRFC is designed it might include named types and, naturally, +Another reason is that depending on how the [[https://wiki.php.net/rfc/union_types|Union Types]] RFC is designed it 
-named callable types would be supported.+might include named types and, naturally, named callable types would be supported. This bit could be added through another RFC, 
 +and probably should, because if typedefs are to appear in PHP they should work not only for callable types but for all other types as well.
  
 The following could also be an option for a dedicated named callable type syntax if union types (or any other RFC introducing type aliasing) gets rejected: The following could also be an option for a dedicated named callable type syntax if union types (or any other RFC introducing type aliasing) gets rejected:
Line 305: Line 464:
 </code> </code>
  
-==== Reflection API ==== +Besides that, even with named callable types support, inlined callable types could be a way to keep the type unexposed from public API 
- +while PHP lacks first class packages.
-An extension to the reflection API will be proposed in case the RFC is approved.+
  
 ===== Votes ===== ===== Votes =====
  
 This RFC requires a 2/3 majority to pass. This RFC requires a 2/3 majority to pass.
 +
 +<doodle title="Accept callable prototypes?" auth="nikita2206" voteType="single" closed="false">
 +   * Yes
 +   * No
 +</doodle>
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
  
 The work in progress of the implementation can be found at https://github.com/php/php-src/pull/1633 The work in progress of the implementation can be found at https://github.com/php/php-src/pull/1633
 +
 +The patch can be tested through https://3v4l.org
 +
 +===== Known Issues =====
 +
 +==== Syntax ====
 +
 +There is a known syntax conflict with callable types that omit argument names, as in:
 +
 +<code php>
 +function func(callable(int) $callback) {
 + //...
 +}
 +// syntax error, unexpected T_INT_CAST in {file} on line 1
 +</code>
 +
 +The following pull request would fix these argument list edge cases https://github.com/php/php-src/pull/1667
  
 ===== References ===== ===== References =====
Line 326: Line 506:
     - Scala     - Scala
     - Swift https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Types.html     - Swift https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Types.html
- 
rfc/callable-types.txt · Last modified: 2017/09/22 13:28 by 127.0.0.1