Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not-Null pipe operator #180

Open
thekid opened this issue Mar 26, 2024 · 3 comments
Open

Not-Null pipe operator #180

thekid opened this issue Mar 26, 2024 · 3 comments

Comments

@thekid
Copy link
Member

thekid commented Mar 26, 2024

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

@thekid
Copy link
Member Author

thekid commented Mar 27, 2024

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
Copy link
Member Author

thekid commented Mar 27, 2024

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
Copy link
Member Author

thekid commented Mar 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant