-
-
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 union types to Typed GDScript #737
Comments
Isn't |
Yeah, I thought it would be nullable, but no: My initial comment and discussion about this:
Looking at that list, for example, you can't define a function that receives a My other feature requests (just dropping the link here so I don't have to search for it in that crazy long thread again .. will maybe create issues for more of them later) |
I'd say the handling of nullable should be a separate issue and not handled by union types. For Type safe nullables in other languages like TypeScript and Kotlin, you usually just suffix the type with a As for Union types, I'm in full support of this, as it's something that languages which do not allow for method overloading pretty much need to have if they're going to allow for types. Python does it this way too, if we want to continue with the ideology of GDScript should be pythonic. |
I'm not against this, but don't expect it for 3.1. I deliberately made the typing as simple as possible because I knew it would break a lot of stuff (as it did) and complicating it would break even more. We also have to evaluate this carefully, because union types are more prone to runtime errors (though less than a true Variant) and it will lose the benefit of the typed optimizations.
Only Objects can be nulled, Array and Dictionaries are a "primitive" type (that is, they are defined inside Variant), so they are not considered objects.
And that's why
We don't have this idea though. We try to mimic Python for syntax, just because users expect it (since GDScript is already somewhat similar to Python), and it's easier than the bikeshed discussion most of the time. In fact, I'm not sure what's the plan for GDScript, since godotengine/godot#18698 didn't reach a conclusion yet. |
We could have tagged unions (ala Rust, Scala, Haskell) instead of unions if type safety is an issue. It wouldn't solve the problem of compatibility of old code, though. |
Yes, that would also be nice.
|
Just wanted to say that I also would love union types but not for nullable, but for errors, for example I'd love to be able to have my parser do something like:
and if we're going the typescript/haskell route, then being able to define types as union of other types ( For now, I'll just stick to not having a type (which is kinda sad, would at least like to have a |
Having full-on unionized types seems like overkill to deal with the actual problem of needing to handle nullable / optional parameters in a method. As mentioned, only Objects, not Arrays or Dictionaries, can be null for a reason. Given this, I think the only appropriate solution to this problem would be to add support for method overloading to GDScript. This way, you can re-define a method multiple times with multiple sets of typed parameters and have confidence that the logic you write will apply only to those particular objects. I suppose, to play Devil's Advocate, that might lead to people creating empty implementations of a method that just accepts null, purely so they can create optional operations...
...but that also seems like more of a problem regarding one's use of the method itself. Why call |
@chanon Can you adjust the OP to fit the proposal template? |
EDIT: ok I will try to adjust it. |
I create a method that gets Imo In such cases some kind of union types really could be helpful and flexible func(arg: Array | PoolStringArray): |
I agree with @willnationsdev #737 (comment) that method overloading (#1571) can be useful in solving problems related to the lack of nullable types, although I anticipate cases where nullable types can still be more convenient (when we have several parameters). so I could rewrite my previous comment this way (when method overloading would be implemented): func(arr: Array):
...
func(arr: PoolStringArray)
... although in this case it might cause code duplication if I want to call the same methods for func(arr: Array):
...
arr.append(1)
func(arr: PoolStringArray)
...
arr.append(1) instead of just func(arg: Array | PoolStringArray):
...
arr.append(1) however nullable / union types also solve some other problems for example now I can't return func foo()->int:
return null # if something went wrong I believe nullable like |
Even if common in JavaScript/TypeScript, I'm not sure if untagged unions are good practice to justify being language feature. Parameters like Instead, I agree with @raymoo that if unions are supported, we should aim for something like Rust enums, maybe simplified (i.e. a variant type). Rust provides the best and most concise implementation of tagged unions I've seen so far. The pattern matching makes it easy to use, but is not strictly necessary -- however there should be a way to enforce checking the type tag before accessing the variable. The main difference would be that the union type gets a name, with named fields. For example: variant Target:
enemy: Enemy
player: Player
func apply_damage(target: Target)
pass The advantage is that this works even when the same type is used multiple times, while the variant Cash:
euro: int
dollar: int Also, I think nullable types are completely independent of unions and occur much more frequently. They also deserve a special syntax Kotlin is a very good inspiration for nullable type design. Java, C#, and even more so JS and PHP are great examples of how not to do it. |
Neither of these are true. Just look at TypeScript. If you try to call a method which is only available on one member of a union, compilation fails. And to the second point - IntelliJ IDEA and VSCode suggests only available methods/fields, so before you do narrowing, you get methods/fields which are same (common) for all members of the union, after narrowing you are getting suggestions only for the narrowed type. class Enemy {
die() {}
applyDamage() {}
}
class Player {
killTarget(x: Enemy) { x.die(); }
applyDamage() {}
}
type Entity = Enemy | Player;
const testEnemy = new Enemy();
const applyDamage = (target: Entity) => {
target.applyDamage(); // correctly no error, only suggested method is applyDamage
target.die() // correctly error: Property 'die' does not exist on type 'Player'.
if (target instanceof Player) {
// only methods for Player are suggested
target.killTarget(testEnemy); // correctly no error
} else if (target instanceof Enemy) {
// only methods for Enemy are suggested
target.die() // correctly no error
}
}
type PrimitiveUnion = string | number;
const g = (x: PrimitiveUnion): string => {
// primitive types are type-safe as well
switch (typeof x) {
case 'string':
x.toFixed(2); // correctly error (number method): Property 'toFixed' does not exist on type 'string'.
return x.toUpperCase();
case 'number':
x.toUpperCase(); // correctly error (string method): Property 'toUpperCase' does not exist on type 'number'.
return x.toFixed(2);
}
} Code is a bit cumbersome without pattern matching, but it is type safe and autocompletion works well. I too like tagged unions better, here's the cash example type in Haskell: data Cash = Euro Int | Dollar Int I personally don't think it's a great type - IMO having a currency type and amount separate works better, but it's just an example. |
Ah, interesting, thanks for the clarification! Out of curiosity, do you know if the inspection is also "deep" in the sense that not only method names, but also parameter types (when typed) of the methods are checked? class Enemy {
applyDamage(damage: float) {}
}
class Player {
applyDamage(damage: int) {}
}
const applyDamageUntyped = (target: Enemy | Player, damage /* untyped/any */) => {
target.applyDamage(damage); // should compile...?
}
const applyDamage = (target: Enemy | Player, damage: int | float) => {
target.applyDamage(damage); // should not compile...?
} Yep, currency is not the best example, especially for games 🙂 If we go the Rust route, variant types are an extension of C enums -- so they can have a simple type tag without extra data, or they can associate fields with that enum. Sticking to existing GDScript syntax, such "without data" variants could have the type variant TileType:
empty: void
full: void
diamonds: int # stores the amount of diamonds in the tile
switch: bool # stores whether the switch is pressed or not or: class DiamondTile:
amount: int
class SwitchTile:
enabled: int
variant TileType:
empty: void
full: void
diamonds: DiamondTile
switch: SwitchTile In Rust, it would look like this, which is clearer and more expressive, since the fields have names but don't require an external type: enum TileType {
Empty,
Full,
Diamonds { amount: int },
Switch { enabled: bool },
} |
This comment was marked as abuse.
This comment was marked as abuse.
I'll offer a word of support for sum types, algebraic data types, tagged unions, sealed classes, or anything you want to call them. By modeling data with sum types in combination with product types, you can shrink-wrap a type to outline only valid data possibilities and offload significant programmer responsibilities to the compiler. A product type is perfect for expressing For instance, if you've got a GUI screen that shows data you load asynchronously, you might represent that loading data as a product type with nullable fields. // Product type representing an asynchronously-loaded high score list
struct RemoteLeaderboard {
var leaderboard: Leaderboard?
var myRank: Int?
var error: Error?
} Here it would not make sense to have both fetched the leaderboard successfully and gotten an error while fetching the leaderboard. At least one of them should be The valid major cases are that you are still loading, or you have the data, or you got an error. And only if we did get the data, maybe I'm ranked and maybe not. We can express that with a sum type of three cases, where each case can include a product type: // Sum type representing an asynchronously-loaded high score list
enum RemoteLeaderboard {
case loading
case done(leaderboard: Leaderboard, myRank: Int?)
case failed(error: Error)
} These examples have been in Swift, a close relative of Rust in this regard. If you've done this kind of data modeling for some time in a language that supports it well, you just don't want to go back. Returning to modeling data with only product types feels like working with blunted tools. In recent years, mainstream app development has taken certain lessons from functional programming. Game development has left some of those opportunities on the table, and this is one of them. Godot could take advantage and gain some true diehard users. On the topic of nullable types, since that's how this thread started: If you combine sum types and generics, you can offer |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
Yup. Got to agree. Union types are the best thing in TypeScript & would love to see them in GDScript. I think one of the best use cases would be for dropdowns. 'Up' | 'Down' | 'Left' | 'Right' could be an union type exported to a dropdown, and you would get the string back in return depending on selection, rather than a number like in an Enum. |
Note that you can already do: @export_enum("Left", "Right", "Up", "Down")
var direction: String Which exports a dropdown in the inspector and the result is a string. I know this does not cover what is asked in this proposal, but it is something you can use in the meantime. |
This comment was marked as abuse.
This comment was marked as abuse.
There's always the chance that a contribution in the thread is not interesting for you personally. There's no GitHub setting like "only notify me on comments from org members" or similar, so you'll have to live with it or write a bot. However, your criterion "without contributing significant new information" does not apply here: callus mentioned the dropdown use case. I do agree that pure "+1" style comments are not helpful. Re-initiating an old discussion is also a good way to express "there is still interest in this feature", rather than "it used to be a big problem in Godot 3.1, but no longer applies". From the number of upvotes alone, it's not visible when they happened. Furthermore, thread activity is also one way to sort issues (through "Recently updated"). |
This comment was marked as abuse.
This comment was marked as abuse.
It's hard to assess what the community want because the community is just too big. This repository is one attempt of concentrate the needs of the community. Keep in mind that just because you find something essential, it might not be the case for the vast majority of other users and thus will hardly be a priority. There are also frequent meetings with the maintainers to review proposals (which was put on hold to focus on the 4.0 release but will resume soon). Adding a 👍 reaction does help because we can sort issues by reaction as well and make those more likely to be discussed sooner. Bumping for the sake of it won't help, it will just annoy the subscribers. That said, detracting from the proposed subject is just going to get this discussion locked, which we prefer to avoid. There are other places to discuss the workflow of proposals and pull requests. Putting this here won't help as it won't be the place we'll go looking if we want to search for suggestions, and only a limited number of people will see this here anyway. So let's keep the discussion on topic, please. |
Just my two cent, I'm extremely interested in features like this, as a Haskell and Rust developer I find pure object oriented imperative languages extremely cumbersome to work with, when one has to express logic more complicated then "if a>b then" |
I'm assuming this includes generic types? Here is an example of unions where the type hierarchy is not shared: class Player: pass
class Enemy: pass
var target_entities: Array[Player | Enemy] = [] What to do... Example where the type hierarchy IS shared: class Entity: pass
class Player extends Entity: pass
class Enemy extends Entity: pass
var target_entities: Array[Player | Enemy] = [] Then it would be much simpler, I think, since internally you could say that If you want a solution RIGHT NOW, do that, it's annoying... but it works. Contrived example: class Fish:
var name = "Fish"
class Tuna extends Fish:
func _init():
name = "Tuna"
class Cod extends Fish:
func _init():
name = "Cod"
func _ready():
var fish_can: Array[Fish] = []
fish_can.push_back(Tuna.new())
fish_can.push_back(Cod.new())
# I could also put it inline, but just wanted to show that everything works as expected
# This includes using other methods
print(fish_can.map(func(fish): return fish.name))
# Prints `["Tuna", "Cod"]`. This could also be done for non generics: var fish: Fish = Tuna.new()
fish = Cod.new() Well, that's the easy way. If we (sigh I wish I had the time or the wants) are going to do this properly (according to my own definition), then I suggest we expand it to some sort of rigorous type algebra, off the top of my head that would mean adding a class Entity: pass
var name: String
var id: int
class Player:
var name: PlayerName # Where `PlayerName` is an inner class.
var speed: float
var player1: Entity & Player = new() # No type is indicated, so it is inferred instead
# Options:
# 1. Error. Types have a field of the same name with clashing types.
# 2. The last (or first) type definition for a field is used. I believe Scala does this.
# - Last: { name: PlayerName, id: int, speed: float }
# - I prefer this.
# - First: { name: String, id: int, speed: float }
# That's all I can come up with right now. But that's a bit out of topic. |
I'm a novice Godot user, but I've been spending time with GDScript and have definitely encountered problems and data that are best modeled with the "or" relationship of sum types. Glad to find that this is being discussed! 😄 Many of the proposed examples in this issue so far seem to describe anonymous sum types, where the sum type and/or its variants are not explicitly named. Anonymous sum types can be very useful and convenient, but I feel a more explicit design may be a better place to start. Importantly, explicit sum types interact better with pattern matching and probably require less varied syntax. Perhaps For example, perhaps the state of a mob can be described using something like this explicit syntax: enum MobState:
IDLE:
pass
ATTACKING:
var target: Node2D
var power: int
var state = MobState.ATTACKING:
target = %Player1
power = 1
match state:
MobState.ATTACKING { var target }:
print("changing mob target")
target = %Player2
_:
print("mob isn't attacking") In this example, This of course composes well with product types. Combining this kind of sum type with inner classes (or even proposed class Mob:
var state: MobState
var hp: int:
set(value):
hp = max(0, value) Here, the Anonymous sum types can support this kind of construction and pattern matching syntax too, but it may require some specific knowledge of the types involved. In particular, the syntax of initialization and pattern matching of anonymous variants of the same type can get quite complex and/or bespoke, such as in types like Thanks for all the thought and discussion! I'm excited about something like this making it into GDScript at some point. |
I would like to make a case for something similar to anonymous sum types, which I'll call "algebraic type hints". I would prefer these because I think explicit sum types, which I'm just going to call "tagged unions", would integrate poorly with Godot's gradual typing system and require a generic system to use properly. On the other hand, algebraic type hints would allow users to implement their own tagged unions if they want. Algebraic Type HintsI agree with @chanon and others that GDScript would be massively improved by allowing users safely define variables with the form I'm wondering if we can accomplish this by taking advantage of the fact that all data is passed around the engine through a
My knowledge of Godot's source code isn't very strong yet, but based on my understanding of the GDScript module's source code, it looks like my Anyway, the
The
Gradual TypingMy biggest concern about eventually adding Rust-style tagged unions to GDScript is that I think they would interact poorly with Godot's gradual typing system. GDScript's being gradually typed makes it easy to write quick, dirty code to test some functionality, while giving users the option to make it more stable by adding type info later down the line. It also makes the syntax look simple and friendly for beginning users. Having a tagged union type undermines these benefits by forcing users to worry about whether an object is a union before they can use all of its functionality. Basically, when using the explicit sum types with gradual typing, you can make an assumption about what type of union it is (e.g.
Now every time I want to use the return value from do_something(), I have use a match statement. Suppose I want to
Remember that because we are assuming that Also note the use of a brand new Anyway, here's the alternative:
If we want to add type safety down the line, we can do that:
Note that with algebraic type hints, we don't need any special operator for checking discriminators, since Godot's Someone still committed to using tagged unions might argue that in order to reduce the amount of boilerplate, we should add implicit casting rules. For example, if tagged unions are only allowed to hold one data per union variant, we could allow unsafe casts to any of it's variant's types (e.g. However, moving data into unions would still be tedious. One of the main advantages tagged unions allow over algebraic type hints is that they would allow distinguishing between different cases of the same type (e.g.
However, I suspect that more often, it will make more sense to simply define the Tagged Unions and GenericsA short term concern about using tagged unions instead of algebraic type hints is that GDScript currently lacks generic types. In my opinion, this is a significant limitation with the language's static typing system which should be addressed at some point, but that's outside the scope of this issue. Nevertheless, I bring generics up here because a some of the most salient use cases for tagged unions, including the Again, my point is not that GDScript should never have generics. I think it should and I agree with @julian-a-avar-c that algebraic types and generic types would be complementary (in fact, by relaxing the assumption that GDScript type hints correspond one-to-one with a specific types, we may even carve out space for Java-style generic wildcards, which may be necessary for creating ergonomic, type-safe signals). As things stand now though, GDScript doesn't have generic types, and adding tagged unions without generic types would be underwhelming. Anonymous Sum Types can be used to implement Explicit Sum typesEven though I think adding tagged unions to the engine's main APIs would make them unnecessarily tedious to work with, I do think that they have legitimate uses which the algebraic type hints don't cover directly. The most notable example, pointed out by several people including @olson-sean-k, is the case of
I'd like to call attention to a couple of salient details about the
On the question of nullability@chanon's original post was concerned with Godot's lack of nullable types. In this thread, there have been a variety of proposed solutions to this. I agree with the people who have suggested treating nullable and sum types as a unified concept, albeit not by adding an @Bromeon has voiced two arguments against treating union and nullable types as a unified concept:
I agree that nullable types occur more frequently than other union types and therefore deserve a special syntax I'm not sure I agree with @Bromeon's second concern, which is that there's no need to add a |
@unfallible I also think that first-class types would provide a more universal and reliable solution to many of the problems with Godot's type system, including nested types (like See also my gist. Instead of inheritance, we could use type parameters. That is, the union type As for property info hint string, I think we could introduce the FQTN (Fully Qualified Type Name) string format instead. The I plan to write a separate proposal about the unified type system with a description of the current state, problems and a possible solution as I see it. |
@dalexeev Thank you for bringing your I want to clarify that what I was calling a I was envisioning the compiler doing something like the following:
Here's an imperfect analogy for thinking about what I meant by My concerns were primarily directed at the ergonomics of Rust-style enums in GDScript, particularly when writing untyped code. Again, I haven't gotten the chance to properly dig into the code in your gist yet, so I my concerns would be applicable to your solution, but I do want to make sure people understand what I was suggesting. Update:I've had a chance to study your I kept emphasizing that What I do care about is how sum types should behave. In Rust (and maybe Swift? I've never done anything in Swift, but a cursory google search suggests it works the same way), if you want to assign a value to an enum, you must explicitly specify the enum variant you're assigning. For example: My main contention is that both of these demands are too onerous for GDScript. GDScript is supposed to let developers mostly ignore the type system in order to make the language easier to learn and and develop in. Since Rust enums force you to interact with the type system to perform simple actions such as assignments, they would be a bad fit for the language. In my view, the following should be type safe in GDScript:
Maybe someone knows something I don't, but I don't see a way to recreate this syntax without implementing a convoluted set of rules for implicitly casting into tagged union variants. I'm interested to hear others views on all this though. |
For me this would be more beneficial for type safety when handling nulls simplified example of possible null values from inputs or array indexing but null vs Object return types in my experience coming from using Typescript are very common, and would be great to safeguard against null runtime errors better @export var item_data: ItemData | null # Can be a null here since the user can leave this out in the node tree (weather accidentally or on purpose) func grab_slot_data(index: int) -> SlotData | null:
var slot_data: SlotData | null = slot_datas[index]
return slot_data Unless im missing another way to mark nullable values, in that case plz let me know thanks :) |
@deniszholob That is under work, iirc it's under review? Issue #162 & godotengine/godot#76843. The approach that is being taken right now is nullable types as its own implementation separate from union types. As far as I can see, we are moving away from implicit nulls 🎉 |
Bugsquad edit: This proposal is a superset of #162.
EDIT: Tried to format it according to the 'template'.
Describe the project you are working on:
I was working on a game using Godot. (Have since moved to another engine due to many issues with Godot.)
Describe the problem or limitation you are having in your project:
(Original text)
The main reason I want it is due to Array and Dictionary type not being able to receive null values and I want to be able to use null values as the "default" value for functions.
This is a very common pattern in my pre-existing code and without this ability I'm not sure what to use as "default" values. Empty arrays and dictionaries? That could cause needless memory allocations I think. Also, there could be a difference in meaning between passing an empty array and passing null to a function, for example. Also it is simpler to just use null and compare/check for null.
Dictionary is used as an 'anonymous object' very often, so it makes a lot of sense to allow null. Since the type does not allow it, union types could help.
This is the biggest issue that makes me not able to use Typed GDScript more in my code.
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
Union types would be written as
Type1 | Type2
in place of the type.Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
Examples:
If this enhancement will not be used often, can it be worked around with a few lines of script?:
The workaround is to not specifiy types and thus lose type safety.
Is there a reason why this should be core and not an add-on in the asset library?:
It is part of GDScript which is core.
The text was updated successfully, but these errors were encountered: