-
-
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 generic parameters to allow strongly typing for collections #1207
Comments
I agree this is useful for containers like Array and Dictionary, however I don't think it makes sense for Vector classes. The only two real possible types for Vector components are int or float and there is really no good reason that you cant just assign an int as a float. If you really do want to differentiate here, I'd prefer just having a second explicit type for ints, like Vector2i. That said, I believe I read somewhere that typing for Arrays and Dictionaries was coming in gdscript 2.0 (though I don't know if it will take the form of generics or not). |
@jonbonazza The issue is I would like to be able to create my own generic classes and this is a good way to do it |
Unfortunately, generics are not possible to implement in a dynamically typed language. They don't really make sense either, as variables don't have an underlying type. Since types are optional in gdscript, generics are simply not something that can be added due to technical limitations. |
That said, you can utilize dynamic typing to get the same effect. For instance:
In this example, |
While it's true that it won't be enforced language-wise, it's still possible to add it to the annotation system (see python as an example). One use-case that I keep getting is in exports, right now there are two different syntax options for exports # Type annotation
export var color: Color
# `export` function, this case is not possible without generics
export(Array, Color) var colors Adding generics to the annotation system would allow always using the type annotation syntax which will eliminate |
@Tadaboody You should be able to export PoolColorArray (PackedColorArray in 4.0) for this particular use case. |
@Calinou And for the case of |
@Tadaboody There's PoolIntArray and PoolVector2Array too. A typed "generic" array will be different from a pooled array (these were replaced with packed arrays in 4.0). Pooled arrays are mainly suited for storing large amounts of elements (thousands of them), but you can also use them for smaller arrays to get type safety. In contrast, a generic array will always hold a Variant which uses more memory. This will probably be the case even if type hints are used. Still, GDScript will most likely feature typing for "generic" arrays in 4.0. This is mainly useful for typing arrays with user-made types. See also the core refactoring progress report #1. |
so what about generic pool_arrays?
the main issue is not just lack of generics but lack of support for static typing in alot of things |
Static typing is just optional hints in gdscript. It never will be more than that, so you never will have all the luxuries a true statically typed language will afford. |
That said, dictionaries could support gebrics since tgey are implemented in the core, and i would agree that should be considered for gdscript 2.0, however custom type generics in gdscript simply don't make sense, so don't count on them being implement. |
@jonbonazza I know gdscript will never be a statically typed language but having the option is a good thing limiting the user to what they can do in gdscript is never a good thing. EDIT: btw this is what I think one of godot's biggest flaws are is the lack of support for static typing. |
I just want to mention if this gets added I REALLY think it should be with <> instead of () |
I would like to advocate in favor of generic syntax for user scripts, that is, generics implemented with type erasure, like in java and typescript, where the generic type info is lost at runtime. See typescript: since it became such a popular language, the benefits of compile-time/parse-time static analysis of dynamic typed languages cannot be denied. I admit that in the specific case of typescript generics is such a small part of the whole package that it would still be a great language even if it wouldn't have generics at all. But the thing is that the programming community has been taking great advantage of generic syntax since C++98, and again with java 1.5 in 2004 and again in many other languages that support generic syntax, be it as a code generation template processing engine as in C++ or only as a compile time type check in java or with full runtime with generic type introspection like in .net. Even though runtimes like .net provide runtime type safety for generics, I argue that the runtime safety is less important than the compile-time/parse-time type safety given by the syntax alone. Letting aside container types, with generics in gdscript we would have increased type-safety for custom user types. For example, I could model a generic weapon class where the generic type can be any ammo class, or a generic powerup class where the generic type is any behavior class that can benefit from that powerup. Even though we could perfectly model and implement such examples without generics at all, the point is that with generic syntax we get compile-time type safety, which is the whole point. The type hints introduced in 3.1 are the same: even if there would never be runtime type safety, the bulk of the benefit for the developer is the parse-time errors he gets while developing, because if he just want runtime errors, he can already assert() conditions of arbitrary complexity, not only just type checking. |
So, I really wanted to add here because I think that one of the greatest features of generics wasn't truly addressed here. Recently I really felt the need of using it for better developer experience. To illustrate it, I'll use a generic "component system", but I can see this working for many other cases such as State machines. # First scenario: how it would be done at the current state of the language
(get_component(StatusComponent) as StatusComponent).physical_damage
(get_component(HealthComponent) as HealthComponent).value
(target.get_component(HealthComponent) as HealthComponent).apply_damage(
(get_component(StatusComponent) as StatusComponent).physical_damage
)
# Second scenario: using return type generics
func get_component<C>() -> C:
for component_node in components:
if component_node is C:
return component_node
return null
get_component<StatusComponent>().physical_damage
target.get_component<HealthComponent>().apply_damage(
get_component<StatusComponent>().physical_damage
) I made a point of explicitly showing how # Component.gd
# [...]
# This would be needed for each subclass of it
static func get_component_name() -> String:
return "Component"
# ComponentsManager.gd
# [...]
func get_component(_component: GDScript) -> Component:
for component_node in components:
if component_node.get_component_name() === _component.get_component_name():
return component_node
return null I not only have the function, as now I need a static method on the base class that I will be overwritten every time I make a new component (as I said, this is just a generic example). |
What about variance? Unless we figure out that can of worms, I don't see a reliable output out of this issue. Now... we don't even have generics, so why worry, because, it will eventually be a problem for someone somewhere, so it's best to discuss it. # I know a lot of you want `House<T>`, but we already have `Array[T]`.
# No need to change what ain't broken.
# Since we don't have class generics, let's use arrays for now, since those work similarly.
# Every time you see `Array`, imagine `House`)
# class House[T]: var data: T
class Animal:
var name: String
func _init(_name: String):
self.name = _name
class Dog extends Animal: pass
class Cat extends Animal: pass
# Given a cat, give me their residency
func where_is(cat: Cat) -> Array[Cat]: return [cat]
func _ready():
# This is fine of course...
var favorite_pet : Animal = Cat.new("Felix")
# Looking good...
var cat_house : Array[Cat] = where_is(favorite_pet)
# This should not compile.
# We currently get a runtime error, which is better than nothing?
var animal_house: Array[Animal] = cat_house as Array[Animal]
var animal: Animal = animal_house[0]
# But why? It doesn't look dangerous?
# Dogs can go in animal houses...
animal_house[0] = Dog.new("Fido")
print(animal_house.map(func(animal): return animal.name))
# ... Ah! we defined our `animal_house` to be a `cat_house` :facepalm:
# If we did this...
var housed_cat: Cat = cat_house[0]
print(housed_cat)
# ... we get "Fido"... He's not supposed to be a cat tho I actually never tried this in GDScript... I should probably file a bug on I'm no expert, but I am aware of this issue in other languages, and how it is usually solved: Adding a variance marker: # How it is done in Scala, `+`/`-`
class House[+T]: var data: T
# How it is done in C#, `out`/`in`
class House[out T]: var data: T If you don't include it, the default is invariant, which I think is nice. I prefer the Scala version because types can get hard, and sometimes the meaning of the two markers flip, because variance is hard. That said, the best option, might just be to make all types invariant, meaning that this would never compile or run: By the way, if after reading this you think that we can just not have generic types Adapted from https://docs.scala-lang.org/tour/variances.html |
Another topic to cover in relation to generics, is how do we want to implement constraints: class Animal:
var name: String
func _init(_name: String):
self.name = _name
class Dino extends Animal: pass
class Fish extends Animal: pass
class Tuna extends Fish: pass
# Scala style
class House[T <: Fish]: var data: T
# Java style
class House[T extends Fish]: var data: T
# C# style
class House[T] where T : Fish: var data: T
# ... there are more I think using the I'm not even sure if it would be worth it to add constraints, but it is something to consider. |
I feel that introducing new operators for this is overkill. I think just using |
While we wait for "Generic types" to be added to GdScript, we can achieve the exact same functionality (Base class being "Type agnostic" and specific instances being "Type safe") with just a bit of boilerplate, here you have the example for a very basic implementation of First we need to define a "Type agnostic" version of what we want to do: # generic_pair.gd
class_name GenericPair
var key
var value
func _init(key, value):
self.key = key
self.value = value
func get_key(): return key
func get_value(): return value This is the base class, and it is NOT meant to be used "As is", just think of it as an Then we have the type-safe extension of the base class: # int_string_pair.gd
extends GenericPair
class_name IntStringPair
func _init(key:int, value:String): super._init(key, value)
func get_key() -> int: return super.get_key() as int
func get_value() -> String: return super.get_value() as String Bear in mind that you need to create one implementation per each differently typed Finally here is how its usage looks like: # We do this instead of `var int_string_pair := Pair<int, String>.new(3, "The third")`.
var int_string_pair := IntStringPair.new(3, "The third")
# typed_key is resolved as `int`, not as variant.
var typed_key := int_string_pair.get_key()
# typed_value is resolved as `String`, not as variant.
var typed_value := int_string_pair.get_value()
# Fails due to incompatible types:
var wrongly_typed_key: Vector2 = int_string_pair.get_key()
var wrongly_typed_value: Vector2 = int_string_pair.get_value()
var wrongly_typed_pair := IntStringPair.new("The third", 0) It is a bit verbose and requires to create an extra class for every type permutation used... but you will achieve the exact same behaviour and benefits of the "Generic types". Although it will be GREAT if we can save all that boilerplate and have native support to it so that we can save the boilerplate. Note: How can we implement constraints with this work-around? If the base class ( |
The whole point, IMO, is to eliminate boilerplate code. Both to improve ease of development and reduce the possibility of bugs creeping in over time. Gnumaru's comment in October 2021 echoes a lot of my sentiment, though if we can get more than what he's asking for, I'm obviously all for it. |
Maybe we can at least start with (if I understand it correctly and not mix things) template classes used in C++? |
Can we not have C++ style generics, AKA template classes? It's a hole that's difficult to climb out of. |
Do you mean the syntax or the specifics of having each template compiled only when needed and fully individually? |
I don't have much experience with C++, but just thought that at least generating code, perhaps, not that hard as implementing full-blown generics, - and we would all benefit from at least this very much. However, if there are serious drawbacks, I agree that implementing templates may worsen the situation because of compatibility commitments (the engine would need to continue supporting this thing even if it's bad). Again, this is just a cheaper (I hope) alternative. If we are choosing between no generics at all and at least something - I would prefer at least something (but it would be great to have real generics, of course)
More of the latter. I think that generating code is sort of syntactic sugar that is easier to implement, and maybe this is even easy to turn on / off with some flag in the editor. I mean, maybe this could be a small "patch" to not keep everyone waiting for ages while the Godot team works on the real good implementation of that |
Sorry, let me try being clearer, I'm concerned about type safety. I'm against some simple text processing like C++ templates. @AThousandShips @smedelyan |
For my part, I think Java-like type erasure should be the bare minimum to aim for. |
For another use case, I'm commonly having to wait for parameters to be set in a networked game. I'd really like to have a generically typed container of a single value. class_name Synced<T>
var data : T
var is_valid := false
signal is_set()
func get():
if !is_valid:
await is_set
return data
func set(value: T):
data = value
is_valid = true
is_set.emit() Just wanted to throw this example out there because the word "collections" in the title might imply more than one value, although I don't think it really makes a difference. This example would also benefit from the terrible-but-yet-useful code generation without being terribly impacted by type safety. With only one value in the container, it becomes a lot simpler to deal with the generic type. I'm also quite used to Java where I have things like |
To add my own use case to the pile, I'd love to be able to do something like.. class_name Observable<T> extends RefCounted
signal on_change(v: T)
var value: T:
set(v):
if v != value:
value = v
on_change.emit(value)
func _init(v: T) -> void:
value = v
# Assuming Callable gets more type information with an arbitrary
# number of param types and its return type specified last..
func observe(callback: Callable<T, void>) -> void:
on_change.connect(callback)
callback.invoke(value) As is, I've had to implement my own |
In my opinion, it would be preferable to utilize square brackets ( var arr: Array[int] or in the 4.4 version: var dict: Dictionary[String, int] Therefore, a generic function could be defined as follows: func generic_function[T](some_variable: T):
pass
func generic_function[T extends Node](some_variable: T):
pass While I understand that there may be alternative perspectives, I believe this approach aligns more closely with consistency of existing features in GDScript language. |
IIRC it's not strictly generics under the hood, but syntactically, yeah, your logic checks out. I'm largely indifferent to the bracket choice, though, as long as it gets implemented at all! 😂 |
Yeah, I'm on the same page as @DaloLorn I used angled brackets in my example because that's muscle memory from years of Java, but I'm indifferent to whether Godot adopts |
Just to put one more voice on this, I think generics are one of the most important language features yet to be implemented. They are broadly and extremely useful when attempting to write type safe, reusable and extensible code such as add-ons and libraries (which often do not know which types will exist in the projects using them). Beyond the problem this causes me individually when attempting to write code usable across multiple projects, this also means the lack of generics limits people's ability to write re-usable and sharable GDScript code which could be benefitting the whole community. The closest workaround is as @AlbertoRota suggested, but this involves quite a bit of boilerplate and since abstract classes do not exist in GDScript (#5641 ) it is still prone to error if the base class (intended as abstract) is accidentally used directly. One other use case I have is to allow type safe null responses by making a Nullable (similarly to @shrubbgames's Synced and @FabriceCastel's Observable). I'd use this if this proposal was completed before the nullable types proposal (#162 ) as this would be a reasonable workaround until then, though it would have to be class based until structs are implemented (#7329 ). Would it be helpful to create a summary here of the questions yet to be answered (from here and #10188 which was consolidated into here), or is there some other place where that kind of discussion usually takes/is taking place? I'm not in a position to work on such a significant PR myself but I am keen to see this happen, so if I can support it in some other way I'd be happy to. There are minor issues raised here such as which bracket type to use, but also issues which have a larger impact on scope and implementation such as variance and constraints. Another minor thing: the issue title here specifically mentions collections, while the discussion has evolved to encompass full generics (to include generic classes, methods, properties, etc.). Since #10188 was consolidated into here as well, it doesn't seem to represent the current scope. Is someone able to update the issue title to better represent this? |
Describe the project you are working on:
A space ship battle game
Describe the problem or limitation you are having in your project:
Just the general lack of static typing features in the gdscript language for some things
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
I was wondering if gdscript could get generics simular to C#..
it would allow us to make classes the require a type to be passed like so..
func foo<T>(t:T)
they could also get constraints to allow static typing
func foo<T>(t:T) T : int, CustomClass
this could be used to do things like so..
things like so wouldn't be allowed..
but things like so would..
Things to note..
<>
to remove confusion between()
which is used for calling a functionDescribe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
see above
If this enhancement will not be used often, can it be worked around with a few lines of script?:
this would be used whenever someone wants static typing for a collection like a array or dictionary or numeric struct like a vector
it can be worked around but it takes a bunch of type checking for everything you do and this easily because unmanageable and ugly
Is there a reason why this should be core and not an add-on in the asset library?:
its another static typing feature which I think is good
The text was updated successfully, but these errors were encountered: