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

Add ??? Empty Coalesce Operator #2787

Open
wants to merge 6 commits into
base: 2.x
Choose a base branch
from
Open

Add ??? Empty Coalesce Operator #2787

wants to merge 6 commits into from

Conversation

khalwat
Copy link

@khalwat khalwat commented Dec 19, 2018

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

Signed-off-by: Andrew Welch <[email protected]>
@stof
Copy link
Member

stof commented Dec 19, 2018

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

stof commented Dec 19, 2018

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

@khalwat
Copy link
Author

khalwat commented Dec 19, 2018

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.

@michaelrog
Copy link

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.

@adrienne
Copy link

+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.

@nicolas-grekas
Copy link
Contributor

Why not use ?: to provide this behavior?

@khalwat
Copy link
Author

khalwat commented Dec 19, 2018

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.

@michaelrog
Copy link

michaelrog commented Dec 19, 2018

@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.)

@khalwat
Copy link
Author

khalwat commented Dec 19, 2018

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 ???

@nicolas-grekas
Copy link
Contributor

and would throw an error if a variable was undefined

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

@khalwat
Copy link
Author

khalwat commented Dec 19, 2018

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.

@stof
Copy link
Member

stof commented Dec 20, 2018

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.

@brandonkelly
Copy link
Contributor

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

@khalwat
Copy link
Author

khalwat commented Jan 22, 2019

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!

lib/Twig/Node/Expression/EmptyCoalesce.php Outdated Show resolved Hide resolved
@brandonkelly
Copy link
Contributor

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.

@khalwat
Copy link
Author

khalwat commented Apr 1, 2019

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.

@jfcherng
Copy link

jfcherng commented Apr 1, 2019

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?

@khalwat
Copy link
Author

khalwat commented Apr 30, 2019

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

@jfcherng
Copy link

jfcherng commented Apr 30, 2019

@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?

@fabpot fabpot force-pushed the 2.x branch 3 times, most recently from c2a7ac3 to d997512 Compare May 1, 2019 13:44
@dharkness
Copy link

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

@brandonkelly
Copy link
Contributor

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

@dharkness
Copy link

@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

lib/Twig/Node/Expression/EmptyCoalesce.php Outdated Show resolved Hide resolved

@trigger_error(sprintf('Using the "Twig_Extension_Core" class is deprecated since Twig version 2.7, use "Twig\Extension\CoreExtension" instead.'), E_USER_DEPRECATED);
final class CoreExtension extends AbstractExtension
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is wrong

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the error message is wrong (a copy and paste error) but if we want this removed as per your earlier comment, then it probably doesn't matter. It'd be fixed by just removing the whole deprecation nonsense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a change to remove the pointless Twig_Node_Expression_EmptyCoalesce class entirely, which should resolve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not fixed. You are still making a huge mess in the CoreExtension file, reverting the legacy file to a full class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using this to merge into: https://github.com/twigphp/Twig/blob/2.x/src/Extension/CoreExtension.php

Should I be using it from a different branch?

Copy link
Author

@khalwat khalwat Jul 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof I'd love to get this addressed, but I could use some guidance.

The only reason there is a change to lib/Twig/Extension/Core.php is to include the ??? operator. To fix this so that it's not creating a "huge mess" I just need to know what branch/source I should be diff'ing with to add this line?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should not be making any change in lib/Twig/Extension/Core.php, which is a legacy BC file (except you are reverting this to not be a BC file anymore). The change should be done only in src/Extension/CoreExtension.php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your PR is reverting #2863 on this file, which is what the huge mess is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That change to make lib/Twig/Extension/Core.php wasn't made until after the PR I made, thus my confusion about what's going on here.

I will revert the now legacy lib/Twig/Extension/Core.php and make my changes in src/Extension/CoreExtension.php

@GromNaN
Copy link
Contributor

GromNaN commented Feb 21, 2020

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 }}

@khalwat
Copy link
Author

khalwat commented Feb 21, 2020

@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.

@GromNaN
Copy link
Contributor

GromNaN commented Feb 28, 2020

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' %}

@brandonkelly
Copy link
Contributor

brandonkelly commented Feb 28, 2020

@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.

@GromNaN
Copy link
Contributor

GromNaN commented Feb 28, 2020

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

@acalvino4
Copy link

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?

@khalwat
Copy link
Author

khalwat commented Aug 26, 2022

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

@maxstrebel
Copy link

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.

@jdreesen
Copy link
Contributor

Wouldn't ??: be the better choice, since it's a combination of the operators ?? and ?:?

@maxstrebel
Copy link

maxstrebel commented Sep 13, 2022

Wouldn't ??: be the better choice, since it's a combination of the operators ?? and ?:?

I guess you're right.

I stumbled upon this via a plugin that allows to use ??? and it felt natural to me. But I actually do not care too much as long as I can use it 😄

@VincentLanglet
Copy link
Contributor

Just my two cents, but the first thing I thought about is ??? was "?:` does the same thing until I read

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

??: seems to be more understandable then.
$a ??: $b = ($a ?? null) ?: $b

It could even be a RFC proposed to PHP first, in order to have the same symbol for both twig and php.

@khalwat
Copy link
Author

khalwat commented Oct 20, 2022

@VincentLanglet would love to see an RFC for it in PHP, too.

The reason I prefer ??? is simply that it's easier to type, and it's been used in other languages before, to do the same thing.

@VincentLanglet
Copy link
Contributor

The reason I prefer ??? is simply that it's easier to type, and it's been used in other languages before, to do the same thing.

Do you have examples of other languages with ??? ?

@khalwat
Copy link
Author

khalwat commented Oct 20, 2022

@VincentLanglet link is in the first post in the thread; someone added it to Swift using ???

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

Successfully merging this pull request may close these issues.