compiler
compiler copied to clipboard
Not-Null pipe operator
Proposal
Complement the null-coalescing operator:
$user= $context->userName() ?? 'guest';
Sometimes we want to perform an action if the value is null, and perform an alternative instead:
$user= $context->userName();
return null === $user ? null : strtoupper($user);
We already have a "standard" for chaining expressions with the pipe operator. The above without null handling would be:
return $context->userName() |> stroupper(...);
👉 To accomplish this, this feature request suggests a null-safe pipe operator, which would make the following an equivalent of the above:
return $context->userName() ?|> strtoupper(...);
Consistency
This is consistent with the null-safe object operator. If we rewrote the above using a String class, we could have the following:
return $context->userName()?->toUpper();
However, wrapping every primitive string in a String instance would introduce quite a bit of runtime and development overhead!
See also
- https://stackoverflow.com/questions/62929428/opposite-of-nullish-coalescing-operator
- https://wiki.php.net/rfc/pipe-operator
- https://wiki.php.net/rfc/pipe-operator-v2
- https://github.com/php/php-src/pull/7214
- https://docs.hhvm.com/hack/expressions-and-operators/pipe
- https://github.com/dotnet/roslyn/issues/15823
- https://github.com/tc39/proposal-pipeline-operator
- https://github.com/tc39/proposal-pipeline-operator/issues/159 - brings up
?> - https://github.com/tc39/proposal-pipeline-operator/issues/210 - more pipeline operators, includes
?|> - https://elixirschool.com/en/lessons/basics/pipe_operator
- https://gleam.run/cheatsheets/gleam-for-php-users/#piping
- https://externals.io/message/107661 - anti-coalescing via
!?? - https://externals.io/message/107661#107670 -
?|>suggested
In Kotlin, scope functions solve this:
Person("Tom", age = 12)
|> findFriends(it)
|> storeFriendsList(it)
// Equivalent
Person("tom", age = 12)
.let { findFriends(it) }
.let { storeFriendsList(it) }
Source: https://discuss.kotlinlang.org/t/pipe-forward-operator/2098
If we were to adopt this into PHP, this could be written as:
new Person('Tom', age: 12)
->let(findFriends(...))
->let(storeFriendsList(...))
;
// Yes, also works on non-objects!
$context->userName()->let(stroupper(...));
We could then simply reuse ?-> for null handling!
In Kotlin, scope functions solve this:
The let function can be implemented relatively easily, even as an optional extension:
diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php
index ac3d93a..adbe64b 100755
--- a/src/main/php/lang/ast/emit/PHP.class.php
+++ b/src/main/php/lang/ast/emit/PHP.class.php
@@ -1076,6 +1076,24 @@ abstract class PHP extends Emitter {
}
protected function emitInvoke($result, $invoke) {
+ if ($invoke->expression instanceof InstanceExpression && 'let' === $invoke->expression->member->expression) {
+ if ('nullsafeinstance' === $invoke->expression->kind) {
+ $t= $result->temp();
+ $result->out->write("null===({$t}=");
+ $this->emitOne($result, $invoke->expression->expression);
+ $result->out->write(')?null:(');
+ $this->emitOne($result, $invoke->arguments[0]);
+ $result->out->write(")({$t})");
+ } else {
+ $result->out->write('(');
+ $this->emitOne($result, $invoke->arguments[0]);
+ $result->out->write(')(');
+ $this->emitOne($result, $invoke->expression->expression);
+ $result->out->write(')');
+ }
+ return;
+ }
+
$this->emitOne($result, $invoke->expression);
$result->out->write('(');
$this->emitArguments($result, $invoke->arguments);
⚠️ However, this would be a BC break for classes with a let method!
Here's a real-world example from https://github.com/thekid/dialog:
if ($prop= $env->properties($config, optional: true)) {
$this->sources['config']= $prop->readString(...);
}
This could be rewritten as follows:
// Scope function
$env->properties($config, optional: true)?->let(fn($prop) => $this->sources['config']= $prop->readString(...));
// Pipe operator
$env->properties($config, optional: true) ?|> fn($prop) => $this->sources['config']= $prop->readString(...);
// Hacklang pipes with $$ placeholder
$env->properties($config, optional: true) ?|> $this->sources['config']= $$->readString(...);