Skip to content

Instantly share code, notes, and snippets.

@IMSoP
Last active March 14, 2023 21:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IMSoP/4157af05c79b3df4c4853f5a58766341 to your computer and use it in GitHub Desktop.
Save IMSoP/4157af05c79b3df4c4853f5a58766341 to your computer and use it in GitHub Desktop.
PHP Brainstorming: inline syntax for lexical capture
<?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);
}
};
}
<?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;
};
};
}
<?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;
}
<?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;
}
}
<?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