godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.13k stars 69 forks source link

Implement a null-coalescing operator (`??`) #1321

Open nathanfranke opened 4 years ago

nathanfranke commented 4 years ago

Supersedes https://github.com/godotengine/godot/issues/7223 CC @Zylann

Describe the project you are working on: This can work on any project, but currently I want to implement a "Fallback" color if the given color is null.

Describe the problem or limitation you are having in your project: The ternary operator can be used as a fallback, but it is...

Describe the feature / enhancement and how it helps to overcome the problem or limitation: The enhancement would be to implement an operator that simplifies this workflow. See the code example below.

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams: To avoid ambiguity from boolean or, I propose operator else with alternate symbol counterpart ??.

func set_color(color: Color):
    modulate = color else Color.white

If this enhancement will not be used often, can it be worked around with a few lines of script?: This will be used often. Currently, the workaround for slow or non-const function is:

var http_result = http_get()
var result = http_result if http_result else cached_value

Is there a reason why this should be core and not an add-on in the asset library?: It would not be possible to implement this in an add-on so that the code is brief. The only simplification would be a coalescing function like

func coalesce(a, b):
    return a if a else b

Even with the function, b would always be evaluated even if a is valid.

Calinou commented 4 years ago

To avoid ambiguity from boolean or, I propose operator else with alternate symbol counterpart ??.

I would allow only one syntax for this, so we don't have multiple ways to achieve the same thing (due to style guide complexity, among other reasons).

I'm not sure if we should go for else or ?? though.

nathanfranke commented 4 years ago

I'm not sure if we should go for else or ?? though.

I would choose else because we went with if and else for ternary operator.

so we don't have multiple ways to achieve the same thing

What about operators or and ||?

Calinou commented 4 years ago

What about operators or and ||?

|| has been deprecated in the master branch. It still works, but it prints a warning message by default :slightly_smiling_face:

The same goes for &&.

cgbeutler commented 3 years ago

I would advocate for adding ?? as symbols.

The operator ?? is becoming ubiquitous. It has been added to C#, JavaScript, PHP, Powershell, and Swift. There are plans to add it to more languages. While PEP-505 was deferred, I think python may add it eventually. Either way, many users are coming from Unity with C# experience or have had some JavaScript experience. Now, I normally prefer english words, like else, but I would prefer symbols in this case only because I think english words are more confusing. English just doesn't capture the concept very well.

Adding ?? as symbols also makes more sense if also adding the ?. operator. (Using a word like else. would be real confusing.)

I, personally, love both ?? and ?. a lot and would love to see both added. Many godot calls can return null references and godot makes so much null by default. I feel like both are needed.

For example, getting data from a possible child node could be reduced from 6-ish lines to one with both operators in use:

# Current method of getting child data
var tex
var sprite = get_node("Sprite")
if sprite:
    tex = sprite.texture

if not tex:
    tex = load("res://...")
...

#### VS ####

# Null coalesce method
var tex = get_node("Sprite")?.texture ?? load("res://...")
...

Just adding ?? alone doesn't help much in the above case. (Since we are checking for two possible nulls that can come from get_node("Sprite")?.texture. The one from get_node and the one from .texture)

Also, using else would maybe have parse confusion. Case:

var a = a if b else c else d
# Which do you mean?
var a = a if (b else c) else d
var a = a if b else (c else d)

Not saying folks should use that syntax, just that parsing it out seems problematic. Some languages us a x ? x : y shorthand of x ?: y. Doing a similar thing would mean the shorthand in godot should be x if else y, but, yeah, that's kinda ugly...

cgbeutler commented 3 years ago

As a side note, it's nice if you can do multiline so it flows. In the below example, if a line ever results in null then the execution moves to the next line.

var tex_size = \
    get_node("Sprite")?.texture?.get_size() ??
    get_node("Sprite_2")?.texture?.get_size() ??
    Vector2.ZERO

Having it flow like that makes it real easy to read and track. Pretty sure this may require ?? \ in godot, though.

Calinou commented 3 years ago

Pretty sure this may require ?? \ in godot, though.

You can also use parentheses to wrap long statements without requiring \ at the end of every line:


var tex_size = (
    get_node("Sprite")?.texture?.get_size() ??
    get_node("Sprite_2")?.texture?.get_size() ??
    Vector2.ZERO
)
cgbeutler commented 3 years ago

You can also use parentheses

Oh, yeah, haha. That works.

cgbeutler commented 3 years ago

Side note for future implementer: if adding ?. for function calling like $Sprite?.position as a part of this proposal, we may want to match other languages and also add a ?[] operator for the case where the lhs is null. Operator ?[] is fairly ubiquitous when supporting ?., as it is kinda shorthand for a member function. Example: nullable_dict_ref?["blah"]

nathanfranke commented 3 years ago

Side note for future implementer: if adding ?. for function calling like $Sprite?.position as a part of this proposal, we may want to match other languages and also add a ?[] operator for the case where the lhs is null. Operator ?[] is fairly ubiquitous when supporting ?., as it is kinda shorthand for a member function. Example: nullable_dict_ref?["blah"]

$Sprite?.position The left hand side is still an expression just like value?.position, so this will be supported

I agree that ?[] should be supported. Please, let it not be ?.[] though, :vomiting_face:

nathanfranke commented 3 years ago

Adding ?? as symbols also makes more sense if also adding the ?. operator. (Using a word like else. would be real confusing.)

This is being discussed at https://github.com/godotengine/godot-proposals/issues/1902

ghost commented 3 years ago

Hey guys I was thinking about this right now and I have a slighty different solution. Vnen suggested I should provide my solution in this post.

It occured to me we could modify the ternary operator to behave like a nullish coalescing operator when it has no else:

"default" if not a

Would return "default" if a is == null otherwise returns a so is equivalent to a if a else "default"

The missing else already exists as an error check so you could re-purpose and use it to switch ternary behavior to a nullish coalescing behavior. You could also make the not mandatory for the coallescing behavior in addition to the missing else. Also if not actually functions like an else in English. b if not a has the same meaning as a else b.

cgbeutler commented 3 years ago

Godot has so many null things that I, personally, would like to see more than just null coalesce. There's the coalescing dot operator, the coalescing accessor, etc. The proposals that use words I feel lack foresight for those other operators and offer no more clarity than the null operators that are becoming ubiquitous already.

Also, default if not a kinda breaks the rules of how the not operator works. You are essentially inverting 'a' to a boolean, then getting 'a' from nowhere? I get that this is proposed as a special case or something, but it'd be nice if the operators and rules were still consistent.

goblinJoel commented 2 years ago

It sounds like this operator would only return the right value if the left value was null. If the left value was false, Color(0, 0, 0, 1), Vector2(0, 0), or (if my Python-y assumptions are right) [], 0, or "", then the left value would be returned.

Because else usually cares about falsiness, not just null, I favor the ?? syntax over just else. That way, else doesn't treat false and other falsy values differently in this situation than it does in an if-else.

EDIT: Other possible symbols could be \\ (Elixir) or // (Perl), though I think ?? is just fine and less likely to get confused with division or comments.

joao-pedro-braz commented 1 year ago

This might be closed if the discussion over at: https://github.com/godotengine/godot-proposals/issues/162 gets resolved.

adamscott commented 1 year ago

Prefer the plain English versions of boolean operators, as they are the most accessible

Is ?? non accessible to the non initiated? Would a null? operator be preferred?

Maybe ?? could be an alias.

BtheDestroyer commented 1 year ago

Is ?? non accessible to the non initiated? Would a null? operator be preferred?

I think just or would be clear enough (hence #7255); that's what Lua does. An issue could be situations which previously automatically cast both sides to bools, though, eg:

var a = null
var b := 5
# Previously would be `true` with `c` being a `bool`
# Would suddenly become `5` with `c` being `int`
var c := a or b
maxatwork commented 11 months ago

In my opinion, naming the operator or is suboptimal. The issue lies in its ambiguity, as it shares the same name with the boolean or operator, despite serving a different purpose.

var a = null ?? 42 # expected 42, because left is null
var b = 0 ?? 42 # expected 0,  because left is non-null

# BUT:
var c = 0 or 42 # expected 42 because left is falsy

While the or operator's logic might seem functional on the surface, it can lead to unexpected behavior, particularly in larger codebases. Consider a common scenario when dealing with user input, which can be null when empty and should default to a value greater than 0:

const default_value = 42
var user_entered_number = 0 # null if nothing entered, or parsed number if there's something entered

# ?? operator
var value = user_entered_number ?? default_value # 0 (user entered value)

# or operator
var value = user_entered_number or default_value # expected to be 0 (user entered value) but it's 42 (default_value)

In this scenario, a stricter null check is necessary to ensure correct handling of null values. Using the or operator logic can lead to unexpected results and potential issues due to its handling of various falsy values. Thus, choosing the right operator name is crucial to avoid confusion and pitfalls in code, especially in complex projects. In the JavaScript world, a similar problem was resolved by introducing the ?? operator for handling nullish values.

While the exact naming is not critical, I believe it should not be named or.

Calinou commented 11 months ago

orelse is a keyword I've considered for this purpose, but the likelihood of confusing it with or (which essentially acts like the Elvis operator) is too high.

maxatwork commented 11 months ago

I personally prefer ?? because it aligns with C#, JS, Swift, and Kotlin, making it a common choice. However, if there's a requirement for the operator to be a word, default could also be a suitable option:

var value = user_input ?? 42; # using ??
# or
var value = user_input default 42; # using 'default'
goblinJoel commented 11 months ago

I quite like the suggestion of default as the operator. This fits with the preference in the docs (at least for v4) for words for logical operators, and I don't think it's used as a GDScript keyword anywhere else already, is it? However, I'm also fine with ??, especially if you want multiple new operators for null-related things. It's not in any languages I personally use, but it's not hard to memorize.

cgbeutler commented 11 months ago

I don't think folks are reading the comments and the issue hasn't been updated to reflect them.

Using an operator makes more sense than a word, as more should be supported than just ??.

The operators . and [ ] should be expanded with ?. and ?[ ], respectfully, as a part of this feature.

Since both . and [ ] are symbol-based operators to begin with, it makes more sense to use existing language conventions that mirror those rather than invent new ones for just this one niche scripting language.

Also, inventing new conventions means re-teaching everyone. Using existing conventions means new folks can be sent to preexisting guides that happen to fit.

For the love of Hal9000... please don't re-invent this wheel.

CamBrown00 commented 10 months ago

^ what he said

baiyanlali commented 5 months ago

I propose considering the null-coalescing operator purely as syntactic sugar. It should maintain backward compatibility, so using "or" as the operator is not a good idea.

A novel symbol could be beneficial for clarity. I will use "elthen" in this case.

And it should be very easy to implement. So I think it can convert into a ternary operator in gdscript parsing procedure.

For example:

var result = happy elthen "sad"

will turn into

var result = happy if happy else "sad"

Such an addition should pose no issues. Aas an optional operator, it enhances readability and understanding.

baiyanlali commented 5 months ago

I don't think folks are reading the comments and the issue hasn't been updated to reflect them.

Using an operator makes more sense than a word, as more should be supported than just ??.

The operators . and [ ] should be expanded with ?. and ?[ ], respectfully, as a part of this feature.

Since both . and [ ] are symbol-based operators to begin with, it makes more sense to use existing language conventions that mirror those rather than invent new ones for just this one niche scripting language.

Also, inventing new conventions means re-teaching everyone. Using existing conventions means new folks can be sent to preexisting guides that happen to fit.

For the love of Hal9000... please don't re-invent this wheel.

Rolling out a pipeline operator is definitely more useful than just ?. or ?[]. Plus, it works super well with the null coalescing operator. Check out the JavaScript proposal for the pipeline operator for more info: https://tc39.es/proposal-pipeline-operator/ I'm proposing we use then instead of |> and prev to replace $ for the pipeline symbol. Also, swapping in elthen for ?? as our null coalescing operator.

Like, take C# for example, where you might access something with:

var answer = Hitchhiker?.galaxy?.answer ?? 42

With a pipeline function, it would be:

var answer = Hitchhiker then prev.galaxy then prev.answer elthen 42

Pipe operator can do even more:

var answer = await get_answer_from_website() then prev.parse_json() then prev.answer elthen 42

Pipeline operator is more versatile than ?. and ?[], and they're not that complex, syntax-wise.

theDapperRaven commented 2 months ago

So if I understand correctly, "or" cant be option because of backward compatibility? That sucks. "or" (to me) is the easiest to read. Although, "else" looks nice too.

What about "otherwise?"

I suppose ?? will work, but if you don't come from a language that uses it, you will need to look it up in the docs. With "or" or "else" or "otherwise", you can at least guess what they do.

Ferk commented 2 months ago

With "or" or "else" or "otherwise", you can at least guess what they do.

You'll likely guess wrong if you mistake it for the or used in boolean evaluations.

false or true as a boolean operator will return true, but false ?? true should return false, because false is non-null.

This can trip people off, it's better to use ?? precisely because that way it's less likely for people not familiar with the operator to make the wrong assumptions. If they don't want to look it up in the docs, then better not use that operator, that would be better than misinterpreting what it does and then get confused when things don't work as they expected.

theDapperRaven commented 2 months ago

You'll likely guess wrong if you mistake it for the or used in boolean evaluations.

If they don't want to look it up in the docs, then better not use that operator, that would be better than misinterpreting what it does and then get confused when things don't work as they expected.

Hmmm. That's true. So I suppose I will just recommend otherwise and else

it's better to use ?? precisely because that way it's less likely for people not familiar with the operator to make the wrong assumptions

Going off of that, I think otherwise rather than else would be preferable, since else already has a meaning. If I saw it and didn't know what it meant I would think "otherwise is basically another word for else, but it isn't the same word, so it must do something different."

In conclusion: I think ?? works, but you only have other languages and context to go off of. Like I said in my previous comment. I prefer otherwise because you have the word and its context to go off of.

cgbeutler commented 2 months ago

@nathanfranke It may help to change the title to "Implement Null-coalescing Operators ??, .?, and ?[]"

That may help with the comments? I don't expect everyone to read the whole comment thread, but anything we could do to help stem how often it repeats itself would be nice. (Though I suspect some comments are just bumps in disguise, which can't be stopped.)

geekley commented 2 months ago

Yes please use the symbols rather than words. And it's ?., not .?.

Regarding ?. operator, note that you need to implement the precedence right, so it can be combined with . like in JS and C#, and you can be explicit about where there is actual nullability. This is very important to get right, in case nullability type checking is added in the future (e.g. Array[Vector2?]?).

I believe I've seen a language (can't remember which) not allow a . after a ?. because of a bad simplistic left-to-right precedence. You were forced to use ?. in the entire chain to not get error:

a?.b.c # would be a nullability error with left-to-right precedence
(a?.b).c # added parenthesis shows BAD precedence implementation! .c MUST NOT run if a is null!
a?.b?.c # you would be forced to do this, checking null twice

The correct way is that . has MORE precedence than ?. (even though left side evaluates first).

a?.b.c # valid syntax; a is nullable, but b and c are not nullable
a(?.b.c) # added fake parenthesis to explain CORRECT precedence for the above
a.b.c if a != null else a # meaning

Regarding ?.[] operator in JS, I think it's to avoid conflicts with the ternary a ? b : c operator and array constructor [a, b] syntax? Shouldn't be a problem in GDScript which uses words for ternary operator, so it can be ?[] like in C#.

Same goes for the null-safe call (wasn't mentioned here?) which can be just ?() instead of JS ?.() or C# ?.Invoke(). EDIT: GDScript already requires .call() for Callable, so this would be ?.call(). No new syntax here.


So IMO null-safe operators should be:

bbb651 commented 2 months ago

Note that a?[i] and f?(a, b, etc) differ from JavaScript where it's a?.[i] and f?.(a, b, etc), which is not necessarily a bad thing but this means ? might conflict with #162 when combined with is/as:

# Calling foo or syntax error after cast
if foo as Bar?():
    pass

# Indexing into `Bar` or syntax error after cast
if foo as Bar?[2]:
    pass

# Typed array of `Bar?` or invalid indexing into a boolean
if foo is Bar?[]:
    pass

Typescript completely forbids ? inline and instead makes you type T | undefined but that doesn't make sense for GDScript in terms of complexity of the type system, maybe forcing parentheses and precedence in certain places can solve this but it seems a bit risky in terms of syntax for future syntax additions...

geekley commented 2 months ago

Typed array of Bar? or invalid indexing into a boolean

@bbb651 Remember, typed array syntax in GDScript is the "generics" syntax Array[T] not the objectively worse T[].

maybe forcing parentheses and precedence in certain places can solve this but it seems a bit risky in terms of syntax for future syntax additions...

Well, in is/as case, you already have to use parentheses, I believe, in most cases where you add something after it. For example . member access (the thing you'd use the most, arguably) x as Object.get_script() would make it think Object.get_script is a inner type. So you already need to do (x as Object).get_script(). Side note: Honestly, I'd prefer is/as syntax had been designed like x as[T] or similar but oh well, it is what it is now and I often have to go back to add parenthesis.

Regarding a?[i], it doesn't need to conflict with Array[T] syntax because even with nullability, you would not need do Array?[T?] but instead Array[T?]?. So you could have e.g. x as Array[T?]? ?[0] though I'd still prefer parenthesis for legibility (x as Array[T?]?)?[0].

Regarding f?(a, b, etc) syntax, I've checked now and to my surprise GDScript already forbids calling expressions, like Callable variables using variable() syntax. It already forces a .call(), so I guess there's no need to introduce this one, as it would be ?.call(). Honestly I actually prefer x.call() instead of direct x(). Or at least I would if it meant we get the right signature completion, but that feature would likely require implementing an evolution like Callable[return: T, param1: T1, param2: T2] or whatever.