Hello list,
I don't know if something like this was already proposed in the past,
I did not find anything.
Sometimes it would be nice to have a code block inside an expression, like this:
public function f(string $key) {
return $this->cache[$key] ??= {
// Calculate a value for $key.
[...]
return $value;
}
}
Currently, to achieve the same, we have two options which both add
overhead both in code verbosity and in performance:
- Call another method or closure after the ??=.
- Use if (isset(...)) instead of the ??= shortcut. This results in
repetition of other parts of the expression, because the if/else
cannot be inside the expression.
E.g. this is option 1 with a closure:
public function f(string $key) {
return $this->cache[$key] ??= (function () use ($key) {
// Calculate a value for $key.
[...]
return $value;
})();
}
Option 1 with a real method would look like this:
public function f(string $key) {
return $this->cache[$key] ??= $this->calc($key);
}
private function calc(string $key) {
// Calculate a value for $key.
[...]
return $value;
}
The {}
syntax seems like the most obvious choice at first, but I
think it would not work.
Statement groups with curly brackets are already possible right now,
see https://www.php.net/manual/en/control-structures.intro.php, but
they are not really useful for anything: They cannot be used as
expressions, they don't have their own return value, and they don't
isolate their variables.
Another option would be a special keyword before the curly block.
We could introduce a new keyword like expr
, or use an existing one like fn
.
$x = 4 + fn {return 3;};
// or
$x = 4 + expr {return 3;}
The compiler/interpreter could either convert the block into a
closure, or treat it as a new language feature, which might bring
performance benefits.
Any thoughts?
-- Andreas
Hi Andreas
Hello list,
I don't know if something like this was already proposed in the past,
I did not find anything.Sometimes it would be nice to have a code block inside an expression, like this:
public function f(string $key) {
return $this->cache[$key] ??= {
// Calculate a value for $key.
[...]
return $value;
}
}
This has been discussed a few years back when match expressions were
proposed. I originally wanted to include support for code blocks along
with expressions to offer a more complete alternative to switch
statements. The other major use-case for block expressions are arrow
functions.
Unfortunately, a general solution seems suboptimal due to the subtle
semantic differences. See this message for my detailed thought
process.
https://externals.io/message/109941#109947
I believe it would be best to address blocks for match arms and arrow
functions separately.
I don't believe blocks for general expressions are that useful in PHP
due to the lack of block scoping. Your suggestion to make the block a
separate closure could avoid that (as well as the optimizer issue
mentioned below) but comes with new issues, like making modification
of captured values impossible without by-ref capturing. It seems
confusing that fn {} is auto-executed while fn() {} isn't, as the
former looks like a shortened version of the latter. fn() => fn {}
would also look quite weird. match ($x) { 1 => fn {} } seems ok,
except for being somewhat lengthy.
On another note, the vote for blocks in short closures has failed
lately (https://wiki.php.net/rfc/auto-capture-closure).
The message above also addresses the syntax ambiguity you mentioned.
The {} syntax would be unambiguous in the most useful contexts (e.g.
function parameters, match arms, arrow function bodies, rhs of binary
operators, etc.). It is ambiguous in the general expression context
due to expression statements (statements containing a single
expression followed by ;
), where it's unclear (without lookahead)
whether the {
refers to a statement block or a block expression.
Replacing all statement blocks with block expressions comes with the
added difficulty of allowing to omit the ;
of block expressions in a
expression statement.
I remember there also being issues with the optimizer (related to
https://www.npopov.com/2022/05/22/The-opcache-optimizer.html#liveness-range-calculation).
The details went over my head at the time.
I'm interested in picking this back up at some point, at least for match arms.
Ilija
I don't believe blocks for general expressions are that useful in PHP
due to the lack of block scoping. Your suggestion to make the block a
separate closure could avoid that (as well as the optimizer issue
mentioned below) but comes with new issues, like making modification
of captured values impossible without by-ref capturing.
I've been pondering various things in this space for a while,
particularly since the last auto-capture closures RFC. I haven't quite
coalesced on a coherent concept, but there are a few things that I think
inter-relate:
- Opting into block-scoped variables, while retaining full access to the
outer scope, like JS "let" - Code blocks as some kind of first-class thing, distinct from closures
- Macro-like constructs that take a block of code and do something
special with it, or something like Python's "context managers"
One of the use cases people often cite for auto-capture in closures is
for use in patterns like this:
$db->doInTransaction(function() use (...) {
$db->execute('some SQL here');
$db->execute('some more SQL');
// etc
});
Or this:
$name = $cache->memoize(function() use (...) {
$result = $db->query('some SQL');
return $result['item_name'];
});
This is a nice pattern, but closures are quite a heavy tool for this
job, requiring two extra stack frames (the doInTransaction or memoize
method, and the closure itself), and boilerplate to pass the result back
through.
As I say, I haven't got a coherent design, but I think it would be great
to have something context-manager-like:
$name = with($cache->memoize() as $cacheItem) {
let $result = $db->query('some SQL');
$cacheItem->save( $result['item_name'] );
} );
... or macro-based:
$name = null;
$cache->memoize!($name, block {
let $result = $db->query('some SQL');
$name = $result['item_name'];
} );
Regards,
--
Rowan Tommins
[IMSoP]
Hello Rowan, Ilja,
(sorry I did not see these replies sooner, I do some forwarding but it failed)
I don't believe blocks for general expressions are that useful in PHP
due to the lack of block scoping. Your suggestion to make the block a
separate closure could avoid that (as well as the optimizer issue
mentioned below) but comes with new issues, like making modification
of captured values impossible without by-ref capturing.I've been pondering various things in this space for a while,
particularly since the last auto-capture closures RFC. I haven't quite
coalesced on a coherent concept, but there are a few things that I think
inter-relate:
- Opting into block-scoped variables, while retaining full access to the
outer scope, like JS "let"- Code blocks as some kind of first-class thing, distinct from closures
I think this would be my preferred option.
We would have to discuss how variables are scoped and captured, but I
could myself being happy with different options.
Currently variables from outside in short closures are auto-captured
by value, and nothing leaks outside:
https://3v4l.org/mdJvM
I would be happy if it works the same for expression code blocks.
Hello Ilija,
Hi Andreas
Hello list,
I don't know if something like this was already proposed in the past,
I did not find anything.Sometimes it would be nice to have a code block inside an expression, like this:
public function f(string $key) {
return $this->cache[$key] ??= {
// Calculate a value for $key.
[...]
return $value;
}
}This has been discussed a few years back when match expressions were
proposed. I originally wanted to include support for code blocks along
with expressions to offer a more complete alternative to switch
statements. The other major use-case for block expressions are arrow
functions.Unfortunately, a general solution seems suboptimal due to the subtle
semantic differences. See this message for my detailed thought
process.
I looked at this and I think all of this can be solved.
About ambiguity with existing code blocks:
I think we should prepend a new keyword like "expr".
I was a bit hesitant to introduce a new keyword, but I think it is the
cleanest solution.
So it becomes $x = expr {return 5;};
.
For the return syntax, let's simply use return
, instead of a new
language construct like "<=".
About return being required or not:
Let's do the same as in a regular function body:
- If return value is omitted, an implicit
NULL
is returned. - IDEs / static analysis can look at the wider context and complain if
it thinks a return value should be added or omitted. PHP can simply
ignore the problem.
This means that the following will be true:
assert(NULL === expr {});
assert(NULL === expr {return;});
assert(NULL === expr {return NULL;});
I believe it would be best to address blocks for match arms and arrow
functions separately.
The new syntax could be used in all 3 cases:
$y = match ($x) {
1 => expr {}, // IDE complains, but PHP does not care.
}
$fn = fn () => expr {}; // Totally ok.
$fn = fn (): array => expr {}; // Error at runtime when it is executed.
$fn = fn (bool $arg): array => expr {if ($arg) return [];}; //
Possible error at runtime when it is executed.
We could also declare a return type on the expr, like so:
But I don't see a big advantage.
$x = expr: array {return [];};
We can also use generators:
$generator = expr {yield 'A'; yield 'B';};
assert($generator instanceof \Generator);
I don't believe blocks for general expressions are that useful in PHP
due to the lack of block scoping.
We could introduce block scoping for that specific language feature.
And even without it, to me these expression blocks would still be useful.
If the default is "use all by ref, leak all", it would feel more like
other language constructs like foreach(), if/else etc.
If the default is "capture all by value", it would feel more like a
short closure.
Perhaps there could be something to control the capturing type.
E.g. a use
part?
expr {..} // Capture all by value, don't leak inner variables.
expr use () {..} // Capture nothing.
expr use ($x) {..} // Capture specific variable $x, by value.
expr use () {..} // Capture all by value, don't leak.
expr use (&) {..} // Capture all by reference, leak all?
Or we could have different keywords:
expr {..} // Capture all by reference, leak all inner variables.
scope {..} // Capture all by value, don't leak.
I personally think mostly of uses cases with cache and ??=.
These exist a lot within Drupal, both with runtime cache and persistent cache.
With the expression block these could be shortened like this:
$x = $runtime_cache['x']
??= $persistent_cache['x']
??= {
... // Calculate value for x.
return $value;
};
I could also see it used in generated code that behaves as a huge
nested expression.
Your suggestion to make the block a
separate closure could avoid that (as well as the optimizer issue
mentioned below) but comes with new issues, like making modification
of captured values impossible without by-ref capturing. It seems
confusing that fn {} is auto-executed while fn() {} isn't, as the
former looks like a shortened version of the latter. fn() => fn {}
would also look quite weird. match ($x) { 1 => fn {} } seems ok,
except for being somewhat lengthy.On another note, the vote for blocks in short closures has failed
lately (https://wiki.php.net/rfc/auto-capture-closure).The message above also addresses the syntax ambiguity you mentioned.
The {} syntax would be unambiguous in the most useful contexts (e.g.
function parameters, match arms, arrow function bodies, rhs of binary
operators, etc.). It is ambiguous in the general expression context
due to expression statements (statements containing a single
expression followed by;
), where it's unclear (without lookahead)
whether the{
refers to a statement block or a block expression.
Replacing all statement blocks with block expressions comes with the
added difficulty of allowing to omit the;
of block expressions in a
expression statement.I remember there also being issues with the optimizer (related to
https://www.npopov.com/2022/05/22/The-opcache-optimizer.html#liveness-range-calculation).
The details went over my head at the time.I'm interested in picking this back up at some point, at least for match arms.
Ilija
--
To unsubscribe, visit: https://www.php.net/unsub.php