Replies: 19 comments 48 replies
-
I would lean toward making comparison of typed arrays require type equality, regardless of empty, but untyped not: var arr1: Array[int] = []
var arr2: Array[String] = []
print(arr1 == arr2) # False
print(arr1 == []) # True
print(arr2 == []) # True Treating typedness as a "weak trait" |
Beta Was this translation helpful? Give feedback.
-
Great write up vnen! I don't have any specific comments yet, but I do have a general note. I think it's important to consider how each of these decisions impacts whether For example, if we allow |
Beta Was this translation helpful? Give feedback.
-
I think typed array as a way to say "I swear I won't put anything other than TYPE into that array". So for equality, I like ignoring the type. Assignment: If the type is different, create a copy. But it can still be reference under the hood and copy-on-write. I don't think we should forbid modifying either array after the assignment, so they have to be separate eventually. And this plays nicely for arguments I think. |
Beta Was this translation helpful? Give feedback.
-
For the following case here:
Could it be possible to "double-reference" them? Say, through the assignment, Regarding equality, I think the array's type should be ignored outright. It is beyond the common user's expectation, and can be checked with other methods if necessary. On the surface and at first glance, there's no difference between [1, 2, 3] and... [1, 2, 3] but only integers. var children = get_children() # This is a Array[Node]
var my_array = [$Child1, $Child2, $Child3] # This is a standard, untyped Array
# Should this seriously print "false" because the array's types are not equal?
print(my_array == children) Furthermore, the operator in Godot 4 checks by equality, not by reference. Being that the expected behavior, requiring such precision from users that may not even make intentional use of the feature (through built-in methods returning typed arrays) may be excessive. |
Beta Was this translation helpful? Give feedback.
-
This may actually be the right approach. I think we should stick to the philosophy that Imagine I have an untyped script that looks like @onready var sprites = [$DeadSprite, $AliveSprite]
func _ready():
if not get_children() == sprites:
reset_sprites() If I later edit this to be a typed script @onready var sprites : Array[Sprite2D] = [$DeadSprite as Sprite2D, $AliveSprite as Sprite2D]
func _ready() -> void:
if not get_children() == sprites:
reset_sprites() I should not see any change in behavior. |
Beta Was this translation helpful? Give feedback.
-
I think this is an excellent design principle. See also e.g. #37312 where this principle was violated. This principle also rules out any solutions that require copying the array if types don't match, because changes to the copy aren't visible in the original and vice versa. Edited to add: how do we feel about adding type hints changing the behaviour of the code by causing runtime errors? Such errors would only be informational, the code should keep running as if they never happened. E.g. the current |
Beta Was this translation helpful? Give feedback.
-
Are there real use cases for approach 2? Java does this and it's tripping over even experienced developers. Short x = 7;
System.out.println(x.equals(7)); // false, because Short != Integer I'd heavily advocate structual equality, checking whether contents are equal. This is almost always what users would expect. Regarding dictionary keys, we should make sure that hashing and equality behave in a consistent manner. var a: Array = [1, 2, 3]
var b: Array[int] = [1, 2, 3]
# So these two dictionary accesses should be equivalent:
dict[a]
dict[b]
Approach 3 seems outright unexpected (introducing copy semantics for a pass-by-reference type in a very specific case). Apart from correctness, it can also introduce hidden perf issues. Approach 1 also doesn't seem ideal, because you move the problem from one boundary check (when converting) to an infinite number of follow-up checks (when accessing elements). I would recommend approach 2 because of correctness. As a user, I expect the declaration var b: Array[Sprite2D] to mean " As you mention correctly, it does mean there are cases where an explicit copy will be needed -- but I think that's better because people understand what's happening, and it's obvious to readers of the code that reference semantics no longer uphold. An error message could also give clear directions on what to do if that behavior is desired.
Fully agree on making argument passing behave consistently with assignment. Regarding this code in particular, I'd like to point to my comment godotengine/godot#73005 (comment):
This would mean that |
Beta Was this translation helpful? Give feedback.
-
Separate response because it's not directly answering the questions, but may need discussion on its own. I think most of the current issues with typed arrays can be reduced to one principle: use of covariance where contravariance is appropriate. To elaborate: Reading arrays is covariant:
var array: Array[int] = [1, 2, 3]
var read_only: Array = array
var elem = read_only[0] # this is always safe, result is Variant Writing arrays is contravariant:
var array: Array = [1, "string", false]
var write_only: Array[write int] = array # hypothetical
write_only[0] = 7 # this is always safe! GDScript however treats both reading and writing as covariant, while the latter is not. Kotlin approached this problem by having explicit variance specifiers |
Beta Was this translation helpful? Give feedback.
-
I agree that many problems are caused by the ambiguity of the
Equality I'm not sure about this, as the
For options 1 and 2, we can also add a warning. However, it should probably also be added at runtime (in debug builds), for untyped code. Assignment I think for the most part it's ambiguity problem of the As for the exception we made for Arguments I think it's the ambiguity problem of the |
Beta Was this translation helpful? Give feedback.
-
To the extent possible, it would be great if the following always either returned true, or failed to compile: var x : MyType = y
x == y In addition, it might be worth considering how important it is for the semantics of typed arrays to be consistent with other typed containers such as PackedArrays and Vectors. For example, how confused might users be if var my_packed_int_32_array : PackedInt32Array = [1, 2, 3]
var my_packed_float_32_array : PackedFloat32Array = my_packed_int_32_array behaved differently from var my_int_array : Array[int] = [1, 2, 3]
var my_float_array : Array[float] = my_int_array To clarify, I'm not saying that the above blocks need to behave identically. I'm just pointing out that if they do need to behave differently, it could be a point of confusion and the differences should be clearly explained and documented. |
Beta Was this translation helpful? Give feedback.
-
Here is a thought for handling typed array assignment: what if assigning one array to another caused the array of supertypes to be marked as "read-only". For example, if an array of subtypes is assigned to an array of supertypes: var node_2d : Node2D = Node2D.new()
var array_node_2d : Array[Node2D] = [node_2d]
var array_node : Array[Node] = array_node_2d # array_node is now read-only
array_node.append(Node3D.new()) # error: array_node is read-only
array_node_2d[1] # this problematic line doesn't run because of the error above If an array of supertypes is assigned to an array of subtypes: var node_2d : Node2D = Node2D.new()
var array_node : Array[Node] = [node_2d]
var array_node_2d : Array[Node2D] = array_node # array_node is now read-only
array_node.append(Node3D.new()) # error: array_node is read-only
array_node_2d[1] # this problematic line doesn't run because of the error above In this approach, declaring a variable as an I see a few advantages of this approach.
I'm sure there are issues with this approach, and I welcome others trying to point them out. One issue I can already see is with function arguments. Consider the following example. func append_new_node_2D(array : Array[Node]) -> void:
var node_2D : Node2D = Node2D.new()
array.append(node_2D)
func _ready() -> void:
var array_of_nodes : Array[Node] = [Node.new()]
append_new_node_2D(array_of_nodes) # this is fine, no array is marked as read-only
var array_of_node_2Ds : Array[Node2D] = [Node2D.new()]
append_new_node_2D(array_of_node_2Ds) # this causes an error, because the proposal forces the passed array to become read-only I'm not sure how fatal the above example is to the idea, or whether there are clever ways to work around it. |
Beta Was this translation helpful? Give feedback.
-
List when an array literal is treated as typed 1. Declaring a variable, constant, or function parameter: var a: Array[int] = []
const A: Array[int] = []
func f(a: Array[int] = []): pass 2. Returning an array literal from a function, if return type is specified: func f() -> Array[int]: return [] 3. Assignment, if the assignee type is known at compile time: var a: Array[int]
a = []
# Works with complex expressions like `obj_arr[0].a = []`,
# but the type must be known at compile time too. 4. Passing a parameter when calling a function, if the type is known at compile time: func f(a: Array[int]): pass
f([])
# Works with complex expressions like `obj_arr[0].f([])`,
# but the type must be known at compile time too.
# Doesn't work with `Callable`s obviously. 5. Type casting operator: [] as Array[int]
# Works only with literals. `a as Array[int]` will not work.
# In a sense, this is an alternative to the typed constructor (`Array[int]()`),
# but I don't recommend it, since type casting has different semantics
# and doesn't work with arbitrary expressions, only with literals. 6. The for x: int in []:
pass
# You can't access the array, but this limits the type of elements in the array. Notes:
|
Beta Was this translation helpful? Give feedback.
-
I believe I found a problem related to arrays: |
Beta Was this translation helpful? Give feedback.
-
Hi all! This discussion came up again recently on the rocket chat and I've learned a lot about typing since I first saw this post and commented on it. For anyone interested, I made some notes that summarize what I've learned from some Type Theory self study: https://gist.github.com/nlupugla/252a23e3c605c4c92c29eeed5d00c6bc One of my main takeaways related to typed arrays is that the issues that come up are basically all related to the fact that they allow both reading and writing. @Bromeon touched on this a bit when talking about covariance and contravariance. What I'd like to highlight is that the issues that come up with typed arrays also come up when dealing with a language that supports typed references. This is a simpler setting to work in as it removes the complexities of dealing with a container type and focuses the issue on just reading/writing. Let me explain with an example written in C++. Note: I'm just using the syntax of C++ for clarity. The actual C++ typing rules are overly complicated for our purposes. // Assume Dog inherits Animal and adds a "bark" property, which Animal does not have.
// References can be read from, so they are not contravariant
Dog dog_1;
Animal animal_1;
Animal *animal_ref_1 = &animal_1 // Animal reference to Animal value, totally fine.
Dog *dog_ref_1 = &animal_1 // Dog reference to Animal value, not good because not all animals are dogs.
(*dog_ref_1).bark; // uh oh, animal_1 can't bark: reference is not contravariant
// References can be written to, so they are not covariant either
Dog dog_2;
Animal animal_2;
Dog *dog_ref_2 = &dog_2; // Dog reference to Dog value, totally fine.
Animal *animal_ref_2 = &dog_2; // Animal reference to Dog value, what could possibly go wrong?
*animal_ref_2 = animal_2; // the referenced value (dog_2) has been replaced with an animal
dog_2.bark; // uh oh, animal_2 can't bark: reference is not covariant. The reason these kind of typing issues are typically discussed in the context of arrays/collections in scripting languages like GDScript is that they typically don't let the programmer be explicit about how single reference values are read to and written (hence why I switched to C++ syntax for the example above). One "solution" to the invariance of reference types is to have a read-only reference (sometimes called "const" or "source") and a write-only reference type (sometimes called "sink"). As @Bromeon points out, Kotlin applies this ideas to arrays by including the notion of "in" (sink) and "out" (source) arrays. This level of specificity is probably a bit much for GDScript, but I think the ideas can still be incorporated via e.g. smart defaults (such as making TypedArrays read-only by default unless explicitly marked as writeable or write only). TL;DR: The "problems" with typed arrays have nothing to do with them being a collection and everything to do with being a readable and writable reference. Thinking about how to handle references can simplify the problem and help pinpoint exactly where the issues come from and how they could be fixed. |
Beta Was this translation helpful? Give feedback.
-
Suppose the solution we end up going with is to provide a way to mark arrays as being read only in some way. Here are a few points to consider
Here's my personal thoughts, in reverse order: a) var a : Array[const Node] |
Beta Was this translation helpful? Give feedback.
-
Edit: it got explained to me. Original Comment: I find it really confusing, why it is not possible to assign a typed array to an array of a parent type.
I mean it is not too bad, as this is a possible workaround
but this really seemed like a bug and not expected behavior to me. |
Beta Was this translation helpful? Give feedback.
-
First, we should determine whether array is reference type or value type. For Equality:
As reference, I just care memory address. Obviously, A and B are not the same object. So I want here print false. note that the syntax design of gdscript is based on Python. Many people choose Godot because they already know how to use Python. So consistent is better. For Assignment
Why do we need typed arrays?Because we need the compiler to tell us what is wrong. Because OOP principles are consensus, we don't need to think too much. case a = b:
First should print true. because a and b use same memory address.
Second should print false. because a and b are different array. VariantVariant , just comprehend like every class is extend from Variant. Question about []
In this case, I think "==[]" is not mean a == Array.new().
|
Beta Was this translation helpful? Give feedback.
-
I did not see this mentioned above, but I found a bug with typed-arrays, regarding Types that inherit from other base types. As an example:
Since the custom type |
Beta Was this translation helpful? Give feedback.
-
The addition of typed arrays seems to be welcomed by many people, but unfortunately we (mostly I) did not have the proper time to polish before 4.0 was out (and now 4.1). There are some issues that depends a lot on how we expect typed arrays to behave, as they have some quirks compared to regular untyped ones, and the we didn't reach a consensus on how they should work. There are multiple conditions that aren't clear which would be the most intuitive and thus the best option.
So I'm listing here a few things that are problematic with typed arrays, especially in relation to what's expected from untyped ones, and the options to solve the problems (or, rather, the trade-offs of each option).
This has become quite long, sorry. I wanted to be thorough in the examples and conditions so it's in everyone's mind when potentially making a final decision.
Equality
Arrays are compared by value:
They aren't the same reference but have the same contents, so they are considered equal. In this case, both are Variant arrays, so they have the same type, the question arises for typed arrays when they don't share the type:
Assuming they are still compared by value, there are two options:
Consider also this for the empty array case:
If ignoring the type, they are equal, otherwise they are different. Also, consider that even if
a == b
istrue
,a = b
is an invalid assignment given the incompatible types.This can be used in the of checking if the array is empty, or if it contains specific elements:
The weirdest case, while still technically possible, is when using arrays as dictionary keys:
Note that there is a proposal for typed array constructors, which can mitigate some of this (by requiring you to use them), but will also incur in an extra array copy just for the comparison which is quite an overhead.
Regardless of the choice, we can always provide a function to do the other type of comparison. While inconvenient it works for most cases (not for dictionary keys for instance). It should be the least used option to become a function though.
Assignment
This is probably the most difficult to grasp and solve. Let me try to show it with an example:
Consider that arrays are passed by reference. Given the underlying reference (
a
) carries elements of typeNode
, not all of them can fit into the type ofb
. In this example they do, but there's no future guarantee that it will stay so. Further restricting the underlying reference is not feasible because it is still of typeArray[Node]
and you wouldn't expect it to fail to add Node of some other type.Also, consider the case
a = b
. All elements ofb
can be included ina
given the types. However, since the underlying reference is stillArray[Sprite2D]
, you cannot add other types of Node, even when usinga
as the base. This can become confusing since the compiler would be happy type-wise but it would fail at runtime.For one more example, take this:
In this case,
b
is untyped. This can be treated the same way as the previous example if you think an untyped array is the same asArray[Variant]
. But given Variant itself is not an actual type, just a catch all for any type, it is a bit special, so it could be treated differently. Even so, if assigning a non-Node tob
in this example would also compile fine type-wise but fail at runtime.Given this, we have a few options:
Note that the examples are very specific which looks obvious but there other nuances especially when considering the boundaries between typed and untyped code (where GDScript cannot see the original type of the value. For instance:
One can assume that since
node.a
is ultimately an Array, the assignment should be allowed. Or you can think thattmp
is anArray[Variant]
and given the type mismatch the assignment should fail (all of this at runtime given the lack of information at compile time).This also applies to the reverse case, when assigning to the property, the type of the array might be conflicting, and this might happen in completely untyped code when interfacing with the engine API or typed addons:
Given GDScript is still dynamically typed, this can become quite inconvenient if forbidden.
Note again that typed array constructors will eventually be included, so "forbidden" can also mean "make an explicit copy":
This forces the user of untyped code to think a bit about types, which is not ideal, but it gives a compromise. There could be another way to create an explicit copy without actually spelling out the types.
Arguments
Similar to assignment, there's the question of how to treat array types when passing them as function arguments.
The options are the same as the ones for assignment and arguably should use the same solution for consistency. But these cases can happen more often in untyped code than with properties, so it has the potential of becoming more annoying in general.
To also keep this section with an example, consider this:
While this is pretty much the same as the assignment, I wanted to make it clear that this case needs to be considered as well when making the solution.
Conclusion
While sometimes it might seem "obvious" what the proper behavior should be, there are a lot of nuances and special cases hard to notice in a cursory look. I think it's important we consider the potential cases before jumping into a solution.
Also keep in that this lays some groundwork for potential future features like typed dictionaries and, perhaps, full-fledged generics (not promising anything).
I want to hear the thoughts of the community about this so please do comment with your preferred solutions and reasons.
Beta Was this translation helpful? Give feedback.
All reactions