Last active
March 14, 2023 21:12
-
-
Save IMSoP/4157af05c79b3df4c4853f5a58766341 to your computer and use it in GitHub Desktop.
PHP Brainstorming: inline syntax for lexical capture
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Existing functionality | |
function wrapLogger(LoggerInterface $existingLogger, string $myExtraContextValue) { | |
// Values can only be passed in via the constructor | |
$delegatingLogger = new class($existingLogger, $myExtraContextValue) extends AbstractLogger { | |
public function __construct( | |
// Constructor property promotion simplifies this, but we still need to declare private properties | |
private LoggerInterface $delegateTo, | |
private string $extraContextValue | |
){} | |
public function log($level, string|\Stringable $message, array $context = []): void { | |
// This is where we actually wanted the captured values | |
$context['my_extra_context'] = $this->extraContextValue; | |
$this->delegateTo->log($level, $message, $context); | |
} | |
}; | |
} | |
// Concept: $^foo means "read value of $foo from lexical scope" | |
function wrapLogger(LoggerInterface $existingLogger, string $myExtraContextValue) { | |
$delegatingLogger = new class extends AbstractLogger { | |
// The constructor is no longer needed, as the values can be passed as initial values | |
private LoggerInterface $delegateTo = $^existingLogger; | |
private string $extraContextValue = $^myExtraContextValue; | |
public function log($level, string|\Stringable $message, array $context = []): void { | |
$context['my_extra_context'] = $this->extraContextValue; | |
$this->delegateTo->log($level, $message, $context); | |
} | |
}; | |
} | |
// Enhancement: $^foo means "capture lexical $foo and store against the object" (similar to how a closure captures) | |
function wrapLogger(LoggerInterface $existingLogger, string $myExtraContextValue) { | |
$delegatingLogger = new class extends AbstractLogger { | |
public function log($level, string|\Stringable $message, array $context = []): void { | |
// The captured values are available for the lifetime of the object, so no need for private properties | |
$context['my_extra_context'] = $^myExtraContextValue; | |
$^existingLogger->log($level, $message, $context); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// The same idea could be used as an alternative to use() for multi-statement anonymous functions | |
// These two examples are taken from Bob's 2015 short closure RFC | |
// Before: | |
$myFile = "/etc/passwd"; | |
runDebug(function() use ($myFile) { | |
if (!file_exists($myFile)) { | |
throw new Exception("File $myFile does not exist..."); | |
} | |
}); | |
// After: | |
$myFile = "/etc/passwd"; | |
runDebug(function() { | |
if (!file_exists($^myFile)) { | |
throw new Exception("File $^myFile does not exist..."); | |
} | |
}); | |
// Nested captures might need further consideration | |
// Before: | |
function reduce(callable $fn) { | |
return function($initial) use ($fn) { | |
return function($input) use ($fn, $initial) { | |
$accumulator = $initial; | |
foreach ($input as $value) { | |
$accumulator = $fn($accumulator, $value); | |
} | |
return $accumulator; | |
}; | |
}; | |
} | |
// The syntax could perhaps explicitly indicate deeper capture by repeating a token | |
// Here, $^^fn means "capture $fn from two lexical scopes out", i.e. we're skipping two use() statements | |
function reduce(callable $fn) { | |
return function($initial) { | |
return function($input) { | |
$accumulator = $^initial; | |
foreach ($input as $value) { | |
$accumulator = $^^fn($accumulator, $value); | |
} | |
return $accumulator; | |
}; | |
}; | |
} | |
// Alternatively, we might be able to use the same logic as arrow functions to resolve upwards | |
function reduce(callable $fn) { | |
return function($initial) { | |
return function($input) { | |
$accumulator = $^initial; | |
foreach ($input as $value) { | |
$accumulator = $^fn($accumulator, $value); | |
} | |
return $accumulator; | |
}; | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Lexical values could be used to initialse private, protected, or public properties | |
// The same lexical value can be used any number of times | |
$x = 1; | |
$example = new class { | |
private $x = $^x; | |
protected $sharedX = $^x; | |
public $alsoX = $^x; | |
} | |
// Lexical values are only visible in lexical scope, they are not resolved like private properties | |
// So this would NOT work | |
trait FooTrait { | |
public function foo() { | |
// $^x has no meaning here | |
return $^x + 1; | |
} | |
} | |
$x = 1; | |
$example = new class { | |
use FooTrait; | |
} | |
// Again, converting to a private property via a normal initialiser WOULD work | |
trait FooTrait { | |
public function foo() { | |
return $this->x + 1; | |
} | |
} | |
$x = 1; | |
$example = new class { | |
private $x = $^x; | |
use FooTrait; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Lexically captured variables would be initially assigned by value, and readonly | |
// For simple values, | |
// This would be forbidden - can't use a lexical variable name as a writable variable | |
$x = 1; | |
$example = new class { | |
public function foo() { | |
$^x = $^x + 1; | |
return $^x; | |
} | |
} | |
// Instead, explicitly copy the lexical value to a local variable, and work with that | |
$x = 1; | |
$example = new class { | |
public function foo() { | |
$localX = $^x + 1; | |
return $localX; | |
} | |
} | |
// Or, use it as the initialiser for a private property | |
$x = 1; | |
$example = new class { | |
private $x = $^x; | |
public function foo() { | |
$this->x = $this->x + 1; | |
return $this->x; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// As with all "by value" operations, objects are not cloned when they are captured | |
// As such, captured object values could be directly manipulated | |
class Foo { | |
public $myVar = 1; | |
} | |
$foo = new Foo; | |
$anon = new class { | |
public function manipulateFoo() { | |
$^foo->myVar++; | |
} | |
} | |
var_dump($foo->myVar); // 1 | |
$anon->manipulateFoo(); | |
var_dump($foo->myVar); // 2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment