-
-
Notifications
You must be signed in to change notification settings - Fork 96
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? |
There is a lot going on here, and i couldn't read it all. If you dislike the Variant type because it cannot guarantee the type, but static typing is too strict, and you want more types, i.e. null. I think this is a nasty code smell. There are many reasons to not make null a first class citizen as it requires all your code to be defensive. What will end up happening is that you will need to check if the value is null in the function. So why not check if the Variant value is the desired type, or null, now? Would this not be the same? |
You're thinking from the direction of the callee. Yes, the callee can simply do type checking. It does create a "But what do you do if the value is neither null nor the desired type" sort of situation, but let's ignore that for a moment, or assume you can do an appropriate error return. The issue is rather in the caller. The autocomplete for function parameters, as well as any auto-docs, simply see a parameter receiving As the OP mentions, and provides examples, there are method, even in core, where a single static type is not an option, and they reluctantly use Making a type system that properly tags what types are or aren't allowed isn't "code smell". Rather, using |
P.S. The fact that you can't make core be type-safe is a pretty big deal. Yes, some people prefer type safety, and some people prefer to run untyped. But you don't get to choose which core you use. Any type system that can't cover the core functionality of the engine, is an incomplete type system. |
"But what do you do if the value is neither null nor the desired type" You treat it as null, assert immediately or do nothing. As this scenario implies you used the function wrong. So why try to handle undefined behavior gracefully? You can't solve all the worlds problems in a single function just tell the programmer as soon as possible that they are not doing something the code can't handle. |
It is the policy of Godot that "a game should never crash". That includes handling developer error gracefully. Even if you do manage to walk out of an assert in one piece, it doesn't change the fact that it would only trigger at runtime, rather than compile time. If you think about real-life-scale projects, where there could be tens of thousands of lines of codes, and many of them might not trigger unless reaching some advanced level or secret area in the game, you do not want to be left with the chance of an unexpected assert, where no automated tool can warn you that it's there before your game lands in the hands of hundreds of thousands of players who absolutely will stumble upon it. And with the issue being present in core, there's no way to design around it, other than limiting to game types that do not require said core functions. |
Test and exercise the code. You will begin to make assumptions when handling null, and what are the consequences to that? soft locking a game? Exploits? Unintended behavior? Choose your poison. I would change the intersection ray class to not return null instead of promoting handle-by-assumption patterns. It puts an undue burden on the developer to handle all scenarios and it will still be an assumption. If i want to add two numbers and return the sum. It doesn't make sense to return a zero when one or more numbers are null as it should be undifined. You will want to tell the developer as soon as possible that the function didn't do what they wanted. And Godot already does this with static typing. Function- "When my function signature conditions are met i will perform my responsibility. Not, i might do something." |
Please explain what you would change the ray class to. If it doesn't return null, what does it do instead? There's also a situation in which a ray might not hit any target, and the code necessarily has to handle that. There's no way around it. It can't assume an intersection and make calculations based on it when none occur. With nullable type, the return type would be The alternatives to returning
If you have a third option, one that prevents the developer from assuming an intersection occurred (unless on purpose), I'm all ears. And, mind you, Godot doesn't exactly have a framework for unit-testing scenes and scripts. So hitting an issue at runtime rather than compile time is a big deal. Although, that might be a different venue to chase after. |
The third option is an class Option[T]:
var value: T
func _init(a: T):
self.value = a
class Some[T] extends Option[T]:
func _init(a: T):
super(a)
class None extends Option[Nothing]:
func _init():
super(pass) # Like Scala's ???, or Kotlin's todo, I suppose the closest thing in gdscript is `pass` since it can be used to not return the thing you promised you were gonna return, but I suppose that's up to debate. The type that you'd be looking at in that case would be: The cool thing about this approach, is the variations, like Another alternative is union types. But the approach that was implemented a while ago iirc makes every variant implicitly nullable, and then has some flags for type safety, which makes it equivalent to a union type that only works for the |
I think a good compromise is to provide an equivalent of rust's var v1: Vector3? = some_function()
var v2: Vector3 = some_function().unwrap() # crashes if null |
Another possible compromise could be a project wide configuration setting like C#, which when on statically enforces handling the possibility of nulls, and when off defers the checks to runtime. |
Union types are indeed a valid generalization. I believe there's already a proposal for it. The issue is that it's too generic, and hence more difficult to implement and maintain. Additionally, it potentially violates Godot's problem->solution principle. i.e. There's a clear concrete case where you'd need As for |
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) |
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: