rfc:allow_casting_closures_into_single-method_interface_implementations

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
rfc:allow_casting_closures_into_single-method_interface_implementations [2023/04/25 09:19] nicolasgrekasrfc:allow_casting_closures_into_single-method_interface_implementations [2023/04/25 19:36] (current) nicolasgrekas
Line 1: Line 1:
-===== Title: Allow casting closures into single-method interface implementations =====+====== PHP RFC: Allow casting closures into single-method interface implementations ======
  
   * Version: 1.0   * Version: 1.0
Line 8: Line 8:
   * Implementation: TBD   * Implementation: TBD
  
-==== Introduction ====+===== Introduction =====
  
 This RFC proposes a new method for the <php>Closure</php> class called <php>castTo()</php>. This method would allow developers to create an instance of a class that implements a specified interface and uses the closure as the implementation. This RFC proposes a new method for the <php>Closure</php> class called <php>castTo()</php>. This method would allow developers to create an instance of a class that implements a specified interface and uses the closure as the implementation.
  
-==== Proposal ====+===== Proposal =====
  
 A new method, <php>castTo()</php>, would be added to the <php>Closure</php> class with the following signature (generics added for extra clarity): A new method, <php>castTo()</php>, would be added to the <php>Closure</php> class with the following signature (generics added for extra clarity):
Line 61: Line 61:
   * **Providing a type-safe alternative to the <php>Closure</php> type**: instead of typing against the <php>Closure</php> type, developers could declare and use interfaces that have a single <php>__invoke()</php> method. While this is possible already, this is quite rare in practice because it comes at a high syntactic cost for end users, which are then required to define a full class. By allowing to quickly turn a closure into an implementation of such interfaces, the <php>castTo()</php> method would greatly reduce this boilerplate, leading to a wider adoption of type-safe code for closures.   * **Providing a type-safe alternative to the <php>Closure</php> type**: instead of typing against the <php>Closure</php> type, developers could declare and use interfaces that have a single <php>__invoke()</php> method. While this is possible already, this is quite rare in practice because it comes at a high syntactic cost for end users, which are then required to define a full class. By allowing to quickly turn a closure into an implementation of such interfaces, the <php>castTo()</php> method would greatly reduce this boilerplate, leading to a wider adoption of type-safe code for closures.
  
-=== Example 1: Using a closure as a default implementation for a TranslatorInterface ===+==== Example 1: Using a closure as a default implementation for a TranslatorInterface ====
  
 This example demonstrates how to use the <php>castTo()</php> method to create a default implementation for a ''TranslatorInterface'' using a closure. It combines this proposal with the first-class callable syntax. This example demonstrates how to use the <php>castTo()</php> method to create a default implementation for a ''TranslatorInterface'' using a closure. It combines this proposal with the first-class callable syntax.
Line 85: Line 85:
 If no custom implementation of ''TranslatorInterface'' is provided during the instantiation of ''SomeClass'', a default implementation is  derived using a closure created from the <php>strtr()</php> function. If no custom implementation of ''TranslatorInterface'' is provided during the instantiation of ''SomeClass'', a default implementation is  derived using a closure created from the <php>strtr()</php> function.
  
-=== Example 2: Type-safe alternative to using Closure ===+==== Example 2: Type-safe alternative to using Closure ====
  
-Imagine that you have the following function, that takes a closure and two number to do some operation on them:+Imagine that you have the following function, that takes a closure and two numbers to do some operation on them:
  
 <code php> <code php>
-function executeOperation(Closure $operation, int $a, int $b): int { +function executeOperation(Closure $operator, int $a, int $b): int { 
-    return $operation($a, $b);+    return $operator($a, $b);
 } }
 </code> </code>
Line 102: Line 102:
 } }
  
-function executeOperation(InvokableInterface $operation, int $a, int $b): int { +function executeOperation(OperatorInterface $operator, int $a, int $b): int { 
-    return $operation($a, $b);+    return $operator($a, $b);
 } }
  
Line 126: Line 126:
 In this example, an ''OperatorInterface'' is defined with a single <php>__invoke()</php> method, which can be used to replace the <php>Closure</php> type on the <php>executeOperation()</php> function. The <php>castTo()</php> method is then used to create instances of the interface, with different behaviors implemented by closures. This approach provides a more type-safe and expressive way to handle closures while adhering to the required interface. In this example, an ''OperatorInterface'' is defined with a single <php>__invoke()</php> method, which can be used to replace the <php>Closure</php> type on the <php>executeOperation()</php> function. The <php>castTo()</php> method is then used to create instances of the interface, with different behaviors implemented by closures. This approach provides a more type-safe and expressive way to handle closures while adhering to the required interface.
  
-==== Backward Incompatible Changes ====+==== Example 3: Decoupling URI template implementations using adapters ==== 
 + 
 +This example demonstrates how the <php>castTo()</php> method can be used to create adapters for 
 +decoupling from different implementations of the URI template RFC from the Guzzle, Rize and League projects. 
 + 
 +<code php> 
 + 
 +// This interface would live in the consumer's code 
 +interface UriExpanderInterface { 
 +    public function expand(string $template, array $variables): string; 
 +
 + 
 +// GuzzleHttp\UriTemplate::expand() is compatible with UriExpanderInterface 
 +$guzzleExpander = GuzzleHttp\UriTemplate::expand(...) 
 +    ->castTo(UriExpanderInterface::class); 
 + 
 +// Rize\UriTemplate::expand() is missing a return type so we need to wrap it 
 +$rizeExpander = (fn (string $template, array $variables): string 
 +    => (new Rize\UriTemplate())->expand($template, $variables) 
 +)->castTo(UriExpanderInterface::class); 
 + 
 +// League\Uri\UriTemplate::expand() has a quite different signature so we need to adapt it 
 +$leagueExpander = (fn (string $template, array $variables): string 
 +    => (string) (new League\Uri\UriTemplate($template))->expand($variables) 
 +)->castTo(UriExpanderInterface::class); 
 + 
 +// Usage 
 +function getExpandedUrl(UriExpanderInterface $expander, string $template, array $variables): string { 
 +    return $expander->expand($template, $variables); 
 +
 + 
 +$template = "https://example.com/{path}"; 
 +$variables = ['path' => 'users']; 
 + 
 +$guzzleExpandedUrl = getExpandedUrl($guzzleExpander, $template, $variables); 
 +$rizeExpandedUrl = getExpandedUrl($rizeExpander, $template, $variables); 
 +$leagueExpandedUrl = getExpandedUrl($leagueExpander, $template, $variables); 
 + 
 +// The 3 implementations return: "https://example.com/users" 
 +</code> 
 + 
 +In this example, an ''UriExpanderInterface'' is defined with an <php>expand()</php> method. 
 +3 different implementations of the interface are derived using the <php>castTo()</php> method. 
 +Finally, the <php>getExpandedUrl()</php> function is defined, which accepts an implementation of 
 +<php>UriExpanderInterface</php> and expands a URI template using the provided implementation. 
 + 
 +Guzzle, Rize and League URI template instances can be used interchangeably with this function, 
 +illustrating the decoupling and flexibility provided by using adapters created with the <php>castTo()</php> method. 
 + 
 +==== Prototype implementation ==== 
 + 
 +Using <php>eval()</php> and anonymous classes, it's already possible to create a function that turns a closure into an implementation of a single-method interface. Conceptually, this RFC could be coded this way: 
 + 
 +<code php> 
 +class Closure 
 +
 +    // [...] Closure is an internal class 
 +     
 +    public function castTo(self $closure, string $interface): object 
 +    { 
 +        static $cache = []; 
 + 
 +        $cacheKey = <compute-key-from-$closure-and-$interface> 
 + 
 +        if (isset($cache[$cacheKey])) { 
 +            return $cache[$cacheKey]($closure); 
 +        } 
 +         
 +        $method = <name-of-the-method-in-$interface>; 
 +        $signature = <signature-of-$closure>; 
 +        $return = str_ends_with($signature, '): never') || str_ends_with($signature, '): void') ? '' : 'return '; 
 + 
 +        $object = eval(<<<PHP 
 +            return new class (\$closure) implements {$interface} { 
 +                public function __construct(private readonly Closure \$closure) { 
 +                } 
 +                public function {$method}{$signature} { 
 +                    {$return}\$this->closure->__invoke(<list-of-arguments-in-$closure>); 
 +                } 
 +            }; 
 +            PHP); 
 + 
 +        $cache[$cacheKey] = $object::class; 
 + 
 +        return $object; 
 +    } 
 +
 +</code> 
 + 
 +A working prototype implementation is provided by this package: 
 +https://github.com/tchwork/closure-caster/blob/main/src/function.php 
 + 
 +===== Backward Incompatible Changes =====
  
 This proposal introduces no backward incompatible changes, as it only adds a new method to the <php>Closure</php> class. This proposal introduces no backward incompatible changes, as it only adds a new method to the <php>Closure</php> class.
  
-==== Open Issues ====+===== Open Issues =====
  
   * exact behavior when checks fail   * exact behavior when checks fail
Line 136: Line 228:
   * would the engine allow implementing this without adding any extra frame in the call stack?   * would the engine allow implementing this without adding any extra frame in the call stack?
  
-==== Unaffected PHP Functionality ====+===== Unaffected PHP Functionality =====
  
 No existing PHP functionality would be affected by this proposal. No existing PHP functionality would be affected by this proposal.
  
-==== Future Scope ====+===== Future Scope =====
  
   * Allow a closure to declare a single-method interface it implements, as in e.g. <php>function () implements FooInterface {...}</php>. See https://wiki.php.net/rfc/allow-closures-to-declare-interfaces-they-implement   * Allow a closure to declare a single-method interface it implements, as in e.g. <php>function () implements FooInterface {...}</php>. See https://wiki.php.net/rfc/allow-closures-to-declare-interfaces-they-implement
Line 147: Line 239:
 Both ideas are complementary to the one proposed in this RFC. Both ideas are complementary to the one proposed in this RFC.
  
-==== Proposed PHP Version ====+===== Proposed PHP Version =====
  
 This RFC targets PHP version 8.3. This RFC targets PHP version 8.3.
  
-==== Vote ====+===== Vote =====
  
 The vote will require a 2/3 majority to be accepted. Voting will start on [Vote start date] and end on [Vote end date]. The vote will require a 2/3 majority to be accepted. Voting will start on [Vote start date] and end on [Vote end date].
  
  
rfc/allow_casting_closures_into_single-method_interface_implementations.1682414379.txt.gz · Last modified: 2023/04/25 09:19 by nicolasgrekas