Twig icon indicating copy to clipboard operation
Twig copied to clipboard

Add ??? Empty Coalesce Operator

Open khalwat opened this issue 6 years ago • 37 comments

Empty Coalesce adds the ??? operator to Twig that will return the first thing that is defined, not null, and not empty. This is particularly useful when you're dealing with a number of fallback/default values that may or may not exist, and may or may not be empty.

The ??? Empty Coalescing operator is similar to the ?? null coalesce operator, but also ignores empty strings (""), 0, 0.0, null, false, and empty arrays ([]) as well.

Because this is an Empty Coalesce Operator, it functions identically to the PHP empty() function in terms of return values.

Example:

{% set bar = null %}
{% set foo = '' %}
{% set baz = [] %}
{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}
{{ foobar }}

This will output:

bark

...because:

  • woof is undefined
  • bar is null
  • foo is an empty string
  • baz is an empty array

There is precedence for this operator in languages such as Swift:

https://medium.com/@JanLeMann/providing-meaningful-default-values-for-empty-or-absence-optionals-via-nil-or-empty-coalescing-379abd22ae77

khalwat avatar Dec 19 '18 02:12 khalwat

What is the difference with the |default filter we have.

Well, one of them is that it relies on PHP's empty, meaning that '0' is empty while |default does not consider that. But is there any other difference ?

stof avatar Dec 19 '18 11:12 stof

btw, your implementation also ignores '0', 0 and false, in addition to what you documented as being ignored.

stof avatar Dec 19 '18 11:12 stof

Thanks for looking @stof !

What is the difference with the |default filter we have.

It's the same reason why the ?? null coalescing operator exist in Twig right now, despite default being available. It is far more convenient and much more clear what is going on to have:

{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}

...using the default filter. It can be done, but it is quite ugly and verbose.

khalwat avatar Dec 19 '18 12:12 khalwat

I'm a big fan of this idea — It comes up frequently enough in my Twig templates (where I'm comparing a chain of values and I want to use/output the first non-empty one) that I've created my own stock extension for it, and it's a default add to all my projects.

I think ??? as an operator could be confusing vis-a-vis ??. And also, the visual difference between ?? and ??? is not super-scannable. Perhaps _? for this.

My only reservation about this in-practice is the case of "0" being empty.... but, one can't really do anything about it; I think ultimately it's reasonable to be consistent with PHP's empty() and expect devs to learn that behavior.

It's definitely a non-standard op, but I think the utility of it is worth adding the sugar to Twig even though no similar operator exists upstream in PHP.

michaelrog avatar Dec 19 '18 18:12 michaelrog

+1 to this PR - i am using @khalwat's null-coalescing operator as a plugin on a site and it is really nice to have in any context where your Twig data contains, for instance, a lot of empty arrays.

adrienne avatar Dec 19 '18 18:12 adrienne

Why not use ?: to provide this behavior?

nicolas-grekas avatar Dec 19 '18 19:12 nicolas-grekas

I think ??? as an operator could be confusing vis-a-vis ??. And also, the visual difference between ?? and ??? is not super-scannable. Perhaps _? for this.

_? is horrible to type; I think ??? is easier to type, and clearer in that it's ?? plus more. The fact that others have independently come up with the same ??? operator for this functionality makes me feel even better about it.

My only reservation about this in-practice is the case of "0" being empty.... but, one can't really do anything about it; I think ultimately it's reasonable to be consistent with PHP's empty() and expect devs to learn that behavior.

It actually uses empty() under the hood, so yes, it'd be consistent with empty()'s behavior, which I think makes sense.

khalwat avatar Dec 19 '18 19:12 khalwat

@nicolas-grekas — The primary benefit is that ?: will throw an error if it encounters an undefined operand. We'd really like this to behave like the null-coalescing operator, which treats undefined things as null.

(Also, it needs to be right-associative... I think ?: is, but I don't remember offhand.)

michaelrog avatar Dec 19 '18 19:12 michaelrog

Why not use ?: to provide this behavior?

@nicolas-grekas what would the Twig code for this be using the ?: operator for this construct:

{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}

In PHP ?: just determines truthiness, and would throw an error if a variable was undefined, unlike ???

khalwat avatar Dec 19 '18 19:12 khalwat

and would throw an error if a variable was undefined

twig is a different language, we can remove this behavior if we want to.

nicolas-grekas avatar Dec 19 '18 19:12 nicolas-grekas

twig is a different language, we can remove this behavior if we want to.

That's definitely true, but I'd rather define a new operator for new functionality than hoist new functionality on an operator for which there's already a defined behavior.

Many people who write in Twig also write in PHP, and it might be very confusing if ?: operated differently in Twig than it does in PHP... at least to me.

khalwat avatar Dec 19 '18 19:12 khalwat

twig is a different language, we can remove this behavior if we want to.

that would require changing the existing operator, and that makes mistake detection harder in case you make typos in your variable names. I would vote for keeping the existing behavior of the operator.

stof avatar Dec 20 '18 08:12 stof

Another vote to not modify existing operator behaviors! (And a vote for ???)

brandonkelly avatar Jan 16 '19 23:01 brandonkelly

Welp, looks like the ??= operator is being added in PHP 7.4: https://twitter.com/nikita_ppv/status/1087662379037528064

It's not the same thing, but it does show some precedence for a three character operator... so I hope ??? can make it into Twig!

khalwat avatar Jan 22 '19 13:01 khalwat

btw, your implementation also ignores '0', 0 and false, in addition to what you documented as being ignored.

If we’re calling this the “Empty Coalesce Operator”, I would expect that any value PHP considers “empty” would also be considered empty here, so '0' and 0 should in fact invoke the following operator argument. Safe to assume that if PHP ever adds the same operator, they would go with the same behavior as empty() as well.

brandonkelly avatar Apr 01 '19 19:04 brandonkelly

Yeah that's the idea @brandonkelly -- it's called the Empty Coalesce Operator because the behavior is what we'd expect from chained/nested empty() calls.

khalwat avatar Apr 01 '19 19:04 khalwat

https://github.com/twigphp/Twig/pull/2787/files#diff-b445caeb1cc1391b4cc1966bd1b1f76cR28

After compilation, wouldn't the current implementation eventually evaluate both the left-hand side and the right-hand side twice at runtime? I mean it's possible that func1 or func2 in func1(arg1) ??? func2(arg2) be executed twice? In PHP, both L and R in L ?? R would be only evaluated once.

If there is a static variable (such as a static counter to log how many time the func has been executed) in func1 or func2, a counterintuitive behavior would happen?

jfcherng avatar Apr 01 '19 19:04 jfcherng

@jfcherng suggestions on how to mitigate the behavior you've mentioned?

khalwat avatar Apr 30 '19 03:04 khalwat

@khalwat Sorry I cannot really answer that. But that is what I saw when I was following the ??= RFC. Initially, L ??= R was implemented as a simple syntax sugar for L = L ?? R, but soon problems came out.

For example, consider the expression $a[print 'X'] ??= $b. A simple desugaring into $a[print 'X'] = $a[print 'X'] ?? $b will result in 'X' being printed twice. However, this is not how all other existing compound assignment operators behave: They will print X only once, as the LHS is only evaluated once. I assume that ??= would behave the same way.

        $compiler
            ->raw('(('.self::class.'::empty(')
            ->subcompile($this->getNode('left'))
            ->raw(') ? null : ')
            ->subcompile($this->getNode('left'))
            ->raw(') ?? ('.self::class.'::empty(')
            ->subcompile($this->getNode('right'))
            ->raw(') ? null : ')
            ->subcompile($this->getNode('right'))
            ->raw('))')
        ;

With this, I am assuming L ??? R will be de-sugared into something like

(empty(L) ? null : L) ?? (empty(R) ? null : R)

The worst case is evaluating both L and R twice when executing the compiled template.

Maybe worth a note in the docs if this cannot be resolved?

jfcherng avatar Apr 30 '19 06:04 jfcherng

How about using ??: instead? From what I can tell, this new operator combines the effects of the existing ?? and ?: operators in PHP.

dharkness avatar May 22 '19 20:05 dharkness

@dharkness ?: can’t be used in succession like ?? can though, e.g. foo ?? bar ?? baz

brandonkelly avatar May 22 '19 20:05 brandonkelly

@brandonkelly I can only speak for PHP, but both ?? and ?: can be used in a series.

> $y = null;
> echo $x ?? $y ?? 'foo';
foo
> echo 0 ?: '' ?: 'foo';
foo

dharkness avatar May 22 '19 21:05 dharkness

Your example is easy to do with operators ?? and ?:.

{% set bar = null %}
{% set foo = '' %}
{% set baz = [] %}
{% set foobar = woof ?? null ?: bar ?: foo ?: baz ?: 'bark' %}
{{ foobar }}

GromNaN avatar Feb 21 '20 15:02 GromNaN

@GromNaN that presupposes that you know the state of the variables. The point is that any of them could be undefined, null, or empty, and you don't know ahead of time.

khalwat avatar Feb 21 '20 15:02 khalwat

I did't imply this feature was not useful, just trying to help using the existing features. So <variable> ??? <default> would be a shortcut for (<variable> ?? null) ?: <default>.

If the existence of any variable is unknown, the example can be:

{% set foobar = (woof ?? null) ?: (bar ?? null) ?: (foo ?? null) ?: (baz ?? null) ?: 'bark' %}

GromNaN avatar Feb 28 '20 09:02 GromNaN

@GromNaN I don’t think anyone here is unaware of existing syntax options. You could have made the same point about ?? … that <variable> ?? <default> it is the same thing as doing (<variable> is defined and <variable> is not same as(null) ? <variable> : <default>. The point is to simplify, to make templates more readable & maintainable.

brandonkelly avatar Feb 28 '20 13:02 brandonkelly

Excepted that with ?? the <variable> have to be repeated several times. Which is very verbose when <variable> contains a complex expression.

GromNaN avatar Feb 28 '20 14:02 GromNaN

What's the status of this? Thought I'd bump it up since it seems it got abandoned without any particular reason.

Did the team decide this isn't desirable? the comment voting from the community seems to suggest otherwise. Is it that the PR needs more work? as far as I can tell there isn't any specific request of what it needs.

So I guess the question is

  1. Are the maintainers open to this?
  2. If not, why, given that the community wants it and every objection so far has been answered?
  3. If so, is there something that needs to be done on the PR to get it merged?

acalvino4 avatar Aug 26 '22 01:08 acalvino4

Currently, the PR is borked. I'd be happy to attempt to redo it if there is interest from the maintainers.

khalwat avatar Aug 26 '22 05:08 khalwat

I want to add my +1 on that. And thanks for the work on that.

{% set theme = entry is defined and entry.theme != "" ? entry.theme : defaults.theme != "" ? defaults.theme : "dark" %} vs. {% set theme = entry.theme ??? defaults.theme ??? "dark" %}

The readability is just a whole other level.

maxstrebel avatar Sep 13 '22 13:09 maxstrebel