-
-
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 array unpacking/destructuring to GDScript #2135
Comments
I would go for this syntax personally as it reminds me of JavaScript's destructuring syntax: var [positions, angles] = node.get_points() I find the other syntaxes much less readable, especially |
Some way to make that syntax type safe would be nice too. Maybe using the var [ positions as PoolVector2Array, angles as PoolRealArray ] = node.get_points() |
@pycbouh Couldn't we reuse the existing type declaration syntax? var [positions: PoolVector2Array, angles: PoolRealArray] = node.get_points() Either way, I wonder how inferred type ( |
I don't mind either way, I just thought Inference may be the tricky part. Especially if some of the values can be |
var [positions:, angles:PoolRealArray] = node.get_points() which means |
In theory, it should still be possible to mix existing variables with introducing new ones. If a variable is already declared, it becomes an assignment only. |
hmm .. [a, b, c] = x #a,b,c were declared earlier
[var a, var b, var c] = x #a, b, c are new vars
[var a, b, var c] = x # var b was declared earlier
[a:int, b:, c] = x #a= int, b is inferred, c is Variant I see no problem writing btw var a, b, var c = x
var positions, var angles = node.get_points() vs [var a, b, var c] = x
[var positions, var angles] = node.get_points() |
Do I understand correctly that the implementation of this proposal would allow to implement for item, value in enumerate(array) and for key, value in dict.items() ? |
Yes, though adding those iterators to the standard classes can be a separate proposal if it suits. |
I don't think that the iterator syntax has anything to do with unpacking/destructuring. It should be a separate proposal, if it doesn't exist already, and it's probably easier to implement as well. |
Iterators would just need to return arrays for them to work with this proposal, they'd just be one application of the syntax. |
Another usefull feature related to this would be dictionary destructing |
This comment was marked as off-topic.
This comment was marked as off-topic.
@blipk Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
It has been brought to my attention that C++17 has this syntax for tuples: (matches third suggestion in OP) auto [x, y] = z; Rust has a practically identical syntax let (x, y) = z; Surely we don't want our scripting language to lose on ergonomics to C++ and Rust, right? 😛 |
I like this proposal as JS destructuring is quite nice. If we go this route we should include the rest operator as well:
|
I would love to see a destructuring and pattern matching syntax similar to the Rust struct Person{
name: String,
age: u32,
height: u32
}
let ferros = Person{
name: "Ferros".to_string(),
age: 19,
height: 1.80,
};
let Person{name, age, ..} = ferros;
println!("{name} {age}");
let john = Person{
name: "John".to_string(),
age: 22,
height: 1.76,
};
if let Person{age: 22, name, ..} = john{
println!("{name} is 22 years old");
}else{
println!("not 22 years old");
}
let vec2 = (10, 20);
match vec2{
(0, 0) => println!("zero"),
(_, 1) => println!("Y is one"),
(1, y) => println!("x is one and Y is {y}"),
(x, y) => println!("it's not zero or one, it's {x} {y}")
} |
That is a dynamically created C struct! If you could declare it seperately (even in the same .gd, so it has the same scope), this would mean GDScript supports C structs (basically, variables which contain exclusively variables) So this code would be the same #OG
let struct_instance = { a: 100, b: false ... myObject};
#Same as the below
var struct_template: struct = { a: int, b: bool ... myObject: myObjectType}
let struct_instance = struct_template.new( 100, false, myObject) Note that the existence of the above helps also in netcoding, as you can send RPC arguments which are not primitives (see #6216) |
I just want to point out that while destructuring itself is nice, array destructuring could actually lead to an anti-pattern. It basically encourages the use of untyped array as a makeshift tuple type. This is fine for untyped languages, but is a step in the wrong direction for GDScript which is gradually moving towards type safety. Destructuring requires first-class tuples first. Or structs, which is essentially the same. Technically, in current GDScript, you can just define an inner class, and then return an instance of it (A much better pattern than abusing arrays or dictionaries). So, destructuring assignment for classes is also an option. |
Instead of necessarily dealing with arrays, I think we could defer to only allocating one if the "spread" syntax is used, like: var [a_cool_variable, ...everything_else] = a_multiple_returning_function() In this context, The signature for the function func a_multiple_returning_function() -> [String, int, Vector2]:
return ["Hi!", 42, Vector2.ZERO] I think we can get away with not allocating anything unnecessarily. |
@joao-pedro-braz A signature like If tuple types are a prerequisite for this proposal, at least one tuple type proposal should be linked from it. |
GDScript is a fully dynamic language that is bolting on gradual typing. We don't need to obstruct this proposal with typed tuple prerequisites, it is sufficient to leave room in the syntax to allow for them.
Surely if the goal was to never encourage anyone to write untyped GDScript, documentation would be a top priority. So no, tuple types are not a prerequisite for this proposal, they are merely a mutually beneficial yet still independent feature, and impeding either for the sake of the other is just putting up with a worse GDScript for longer. |
@SlugFiller
Doing both simultaneously would indeed require a new type "Tuple" which would (likely) be a subset of Array, but heterogeneously typed (and fixed sized, but that's the same as it being read-only). The issue with this approach (which is not what I'm proposing, more below), besides the lack of consensus, is that adding new types is costly due to the way Variant works. We would have an easier time getting a solution merged if it didn't involve extending the base Variant class. What I am proposing is the de-structuring facet, which is essentially syntax-sugar for: match note.get_points():
[var positions, var angles]:
# ... But without an intermediate Array (unless the spread operator is used). The The func multiple() -> (String, int, Vector2):
return ("Hi!", 42, Vector2.ZERO) |
I guess we could be conservative here and just add the de-structuring "operator" for Arrays, but since typed Arrays are homogeneously typed, the user would probably have to de-structure into untyped variables, which isn't very ergonomic. As @YuriSizov suggested, we could make these de-structured variables typables ( |
I think we can make this work in a really nice way by implementing it in two "phases":
As for the TBD topic:
var [foo, bar], original = [123, "123"]:
set(value):
# refers to "original = something"
get:
# refers to "print(original)", for instance
return original |
@Birdulon That's interesting. I'd be interested in knowing the discussion that lead to this choice. But I imagine a factor of it was not confusing users that are new to programming. Typeless is indeed useful for learning or fast prototyping. But should most likely be gradually reduced when moving towards production. There's also the question of when it was written. If it's a leftover from Godot 3, then typing would have had very different implications back then. @joao-pedro-braz func cool_func() -> [String, int]:
if randf() > 0.5:
return ["123", Node.new()] # Type mismatch
else:
return ["123", 123, 456] # More elements than expected
var [foo, bar, blah] := cool_func() # Too many elements in deconstruction The only difference is whether it's a first-class type or not. i.e., if this is allowed: var foo : [String, int] = cool_func() # First-class tuple type
var foo := cool_func() # If tuple types are not first-class, then this is an error, and you MUST use cool_func ONLY with destructuring
foo[0] = "abc" # Alternately, if we define a "virtual tuple" as a "read only array" rather than a "fixed size array", then this is an error. Although tuples being read only is actually perfectly reasonable, for the same reason strings are read only.
var a : String = foo[0] # But what about this? Should this be an error? Should it be allowed? Alternatives that don't involve a tuple type (first class or otherwise) would be:
func cool_func() -> Array[int]:
return [1,2,3]
var [a, b, c] := cool_func() # Perfectly fine. All are ints BUT, this will not work for the OP's request, since
func cool_func() -> Array:
return [123, "123"]
var [a, b] := cool_func() # a and b are Variant
var [c : int, d : String] = cool_func() # Error. You can't guarantee that cool_func will only return int and string
class MyClass:
var a : int
var b : String
func _init(p_a: int, p_b: String) -> void: # It would be nice if this could be auto-generated using some sugar syntax
a = p_a
b = p_b
func cool_func() -> MyClass:
return MyClass.new(123, "123")
var [a, b] := cool_func() # a is int, b is String
# But this is not really a huge save over:
var foo := cool_func()
print(a)
# vs
print(foo.a) I think @Birdulon is aiming at alternative 2 above as the desired form. But as I've said above, I believe it's an anti-pattern. It will get people into the habit of writing error-prone code. And the fact that the documentation is making the same mistake does not make two wrongs a right. |
@vnen Since this is on the roadmap, could you chime in? |
I think creating objects is a better solution to the problem this is trying to solve. var positions, var angles = node.get_points() it could be done as something like: var points: Array[Point] = node.get_points() or var points: PointSet = node.get_points() Godot and GDScript are built around object-oriented programming. There's also the return arguments' order. Forgetting it will cause problems: var angles, var positions = node.get_points() And if |
The "cure" of using objects is far worse in many cases than the problems you run into when you don't have unpacking/destructuring. Unpacking/destructuring is a common feature even in major mainstream object-oriented programming languages like C#, Kotlin, etc, and it is not out of place there. Forcing values that are packaged together solely for data flow/control flow reasons to be part of the same object does not aid in forming abstractions as OOP is meant to do; it's just paradigmatic overreach, like the NetBeanSingletonFactoryBuilderSpawners of yore. As for "but it's so bad in javascript": this already works in gdscript match statements, and nobody seems to have any issue with it there. Real tuples would be nice, though, unrelated to this proposal. |
Destructuring isn't only useful for function returns. var { var x: float, var y: float } = Vector2(0.2, 0.8)
prints(x, y) # Will print "0.2 0.8" Both of which can be nested together, as to allow a simpler "complex deep access", like: class FooBar:
var a_cool_property := "hello"
var another_cool_property := 123
var an_array := ["foo", "bar"]
# This
var { var a_cool_property: String, var another_cool_property: int, an_array = [_, var bar: String] } = FooBar.new()
prints(a_cool_property, another_cool_property, bar)
# Versus
var foo_bar := FooBar.new()
var a_cool_property := foo_bar.a_cool_property
var another_cool_property := foo_bar.another_cool_property
var bar := foo_bar.an_array[1] |
And why do you need to write This whole concept still seems to me like a solution to a problem that doesn't exist. |
@jtnicholl Array destructuring could be very useful for quickly naming things that are coupled together in array. For an example, when creating a command in some sort of developer console, and you want to destructure the command arguments and name them as variables: func give_player_item(args: Array):
var command_name: String = args[0]
var item_name: String = args[1]
var item_amount: int = args[2]
var item_data: Dictionary = args[3]
Logger.info(command_name + " executed")
var item = Item.from_name(item_name)
item.load_data(item_data)
$Player.give_item(item, item_amount)
CommandManager.add_command_callback("give", give_player_item) Using array destructuring, you can forget the func give_player_item(args: Array):
# array destructuring
var [ command_name: String,
item_name: String,
item_amount: int,
item_data: Dictionary ] = args
Logger.info(command_name + " executed")
var item = Item.from_name(item_name)
item.load_data(item_data)
$Player.give_item(item, item_amount)
CommandManager.add_command_callback("give", give_player_item) |
Object/Dictionary destructuring would be helpful too, not only with arrays. # Only grab the keys/properties that we need (the new `var` must be the same as the `key`/`property`)
# Destructure a Node3D object
func reset_transform(node: Node3D):
var { position, rotation } = node
position = Vector3(0, 0, 0)
rotation = Vector3(0, 0, 0)
# Destructure a dictionary
var dict: Dictionary = { 'a':1, 'b':2, 'c':3 }
var { a, b } = dict Not sure if it was mentioned (as I didn't see it) in the proposal or comments, but allowing destructuring in the function params could be an option too. # Parameter destructuring with an object
func reset_transform({ position, rotation }: Node3D):
position = Vector3(0, 0, 0)
rotation = Vector3(0, 0, 0)
# Parameter destructuring with an array
func set_position([x, y, z]: Array[float]):
position = Vector3(x, y, z) |
This comment was marked as off-topic.
This comment was marked as off-topic.
@Ury18 Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
Just to chime in, I arrived here looking for a simple "Go"-like error handling paradigm. My biggest issue with GDScript in production is in writing functions that could return errors. A typed Rust-like func join_server() -> Variant:
# ...
return [session, null]
# ... or
return [null, "no connection"] I'm okay dealing with the lack of typing by using assertions, but consuming the result of the function is not convenient without destructuring. var session_res = join_server()
if session_res[1] != null:
handle_err(session_res[1])
return
var game_res = join_game(session_res[0].game_id)
if game_res[1] != null:
handle_err(game_res[1])
return
# ... With destructuring, I could do: var [session, err] = join_server()
if err != null:
handle_err(err)
return
var [game, err] = join_game(session.game_id)
if err != null:
handle_err(err)
return
# ... An additional question though, does this pattern cause GDScript to allocate an array in the compiled code? |
I come from JavaScript / Golang, and I also follow this exact pattern of returning an array of values (Go doesn't quite do that, but it allows multiple returns which is visually very similar). In React it appears everywhere as e.g. const [ state, setState ] = useState(0) This would help clean up a lot of my code to be able to destructure the returns directly into variables as such! |
I would assume the initial implementation would allocate, and a separate issue could be created to optimize that case, but would not get addressed as a priority. An occasional extra array allocation here would likely not dominate your performance, and if you are doing something 1,000s of times per frame in gdscript maybe it'd be best to move it to a complied language anyways |
In godotengine/godot#77102 arrays are only allocated to slice the original array in a spread operator ( |
My two cents. var a, b, c
var a: int, b: String
var a: int, b: String = 3, "33"
var a, b := 3, "33" Personally I feel like the square brackets are not needed here. Encountering a comma in expression should be enough to transform expression result into a tuple. |
This comment was marked as off-topic.
This comment was marked as off-topic.
@repsejnworb Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
Describe the project you are working on
A rhythm game, though more importantly a project where it makes sense for certain functions to return multiple values.
Describe the problem or limitation you are having in your project
I have a number of functions that return 2-3 distinct values, packed into an Array in GDscript. A simple example would be one that returns
[PoolVector2Array positions, PoolRealArray angles]
.Currently, I have two options to unpack that:
And what I'm currently using where convenient, binding match:
Except it's not as convenient as it could be since it introduces new scope and two indent levels.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
Arrays will be able to unpacked on a single line similar to Python or Lua. It will be possible to assign (and sometimes declare) multiple variables on a single line to the inner contents of an array.
a, b, var c = [1, 2, 3]
will set existing vars a to 1, b to 2, new var c to 3.In the case of my earlier specific example, it would become:
Which saves the visual noise of both workarounds and the scope+indents of the binding match.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
Python and Lua have it a bit easier with implicit declarations, but there's still a number of ways we could have this with explicit declarations:
If this enhancement will not be used often, can it be worked around with a few lines of script?
Yes, it can be worked around with a few lines of script per call. This adds a lot of visual noise though as it can come up quite frequently.
It can also be worked around by using Dictionaries for everything, but that's an uncomfortable amount of overhead for such simple functionality, and is only a drop-in replacement for new variables being declared, not reusing existing ones.
Is there a reason why this should be core and not an add-on in the asset library?
It should be core to the language syntax.
Lacking this language feature isn't the worst thing in the world but having it enables other convenient features like python-style dictionary iteration.
Related issues:
godotengine/godot#23055
#1965
Additional keyword for anyone who uses the same search terms as me: Tuple
The text was updated successfully, but these errors were encountered: