xp-framework / compiler

Compiles future PHP to today's PHP.
19 stars 0 forks source link

Not-Null pipe operator #180

Open thekid opened 5 months ago

thekid commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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