compiler icon indicating copy to clipboard operation
compiler copied to clipboard

Not-Null pipe operator

Open thekid opened this issue 1 year ago • 3 comments

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

thekid avatar Mar 26 '24 11:03 thekid

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!

thekid avatar Mar 27 '24 15:03 thekid

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!

thekid avatar Mar 27 '24 16:03 thekid

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(...);

thekid avatar Mar 28 '24 11:03 thekid