-
-
Notifications
You must be signed in to change notification settings - Fork 97
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 support for nullable static types in GDScript #162
Comments
You could pass in Vector2.ZERO and check for it, as you would need a check either way. This is also safer, as null "could be the biggest mistake in the history of computing". |
Note that with nullable types we could require people to handle nulls explicitly, similar to how kotlin does it. So: func whatever(v: Vector2?):
print(v.x) # Warning or error: v could be null
print(v?.x) # (if there is a null-checked dot operator) Prints null if v is null
if v != null: print(v.x) # Does not print if v is null
print(v.x if v != null else 42) # Prints 42 if v is null |
Here's some current use cases of mine (if I understand the proposal correctly): # Use custom RandomNumberGenerator, or the global one:
var ri = RNG.randi() if RNG != null else randi() # Adjust impact sound volume by relative velocity
func _adjust_volume_to_velocity(velocity_override = null):
var strength = max_strength
if velocity_override:
strength = velocity_override.length()
# configure db ...
The logic could fail exactly at var attach_pos = get_attach_point_position()
if attach_pos == null: # not attached to anything
return linear_velocity |
I often use a getter (which returns a "safe" boolean) to check for stuff, for example: if not is_attached(): # not attached to anything
return linear_velocity |
@Jummit See the discussion in godotengine/godot#32614 for why this doesn't work. |
It works, just not everywhere.
I'm not proposing to use this in core functions, but in personal projects this is a good way to avoid null. |
So it's not a good solution.
I'm not agreed with that, basically you return a valid value when you precisely want to warn that something failed. This approach lack of consistency (the value sometime will be -1, some time an empty string, etc.) and limited: what do you do when the function can return the whole range of an int? As far I know, the cleanest solution for the return type case, it's to raise exceptions that must be handled by the region of the code that call the function. But error handling isn't available in GDScript and don't solve the case described by @aaronfranke
At least there is an error visible somewhere, if you return a valid value and forgot to handle it, there will be the worst case scenario for a bug: no hint. I find this proposal essential in a typed environnement, especially for the type returned by a fonction, it also useful for parameters like @aaronfranke suggested, to keep all the logic of a function, inside it. The alternative being to do pre-checks before to call the function, and so have a part of the function's logic outside it. Moreover, it becomes more important in conjunction of #173, where you will be able to force the typing, without this proposal, the solution for returning a "error/null" value from a fonction will be to disable the type system with the |
If we want to get rid of the null and have a safe/clean approach, I was thinking about the Optional object in Java (how java solved the If we implemented something similar in a less verbose/python like way, that a very good solution I think: whatever(Optional(vec)) # or whatever(Opt(vec))
whatever(Optional.EMPTY) # or whatever(Opt.EMPTY)
func whatever(vec: Vector2?):
var result := vec.get() if vec.is_present else Vector2.INF |
@fab918 The only practical difference between your proposal and mine is that you're wrapping the value in an object and using methods and properties instead of using a nullable type. I'm generally opposed to wrapper objects and this just adds complexity to something that would likely be easier if we mimic the way other languages do it. For a nullable type, I would remove
|
@aaronfranke I'm fine with your proposal. But if the problem with your solution for some , it's the usage of Anyways, no matter the implementation, I think this feature is essential to fully embrace the typed GDScript, especially with #173 |
The Kotlin style If you are going to do strict null typing as part of the type system that is. That being said, I too think this proposal is essential for GDscript to be and to be used in larger projects. |
Yeah, this is quite a serious issue, forcing me very often to skip types. From the proposed solutions (assuming no more improvements to the type system are done, like type narrowing), the
This way it's done for example in Scala, Java, Haskell, OCaml, Reason, PureScript and looking at wiki in many more languages I never used. So it's definitely something other languages are doing too. Sum types are stronger (type-wise), because they force user to handle (test) for an empty value.
I personally would prefer I am not sure of the scope of this proposal, but I would like to see supported it everywhere, not just function arguments or return type. |
I've been thinking about this and what I have in mind is that all variable will be nullable by default. So even if you type as a Especially because internally the engine is mostly okay of taking To counter that, we could introduce non-nullable hints, so it forces the value to never be var non_nullable: int! = 0
var always_object: Node! = Node.new() Using This will give an error if you try to set |
@vnen I very much dislike that idea. The amount of projects I've seen currently using static types everywhere shows that the use cases for non-nullable static types are extremely common, with this proposal only relevant for a small number of use cases. If your proposal was implemented, many people would fill their projects with I also think your proposal goes against the design goal that GDScript should be hard to do wrong. If a user specifies I think I would actually rather have nothing implemented than There is also value in the simple fact that |
Hi @vnen. First, thanks for all your good works, really amazing. It’s hard to deal with devs, and harder to make choices, so I wanted to support you Here is my 2 cents, hope that can help. I thought about the right balance between fast prototyping and robust code. I often saw this debate here and on discord, I ended to think the best is to avoid to compromise to ultimately satisfy no one. Instead, stick on 2 ways with opposite expectations:
So in this spirit, I will avoid decrease safety and stick on classic nullables approach in conjunction of #173 (seems directly related to this proposal to fully be able to embrace the type system). In addition, the use of nullable is rather standardized contrary to your proposal. The type narrowing proposed by @mnn is the cherry on the cake but no idea of the implication on the implementation. |
Here is my workaround, that I think works in any case: const NO_CHARACTER := Character.new()
func create_character(name : String) -> Character:
if name.empty():
return NO_CHARACTER
return Character.new(name)
func start_game(player_name : String) -> void:
var player_character := create_character(player_name)
if player_character == NO_CHARACTER:
return You end up with some new constants, but I think it makes it more readable than using null. |
Related issue: godotengine/godot#7223 |
@vnen On another note, what do people think about adding more null conditions in general? |
@Lucrecious The first line you posted doesn't behave as you may expect, since |
While I do like wrapper types, coming from powerful type systems like Rust that use that kind of pattern, an |
To add to my previous comment: If we were to add a syntax that allows generic types like I'm not convinced that |
func a(x:int?) func a()->int? |
When developing, I would much rather have an immediate crash that tells me what went wrong than try to trace my way back to where the type correct but invalid value was introduced. I'm a Haskeller, I've used Elm, I've seen where dogmatic adherence to "all errors at compile time, no crashes" leads. It leads to languages that are a giant pain in the ass to use. You need escape hatches as a concession to practicality. It's irritating and demoralizing to write a bunch of code to deal with cases that you know are impossible but the compiler is too stupid to understand. It impedes rapid iteration, which is something that is a far more important priority for game development. You need to be able to try out gameplay ideas quickly, or you'll never get anywhere. Nobody wants to fight the compiler over code they'll likely end up throwing out. |
I think having a default of forcing programmers to handle nulls is plenty enough of a pit of success. Every programmer should understand that calling "unwrap()" is promising that this will never happen, and they're taking full responsibility for the consequences. If they get it wrong, that's easily fixed by grepping "unwrap". "games should never crash" should not be used as dogma to impede a programmer from doing what they want, if what they need in that moment is to get something up and running fast. That's their decision, and if good defaults are provided, it's paternalistic to prevent them from opting out. |
As I have said before in this thread, nullable types are just syntatic sugar over unions with null. And unions are, again, just an specialization of the catch-all godot Variant. For nullable types in gdscript to be implemented first we would need to define a standard way of implementing generic classes in core. I haven't looked at the current generics implementation in the Array class (and the pending PR for Dictionaries), but we would need to set in stone a standardized system for doing generics in core that can be exposed to script. After implementing generics, we would need a new variant similar to c++17 std::variant, but with a different name to not cause confusion. Suppose it is called Union and used with variadic generic parameters like Union<null, int>, Union<null, int, float>, Union<null, int, float, String> and so on up to some maximum number of parameters like 4, 8 or whatever. Once that is done, the core c++ implementation of the functions that receive and return Variant could be refactored to receive and return things like Union<int,null>. Up to this point whe whould have only implemented things in core, nothing related to gdscript. Then, we could finally change the gdscript module to add functionality to use Unions and first add the syntax needed to parse edit: We should not that there is already a union that occurs all the time, the union between TYPE_NIL and TYPE_OBJECT. I have no idea if this was implemented only in gdscript or if it also happens in core, but at the variant level object and null are diferent variant types, and yet, we have nullable objects by default. In gdscript a variable declared as object can be in fact three things, a null (TYPE_NIL), wich is 'falsy', an object (TYPE_OBJECT) wich is an already freed instance (wich is falsy) and a non freed instance, wich is 'truthy'. edit 2: besides adding things to core, due to the flexibility of the catch-all variant type, we could also not implement anything in core, only implement things in the gdscript parser, and do type erasure with things like union types and nullable types. The syntax for nullable and option types would exist in the parser only, but under the hood variables would be just plain variants. Most of the time type safety is only needed at compile time, not at runtime. Ask anyone that has used type erased generics for years (java and type-script developers) if not having runtime type safety is realy such a big problem. |
On the flip side, there are too many examples of games on Steam that, on occasion, "poof" while you are playing them. This is the worst case scenario. Much worse than having difficulties during development. For that reason, GDScript doesn't so much as have a concept of exceptions. As a result, there's nothing sensible the GDScript VM can do if If you're trying to fast iterate, and just want to get something up and running, the solution is to be untyped. That way, Godot can still do whatever it considers to be "sensible" in terms of operations involving To be clear: The use of typed code doesn't only add checks at compile time. It also removes checks at run time, to improve performance. Except, these checks can sometimes be the difference between getting an alternate value, and either corrupting memory or outright crashing. Any operation between two |
I also strongly believe I also think constructs like I strongly believe nullability needs to work for all types, as the benefit is too great to ignore on objects like Since we can't break current syntax, I suggest with this feature we make some assumptions by default:
You can use either syntax for those. With no breaking changes we can still start using nullable basic types like To change this behavior, add these, which apply in order of precedence (higher first):
This mode would go further to make even Just adding my 2 cents (sorry if this was already mentioned, thread too long). |
Variant and Object should not be implicitly null.
Do not make nullables optional like in C#, it completely makes nullables and the type safety pointless because it's optional. If you want all your types to be nullable then use Value and Reference types should both require a non null value if it's not nullable |
@Shadowblitz16 I agree having nullability in all types by default would be the ideal, but it's impossible to do this for
I don't think forced null-safety is realistic (at least until whenever Godot 5 / GDScript 3.0 comes), considering that in GDScript, even having type-safety at all is optional. That's why it's unavoidable to leave the decision to the programmer. In any case, the point of my proposal, like in C#, is not to make null-safety optional (I edited my comment to emphasize it should be enabled by default on new projects). Null-safety being optional is an unavoidable consequence of the way the language was designed. The point of the option is that it allows you to port code and introduce nullability gradually, until your existing code is adapted (and checked, where hopefully you catch some bugs in the process) until your whole project is null-safe. When you have lots of code, you need a way to test it gradually while you're partially porting it. |
I would be up for T! instead of T? but I still strongly disagree with nullable's being optional. Even if T? was done and forced it would most likely be done for 5.0 so when people switch to 5.0 and update their code to the 5.0 api, updating their nullables wouldn't be too much more work. Besides plugins break for each major godot release anyways and nullables are a good reason to break something so better code can be achieved in the future. |
@Shadowblitz16 I do agree with you that forcing null-safety on 5.0 is the best course of action, as major upgrades can (and should) break code. But that's probably going to be years from now. So, I'm not sure if I agree on what to do w.r.t. 4.x, as I'm not sure what you're suggesting.
My opinion:
|
|
As a new Godot dev I find the current way things work in GDScript to be quite confusing. Some types are allowed to be EDIT: in response to people concerned that this will confuse newbies or become a major foot-gun for lazy devs, I suggest that by default nullable types could show warnings, just like unused variables currently do. That way, those of us who know what we're doing can simply disable this warning in the options and still be able to code how we want! |
All types are non-nullable except any Object-derived type, which includes Resources, Nodes, and custom script classes. This is because all the other Variants except Arrays, Dictionaries, and PackedArrays are value types, not reference types. Variables that store such types store the values themselves, not a reference to the value, and null is just a shorthand to create a variable that references nothing, which is why it doesn't work on some types. |
Thank you, this was a very helpful explanation. I assume this is probably written somewhere in the docs already but this has definitely made things clearer to me. |
Unfortunately I don't think this is referenced anywhere in the docs. Maybe that could be a separate issue? |
@sockeye-d Yes please make a separate issue for documenting this better (IIRC there recent changes involved too) |
I disagree with this. This would be incredibly confusing for newcomers if Variant would default to Currently, the default today is that Variant is indeed an alias to We don't even have consistent behavior for static typing as a result, A breaking change such as this should certainly be in a 4.X update but it should not have to wait until 5.0 to be implemented. It's better for debugging, as you get to understand the exact point in which the unexpected null is introduced instead of when a method is called on the null, which could be hundreds or thousands of lines of code away from the introduction of the bug. And as mentioned, extensions break every update anyways. We could create a script that can help migrate users to the new nullable static types and migrate extensions they use to nullable static types. This would transform all instances of "Variant" to "Variant?" in the code. I'm a proponent that we use the "T?" syntax in which ? represents a |
I don't think this is a good idea, this change breaks compatibility in a major way for most (if not all) projects. This cannot be changed easily and cannot provide you with complete null safety due to fundamental memory management problems. I opened proposal #11054 with my thoughts on this. |
@dalexeev It does not need to break compatibility.
|
@MaximoMachado This is already how it works currently. I agree that it shouldn't be like this (at least on 5.x), but if we're talking about implementing this for 4.x while maintaining compatibility, it's sort of unavoidable to go the C# route IMO, like said above. |
So, seeing as it will be quite a while before this gets implemented, I'm curious as to what you guys think is the best way to currently handle type hints for methods that return optional/nullable values. Here's a common example. In a 3d scene, we might want to find the coordinates of the GridMap that we're hovering over, so a player can place a building in a Grid-based system. So, we calculate the coordinates as follows: func get_coord_at_mouse():
# Get the mouse position on the viewport
var mouse_pos = get_viewport().get_mouse_position()
# Create a ray query
var ray_query = PhysicsRayQueryParameters3D.new()
ray_query.from = camera.project_ray_origin(mouse_pos)
ray_query.to = ray_query.from + camera.project_ray_normal(mouse_pos) * 1000
# Cast the ray and get the result
var space_state = get_world_3d().direct_space_state
var ray_result = space_state.intersect_ray(ray_query)
return ray_result.position if ray_result else null Say the player hovers over a place outside of the map, we may not get an intersection, and thus return null. In a project that is otherwise following type hint conventions, I am not able to add a proper type hint to this method. I could add So the only 'correct' choice here is to leave out the type hint completely, which looks very incomplete in a project that uses type hints for all the other functions. Besides, this 'correct' choice still doesn't give the developer any indication that we're dealing with a nullable object. So what this really does for me, is that type hints don't feel robust anymore. I guess the most 'clever' thing I can come up with is to build a custom I'd be interested to see how you would work around this until we do get Optionals in gdscript. |
@codevogel Documentation. It's currently the only way if you want to go with e.g. the ## Blah blah.
## Returns null if not found, or a Vector3 otherwise.
func get_coord_at_mouse() -> Variant:
...
## Blah blah.
## Returns Vector3(NAN, NAN, NAN) if not found.
func get_coord_at_mouse() -> Vector3:
...
## Blah blah.
## Returns [param fallback] if not found.
func get_coord_at_mouse(fallback: Vector3 = Vector3(NAN, NAN, NAN)) -> Vector3:
... Between these, the downside of the Variant method is that you can't get a type-safe line. Of course you can make a custom class, if the extra overhead of creating and destroying a class doesn't matter (as GDScript doesn't support structs either). And btw, you wouldn't even need an Depending on your use case, another option might be setting or not the first entry of an Array parameter input, or returning an Array with zero or one entry. This has the extra overhead of creating the array on the 2nd case, and on the 1st case if you weren't already gonna create it. Regardless of the route you take, you will have to rely on properly documented methods. |
Some times a fallback behaviour makes sense. For example, you could set a maximum distance for the raycast, and then when the raycast fails return a position at that distance. Of couse if that makes sense or not depends on context. Returning Other options include:
And if you are considering making a type to fascilitate this. Another option is to implement a promise. Sadly there isn't one agreed upon promise implementation, but I can tell you I have one for internal use and other developers have also made their own. But know that it is an extra allocation. |
@codevogel Alternatively, you can return a PackedVector3Array which is either empty or has one element. Since empty arrays evaluate as falsy, it's pretty easy to check as well |
Describe the project you are working on: This applies to many projects. This is an offshoot from #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:
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: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 innull
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:
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 ornull
when there is nothing else to return. For example,Plane
's intersect methods return aVector3
if an intersection was found, ornull
if no intersection was found. Nullable static typing could essentially self-document such methods by showing that the return type isVector3?
instead ofVector3
.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.
The text was updated successfully, but these errors were encountered: