godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.03k stars 21.18k forks source link

GDScript idea: add a null-coalescing operator, which returns first non-null value #7223

Closed Zylann closed 4 years ago

Zylann commented 7 years ago

In Python, Lua and Javascript (I think?), you can write this shortcut:

    a = None
    b = "Hello"
    print(a or b)

prints "Hello", and still evaluates to True if used in an if. Booleans behaviour is unchanged.

In GDScript, print(str(a or b)) would be readable and shorter to write than:

    var result = a
    if a == null:
        result = b
    print(str(result))

And even shorter than ternary, and also allows to write and evaluate a only once instead of two. it could be chained as well: a or b or c or d would fall back to the first one that is not null. What do you think?

ajacobsen commented 7 years ago

I would expect that

 print(a or b)

prints "Hello" if either a or b evaluates to true

I think it would make more sense to write

print(a else b)
RebelliousX commented 7 years ago

I am not against this, but "shorter" than ternary doesn't mean clearer. I am not familiar with python nor with this weird syntax, I would have paused a few seconds to digest it. I prefer ternary operator which can test any condition for being true or false, not just null. Besides, if both a and b are not null, returning the first operand, say a, seems random to me.

akien-mga commented 7 years ago

Besides, if both a and b are not null, returning the first operand, say a, seems random to me.

My thoughts precisely. I was also not aware that Python and Lua support such a syntax, but at first sight it seems highly confusing to me. What would be the output of print("Hello" or "world")? Or print(null and "Hello")?

IMO it makes more sense to stay consistent and treat a or b and a and b as conditionals in all situations, such they should always resolve to True or False.

What you suggest in the OP can be achieved with print(a if a else b), which is IMO clearer than print(a or b).

Zylann commented 7 years ago

print("Hello" or "world") would print "Hello", and print(null and "Hello") would print null, just as in Python and Lua. The logic behind is that the operators don't return True or False, but the last evaluated argument (that was evaluating as True or False anyways). However if you use comparison operators in the expression (<>=!=not) then you'll get booleans as usual.

I've mostly used this in Lua, where ternary doesn't exist http://lua-users.org/wiki/TernaryOperator. I was just doing the suggestion because I've seen this technique used not only in Lua, but also in Javascript, and the fact Python supports it could mean it could have been requested and used for a reason.

I'm ok with explicit ternaries, just wanted to know the community opinion.

Edit: another point is that the first operand is evaluated only once.

bojidar-bg commented 7 years ago

I think that @ajacobsen's binary-ternary-else idea is pretty good actually.

Php has it as the "elvis operator":

<?= escape($_GET['title'] ?: 'No title in GET') ?>

In GDScript it would look like:

var x = selection else defaultSelection

Finally, in JavaScript this is used and overused all the time, so it might be either somewhat good or terribly bad...

hubbyist commented 7 years ago

Php7 added "Null coalescing operator '??' " <?= ($a ?? $b) ?> will print b if a is null. This is different then elvis operator.

bojidar-bg commented 7 years ago

Ouch, my bad... anyway, ?: would work even in cases where a is null.

ghost commented 7 years ago

Would this be the same as the double pipe operator in JS?

var a = null
var b = 25
print(a||b)

Should print b, since a is null? If so, yea would love this!

bojidar-bg commented 7 years ago

Hmmm, I still don't see the real in-game use for this, since this would fail:

var id = dictionary.id || generate_new_id()
# Error: No key id in dictionary
Zylann commented 7 years ago

@bojidar-bg in your example you would need a "tryget", not a "get". But GDScript Dictionaries don't have such a thing as far as I know.

tryget is not the same topic but also has the advantage of fetching the item only once in case of a read and be able to use it in a one-liner (so instead of if d[key]: d[key] you can write d.tryget["key"] which returns null if not found).

I see you wrote a helper to generate the entry, so you could write this if you really want to:

var id = tryget(dictionary, "id") || generate_new_id()

But this is a bad specific example, I would just have a helper doing this:

var id = get_or_create_id()
ghost commented 7 years ago

A good example that I could use this for would be for export keywords.

Example:

export(int) var Chat_Limit

However, if Chat_Limit is not set, I would need to add:

if !Chat_Limit: Chat_Limit = 5

If or operator, now I do:

export(int) var Chat_Limit || 5
export(int) var Chat_Limit or 5 (||, or, or w/e)

This is kind of nitpicking as it only saves about a line of code, but damn it would be sexy.

bojidar-bg commented 7 years ago

@Dillybob92 Really bad example tho, as you can already do:

export(int) var Chat_Limit = 5
Zylann commented 7 years ago

(sorry stupid misclick)

ghost commented 7 years ago

@bojidar-bg Oh true. Wow, didn't know that, so it defaults to 5 already? That's cool, thank you! That means Chat_Limit isn't set though?

bojidar-bg commented 7 years ago

@Dillybob92 Well, it would default to 5 in case it isn't set in the scene (you can unset it via the "refresh" button next to the value).

We might make the expression else value operator evaluate the expression as normal, but, instead of failing on errors, make it take on the value part, WDYT? (instant try-catch, but, who cares...)

TeddyDD commented 7 years ago

Now with ternary-if we can do something like this: a = something if something else 3 and it looks kinda funny. Some syntax sugar could be nice. expression else value proposed by @bojidar-bg seems to nice be idea: a = something else 3

bojidar-bg commented 7 years ago

Ok, let's get started counting if there are many people who want it. As @akien-mga said:

With our fingers. If it doesn't fit on one hand, it's probably many.

So far I count 3 ± 1 people, still not enough for a hand.

Faless commented 7 years ago

What do you think?

I find it very confusing. you can simply do:

print(a if a else b)

or, to be more explicit (and include the empty string):

print(a if a != null else b)
Zylann commented 7 years ago

But you had to write a twice^^

neikeq commented 7 years ago

I'm against using the or keyword for this. a or b is a boolean operation, it's already taken. I propose using ?? like C# does. e.g.: a ?? b.

RebelliousX commented 7 years ago

What's wrong with C++'s ternary operator syntax?? It is great, easy and intuitive.

ghost commented 6 years ago

Is this still a desired behavior?

akien-mga commented 6 years ago

I'm still against this proposal and several other core contributors confirmed that sentiment, so I think it won't happen. Maybe with another syntax as proposed by @neikeq, but I don't see a lot of interest in the feature itself, so not sure it's worth introducing sugar at all.

malucard commented 6 years ago

So it's not going to happen? Not even as ??? This is really useful. I don't see why not include it.

vnen commented 6 years ago

It's not that useful, it's just syntax sugar for the regular ternary operator.

malucard commented 6 years ago

No, it's not. If you did, say, a() if a() != null else b(), it would run a() twice (if it's not null), because it doesn't remember the value, making for an unnecessary and maybe big slow operation. You can replicate the null coalescing operator with

var tmp = a()
var val = tmp if tmp != null else b()

, but a or b/a ?? b is way shorter, which is the point.

stwupton commented 6 years ago

I have very recently written some code wishing that I had something like Dart's null-aware operators.

So for example, an excerpt from the code I wish I had null-aware operators for...

Without Null-aware Operators

var last_path = _move_to_paths.back() if _move_to_paths.size() != 0 else Vector3()
_move_total_magnitude += (path - last_path).length()

With Null-aware Operators

_move_total_magnitude += (path - (_move_to_paths.back() ?? Vector3())).length()
mwerezak commented 6 years ago

That's disappointing, this can massively increase the readability of code in some cases.

vnen commented 6 years ago

For @stwupton last example, IMO it decreases readability. Trying to cram everything in a single line is not always helpful.

mwerezak commented 6 years ago

@vnen On the other hand, it is very helpful to avoid conditional branching logic when all you really want to do is express the value of something that could be null.

func example(value):
    if not value:
        value = fallback1
    if not value:
        value = fallback2
    ## do something with value

versus

func example(value):
    value = value if value else (fallback1 if fallback1 else fallback2)
    ## do something with value

versus

func example(value):
    value = value or fallback1 or fallback2
    ## do something with value

This is not about trying to get instructions on a single line. This is about 1. avoiding having to use conditional branching control flow when all you really want to do is express the value of something, and 2. nesting ternaries is terrible.

This example, expressing fallback values for a variable, is pretty much its primary use case. However, this comes in handy surprisingly often when working with a language that allows null values.

It is also useful when you want a function to have default values for an argument that need to be initialized.

func example(thing=null):
    thing = thing or ThingType.new()
    ## do something with thing

That said, the ternary expression can also be used for this, though it is ugly:

func example(thing=null):
    thing = thing if thing else ThingType.new()
    ## do something with thing

It comes down to the fact that the ternary expression is not a good fit for these cases because we don't actually want a 3-ary operation. We just want to short circuit null values.

Zylann commented 6 years ago

I still agree, but I'd maybe amend the issue title because using logic operators for this would feel a bit ambiguous.

bojidar-bg commented 6 years ago

Let me re-propose using the else operator from ternaries instead of the or operator. The rationale is that it has not function on its own currently, except to spam errors.

The examples from the last comments would look like this:

    value = value else fallback1 else fallback2
    ## do something with value

func example(thing=null):
    thing = thing else ThingType.new()
    ## do something with thing

If you think about it, a else b is probably one of the most natural syntax sugars for a if a else b.

I am going to go forth and reopen this issue, just because it has attracted a lot of comments. This does not mean it would be implemented though, since it is here for discussion.

mwerezak commented 6 years ago

I still agree, but I'd maybe amend the issue title because using logic operators for this would feel a bit ambiguous.

If you think about it, a else b is probably one of the most natural syntax sugars for a if a else b.

Fair enough, and that sounds like a pretty compelling reason for else.

neikeq commented 6 years ago

Now that I think of it, wouldn't else be a bit ambiguous with inline if? e.g.: What does a if a else b else c do?

bojidar-bg commented 6 years ago

@neikeq you are not supposed to use the two together without some parenthesis.

There are many operators which are order-dependent or hard to reason about without parentheses. E.g., is "a" + 2 + 3 going to produce "a23" or "a5"? What about a and b or c?

akien-mga commented 6 years ago

E.g., is "a" + 2 + 3 going to produce "a23" or "a5"? What about a and b or c?

Well the former, I hope it produces a runtime error :) The latter has clear precedence rules defined, so it's (a and b) or c, but in the case of a if b else c else d, the precedence rules are yet to be defined so that the result is predictable.

Here it's more than precedence: it's about what the operators actually are, based on the usage or not of parenthesis. If I replace the a else b construct by null_coal(a, b) and a if cond else b by ternary(cond, a, b), the a if b else c else d construct can be:

ternary(b, a, null_coal(c, d))
ternary(null_coal(b, c), a, d)
ternary(null_coal(b, null_coal(c, d)), a, )
# ^ invalid syntax for ternary() - or is `a if b` supported?
# If it is there are even more possible interpretations with a `test(cond, a)` operator.

Might not be that trivial to implement in the parser, and I'm not sure the added confusion is worth what the feature brings.

malucard commented 6 years ago

a if a else b else c doesn't make much sense. If you were to interpret it as a if (a else b) else c, it would imply the condition is not a bool. Which is possible, but a bool is much more common. The other else is solved by the operator precedence (though I don't know what it will be).

mwerezak commented 6 years ago

There's nothing wrong with a condition not being a bool.

Anyways, yeah it could either be a if (a else b) else c or a if a else (b else c) depending on precedence.

Either way it's not syntactically ambiguous.

malucard commented 6 years ago

I guess I didn't say enough. If the precedence was a if (a else b) else c, wouldn't it be harder to parse? It'd break existing code: a if a else b (normally done) would be interpreted as a if (a else b), which is invalid, unless the parser can backtrack a bit. I said a bool is more common, as one reason the precedence should bind the first else to the if, whether the parser is smart or not.

mwerezak commented 6 years ago

I don't have much to say on which precedence is better. But it can be done either way depending on what the language designers decide.

mwerezak commented 6 years ago

Anyways, since enums were merged with no controversy, I imagine that something like this which adds roughly the same utility to the language (I would argue more, since enums aren't any more readable than const IMO), should not have any issues with being merged if someone can provide a reasonable implementation?

bojidar-bg commented 6 years ago

@mwerezak I wouldn't advise for making new features to the language right now, because they would have to either wait or block the typed gdscript PR (probably). I might help you do it (or do it outright, if you don't want) after it is merged :smiley:.

willnationsdev commented 6 years ago

I would be looking forward to this feature as well, with either the "else" or "??" syntax. Both sound good to me.

akien-mga commented 4 years ago

Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.

The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.

If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!

nathanfranke commented 4 years ago

Superseded by https://github.com/godotengine/godot-proposals/issues/1321