Skip to content
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 a Trait system for GDScript #6416

Open
vnen opened this issue Mar 3, 2023 · 242 comments · May be fixed by godotengine/godot#97657
Open

Add a Trait system for GDScript #6416

vnen opened this issue Mar 3, 2023 · 242 comments · May be fixed by godotengine/godot#97657

Comments

@vnen
Copy link
Member

vnen commented Mar 3, 2023

Describe the project you are working on

The GDScript implementation.

Describe the problem or limitation you are having in your project

The main to reuse code in GDScript is via inheritance. One can always use composition, either via instances stored in class variables or as nodes in the scene tree, but there's a lot of resistance in adding a new prefix to accessing some functionality in the class, so many go to the inheritance route.

That is completely fine if the inheritance tree includes related scripts. However, it's not uncommon for users to create a library of helper functions they want to have available in every script, then proceed to make this the common parent of everything. To be able to attach the sub-classes to any node, they make this common parent extend Node (or even Object) and rely on the quirk of the system that allows attaching scripts to a sub-class of the type it extends (cf. godotengine/godot#70956).

The problem with this usage is that a script gets a bit of "multiple inheritance" which is not really supported by GDScript. One script can now extend another that ultimately extends Node but it can also use the methods of, say, a Sprite2D when attached to a node of such type, even when not directly on the inheritance line.

Here I put a more concrete example to understand the issue:1

# base.gd
extends Node
class_name Base

# -------
# sprite_funcs.gd
# Attached to a Sprite2D.
extends Base
func _ready() -> void:
	# Here using `self` as a hack to use dynamic dispatch.
	# Because GDScript won't know what `centered` is, since it's not on the inheritance tree
	# and it does not know the attaced node.
	self.centered = true

# -------
# body_funcs.gd
# Attached to a RigidBody2D
extends Base

# This method can't be validated in signature to match the super class since RigidBody2D is not being inherited.
# So it won't catch a potential user error.
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
	print(state)

All of this works against the user since they lose optimizations and compile-time checks for potential errors. It's a bigger loss if they use static types since not knowing the actual base class will make the compiler not know the types of properties and methods, making the checks less reliable.

Another problem for users of static typing is checking if a particular method implements a given interface. You can use has_method() but that's often not enough since you can't tell what the signature of the method is. Even with introspection, it would require a lot of boilerplate for each check and you still lose the benefits of static type check. You can only check for inheritance but since that is linear you cannot use for a class that needs to implement multiple interfaces.

Previous requests for a feature like this, or problems that could be solved by this feature:

Describe the feature / enhancement and how it helps to overcome the problem or limitation

For solving this, I propose the creation of trait in GDScript.

What is a trait?

It is a reusable piece of code that can be included in other scripts, parallel to inheritance. The included code behave pretty much as copy and paste, it adds to the existing code of the script, not unlike the #include directive in C/C++. Note that the compiler is aware of traits and can provided better error messages than an actual #include would be able to.

Its contents is the same thing that can be included in a script: variables, constants, methods, inner classes.

Besides that, it can include method signatures, which are methods without an implementation defined, making the trait acting like an interface.

How is a trait defined?

In its own file, like a GDScript class. To avoid confusion, I propose using a new file extension for this, like .gdt (for GDScript Trait), so it won't be usable for things that requires a script. For instance, you won't be able to attach a trait file to a Node directly. I'm still considering whether trait files should be resources. They will only be used from the GDScript implementation, so the resource loader does not need to be aware of them. OTOH, it might make the filesystem scan more complex. This will be investigated further during implementation.

Traits can also be nested in trait files or in GDScript files, using the trait keyword:

# script.gd
class_name TraitCollection

trait NestedTrait:
	pass

That would be accessible with TraitCollection.NestedTrait.

Example of a trait file:

# trait.gdt
const some_constant = 0
var some_variable := "hello"
enum Values {
	Value1,
	Value2,
}

func some_func_with_implementation() -> void:
	print("hello")

func some_abstract_function() -> void # No colon at end.

# This is independent of the outer trait, it won't be automatically used.
trait InnerTrait:
	pass

I consider also using a trait_name keyword that would work similar to the class_name keyword, making the trait available by name in the global scope. I prefer not to reuse class_name for this as I believe it might be confusing, since the trait file is not a class.

Trait files cannot contain inner classes.

Traits can use the extends keyword to select a base class. That will mean that any script that does not derive from this class cannot use the trait:

# trait.gdt
trait_name MyTrait
extends Node2D

func flip():
	rotate(PI) # Works because `rotate()` is defined in Node2D.

# node3d.gd
extends Node3D
implements MyTrait # Error: MyTrait can only be used with scripts that extends Node2D.

# sprite2d.gd
extends Sprite2D
implements MyTrait # Valid: Sprite2D inherits from Node2D

The extends keyword can also be used with a custom GDScript type. Traits can also override methods of the base class. This means they need to match in signature and will give an error otherwise. Note that for native classes the overriding of methods can be unpredictable as the overrides won't be called internally in the engine, so this will give a warning (treated as error by default) as it does for when this happen with a GDScript class.

How to use a trait?

I propose adding the implements keyword2. This should be added at the beginning of the class, together with extends and class_name.

# file.gd
extends Node.gd
implements "trait.gdt"
implements GlobalTrait
implements TraitCollection.NestedTrait

Using multiple traits will include them all in the current class. If there are conflicts, such as the same method name declared on multiple used traits, the compiler will give an error, also shown in-editor for the user. If the current class has a method with the same name as one used trait, it will be treated as an override and will be required to match signature. For variables, constants, and enums, duplication won't be allowed at all, as there are no way to override those. If multiple traits has the same variable, then it will be considered a conflict only if they have different types.

Conflicts between trait methods can be resolved by implementing an override method in the script and calling the appropriate trait method:

# trait_a.gdt
trait_name TraitA
func method(a: int) -> int:
	return 0

# trait_b.gdt
trait_name TraitB
func method(a: int) -> int:
	return 1

# script.gd
implements TraitA, TraitB
func method(a: int) -> int:
	return TraitA.method(a) # Or:
	# return TraitB.method(a)

Traits can also use other traits, allowing you to extend on an existing trait or create "trait packages" so you only need to use once in the classes and it includes many traits. It is not a conflict if multiple used traits happen to use the same trait. The nested inclusion won't make it being included twice (which differs from how #include works in C/C++).

Once a trait is used, its members can be accessed as if belonging to the same class:

# trait.gdt
var foo = "Hello"
func bar():
	print("World")

# script.gd
implements "trait.gdt"
func _init():
	print(foo) # Uses variable from trait.
	bar() # Uses method from trait.

Are traits considered types?

Yes. Traits can be used as type specifications in variables:

var foo: SomeTrait

They can also be used in type checks and casts:

var foo := value as SomeTrait
if bar is OtherTrait:
	print("do stuff")

To avoid polluting the global scope, the traits can also be preloaded like classes:

const SomeTrait = preload("traits/some_trait.gdt")
var foo: SomeTrait
if foo is SomeTrait:
	print("yes")

Warning: One thing to consider when using traits as types is that all are considered to be a basic Object with a script attached. That is, even if the instance you have extends, say, Node2D, you won't be able to use the static type checks for Node2D members. That also means that optimizations done by GDScript to call native methods faster won't be applied, since it can't guarantee at compile time that those will be available without knowing the actual super class. Using extends in the trait helps when it's meant to be applied to a particular type, as it will hint the compiler to what is available.

Warning: Consider too that when checking for traits with is and as, the check will be for the use of the exact trait. It won't check if the object implements the same interface as the trait. As an example:

# trait.gdt
func foo(bar: int) -> void

# a.gd
class_name A
func foo(bar: int) -> void:
	print(bar)

# b.gd
const MyTrait = preload("trait.gdt")
func _init():
	var a := A.new()
	print(a is MyTrait) # Prints 'false'.

While the class A implements a method foo with the same signature, it does not use "trait.gdt" so it won't be considered to be this trait when checking with is nor when casting with as. This is a technical limitation since performing an interface validation at runtime would be costly, and not validating at runtime would be unpredictable for users.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Internally, a trait file will be treated a bit differently from a script file, to allow the little differences in syntax (e.g. methods without body). They will use the same parser since the differences are small, but will be treated differently when relevant. This does add a burden for the parser, though I believe it is better than using a different parser that would do most of the same.

The analyzer, which is responsible for type-checking, will also run on traits. They will be required to be internally consistent on their own, even before being used in a class. This allows the errors to be detected when writing the trait, rather than when using it.

Fully defined functions will be compiled and stored in a GDScriptTrait object. This object won't be exposed to scripting and only used internally. It will be used for caching purposes to avoid compiling the same trait multiple times. The defined functions will be copied to the GDScript object that implements the trait, so it won't have any reference to the original GDScriptTrait object.

Used traits will be stored in the GDScript object as a set of strings. The stored string is the FQN (fully qualified name) that uniquely identifies the trait (essentially the file path plus identifiers for nested traits). This will be used for runtime trait checks and casts, created by the is and as keywords.

During parsing, the used traits will be collected and referenced in the resulting parse tree. The parse trees won't be directly mixed in order to allow the parser to detect the origin of code and thus provide more specific error messages.

The analyzer phase will further check if there are no conflicts and if types are correct, including checks for trait types. It will give specific errors and warnings where appropriate.

The GDScriptCache will be responsible for keeping the the GDScriptTrait objects and intermediary compilation steps when necessary. After a GDScript is compiled, the trait cache will be purged. This cache is only used for the compilation of a single file and re-used for multiple uses of the same trait by the script or by its dependencies (either by the implements keyword or using them as types), so after the compilation is complete it needs to be cleared as we can't know when or if other scripts will be compiled or whether they will use the same traits.

For variables/constants/enums, used traits will behave like copies and recompiled in the current class. This is so the constructor is done per class without having to rely on calling other functions compiled into traits. Usually the default values are not complex expression and it shouldn't be a problem compiling them multiple times (this case will be rare too).

If this enhancement will not be used often, can it be worked around with a few lines of script?

Considering all the requests, it will likely be used often. The work around for this can be quite clunky, especially if you are trying to validate interfaces and not only reuse code.

Is there a reason why this should be core and not an add-on in the asset library?

It is an integral part of GDScript, it cannot be implemented externally.

Footnotes

  1. As an extra note, I would like to make self be transparent and work the same as not using it (unless used for scope resolution), since some people like to use it just for clarity and end up losing performance optimizations without realizing. But this is not related to this proposal in particular.

  2. Note that implements won't be a reserved word to avoid breaking compatibility. The context will be enough to not confuse it with and identifier.

@trommlbomml
Copy link

I am very happy with the idea how it works, exactly what I would Need and use a lot! Though one things looks a bit „wrong“ to me. Classes fulfill, Implement or provide a trait, not using it, like a derived class extends a Base class and thats why the keyword is extends, maybe we can stick Here Java like with extends for base classes and implements for traits.

@Gnumaru
Copy link

Gnumaru commented Mar 3, 2023

The link for godotengine/godot#70956 is broken. I mean, you should have pasted the entire url for the pull request for the link to be clickable.

@YuriSizov
Copy link
Contributor

Would abstract functions be exclusive to traits? I don't see why they would, but it's worth clarifying. If they can be declared in normal classes, then that's worth a dedicated proposal, perhaps.

@vnen
Copy link
Member Author

vnen commented Mar 3, 2023

I am very happy with the idea how it works, exactly what I would Need and use a lot! Though one things looks a bit „wrong“ to me. Classes fulfill, Implement or provide a trait, not using it, like a derived class extends a Base class and thats why the keyword is extends, maybe we can stick Here Java like with extends for base classes and implements for traits.

I don't think implements is a good fit because the first use case for traits is code reuse. It's expected the implementation is in the trait itself, so the class does not really "implement" anything, it actually does "use" the implementation provided. While traits can double as interfaces it won't always be the case.

Of course every name/keyword can be discussed. I believe includes could be an alternative, since it works similarly to a file inclusion.

@vnen
Copy link
Member Author

vnen commented Mar 3, 2023

Would abstract functions be exclusive to traits? I don't see why they would, but it's worth clarifying. If they can be declared in normal classes, then that's worth a dedicated proposal, perhaps.

@YuriSizov yes, they will be exclusive to traits. This proposal only adds them to traits, so the classes still remain without abstract methods.

Adding abstract methods would require abstract classes as well, which are not available and beyond the scope of traits. So this idea is more fit to the proposal for abstract classes: #5641 (I'm not saying it won't happen, just that it is a different discussion).

It works for traits because those already can't be instantiated. Classes using traits that have abstract methods are required to provided an implementation.

@WolfgangSenff
Copy link

So this is similar to an interface in other languages? I love this idea and I think you'll see massive usage of it.

@Calinou Calinou changed the title Trait system for GDScript Add a Trait system for GDScript Mar 3, 2023
@jabcross
Copy link

jabcross commented Mar 3, 2023

Can you expand a bit on what are the alternatives (like multiple inheritance) and the tradeoffs? To me, a trait seems almost identical to a class, except it can be used in multiple inheritance, can define interfaces, and can't be instanced. Wouldn't it be easier just to have multiple inheritance and just reuse classes in the exact same way?

@jabcross
Copy link

jabcross commented Mar 3, 2023

For the problem with extra prefixes when using composition, I quite like the Odin approach of importing a member's members into the namespace using the using keyword:

https://odin-lang.org/docs/overview/#using-statement-with-structs

This basically lets you do foo.x instead of foo.pos.x if you write using pos in foo's definition

@okla
Copy link

okla commented Mar 3, 2023

Great, just today I thought about the lack of such feature in GDScript)

Not sure that a new file extension is a good idea since it always complicates everything about the language: third-party tools support (IDEs, linters, build systems etc), even explaining the difference to novices. Maybe simply using a trait keyword would be enough?

I'm also not sure about theglobal_name, shouldn't it be trait_name to better match classes?

Or even more, if there will be a new kind of scripts in addition to classes, maybe declaration of both of them should be changed to something like
class Foo extends Node – named class
class extends Node – unnamed class
trait Foo extends Node – named trait
trait extends Node – unnamed trait
First word in a script always points to its kind, no need for another file extension, no need for separate (class|global|trait)_name line. Probably a breaking change but a pretty simple one to be automatically applied by the project converter.

BTW in Swift this feature called protocols and they use the term "conforms to" for classes that implement them.

@CarpenterBlue
Copy link

CarpenterBlue commented Mar 4, 2023

My vote goes to smurfs.
Really bad jokes aside, how does new file extension complicate anything? Shouldn't that be just registering new file to the IDEs and whatever else that deals with the language it's still the same GDScript no? There is no need for separate implementations or special treatment.

@Anutrix
Copy link

Anutrix commented Mar 4, 2023

I've read the proposal and from what I can understand, this is like preloading/loading another script(https://godotengine.org/qa/53560/can-you-split-a-class-across-multiple-files-in-gdscript) but with added benefit that they'll node-type-checked when adding them to a script so that you can't accidentally add a trait meant for another class.
Please correct me if I'm wrong about this.

Based on this logic, rather than uses word, I would suggest has or contains keyword for using it.
Maybe is keyword also works if traits are supposed to sound like multiple inheritance logic. Like Cat script/class and Dog script/class both have Animal trait (assuming all 3 have same base class).

Note: I'm using up class/script/file interchangeably but I mean the same.

@bitbrain
Copy link

bitbrain commented Mar 4, 2023

I really like the proposal. I can see myself using this to define "interface style" traits. I would also use this to extend the logic of some scripts.

A few questions:

  • what happens if two traits define the exact same function signature but with different implementations? Will this cause a compiler error? For example in Java, implementing two interfaces that each come with the same default method implementations will throw an error
  • a lot of the times, gdscript code will access nodes from the scene tree to do stuff with them. However, traits cannot have "child nodes" nor access nodes via scene tree lookup by the looks of it. What would be the best way to access nodes within a scene tree inside a trait?

@okla
Copy link

okla commented Mar 4, 2023

it's still the same GDScript no?

And that's why IMHO it doesn't need different extension. Extension hints how to read/parse a file, not how to interpret its content after parsing. For example, tscn and scn are different because the content of such files are different, textual vs binary representation of the same data.

Working with a large C project on a daily basis, it's obvious how its separation into c and h complicates things for no reason. You should always treat them differently everywhere, even though they contain exactly the same C code to the point where any program can be written without using headers at all. It can take months for people new to the language to understand this. Probably that's why most modern languages go with a single file extension for everything.

@Scony
Copy link

Scony commented Mar 4, 2023

  1. To be more in line with GDScript, I'd give users a way to control when they load trait exactly, so the syntax like:
trait SomeTrait = "traits/some_trait.gdt"

would be replaced by:

const SomeTrait = load("res://traits/some_trait.gdt")
# or
const SomeTrait = preload("res://traits/some_trait.gdt")
  1. Regarding file extension - @okla you're mistaken - .gd and .gdt are different because the root of the parse tree (global script scope) is different. A good example is an empty file. Imagine you're loading an empty file like this:
const Empty = preload("res://Empty.gd")

is it a class or trait? Well, you cannot tell unless you hint with .gd or .gdt. Even if an empty file would not be empty and contained some constructs like functions etc. it would still be indistinguishable from a class. So unless we want to have scripts that may act as a class or trait we need to have separate extensions.

@winston-yallow
Copy link

winston-yallow commented Mar 4, 2023

However, traits cannot have "child nodes" nor access nodes via scene tree lookup by the looks of it. What would be the best way to access nodes within a scene tree inside a trait?
@bitbrain

I think traits like in this proposal can access the scene tree, as long as they extend a Node derived class. As traits could use all methods from classes they extend, this should be valid (or rather, I would wish this to be a valid trait, and I don't think anything is preventing this):

func start_idle():
    get_node("AnimationPlayer").play("idle")

I would also love if @export and similar work in traits too, and if I understood the proposal correctly that is the case since variables and so on are compiled in the context of the class script that uses the trait.

@vnen
Copy link
Member Author

vnen commented Mar 4, 2023

So this is similar to an interface in other languages? I love this idea and I think you'll see massive usage of it.

@WolfgangSenff it is like interfaces with some extra features.


Can you expand a bit on what are the alternatives (like multiple inheritance) and the tradeoffs? Tot me, a trait seems almost identical to a class, except it can be used in multiple inheritance, can define interfaces, and can't be instanced. Wouldn't it be easier just to have multiple inheritance and just reuse classes in the exact same way?

@jabcross Multiple inheritance has 2 issues that traits avoid:

  1. The infamous diamond problem. Since traits does not inherit anything they don't carry the baggage. Traits are also flattened when included, if there are conflicts there will be an error.
  2. Exactly because they don't carry the baggage of inheritance, they can be applied to classes of different types. With multiple inheritance, if you have class A extends Node2D and class B extends Node3D, if you have a class C extends B, C then to which node can you attach C? Well, you can't really attach to anything because if it uses methods from Node2D it can't be used in a Node3D without crashing, and vice-versa. The trait extends keyword is only for restricting the class that use them and not actually inheriting anything, so if you replace class for trait in my example, there will be no class that could use both traits at once.

For the problem with extra prefixes when using composition, I quite like the Odin approach of importing a member's members into the namespace using the using keyword:

odin-lang.org/docs/overview/#using-statement-with-structs

This basically lets you do foo.x instead of foo.pos.x if you write using pos in foo's definition

That could be done in parallel, since traits are not only to replace composition (it probably doesn't in many cases anyway). But that's another proposal.


Not sure that a new file extension is a good idea since it always complicates everything about the language: third-party tools support (IDEs, linters, build systems etc), even explaining the difference to novices. Maybe simply using a trait keyword would be enough?

@okla IMO it makes easier for IDEs because the files allow and forbid different things. The IDE would have to be aware of the preamble of the file before deciding what rules to apply. Also, Godot sees resources by extension, so if traits also use .gd they will use the same loader as GDScript and then it will be more complex for the loader which will have to pre-parse the file to a certain point to decide whether it is a class or a trait.

I'm also not sure about theglobal_name, shouldn't it be trait_name to better match classes?

Perhaps. I guess it would be more familiar to GDScript users.

Or even more, if there will be a new kind of scripts in addition to classes, maybe declaration of both of them should be changed to something like
class Foo extends Node – named class
class extends Node – unnamed class
trait Foo extends Node – named trait
trait extends Node – unnamed trait

It has been discussed before, it is difficult to use class because it's already used by inner classes. The parser would have to do a bunch of lookahead to disambiguate, which is more complex.

If anything, I would add a @trait script annotation (like @tool). That kind of annotation must be in the beginning of the file, so it would be easier to detect it's presence or absence.


Based on this logic, rather than uses word, I would suggest has or contains keyword for using it.
Maybe is keyword also works if traits are supposed to sound like multiple inheritance logic. Like Cat script/class and Dog script/class both have Animal trait (assuming all 3 have same base class).

@Anutrix I considered includes which is synonym to contains. I guess is could work as it does not introduce a new keyword and correlates to the result of obj is Trait expression.

@vnen
Copy link
Member Author

vnen commented Mar 4, 2023

@bitbrain

  • what happens if two traits define the exact same function signature but with different implementations? Will this cause a compiler error? For example in Java, implementing two interfaces that each come with the same default method implementations will throw an error

I mentioned this in the proposal: the same method name in multiple included traits will give an error. This also happens if they share the signature.

  • a lot of the times, gdscript code will access nodes from the scene tree to do stuff with them. However, traits cannot have "child nodes" nor access nodes via scene tree lookup by the looks of it. What would be the best way to access nodes within a scene tree inside a trait?

You can use extends Node in the trait to limit the usage for only classes that inherit Node (directly or indirectly). This way the trait can use methods and properties available in Node.


@Scony

  1. To be more in line with GDScript, I'd give users a way to control when they load trait exactly, so the syntax like:
trait SomeTrait = "traits/some_trait.gdt"

would be replaced by:

const SomeTrait = load("res://traits/some_trait.gdt")
# or
const SomeTrait = preload("res://traits/some_trait.gdt")

The problem is that the trait is not an object, at least not how GDScript sees it, and it probably won't even be a resource. The trait won't be available at runtime, so it can't be stored into a constant. It could be done as a "syntax sugar" for the familiarity, but I feel that would be more confusing since you cannot access the trait in any way and there's nothing you can do with its "value". TBH I would prefer adding the syntax class A = "res://a.gd" as a counterpart, which I think it's pretty neat.

Also, there's no way to control when a trait is loaded because it can only be done at compile time. They will always be loaded together with the script.


@winston-yallow

I would also love if @export and similar work in traits too, and if I understood the proposal correctly that is the case since variables and so on are compiled in the context of the class script that uses the trait.

I do think this will work, unless I find some problem during implementation.

@dalexeev
Copy link
Member

dalexeev commented Mar 4, 2023

I would prefer trait to be a resource:

  1. const MyTrait = preload("res://my_trait.gdt") will work. This is consistent with class preloading, you can use it for type hints. uses/includes is more like extends, it doesn't need preload.
  2. No need for the slightly ugly var my_var: "res://my_trait.gd" syntax.
  3. When exporting a project, non-resource files are ignored by default. If we make .gdt a resource, then we don't need to add any exceptions.
  4. ShaderInclude is a resource. More consistency!

That also means that optimizations done by GDScript to call native methods faster won't be applied, since it can't guarantee at compile time that those will be available without knowing the actual super class.

If I understood this sentence correctly, then it seems to me that it would be better if the traits and their inclusions check if the trait's method shadows the native method:

# trait_a.gdt
# ===========
global_name TraitA
extends Object

# OK. `Object` does not have `add_child()` method, although its descendants may have it.
func add_child(node):
    pass

# trait_b.gdt
# ===========
global_name TraitB
extends Node

# The method "add_child" overrides a method from native class "Node".
# This won't be called by the engine and may not work as expected.
# (Warning treated as error.)
func add_child(node):
    pass

# script_1.gd
# ===========
extends Object
# OK. None of the `TraitA` methods shadow native methods.
uses TraitA

# script_2.gd
# ===========
extends Node
# The method "add_child" from trait "TraitA" overrides a method from native class "Node".
# This won't be called by the engine and may not work as expected.
# (Warning treated as error.)
uses TraitA

@Bromeon
Copy link

Bromeon commented Mar 4, 2023

I would even go one step further, and discuss if "trait" is the right keyword.

Like an interface, a trait defines one or more method signatures, of which implementing classes must provide implementations.

This is also how e.g. the Rust language uses traits. But that doesn't align with this proposal -- the idea here seems to be primarily code reuse and not polymorphism/interfacing.

Instead, what do you think about "mixin"?

In object-oriented programming languages, a mixin (or mix-in) is a class that contains methods for use by other classes without having to be the parent class of those other classes. How those other classes gain access to the mixin's methods depends on the language. Mixins are sometimes described as being "included" rather than "inherited".

Mixins encourage code reuse and can be used to avoid the inheritance ambiguity that multiple inheritance can cause (the "diamond problem"), or to work around lack of support for multiple inheritance in a language. A mixin can also be viewed as an interface with implemented methods. This pattern is an example of enforcing the dependency inversion principle.

@bitbrain
Copy link

bitbrain commented Mar 4, 2023

@Bromeon my brain actually used the term mixin in a prior comment and I had to edit my comment to replace it with "trait" - mixin definitely makes more sense here as a name.

@dalexeev
Copy link
Member

dalexeev commented Mar 4, 2023

I would even go one step further, and discuss if "trait" is the right keyword.

Good question. What is described in this proposal is very similar to traits in PHP ("language assisted copy and paste"), only with additional static checks.

@Pennycook
Copy link

I would even go one step further, and discuss if "trait" is the right keyword.

I had a similar reaction. I immediately thought of C++ type traits. I'd love a feature like type traits, but I agree that it doesn't seem to align with what is being proposed here.

I'm drawing attention to C++ specifically because Godot itself is implemented in C++, and there seems to be quite a lot of potential for confusion here.

@poiati
Copy link

poiati commented Mar 5, 2023

Good job, @vnen. All the static typing systems, when implemented post a language launch (e.g. python, ruby) come with some interface concept, given that's the only way we can actually depend on abstractions instead of concrete types. I think this is REALLY important.

I don't think implementation is a good fit because the first use case for traits is code reuse. It's expected the implementation is in the trait itself, so the class does not really "implement" anything, it actually does "use" the implementation provided. While traits can double as interfaces it won't always be the case.

I want to discuss this further ☝️. I personally like the implements keyword more. Look at rust traits, this is the closest implementation I can refer to based on your spec. The baseline is:

  • We can define behaviors through traits and abstract methods;
  • Those abstract methods must have a concrete implementation on the type that implements the trait otherwise it's a compile time error;
  • The traits though, can also provide default implementations for those methods, so the class doesn't need to always provide one, that's useful if we expect the behavior to be shared across implementations; (this is analogous to an Abstract Class in Java)
  • The implementation can override the default implementation from the trait.

so the class does not really "implement" anything

It DOES, even if you provide a default implementation from the trait, it still implements that trait and thus, from a typing system perspective it can act as an object of that type.

I dislike uses as it is not really bound to the concept of interfaces or protocols. The way I see uses being used through history is more related to Mixins, importing other modules, etc...

You are right that your proposal can be used in two ways:

  1. Abstract behaviors into interfaces/protocols (today that's just possible through duck typing, we need to "bypass" the typing system)
  2. Reuse concrete method definitions (sort of Mixin like).

IMO, usage number 1 is by far the most important point of this, so maybe we should build this feature around that concept. implements would make much more sense in that case.

@poiati
Copy link

poiati commented Mar 5, 2023

Mixins are usually conceptually different than protocols or interfaces. Mixins can be implemented anywhere and do not require a type system (look at Ruby, PHP...). On the other hand, interfaces do not make any sense in a language without static typing (see duck typing).

Please, let's not do just Mixins here, or name it like that, that's not what we need (IMO, of course). We need a more refined concept, like the one proposed by the author here, IMO we might need to change some keywords though, like the use one. The concept of traits as defined here can be used in both ways, but we must design it in a way this is not confusing.

@winston-yallow
Copy link

winston-yallow commented Mar 5, 2023

If there are conflicts, such as the same method name declared on multiple used traits, the compiler will give an error

I am not sure how I feel about this. I think it should only be a warning, and have a clearly defined order of lookups. So a function call would have a precedence like this:

  1. Override in class script
  2. Implementations from traits
    • traits included later override implementation from traits included earlier
  3. Default implementations from extended base class

To illustrate this more, let's take this example

extends Base
uses Foo
uses Bar

Calling a function would first look in this script for the function, then in Bar, after that in Foo and finally in the Base

@2plus2makes5
Copy link

2plus2makes5 commented Mar 6, 2023

Wow i had a similar proposal that for some reason i have never submitted, but i had no idea of the existence of trait/mixin!

I have a doubt though, imo a trait should be basically a shortcut to copy-pasting a piece of code to another piece of code plus the addition of some functions like has_trait(), get_trait_list() and similar. Simple and clean and can be beneficail to a vast amount of situations.

Admittely i'm ignorant about trait/mixin, but is it really a good idea to treat traits as types? It seems to me that creating trait variables would make things a lot more complex, confusing and error prone only to get some advantages in some situations. Am i wrong?

@winston-yallow
Copy link

Admittely i'm ignorant about trait/mixin, but is it really a good idea to treat traits as types
@2plus2makes5

That kinda is required to make static typing work. If you can't use traits as types, then there would be no way to get static typing for arbitrary objects of potentially different classes that all implement the same trait.

Creating trait variables wouldn't be required, it would just be something that people can do if they want to. And as with all typing in Godot, it's optional. So you don't need to use any of these features, you can just use traits to re-use some method implementations without worrying about using trait types.

@dalexeev
Copy link
Member

dalexeev commented Mar 6, 2023

How will this work with user documentation? There are no interfaces in Godot API (ClassDB, MethodInfo, PropertyInfo). It turns out that the documentation system should be expanded?

@shinspiegel
Copy link

shinspiegel commented May 27, 2024

Here's how we would have to do it without interfaces:

func draw_colors(drawable: Resource) -> void:
  if drawable.has_method("get_color_line"):
      var draw_data : DrawData = drawable.get_color_line()
      # ... draw the line ...

In this case it's just a

func draw_colors(drawable: Resource) -> void:
  if drawable is Curve or drawable is Gradient:
      var draw_data : DrawData = drawable.get_color_line()
      # ... draw the line ...

That would work (with types), right?

edit1: to add extra info
edit2: I'll move away from this conversation, I'm really not able to see the benefit, so I'm just polluting this conversation, hopefully in the future I can benefit from this, and be able to understand how stupid I'm at this point.

@IntangibleMatter
Copy link

func draw_colors(drawable: Resource) -> void:
  if drawable is Curve or drawable is Gradient:
      var draw_data : DrawData = drawable.get_color_line()
      # ... draw the line ...

That would work, right?

I feel like you're missing the point- This means that you don't have to do as many checks. It also means that you get autocomplete, which you don't have with just checking the type manually, and you get errors before runtime if you attempt to pass a resource that doesn't implement Drawable into the function.

It makes things more readable and more type safe.

func draw_colors(drawable: Drawable) -> void:
  var draw_data := drawable.get_color_line()
  # ... draw the line ...

is much easier to read, edit, and understand at a glance what's happening than

func draw_colors(drawable: Resource) -> void:
  if drawable is Curve or drawable is Gradient:
    var draw_data : DrawData = drawable.get_color_line()
    # ... draw the line ...

Not to mention the first is fully type-safe.

While the contrast might not be incredibly clear here, it really adds up with larger codebases. Say that instead of just Curve and Gradient having the function, we had to write this:

func draw_colors(drawable: Resource) -> void:
  if drawable is Curve or drawable is Gradient or drawable is Curve2D or drawable is Curve3D or drawable is CurveXYZ or drawable is CurveTexture or drawable is GradientTexture1D or drawable is GradientTexture2D or drawable is GradientTexture3D or drawable is Texture2D or drawable is Sky or ...:
    var draw_data : DrawData = drawable.get_color_line()
    # ... draw the line ...

(I know that some of these extend the same class so a few of the checks are redundant, but nonetheless)

Interfaces would let you save on checks, save on runtime errors, save on loosely typed code, and give you autocomplete.

They have plenty of use cases.

@Wokarol
Copy link

Wokarol commented May 27, 2024

@shinspiegel

I'm really not able to see the benefit, so I'm just polluting this conversation

It's not polluting, you don't see a benefit and you are likely not the only one. This means explaining it to you can help other people following this discussion that have the same point of view. So because of that, you are an additional point of view we can talk about. It's beneficial

On this system DamageInflictor tells DamageReceiver that should receive damage (that can be just a Resource), the DamageReceiver emits the signal that received the damage, and the Crate asks for the Health to reduce

Using signals for this is an interesting workaround, and it's a bit better than full dynamic typing. But the approach you suggests is in my opinion overly relying on the physics engine and add complexity due to (in my opinion) needless signals. One drawback I also see is that for every new contract, you need a new Area.
So if you have a chest that can be hit, opened, can have elements inserted into it and can be picked and so on. You now have a chest with 4 Areas that occupy the same spot but react to different "Inflictors".

Now, let's see the same problem solved with interfaces (I will use C#, tho my Godot API know-how might be a bit rusty). I will use a Raycast as a way to hit

var spaceState = GetWorld3D().DirectSpaceState;
var node = spaceState.IntersectRay(...)["collider"] as Node3D;
var actor = node.Owner;

if (actor is IHittable hittable)
  hittable.Hit();

Here, we do not care what Hit does. It could run some code to do a backflip or it could delegate the call to a child node like Health.
Let's say now, what if we have a grid game and we have some gridManager that can get the node at specified coords. This would not work with your approach as we no longer use the physics engine for queries

var actor = gridManager.GetAt(...);

if (actor is IHittable hittable)
  hittable.Hit();

This allows us to use the same "hitbox" for shooting, but also interaction, inserting, picking up and so on.

Interfaces allow us to define contracts, which is great for encapsulation. So while it's handy in Unity and Unreal, I would consider it crucial in Godot as the way nodes works encourages encapsulation.

@dylanmccall
Copy link

dylanmccall commented May 27, 2024

Interfaces allow us to define contracts, which is great for encapsulation. So while it's handy in Unity and Unreal, I would consider it crucial in Godot as the way nodes works encourages encapsulation.

One huge benefit that is really worth mentioning is what this type of thing means for documentation, as well. When we're dealing with trees of nodes that expect certain things of their children, it's difficult to communicate that a particular node is expected to provide a particular function. Sure, you can throw an error at runtime, but it's much better if we could be like "GridManager expects each child to have the Hittable trait", and then Hittable has its own documentation page that explains what Hit() should do.

@DaloLorn
Copy link

(And to respond to your newer comment- while that does work, you end up in if-else hell when things get complicated enough. An interface means that you only need to get the data in the first place and you'll know it's valid, and you don't have to do any additional checks on the type or anything unless it becomes specifically relevant.)

You beat me to it! Very well argued all around, both in the quoted comment and the one you posted later.

This allows us to use the same "hitbox" for shooting, but also interaction, inserting, picking up and so on.

This works particularly nicely if we have the ability, as someone proposed waaaay upthread, to delegate trait implementation to a property (such as a child node, or an assigned resource). So for instance, even if a class didn't implement the trait itself, it could say that property so-and-so would implement the trait on its behalf.

One huge benefit that is really worth mentioning is what this type of thing means for documentation, as well. When we're dealing with trees of nodes that expect certain things of their children, it's difficult to communicate that a particular node is expected to provide a particular function. Sure, you can throw an error at runtime, but it's much better if we could be like "GridManager expects each child to have the Hittable trait", and then Hittable has its own documentation page that explains what Hit() should do.

Yes! I hadn't even really thought about that, since as a solo dev (... well, solo coder at any rate...) I have markedly less use for documentation and mostly only worry about ease of implementation/expansion. But it's a very good point for those who do properly document their code.

@dalexeev
Copy link
Member

Thank you for your interest in this proposal! This is a very interesting discussion and it is certainly relevant to the subject. However, I think this is not a good place to compare composition vs inheritance/traits/interfaces. These are different approaches, each approach has its own pros and cons, its supporters and opponents.

We are convinced that traits/interfaces would be a useful addition to GDScript. This proposal is highly supported by the community, based on previous discussions (and also reactions on the first post). Of course, we are still assessing the complexity and feasibility of this feature and there are more important problems at the moment. However, the fact that the community is interested in traits/interfaces does not require further confirmation. We do not consider composition as a replacement for traits/interfaces, they could coexist together.

Please refrain from further comparison of composition vs traits/interfaces within this proposal, as it already has 200+ comments, which is quite a lot. We understand and appreciate that there is an alternative approach (composition) and this was a good addition to this proposal. However it looks like enough has been said on this topic, let's move on to discussing other issues with traits/interfaces. Thanks!

@gamedev-pnc
Copy link

There was a misunderstanding of traits in some comments, so I believe discussion benefited from clarification. Nice to know that this proposal is highly supported. This and also typed dictionaries are very wanted by many (according to the Godot forum).

@apostrophedottilde
Copy link

Does this seem like a decision has been made on this at this point and that some work can begin in implementing this?

@dalexeev
Copy link
Member

dalexeev commented Jun 3, 2024

Does this seem like a decision has been made on this at this point and that some work can begin in implementing this?

vnen:

Can someone tell me what is stopping this proposal from being implemented? With the amount of comments I couldn't really find out what is stopping this from becoming a feature.

I already started implementing it. But I had some unrelated setbacks so I won't finish it in time for 4.2.

Calinou:

Do you need a greenlight before you begin? If so, can we somehow help showing that we want this feature?

vnen already plans to implement this feature, but they haven't started yet. It's not planned for 4.3 but could be implemented in a future release.

Yes, in theory anyone can try to implement this. If you would like to get involved or discuss implementation details, please use the Godot Contributors Chat. However, note that this is a fairly large and complex feature that requires extensive experience in the internals. George has already made some progress, even if he has stopped working on it for now. Please be patient: we are interested in the feature, but it is not the highest priority at the moment.

@AkiYama-Ryou
Copy link

Suggest:
Support add Trait to Inspector.
And support GetTrait(TraitTypeName:String/Type)
So we can use it as tag.And use trait on a part of object. Not all object of class.

@tehKaiN
Copy link

tehKaiN commented Jun 15, 2024

re abstract functions, I'd clarify syntax a bit - gdscript beginners might do all kinds of silly stuff in their first scripts, e.g. skip function body and then they'll receive an error with something about traits/abstract stuff and they'll be confused instead of just getting "function is missing a body" error.

Perhaps this could be solved by adding something instead of colon which would show that user have deliberately omitted specifying the body. Perhaps an exclamation mark?

Pro: in most human languages it's semantically associated with abstract action of request.
Con: introduces new special character in gdscript)

func some_func_with_implementation() -> void:
	print("hello")

func some_malformed_function() # Emits easy to understand error

func some_abstract_function()! # Doesn't emit error

@winston-yallow
Copy link

Suggest:
Support add Trait to Inspector.
And support GetTrait(TraitTypeName:String/Type)
So we can use it as tag.And use trait on a part of object. Not all object of class.

That would completely break the proposed trait feature. The inspector works on a per object base. The proposed traits work on a per script base.

So when you would allow the inspector to add a trait to an object, it would only change that object not the script. So other instances of the same script may not have the same trait.

In contrast the proposed trait system guarantees you that every instance of a script implements the traits it defines.

So what you suggested seems more like a component system for the editor/engine, not a trait system for the GDScript language.

@azur-wolve
Copy link

@tehKaiN
what about

trait MyTrait:

    var field: Type

    func foo(params);
    func foo1(params);

    func foo2(params): # func with default implementation
        # func body

or instead of ; could use , as they're already used to separate elements of data structures

[IDEA] check if a type/instance implements a trait:

  • obj implements MyTrait => bool
    or
  • obj.implements_trait(TraitType) -> bool

@tehKaiN
Copy link

tehKaiN commented Jun 21, 2024

Not sure about semicolons - they're kinda present in gdscript, as you can optionally end lines with them, like in python - it might be a bit confusing that sometimes they're required and sometimes are not. Comma is an interesting suggestion, I can see the rationale as a field separator, but I'm not sure I'm fan of it. I guess @vnen or whoever implements it would have to say a bit more about it.

Re checking for implementing - is separate operation from obj is MyTrait really needed? I don't see scenario where it would be useful. Could you elaborate?

@azur-wolve
Copy link

azur-wolve commented Jul 8, 2024

@tehKaiN you're right obj is MyTrait works fine

about function signatures, could just end with : if they have body, and let func signatures just be without it
as the GDScript compiler only needs to check function signatures within trait body blocks

@DaloLorn
Copy link

DaloLorn commented Jul 8, 2024

I can see a scenario where obj is MyTrait could introduce confusion... but it's not a scenario worth supporting, I think: If there is a trait called MyCamera, and a class called MyCamera, the is operator wouldn't know whether we're checking for the trait or the camera.

My suggestion would be to give them a shared namespace, so this kind of collision can't happen in the first place.

@WagnerGFX
Copy link

My suggestion would be to give them a shared namespace, so this kind of collision can't happen in the first place.

Beyond that, I would also suggest adding a naming convention in the docs for traits, similar to interfaces. Some possibilities could be TMyCamera, TrMyCamera or MyCameraTrait

@DaloLorn
Copy link

DaloLorn commented Jul 9, 2024

Definitely, but don't skimp on collision prevention thinking everyone will use sane naming conventions! 😂

@Shadowblitz16
Copy link

Shadowblitz16 commented Aug 18, 2024

Why do they need to be named traits?

I agree we need a naming convention for them and if they are called traits then they would collide with generics if they are implemented.
for example is TSource a generic T or a trait T?

I think you guys should call them interfaces and treat them as traits.
Then traits would be called ISource and generics would be called TSource.

besides traits are basically interfaces, except they can be attached to any object and be attached after class deceleration.

@gamedev-pnc
Copy link

gamedev-pnc commented Aug 19, 2024

I think you guys should call them interfaces and treat them as traits.

Traits and interfaces are different.

  1. You define interface and then implement it with your own class. But you can define trait and implement it for foreign, even library types, which existed before. (So, traits give more flexibility and support more workflows.)
  2. With a single declaration – you can implement trait for a set of generic types. For example, in Rust:
impl<T> MyTrait for Type<T>
   where T: IsSomeTrait
{
   // ...
}

(So, traits have more declarative power than interfaces.)

  1. You can define static methods in traits, but not in interfaces.

@cesarizu
Copy link

cesarizu commented Aug 19, 2024

I think that a Trait could be done with interfaces and sub-nodes like this:

  1. An interface that requires a sub-node (think of physics body + shape)
  2. The sub-node implements the common code and can call methods on the parent's interface
  3. There could be a warning that the subnode is needed or it could be added automatically or maybe even hidden in the tree

Benefits:

  • No multiple inheritance problem
  • The trait implementation can have it's own lifecycle
  • Code is reused but not mingled
  • Uses Godot's powerful tree semantics

Example:

Interface (with whatever syntax it would be implemented)

interface_name ISomeTrait

@required
var sub_node: SubNode

func some_operation() -> void

func some_other_operation() -> void

Parent node

class_name ParentNode
implements ISomeTrait

@onready var sub_node: SubNode = $SomeSubNode

func some_operation() -> void
    pass

func some_other_operation() -> void
    sub_node.do_something()

Sub node

class_name SubNode

func _ready() -> void:
    parent.some_operation()

func do_something() -> void
    pass

@apostrophedottilde
Copy link

It's a little bit disheartening to see these discussions still going on about whether traits are useful etc. after all this time.
It felt sometime back now that it had been agreed upon and that it was just awaiting dev time to actually implement. But these further discussions make me worry that this feature is not going to make it for a very long time if at all.
I hope it continues to have traction and come to fruition.

@LowFire
Copy link

LowFire commented Aug 20, 2024

It's a little bit disheartening to see these discussions still going on about whether traits are useful etc. after all this time. It felt sometime back now that it had been agreed upon and that it was just awaiting dev time to actually implement. But these further discussions make me worry that this feature is not going to make it for a very long time if at all. I hope it continues to have traction and come to fruition.

I don't think there's any reason to worry. From what i understand, this feature is already planned and vnen stated a while back that he had already started implementing it. Not sure what the status of it is now.

I would like to see it come to fruition in 4.4 if that's possible. It would be nice to finally have a proper interface-like system in GDScript instead of relying on the many flimsy work-arounds that I've seen many people suggest and that I have personally tried. I've finally accepted that no work-around will ever quite match having a proper language-supported feature like this, so I'd say that this is my most anticipated feature right now. It's just a matter of when. :(

@Shadowblitz16
Copy link

Shadowblitz16 commented Aug 20, 2024

I think you guys should call them interfaces and treat them as traits.

Traits and interfaces are different.

1. You define interface and then implement it with your own class. But you can define trait and implement it for foreign, even library types, which existed before. (So, traits give more flexibility and support more workflows.)

2. With a single declaration – you can implement trait for a set of generic types. For example, in Rust:
impl<T> MyTrait for Type<T>
   where T: IsSomeTrait
{
   // ...
}

(So, traits have more declarative power than interfaces.)

3. You can define static methods in traits, but not in interfaces.

interfaces and traits are literally the same thing.
the only difference is how they are implemented

For example C# devs are thinking about possibly having interface extensions in the C# language which would literally be traits.

I am just saying they should be called interfaces so that generics and interfaces would not be both prefixed with T

@KoBeWi
Copy link
Member

KoBeWi commented Aug 21, 2024

I am just saying they should be called interfaces so that generics and interfaces would not be both prefixed with T

It's the user that gives the names. Nothing is stopping you from prefixing traits with I or prefixing generics with G.

@akien-mga
Copy link
Member

akien-mga commented Aug 21, 2024

I'm going to pause this discussion as it's now just bikeshedding.

To clarify, the GDScript team recognizes that traits can be a useful addition to the language, and so it might be implemented in the future.

Right now, though, our focus is to fix known bugs (e.g. make class_name fully reliable) and complete existing features of GDScript (e.g. add typed Dictionary support to complete the typing system). So adding traits now is not a priority, and would likely not be merged for 4.4 as our priority is stability and maturity, not new features.

Once the team considers that it's time to work on trait support, they'll reopen this discussion, if more needs to be discussed. Or possibly they will open a new proposal with the then-consensus on what to implement, so interested users don't have to read through 250 comments to know what's what.

@godotengine godotengine locked as too heated and limited conversation to collaborators Aug 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.