godotengine / godot-proposals

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

Add support for nullable static types in GDScript #162

Open aaronfranke opened 4 years ago

aaronfranke commented 4 years ago

Describe the project you are working on: This applies to many projects. This is an offshoot from https://github.com/godotengine/godot-proposals/issues/737, example use cases and other discussions are welcome.

Describe the problem or limitation you are having in your project:

Let's say you have a method that accepts a 2D position, which would look something like this:

func whatever(vec):

A problem with this is that there's no type safety, so the function could unexpectedly break if the passed-in value is not a Vector2. One option is to use static typing:

func whatever(vec: Vector2):

This works, and now it's not possible for users to, for example, pass in a Color or any other type that's invalid for this method. However, now you can't pass in null to mean N/A or similar.

Describe how this feature / enhancement will help you overcome this problem or limitation:

If GDScript's static typing system allowed specifying nullable types, we would be able to restrict the type to either a valid value or null. The presence of a valid value can then be detected simply by checking if it is not null, as non-null nullable typed values must be valid values.

Show a mock up screenshots/video or a flow diagram explaining how your proposal will work:

My suggestion is to simply allow this by adding a question mark after the type name, which is the same syntax used in C#, Kotlin, and TypeScript. User code could look something like this:

func whatever(vec: Vector2?):

Describe implementation detail for your proposal (in code), if possible:

Aside from the above, I don't have any specific ideas on how it would be implemented.

However, I will add that we could expand this idea for engine methods. Many parts of Godot accept a specific type or null to mean invalid or N/A, or return a specific type or null when there is nothing else to return. For example, Plane's intersect methods return a Vector3 if an intersection was found, or null if no intersection was found. Nullable static typing could essentially self-document such methods by showing that the return type is Vector3? instead of Vector3.

If this enhancement will not be used often, can it be worked around with a few lines of script?: The only option is to not use static typing if you need the variable to be nullable.

Is there a reason why this should be core and not an add-on in the asset library?: Yes, because it would be part of GDScript.

PoisonousGame commented 3 years ago

GDScript is a dynamic and static language, which makes it not too restrictive and too many rules, and the language is kept simple. People tend to turn gdscript into a completely static language, which makes it complicated by introducing too much static language syntax.

ShalokShalom commented 3 years ago

@244164401 What's missing is complete type inference, then this could be a zero issue.

DatBrute commented 3 years ago

Static typing is required for exports, so I can't use any nullable variable in the editor. I have tried using "-1" to represent null instead, but with float values that is very inconsistent.

theraot commented 3 years ago

I suggest the following:

First of all, object types would remain nullable as they are.

For every non-object type there would be two kinds: The type, not nullable. And the type, nullable. The nullable kind, is - as far as interpreter, virtual machine, opcodes, and so on - actually a Variant. The difference is in the editor and the language server. They would care about the type and would raise a warnings about them. In particular when there is a variable or parameter annotated as nullable, and I try to pass or set something that is not null and is not of the annotated type (nullable or not).

That leaves the question: what happens when I try to assign a nullable to a not nullable? Well, what happens today with Variant? That happens.

Thus, since the interpreter, virtual machine, and opcodes, only see Variant. There will be no special opcodes inserted, and no existing opcodes would not have to be modified. Because, again, they would be the opcodes used with a Variant variable.


There are a few possible criticisms of this approach I want to address:


On the topic possible future improvements:

If you also make it a warning when the null is not handled explicitly, Godot would also know - statically, as in without running the code - where the type is not null (because it was checked before using it, avoiding the warning). If that information can be used to avoid any checks that Godot might be inserting, much better. In particular for that case of assigning a nullable to a not nullable. And yes, how much effort is put on the static analysis is a line drawn in the sand, which could be improved after the feature is introduced.

Also, those warnings could turn into errors when running in the debugger. Once strong static checking is in place, perhaps further optimizations would be possible. That could be by creating an option type and have it inserted by the compiler/interpreter (so that they are not Variant, but that wrapper type), or by some other means.

Ah, and editor inspector support. Although, export variable typing is lackluster. See #18. There is interest in nullable export variables.

I also want to point out that union types can be implemented on the same basis. It would be similar to TypeScript, which has a very powerful type system, which disappears before the code reaches execution. A kind of type erasure. Perhaps not the best kind, but useful nonetheless.

timothyqiu commented 3 years ago

I think using func foo(vec: Vector2) as an example here mixed of two different aspects of the issue:

  1. Types like Vector2, String and Dictionary are not Object derived, they are built-in types like an int is
    • this is often surprising for new users, as these types would be Object derived in most other languages
  2. Types are not nullable except for Object derived ones
    • you can't pass null to func foo(n: int), most language won't allow it
    • if you think null as an Object, then it's obvious that null won't be allowed for a parameter marked as Vector2
      • but again, it's surprising for new users

I think the optional type with question mark approach would be an overkill if you don't ever expect a variable to be able to take both int and null. Type hint union #737 would be better as it's lightweight and flexible.

aaronfranke commented 3 years ago

as these types would be Object derived in most other languages

In Godot in both C++ and C#, Vector2 is a struct type allocated on the stack and doesn't have anything to do with Object.

would be an overkill if you don't ever expect a variable to be able to take both int and null

That's literally the point though. int? should be nullable int, a type that can be either int or null.

timothyqiu commented 3 years ago

C++ and C# are the exceptionals from certain point of view. But yeah, I don't mean that these types should be changed to be object derived.

I mean, the need for int? is not the same as the need for Vector2?.

If the Vector2? you presented in OP is just an convenient way of writing Vector2 | null, it won't matter. But a proper optional type system as described in later discussions has a larger impact on the language than union type hints I think.

t3nk3y commented 3 years ago

I'm surprised to see this is a debate. Having come from Lua and JS (I mean recently, I first started on C/C++), this whole Nil or, possibly, undefined thing seems fairly basic for a scripting language.

I've got a Node, which has exports, and I'm trying to make it so that it can have parents that share similarly names exports, and those can have parents with similarly names exports. I then want the most junior node that has an export value set, to be used. This means I need to be able to check each level of this tree from junior to senior to see if the exports are set, and return the first non-empty.

So I think, oh, simple, I'll just make a generically typed function to dig through the tree, and return the first non-empty one. Except I've got multiple types exports, and some support null, and some are floats and can be negative, and some can't be negative, making the whole concept of marking empty floats as -1 a problem, etc. So instead of just saying: if var is Nil: I'm stuck checking the type of each, and running custom logic for each type, and doing dirty things with variables that I bet I will regret way down the road.

I do feel Godot needs a core method for differentiating between an unset variable vs a 0/false/"empty string" on a Class(at least with export's)

t3nk3y commented 3 years ago

I also wanted to point out, as I forgot to, in the last comment. While I do think Godot need this nillable concept, I also understand it's a complex issue to solve at this point, and feel bad for not having an actual solution.

But I also wanted to say, to anyone else running in to the need to consider a float as empty. There is a constant NAN that will make your day.

Lucrecious commented 3 years ago

@t3nk3y No, I think it would be a really bad idea to add logic that allows exports to be in an "unset" state. Currently, differentiating between "set" and "unset" variables doesn't really make sense anyways. In GDScript (and most other scripting languages), all variables are already always set; whether it's an object or a value type, the export variable always has a default value (null, 0, "", Vector2(0, 0, 0), etc) and this is good because it's expected behaviour for variables to have a default value and prevents lots of very annoying bugs.

If you want to do what you're trying to do with the export variable stuff, you need to take a different approach. For example, if you want to check if a "string" export called my_export has been set, you can export another variable, a bool, say,is_my_export_set, and you can check it to signify that my_export has been set. It's a little more work but it's far more clear than what you're currently doing I think.

There are other approaches you can take to do what you want to do. The above example is if you want to use export variables in the way you described, but I recommend looking into a totally different approach. Using export variables how you're using them is pretty unconventional. If it works, it works! But clearly, the approach you're taking isn't good if you're admitting to writing a ton of messy code for your things to work.

theraot commented 3 years ago

@Lucrecious So, if I want to export a String, which could be set or not, I would add bool.

Please draw the line where this becomes a bad idea for you:

I don't think anybody is arguing for allowing all exports in an unset state. Instead, you would be able to explicitly type variables (including exports) as nullable (Edit: or not, well, with the caveat that types that derive from Object are already nullable).


I know not everybody comes from this angle. Personally I prefer to think that String? is shorthand for String|null. And that implementing as a Maybe<String> on the C++ side is a detail behind the scenes that could be done as optimization if necessary.

Lucrecious commented 3 years ago

I like having nullable types, I think that's fine.

But that wasn't what I was addressing from your previous comment, I was addressing this:

I do feel Godot needs a core method for differentiating between an unset variable vs a 0/false/"empty string" on a Class(at least with export's)

Maybe I read that wrong but it sound like you were looking for a "function" that would return true or false if a variable was set or not.

My suggestion for your problem is to take a different approach, and one of them, was to have an extra export variable for value types that indicates whether that export variable is set.

Like literally:

export(int) var my_value
export(bool) var is_my_value_set := false

So instead of checking my_value to see if it's set, you check if is_my_value_set is true.

That being said, to solve the problem you want to solve, I wouldn't use export variables in this way and instead would look for a different solution that doesn't require so much "dirty" logic.

ShalokShalom commented 3 years ago

Ah actually, I have an idea on a way that this could be done, but it's up to @vnen to decide anyway.

In the bytecode, when you use a nullable type for anything, we can emit an extra instruction in the bytecode to check whether its null. This would solve the performance and complexity.problems.

So, what is with this approach?

Lucrecious commented 3 years ago

He's talking about how the interpreter would handle GDScript nullable types.

ShalokShalom commented 3 years ago

Yeah. Are we doing it?

vnen commented 3 years ago

I actually have an idea on how to implement this, but won't be done for 4.0, so I didn't really give it much thought yet. When I have some time I'll try to flesh out my proposal for this.

t3nk3y commented 3 years ago

But that wasn't what I was addressing from your previous comment, I was addressing this:

I do feel Godot needs a core method for differentiating between an unset variable vs a 0/false/"empty string" on a Class(at least with export's)

You were replying to @theraot but that was my comment, not his, might be leading to some of the confusion here. Heh.

the approach you're taking isn't good if you're admitting to writing a ton of messy code for your things to work

No, the point is that if variables had a set/unset state, in general, then I wouldn't have to write a ton of messy code. I WANT to go in to why I'm doing it the way I am, but I don't think that appropriately adds to the discussion at hand.

In GDScript (and most other scripting languages), all variables are already always set

Lua: I can set anything nil, and if that isn't good enough, since everything is in a table, I can always check to see if the key exists. And if not, I can I add it at runtime

PHP: I can always check if a variable is set, with isset, and can always set a variable at runtime

JavaScript: Undefined variables typeof is 'undefined' and I can always set a variable at runtime

Python: I never really liked Py, and don't have enough experience to just say, it does this. But google searches seem to suggest pretty heavily it's quite doable. That said, I'm on the "Python is not a scripting language" camp.

In Summary: I think that accounts for the majority of scripting languages that are heavily used today. And touching on Py at the end, might account for where we are running in to issues. The name GDScript heavily implies that it's a scripting language. For me, the major advantage of scripting languages is that they are heavily mutable at runtime. But I see a lot of immutable concepts that are designed for compiled languages being brought up in the design discussion here.

I understand that performance, and memory efficiency are of a very large concern here(which is good), and I'm sure that accounts for a lot of this push for strong typing, and hard and fast rules(like variables must always have a value), but there is a threshold at which you have to take a step back, and ask "Why am I using a scripting language, instead of just doing this in a C language?". I really think the goal should be to keep GDScript holding on to as many of the advantages of a scripting language as possible, without giving up too much when it comes to performance and efficiency.

The choice that has been provided with Godot, giving access to low level code, higher level code, scripting code, and even visual code, is a blessing, please keep that choice in mind during design discussions.

Lucrecious commented 3 years ago

@t3nk3y The names are similar too haha

Anyways

You can define any variable as null in Godot too, you just don't get the type hint which is why this issue is open. GDScript is first and foremost dynamically typed with the option for typing as I understand.

As for those languages you mentioned:

Remember, you can already do this:

var x = 0
x = null

The reason you can't with exports is because those are tied to the UI, and I'm guessing the Godot developers were not thinking of optional typing from primitive exports at the time, or that setting an integer export to null was something people would like to do very frequently.

t3nk3y commented 3 years ago

Okay, those are very good points. While this does directly effect exported vars due to the auto-typing that's going on, that isn't representative of all vars. I think part of my problem is that I'm also forgetting what does and does not work due to running in to bugs in 4.0 around similar things, and jumbling them up with this issue.

And when it comes to feeding functions, and being able to pass arguments with unset states, that's essentially there, unless the argument has had a type specified. At which point, I'm still on the side of having the ability to pass an unset state. But I've also gotten myself used to not even trusting the compiler to protect me from unexpected values coming through, so I tend to think that if I expect a Vector3, I better plan ahead for getting a null, or maybe something else entirely.

But that's just me, and my preference here, and we need to take in to account those who wish to have the type safety being tested ahead of time.

Let me ask this. What is a "hint" in GDScript. Because for some reason, I got it in my head that these, are hints:

var this_is_a_hinted_int: int

func a_func_with_a_hint(val: int):

So the ":type", is that actually a hint in GDScript? Or is that a straight up type specifier? The docs don't seem to refer to that as a hint.

To me, a hint would say: "I expect this, but, no guarantees, just treat it as if it were an int, okay?" See, I like the idea of being able to tell the editor that I'm expecting a thing, and I want it to do syntax completion, errors, and warning based on that. But I don't need it to act as a safety net for me.

For the most part. What I would like out of "hints" is just something that is basically telling my editor what the parameters are, and what they are expected to be, so when I call the function or reference the object later, it tells me what goes there. Or maybe lets me know if I made a really obvious error, like passing a variable the is def the wrong type. I don't want it forcing me in to anything.

ShalokShalom commented 3 years ago

I love to add a few points:

GDScript is performant enough. It was already, before the types added performance and it will be also in the future, including in completely dynamically typed code bases.

The heavy lifting is anyway done by the libraries and the glue code can not become as slow, that its noticable.

Its a non-issue, from my perspective.

The point two is, that I love to be 'that guy' who notices pedanticly, that Python compiles to bytecode, just as Java and both interpret some intermediate code.

So, just saying: Truly 'interpret' languages dont exist anymore, they got run purely on the syntactic level of the language itself, like Ruby until 1.8/1.9.

To come to the topic:

I think this flies under the radar: https://blog.logrocket.com/what-is-railway-oriented-programming/

Once we can use true Result monads, is it also possible to employ that way of error handling, and the whole fsharp community chose that to be their convention, to handle errors.

And when you look at other languages, they often struggle to employ one such way, since they are all .. suboptimal in a lot of cases.

So I think that alone is this worth it.

P.S: @vnen I see another feature for Godot 4, and that is 'exhaustive checks' on the match statement, if that is possible.

I have only seen it in true pattern matching, and that is probably not possible, while I guess I can just ask you anyway.

I just see that the match statement differentiates relatively little from if then else and it could increase code clarity in my opinion and also substantially add to the Godot 4 experience, for me personally at least.

When you consider that, hit me with an ok and I open an issue.

Thanks

vnen commented 3 years ago

@t3nk3y "type hints" got popular in the community but I avoid calling them so (especially on documentation). They are not just hints, they are indeed type specifiers.


@ShalokShalom if you have something in mind just open another proposal. You don't need my permission to open one, and it's much easier to discuss the feature if we have a proper place to do so.

nathanfranke commented 2 years ago

The Kotlin style ?. Syntax is very nice to work with, but it does lead you down a rabbit hole of dealing with nulls that new people might find wired. ?.let {} in Kotlin, gaurd in Swift.

If you are going to do strict null typing as part of the type system that is.

I'm mostly just lurking here and following along, but the point about nullible types requiring a bunch of extra code from the user can be mitigated quite a bit with things like the null coalescing operator foo.?bar() # only calls bar if foo isn't null

That would be a really nice operator to have even if we don't go the full nullable types route.

Optional chaining would be great too. pos?.x or get_pos?.()?.x

For future reference, https://github.com/godotengine/godot-proposals/issues/1321 and https://github.com/godotengine/godot-proposals/issues/1902 are for null coalescing and safe navigation, respectively.

Calinou commented 2 years ago

We discussed this on a proposal meeting. This proposal has a lot of community support, and we agreed that it would be good to have this feature in a future Godot release (not necessarily 4.0). However, the specifics and internals need to be further discussed, including performance implications.

Ferk commented 2 years ago

In addition to the 2 advantages of type hints that @dsnopek mentioned...

  1. Allowing the VM to optimize performance based on the types (not done in Godot 3.x, but planned for Godot 4.x)
  2. Allowing the developer to depend on type safety so that you can write code with the assumption that a particular method will > always return a certain type, or that your method will be called with parameters of a certain type, to prevent unexpected > crashes.

I'd like to add:

  1. Introspection-based code completion.

One big reason why I enjoy using type hints in my tests with Godot 4.0 much more is how this gives the IDE the chance to be a bit smarter when offering helpers for a variable that holds a complex type. The methods and properties for the type that is hinted get autocompleted.

This actually helps with the idea of making GDScript an "easy to use language".

Personally, I would not mind if nullables are actually as slow as non-typed code, because my main interest when using type hints is actually making things easier for myself when I code. In my experience I find that often it's harder to write good code when you have no compile-time checks that can assist you when you make silly mistakes.

As long as the optimizations for non-nullables stay the same (and they continue being the default), adding the possibility of having nullable type hints that give the same output from the compilation as if they did not have type hints (but performed compile-time checks that resulted in error if there's an assigment with the wrong type in the code) would work for me. After all, the only existing alternative right now is not using type hints for those variables, which means for the VM both cases would be a Variant and the performance in both cases would be the same, right?

Of course, if the hinting can be used to optimize further, then that would be great... but already the worst case scenario of not having any additional optimization at all for nullables (and just continue using Variant in those cases) wouldn't be any less performant than the current approach. Or am I wrong?

TV4Fun commented 2 years ago

Have you guys seriously been arguing about this for 2 1/2 years? It's not hard, null references are exceedingly useful for many cases where other solutions would add needless complication. The initial example was passing a Vector2, and you suggested to just default to Vector2.ZERO and check for that, but that presumes that Vector2.ZERO is not itself a valid input. I could make my own subtype with a is_null member, or add an extra boolean argument, or create some special null_vector subtype, but all of those add a lot of complication to a language that is specifically there for people who care more about implementing something quickly than about how fast it is, as if they were really that concerned about performance, they'd be using C++ or C# or something else that trades ease of use for better performance.

In my case, I'm implementing a dungeon with each room being a separate object and having references to adjoining rooms. Since the dungeon is not infinite, what would make the most sense would be to have rooms on the edges have those references set to null. Would you like me to create a special NO_ROOM constant just because or implement a special subtype for each type of edge or corner?

Seriously, don't overthink it, just pick a nullable syntax and implement it. If you're worried about static type safety, then implement built-in runtime checks on nullable types, or require null-checks or null-coalescing syntax like C#, Java, TypeScript, and many other languages already do.

aaronfranke commented 2 years ago

@TV4Fun To be clear, there is a way to have null values currently: Just don't use the static types. I think this is a better solution than adding a special constant or a subtype or a boolean.

TV4Fun commented 2 years ago

@aaronfranke dynamic typing is its own giant can of worms though. It would be really nice to be able to have a nullable variable without having to completely throw away all type safety. If you're that worried about runtime code safety, I guarantee throwing away static types is not going to improve it.

aaronfranke commented 2 years ago

@TV4Fun Yes, that's why I opened this proposal.

filipworksdev commented 2 years ago

How about adding ? in place of : or -> when typing

var a ? int = 1 # can be set to int or null

func test(a ? int = null) ? int # can return int or null
   return a 

? is used in a number of languages for nullable types and would be recognizeable

nathanfranke commented 2 years ago

How about adding ? in place of : or -> when typing

var a ? int = 1 # can be set to int or null

func test() ? int # can return int or null
   return 1

Not a fan. The type itself is nullable, we shouldn't be making the GDScript syntax any more complicated than it needs to be.

filipworksdev commented 2 years ago

The other alternative would be using ?int ?string or int? string? etc I would make the claim is actually simpler to do a ? int vs a : ?int or a :? int or a : int? etc

@nathanfranke
Or how about adding @nullable tag? Which cannot work for parameters.

nathanfranke commented 2 years ago

See #737 for a related proposal. The bread and butter would be var v: int | null which can be further simplified to something like var v: int?. It is unlikely that we will have an annotation for this since the nullability is part of the type and not the variable/parameter/etc.. I also suggest seeing how Kotlin and TypeScript handle things as I (opinion) find them pleasures to work with, especially when it comes to the static types.

Edit: Note that int | null and int? are not decided on, just an example from the languages I'm familiar with. Looks like in #737 there is a discussion of using tagged unions or related (with an entirely different syntax)

filipworksdev commented 2 years ago

You said "we shouldn't be making the GDScript syntax any more complicated than it needs to be" then provided int | null haha. Anyway jokes aside yes you are right in that nullable types would technically be part of the type. Does that mean we need nullable types int_null float_null aka int? or float?. I don't think this proposal may happen any time soon or even at all if we need alternative nullable types for every Godot type. Maybe has to happen in a completely different way.

I think nullable types could be just generic type which has an internally locked type but is not stored typed. This would be similar to how Godot 3 GDScript 1 worked . All variables were typed but were not stored typed (that is they didn't have a specific opcode all they had is the generic var opcode that was infered) This would negate speed advantage for nullable types at the expense of flexibility. This is more do-able bringing old functionality from GDScript 1 back as nullable rather than adding alternative null type opcodes for every existing Godot type. Nullable types should just be a special generic variable.

nathanfranke commented 2 years ago

Implementing var v ?=, v ? int for parameters, and ?> int for function returns is a lot more complicated from a language parser standpoint. That isn't even considering the future implications, e.g. type unions, type aliases, etc.

filipworksdev commented 2 years ago

Is not necessarily easy to do any of the proposed methods. The issue for me is what is the most elegant solution: not too difficult to implement and easy for users to read an understand.

For example var x ? 1 all it would need is changing this logic

https://github.com/godotengine/godot/blob/4c14bf74820a780d11c786cff8b935b2c64bf3bf/modules/gdscript/gdscript_parser.cpp#L852-L854

Right now it only checks for colon. You would have to trigger that entire block both for : and ? adding a new behavior for ? to be nullable in addition to the chosen type. You could literally add a nullable opcode modifier in front of the typed opcode and avoid creating an entirely new null_int type that has to be added everywhere in the sourcecode.

? is not used anywhere so it doesn't confict with anything.

I find this much harder to read var x : ?int or even :? or any other variation. Though I don't know if I like ?> for functiosn and I would prefer re-using ? for function as well in place of ->. Also the ? adds the advantage of allowing for var ?= 1 which ?int does not. You will STILL have to implement ?= which is easier to read but this would be closer for your method :?= which is much more confusing to read in order to match var : ?int. And I have no idea what := would be for var : int? maybe :=? . I personally still prefer my own method var ? int = null Is convenient to both write and understand.

Either way seems ? is winning out for most people.

There are other suggestions as well which can be considered. Or perhaps a completely novel and interesting way that we haven't thought about yet.

In conclusion I would prefer a way to modify and reuse the current Godot types using syntax trickery over creating an entirely new set of nullable types for every current Godot type.

filipworksdev commented 2 years ago

In fact vnen's solution is probably what I am thinking of. Make everything nullable by default and optionally make it non nullable using ! I was thinking exactly in reverse make everything nullable but by default cannot be null unless you use ? when you type it.

TV4Fun commented 2 years ago

Making everything nullable by default would break a lot of backward compatibility though.

On Thu, Jun 30, 2022, 11:05 PM Filip @.***> wrote:

In fact vnen's solution is probably what I am thinking of. Make everything nullable and optionally make it non nullable using ! I was thinking exactly in reverse make everything nullable but by default cannot be null unless you use ? when you type it.

— Reply to this email directly, view it on GitHub https://github.com/godotengine/godot-proposals/issues/162#issuecomment-1171970707, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABD4ECCCRM24CMPPLC4B5XTVR2DBFANCNFSM4JBFCHMA . You are receiving this because you were mentioned.Message ID: @.***>

nathanfranke commented 2 years ago

In fact vnen's solution is probably what I am thinking of. Make everything nullable by default and optionally make it non nullable using ! I was thinking exactly in reverse make everything nullable but by default cannot be null unless you use ? when you type it.

See the reactions and aaronfranke's response as to why this is undesirable.

Edit: I want to stop making noise so I am editing this comment instead of replying. But my final 2¢ for now is programmers have lived their entire life expecting bool to mean either true or false. What @filipworksdev is proposing means a bool will have three possible values, true, false, and null. I am not a fan.

filipworksdev commented 2 years ago

Making everything nullable by default would break a lot of backward compatibility though.

Not if you make everything nullable but not allowing null by default.

That is you change all Godot types to have the capacity to store nulls . A capacity that they do not use by default unless you tell them to using ? for example. To the user nothing changes. Is all a script change. And using ? switches storing null on thus allowing for the full nullable functionality of the variables.

This avoids the need to have int and int? as separate types. You only going to have int? with the ability to turn ? on and off by default off.

Edit: I want to stop making noise so I am editing this comment instead of replying. But my final 2¢ for now is programmers have lived their entire life expecting bool to mean either true or false. What @filipworksdev is proposing means a bool will have three possible values, true, false, and null. I am not a fan.

That is correct. Bool would technically be able to store true, false and null with null being disabled by default unless you specifically ask for it. This is vnens solution but in reverse. To the user nothing changes.

I will also have to stop posting here but I think I made my point. Sorry for posting and editing to much.

Ferk commented 2 years ago

That is you change all Godot types to have the capacity to store nulls

That goes beyond just the parser. It's possible that doing that might be more complex to integrate than adding extra types that are nullable. You said "I don't think this proposal may happen any time soon or even at all if we need alternative nullable types for every Godot type", but honestly, I wonder if the opposite is more true. Specially considering that they'll want the existing non-nullables to continue being well optimized, and are likely to already have tight constraints in terms of how they are stored so they can be fast.

In any case, that's an implementation detail that is up to the devs to decide, since they are the ones who have better insight of the internal structure. From an outside perspective, a "nullable" that doesn't allow storing null is not really a nullable anyway. And thus, what you are proposing then is no different, from that perspective, than having the default be non-nullable, which was already what we were defending.

YuriSizov commented 2 years ago

That goes beyond just the parser. It's possible that doing that might be more complex to integrate than adding extra types that are nullable.

Everything in scripting is marshalled through Variant, that can be anything, null included. So it is probably not as far fetched as you think.

Ferk commented 2 years ago

Not the strongly typed not-nullable types, those arent Variants. And even if they all were Variant after compilation, then why would that need to change for supporting nullable types? Shouldnt that have been obvious then? Either its a complex structural change, or its already an abstraction in place that wouldnt need mention.

In any case its an implementation detail with no effect on the final outcome, let the dev team decide on that, it goes past the requirements.

Also, editting here my reply to avoid too much pinging: i agree implementation details are important, but I think we should be more careful when crossing that line. Saying "I don't think this proposal may happen any time soon or even at all if we need alternative nullable types for every Godot type" implicitly assumes that adding a new type isnt done already via type hints on the Variant type, which is contradicting. If its true that everything is a particular case of a Variant, then why wouldnt "alternative nullable types for every Godot type" be anything else than Variants?

In fact i was suggesting earlier in a previous comment that they could just be hints used only in compile-time checks, with no change in the output of the compilation (versus just using Variants). Imho, you don't necessarily need to change fundamentally the data structure to add new types at that level.

YuriSizov commented 2 years ago

Variants can be Object with a type hint, they can also be null, or they can be any of the primitive data types. It's our only container for the API interoperability. I can imagine a flag being added to it to indicate that it is a typed, but nullable value.

And implementation detail is important to make a decision on a proposal. We evaluate both the reported need and the proposed solution. Understanding how the internals work is important for proposal approvals.

nathanfranke commented 2 years ago

This would break compatibility, but what if we also have nullable/non-nullable node types (e.g. Node?, Node2D?)?

I think this would be helpful when making methods, as usually node parameters/returns are assumed to be non-null. We could even improve the core methods to utilize these: get_node(path) -> Node, get_node_or_null(path) -> Node?.

There are some big edge cases with this though. For example:

var node: Node! # could be implicit

func _ready() -> void:
    print(node) # null? error?
MikeSchulze commented 1 year ago

Hi i run into this issue today I want to have a function return a typed value OR null

# simple example
func foo(flag :bool = true) -> Array:
    return  Array() if flag else null

It worked in Godot3 but is now broken in Godot4

It would be nice to mark a function is return a type OR null

e.i.

func foo(flag :bool = true) -> Array?:
    return  Array() if flag else null

or

func foo(flag :bool = true) -> Array, null:
    return  Array() if flag else null
Calinou commented 1 year ago

It worked in Godot3 but is now broken in Godot4

Array is not an Object-derived type, so I'm surprised this even worked in 3.x. Only Object-derived types are considered nullable.

Edit: See https://github.com/godotengine/godot/issues/67105.

MikeSchulze commented 1 year ago

func foo(flag :bool = true) -> Array, null: return Array() if flag else null

It works, try it by your self

func _ready():

    prints(foo(true))
    prints(foo(false))

func foo(flag :bool = true) -> Array:
    return  Array() if flag else null
--- Debugging process started ---
Godot Engine v3.5.stable.mono.official.991bb6ac7 - https://godotengine.org
OpenGL ES 3.0 Renderer: NVIDIA GeForce GTX 980/PCIe/SSE2
Async. shader compilation: OFF

[]
Null
--- Debugging process stopped ---
dalexeev commented 1 year ago

It works

But not always:

# v3.5.stable.official [991bb6ac7]
var a = null
var b: Array = a # Error: Trying to assign value of type 'Nil' to a variable of type 'Array'.
MikeSchulze commented 1 year ago

It works

But not always:

# v3.5.stable.official [991bb6ac7]
var a = null
var b: Array = a # Error: Trying to assign value of type 'Nil' to a variable of type 'Array'.

yes it is inconsistent ;) So we need here a solution, e.g. allow to use nullable values like in other languages.

filipworksdev commented 1 year ago

I actually support the idea of typed variables allowing null by default. Technically all Variables can already be null is just the way typing was implemented to not allow nulls. Like YuriSizov has explained above everything is marshalled through Variant class and is potentially an easy change but don't expect it to be popular 🤣 You have the impossible task of convincing the community 🙃

Another controversial idea is to add JavaScript undefined type which can be used for all uninitialized types. It would not allow nullable types but undefineable types lol.

var a;

print(a) #undefined 

A more elegant solution would be to simply add an entirely new core type called Nullable which would be a Variant wrapper class that has typing and nulling. This is easier to implement and completely doges the complex ? implementation in GDScript and VisualScript and the discussions that come with it. As you can see above nobody can agree on the right syntax. I also believe is more Godotesque solution. I think ? is more of a C# or C++ than Godot.

It would work like this:

var x = Nullable(2) # automatically typed to int but can also be null
x = null # works
x = "hello" # fails