-
-
Notifications
You must be signed in to change notification settings - Fork 20.3k
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
Less strict design for typed arrays #73005
Comments
I think it's reasonable because now I can't even export a deep nested element type like 3.x |
I agree that the current typed array design has quite a few flaws and feels unfinished. As a prominent example, covariance ("upcast" That said, where would the point of having a type declaration be if it's not reliable? If I can have an It will be harder to build robust interfaces, because the user needs to manually check whether an Fundamentally, GDScript is a very dynamic language -- there are tons of reflection APIs like |
In this theoretical design an element type is an access guard. Meaning that theoretically So it is not a "hint" as I worded it initially but a specified type (so easier typing of things) and a access type guarantee (so not just comment). |
But that would mean you would propagate runtime errors from the point of declaration until much later, when the array is accessed. If that element is not accessed, but e.g. appended to another (untyped) array, you'd never catch it at all. I personally don't think such a design would be better. Why not?
As I see it, the current approach is strictly more powerful in terms of type safety. |
It is backwards. Currently there is no support invariant/contraviant of types because they are stored in the array. |
I meant "invariant" as "enforced runtime constraint", not in terms of invariance 😉 |
No, you cannot, for that you would need to write such utility access functions for each type that you use, it is straight forward but not acceptable variant (how many functions would you need to write?). Also how would that work with typed arrays in native classes api?
Not everything is needed to be emulated. The question is what gives more usable and useful api. Current design of arrays will stay, it is too late to change it now. This issue is just a note for future and present when dynamic nature of gdscript meets rigidity of typed arrays and creates problems. |
You don't even need such a function, it's just convenience. You can simply declare a local variable with a type: var s: String = array[i] This already creates an error when the element type is
Exactly, and here is where I disagree 🙂 If I type a parameter explicitly as As mentioned, catching errors as early as possible (at the parameter level of a function) allows to enforce invariants that hold inside the function, and thus enables more robust APIs. I no longer need to manually check for contents if the type declaration gives certain guarantees. The opposite, propagating logic errors due to a weaker type system, can cause hard-to-find bugs and shifts more effort to the user (through testing, debugging, etc).
I agree that there are some remaining issues with typed arrays, but I don't think this is one of them -- what you describe can be solved quite well with the current untyped |
It's very difficult to use typed arrays as they are right now. In some cases, they create runtime errors on code that is logically correct, e.g.: var object = load("res://some_script.gd").new()
object.func_with_typed_array_param(["string1", "string2"]) # Error on this line Where some_script.gd is: func func_with_typed_array_param(_arg: Array[String]) -> void:
pass IIUC, array literals use context to infer which TypedArray to create. Since the type of Being able to assign an Array value to an Array[T] parameter/variable, which the proposal would allow, would make typed arrays much more usable. Analogous code where a Variant argument is converted to match a String parameter doesn't cause errors: var object = load("res://some_script.gd").new()
var x: Variant = "hello"
object.func_with_string_param(x) func func_with_string_param(_arg: String) -> void:
pass |
Got pinged, figured I'd link godotengine/godot-proposals#7364, where a discussion on desired behavior of typed arrays is ongoing :) |
The challenge is to allow that without significantly weakening type safety. Such code should not be allowed: var untyped = ["hello", 42]
var typed: Array[String] = untyped The solution to this problem is a more lenient/flexible interpretation of literals:
Note that |
TypeScript might be a good source of inspiration, since it also adds gradual static typing to a previously existing dynamically typed language, and is designed to be pragmatic. In TypeScript, array types can be converted freely: let arr1: number[] = [1, 2]
// Implicit upcast: allowed
let arr2: any[] = arr1
arr2.push("three")
console.log(arr1) // [1, 2, "three"] despite arr1 being number[]
// Implicit downcast: allowed
let arr3: number[] = arr2
function takeTyped(arr: number[]) {}
function takeUntyped(arr: any[]) {}
// All these are also allowed:
takeTyped(arr1)
takeUntyped(arr1)
takeTyped(arr2)
takeUntyped(arr2) I have worked with TypeScript extensively, and I don't recall any problems resulting from this unsoundness. It's definitely not a commonly encountered footgun. Note that TypeScript compiles down to JavaScript, which (unlike GDScript) does not attach any runtime type information to arrays. We might consider its removal in GDScript as well. |
Note that the relationship between TypeScript and JavaScript is not the same as typed GDScript and untyped GDScript. For TS, it all ends up being transpiled to dynamic JS in the end. GDScript however uses the type information to guarantee soundness even when mixed with a dynamic runtime. This creates a tradeoff where some code is slower to check types at runtime but others become quite faster by being validated at compile time. This is an expectation we set from the get go and we should abide by it. That's also why I don't like the idea of being loose with typed arrays. If we are removing the effects of typed arrays, their existence becomes almost pointless. They would do well in a complete typed code but break as soon as they interact with dynamic type land. So except for very local contexts there would be no type optimization possible, and even runtime checks would have to exist on completely typed code (so more of a performance penalty than he opposite). |
This can be done but invariably will have to spill into core code. We would need to differentiate a Variant Array typed value and an untyped literal. This would also hold only for actual literals: var untyped = [1, 2, 3]
var typed: Array[int]
typed = [1, 2, 3] # OK
typed = untyped # Not OK For the core this is all the same, that's why it will need some "mark as literal" functionality in the core Array. Unless we make the type be forced into untyped code: var untyped = [1, 2, 3]
var typed: Array[int]
typed = untyped # Now `untyped` is `Array[int]`
untyped.push_back("a") # Fails at runtime Which makes it workable in more cases but also creates more unexpected scenarios. I do think that having more flexible literals is a good compromise. |
Out of curiosity, aren't flexible literals already implemented to an extent? var n = 7 # int
var n: float = 7 # float
var s = "str" # String
var s: StringName = "str" # StringName
var a = [1, 2, 3] # Array
var a: PackedInt32Array = [1, 2, 3] # PackedInt32Array |
@vnen You're almost describing type inference here. Many languages have local limited type inference for literals. When you write This "superposition" of types is forced to be resolved later on, looking at the usages of this variable. By assigning it to another variable declared to be Array[int], the type of the "untyped" array is contrained to in fact be an array of ints. If there weren't any usages that resolve the type, a sane default of This is of course, just a possibility. But I think it might help solve many of your concerns without breaking type safety! |
Indeed, I was referring specifically for the case of typed arrays. This is actually also the case already for typed arrays, when the type is known:
But this isn't the issue. The issue is when the actual type ins't known at compile time: $SomeNode.int_arr = [1, 2, 3]
It is not exactly type inference, because the type isn't resolved at compile time (basically what I just said above in reply to Bromeon, it could be if the type is known, but I'm thinking in the cases where it isn't). For a user that does not add static typing, this might become unpredictable. The example I posted is self-contained for the sake of illustration, we can create a more expanded example that's not unlikely to happen in real codebases: # addons/some-third-party-addon/CustomNode.gd
extends Node
class_name CustomNode
var arr: Array[int] # my-untyped-node.gd
extends Node
var my_arr = [1, 2, 3]
func called_at_some_point():
$CustomNode.arr = my_arr # (a) # other-untyped-node.gd
extends Node
func called_at_some_other_point():
$MyUntypedNode.my_arr.push_back("a") # (b) If we change the type at runtime, the order of calling can generate one of two errors:
If this is chosen as a solution, then the case |
@vnen As your example illustrates, changing types at runtime is a recipe for confusion. I'm concerned that such an approach would create more problems than it solves. |
@ttencate I agree. While I don't like this approach, I still prefer to present multiple solutions so everyone can be aware of the implications. |
Currently typed arrays do work: there should be no memory corruptions, unexpected overwrites on variable assignments, unexpected inferred element types and so on. But they do so in a very strict manner, a typed array has a type info during runtime and any potential element of the array is tested for a match with the type. It allows to speed up type safe read access -
var a: int = ints[i]
- since an element type is guaranteed a type check can be skipped on such assignment. But it comes with the cost of performance for writing and, more importantly, with loss of flexibility of types.Inflexibility comes from a need to have all type info present at runtime, from how those types are represented and from underlying incompatibility between typed and untyped arrays. Some examples of such problems:
Array[Array[int]]
. Right now this is only about arrays, but then other built-in types can get type parameters and the limitation will be more clear.array.map(to_int)
return a typed array or not? That's a choice and because with current design they are incompatible it will always lead to clumsiness in some cases.Those problems would go away with more relaxed design. Instead of storing type information in the array itself keep type enforcement only to usage sites.
So, yes, the type will become less enforced:
Assignment of
untyped
tostrings
will not lead to an error by itself (like right now) but any consequent place where an element will be accessed and its type will be expected to be a string will produce an error. And with properly typed code many wrong assignments will still be caught during the analysis - sostrings = ints
will not need to get to runtime to see the problem.Maybe it is too late to have a discussion about such change since the release candidate is here, but at least it should be noted that there was such possibility.
The text was updated successfully, but these errors were encountered: