-
-
Notifications
You must be signed in to change notification settings - Fork 98
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
Allow explicitly defining interfaces in GDScript #4872
Comments
Related to #758. |
Another thing I forgot to mention, but with interfaces we can also be explicit about function arguments, for example:
Whereas currently we'd have to do something like this:
But I guess this is slightly less useful because you'd have to know ahead of time that the object implements the interface. But I like that it makes the code cleaner and easier to reason about because the intent is clear. |
Can this be used in cases when Godot throws |
Yes please, in my own projects I have implemented a system for interfaces in GDScript for such cases where inheritance doesn't make sense, but I'd much rather have it be natively available |
It would be awesome if you could tell me how you did it, since I need this the most for my own projects. |
@sairam4123 It's not properly implemented at a language level, but it has worked well enough for my current project. I'll upload it and send you a link when I get home |
This comment was marked as off-topic.
This comment was marked as off-topic.
@bitbrain Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
@sairam4123 @bitbrain sorry for the delay. I decided to clean it up a bit when I extracted the code from my utility library. You can now find the project as an addon here: https://github.com/nsrosenqvist/gdscript-interfaces/ |
Would it make sense that the interface is just available before JIT kinda similiar to interfaces in typescript? This would have the benefit that interfaces have zero runtime overhead but fulfills this use case - with the trade off that at runtime you have no information about the interfaces any more. but you can argue that you have no real architectural improvement if you need to ask a class if it implements an interface and it may be easier at least as a step zero to get this feature. |
I like your proposal. |
@just-like-that I agree, otherwise they too get too bound up with inheritance and don't enable the flexibility sought after. Ideally should traits also be added at the same time as interfaces are implemented. See #758 |
So I agree with you in general that For example currently we set class names like this: Similarly, we declare inheritance like this: I agree that the ability to implement multiple interfaces should be allowed. That was sort of implied but I guess I should have made that more clear in my original post. I like your idea of having a new syntax like In my original proposal, perhaps multiple interfaces can be implemented as so:
Which is a bit wordy, but I think it's okay as a first implementation because I can't imagine a class having to implement more than a handful of interfaces at a time. But perhaps we can add a comma system to this too.
Anyway, I suppose that's getting into the weeds a bit. I'd be happy with any of the implementations mentioned above. Whichever approach has the least friction is most likely to get implemented. I suppose we first need to get philosophical buy-in from the decision-makers. |
What about the ability to also add property declarations in addition to methods as part of interfaces? |
I think signals makes sense, but not properties. They are usually dependent on how one chooses to implement an interface (eg. a data property that the methods operate on, or if another implementation instead fetches data from a remote source and don't operate on data locally). |
Still possible, as long as properties implemented as part of an interface are forced to have a getter and (optional?) a setter. |
That's the biggest reason I am using c# at the moment. |
From my understanding it's called class_name coz it's the name for the whole class file. Inner classes are named "class" only. So interface_name would indicate it's also in its own file and they have to be unique thru out the prj. I see an interface more as a part of a specific class file. The class file acts as a context aka name space to that interface. By pre-loading the class file via
|
If you need to make the interface global, see my suggestion #4740. |
If this were to be a thing in GDScript, I would suggest just having an Regardless of whether an interface system is added though, it'd be important to identify exactly how it would work. The most prevalent forms of interfaces I have observed are those in TypeScript and in C#, but the way they work in each of those languages is vastly different, so people should iron out what they actually want GDScript's interfaces to be first. These will be the "Strict" (C#), "Loose" (JavaScript-ish), and "Hybrid" (TypeScript-ish) approaches: How is an interface evaluated against a type?StrictIs an interface a specific type with specific methods and a given script must explicitly implement the interface in order for it to be recognized as an implementation of the interface? @interface
class IKillable:
## returns points awarded when killed. Prints `p_msg` afterward.
func die(p_msg: String) -> int:
pass
# parse error if no `pass`. Could also be on the next line.
# Must have name `die`.
# Must have 1 argument of type `String`. The name does not need to be `p_msg`.
# Must have `int` return type.
# GDScript parser would need to ignore improper return types on methods with static type hints.
class Ship implements IKillable:
pass
# will not compile until a `die` method matching all the above conditions is defined. OR LooseIs an interface just a way of checking multiple @interface
class IKillable:
func die(p_msg: String) -> int:
pass
# Exact same as the Strict case
class Ship:
func die(p_msg):
pass
# This satisfies an `if ship implements IKillable` check b/c `has_method(...)`
# returns true for all subset of methods in IKillable. OR... HybridSimilar to "Loose" but actually supports any implicit union of types with fully evaluated static type checks, i.e. is it a union of methods with specific names, parameter count, types for each parameter, and a specific return type? # Exact same `IKillable` as the Loose case
class Ship:
func die(p_msg: String) -> int:
return 3
# This satisfies an `if ship implements IKillable` check b/c all signatures of all
# methods in `IKillable` match exactly EVEN THOUGH the class never explicitly
# says it matches `IKillable` (so no static checking of interface conformity).
How does the interface handle edge cases related to method compatibility?StrictWhat about situations where a class implements two interfaces that share method names, but different arguments? @interface
class IBag:
func configure(p_slot_count: int = 0) -> void: pass
@interface
class IMail:
func configure(p_owner: StringName, p_content: String) -> void: pass
class PostalDelivery implements IBag, IMail:
# <implements methods exactly, but triggers parse error>
pass
# GDScript does not support method overloading.
# Do we just forbid these combinations? That'd be a tough sell
# as people start relying on third-party libraries that all define global script classes.
# For "Strict" only, would we need to resort to having interface-specific
# annotations on methods to flag them?
# You'd need to make these hidden methods that aren't publicly accessible,
# likely with a generated name derived from the interface it belongs to.
# That way it doesn't actually occupy the same slot in the method HashMap
# used by the GDScript's C++ engine code.
@interface(IBag)
func configure(p_slot_count: int = 0) -> void: pass
@interface(IMail)
func configure(p_owner: StringName, p_content: String) -> void: pass
# ^ This duplication of method name definitions would need to NOT trigger a parse error. LooseWhat about methods with an extra but optional variable that has a default value - would it still be an implementation? @interface
class IKillable:
func die(): pass
class Ship
func die(p_msg := "") -> void: # does
print(p_msg if p_msg != null else "ship died")
# does this method count? It's POSSIBLE to call w/o args, so it WOULD satisfy duck-typing,
# but it doesn't explicitly match the interface definition. HybridBehaves the same as "Loose". Do interfaces support unions and/or inheritance?StrictCan interfaces extend each other? @interface
class_name IShip
extends IKillable, IRevivable
# has methods `die()` and `revive()`, but no content in script. Is it invalid for an interface to extend a class? class Frigate implements IShip: pass
@interface
class IFleet:
extends Frigate # would this be a parse error? LooseCould you define an interface that is a union of other interfaces? @interface
class_name IShip
extends IKillable & IRevivable
# has methods `die()` and `revive()`, but no content in script.
# Because interfaces are mere combinations of methods,
# you can use bitwise operators on them (potentially). HybridBehaves the same as "Loose" in this regard too. Also, since static typing is optional, what does static type checking on interfaces mean when a method on an interface does not use static type hints? Do you just reduce the number of required conditions an implementing class has to meet in order to conform to the interface? Or do you consider it a compiler error and demand that all methods on an interface use type hints everywhere? I think it would be possible to go with any of the 3 versions. "Loose" would obviously be the most straightforward and simple to implement, but I doubt it would satisfy people who are looking for full static type checks to be honored by interface declarations. Between the "Hybrid" and "Strict" versions, I think "Hybrid" more closely mimics GDScript's "scripting" context, but at the same time feel conflicted since "Strict" more closely honors GDScript's "pythonic C++" style. Ultimately though, GDScript is a scripting language and is meant to be easy to use. Regardless of which approach gets general consensus, I believe that interfaces should not be introduced in such a way that they only work when users elect to use static typing. They must also work with a complete lack of static types. Given that "duck-typing" is already the style used when a lack of static types is present, I would opt to take a "Hybrid" approach as it is the one that is most compatible with dynamic GDScript, and it makes GDScript behave like other scripting languages with type hints. |
What about this temporary solution for now? Interface definition:
Implementing class:
Usage example:
Explanation: The trick: By calling super.is_class() recursively it's ensured that the inheritance hierarchy is also supported. To use the whole interface path ensures that interfaces with the same name with different paths are also possible. |
I just wrote a 400000 paragraph comment responding to the comments here, then I realized, I just want interfaces lol. |
Godot 4.0 is in feature freeze. To allow the language to stabilize, no new GDScript features will be added until after 4.0 is released. |
So, for now the interfaces are not going to be implemented. Is it to promote composition over inheritance? |
Is there any news on this? |
To my knowledge, nobody is currently working on implementing this. 4.2 is in feature freeze, so any new features will be for 4.3 at the earliest. |
I need this for my project, and I've already implemented abstract methods for 4.3 or beyond, in godotengine/godot#82987 , so it's time to take a stab at this EDIT: After talking in the gdscript channel of the contributors chat, it appears that someone is working on the similarly proposed trait system for gdscript. That will appear in 4.3 or beyond as well though. Traits are a more powerful superset of interfaces, so I will hold off on developing this for now. |
What I don't understand is this kind of features being outright missing. Makes me think all those Godot preachers that finally got their wish after Unity did a silly NEVER actually sat down and made something remotely complex and robust |
@Kalto-Mate I have to remind you that we have a Code of Conduct. Please stay constructive. |
It's a shame this feature has not been implemented initially for whatever reason. This feature missing from gdscript means C# interfaces can't be utilized to it's max potential either. See #7971, for example: A potentially powerful feature that isn't particularly hard to implement but will not be accepted as we must respect gdscript first, which does not have interfaces. At this stage of gdscript, I imagine, it will be incredibly hard to implement such a feature as the language has been built around not having interfaces, which might explain why nobody is too eager to pick up this issue. Just a saddening situation all in all. |
It's actually not that hard to implement, it just requires a few building blocks first. A PR which implements an A PR built on top of the other one which implements abstract methods through the same keyword. Also potentially slated for 4.3. With these two PRs, it should be easy to make interfaces, just forbid any other members except abstract functions in an interface class and create an I actually have a branch on my godot repo fork that has most of the implementation finished for interfaces, all that needs to be done is resolving classes that are implemented by another class. https://github.com/ryanabx/godot/tree/gdscript/interfaces The main reason I stopped working on it is because another godot engine contributor @vnen is going to eventually work on Trait support for GDScript, which would be a superset of the features that interfaces provide. Believe me, I'm also anxiously waiting for interface support in GDScript, but it seems we will have to wait a little bit longer for something like it to be implemented in Godot. |
Has there been any progress towards this at all since the last post? I am really interested to get this feature. |
See the above comment:
Work on traits hasn't started yet, but it seems traits are more likely to be implemented than interfaces. |
This comment has been minimized.
This comment has been minimized.
@cyda89 Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
Describe the project you are working on
I'm working on a game where different entities can share similar behaviors, but this proposal is specific to GDScript, so it can apply to a wide range of projects.
Describe the problem or limitation you are having in your project
Let's say we're making a simple shooter game. The player can shoot a
Bullet
. Bullets can hit aShip
andBuilding
. There are alsoTree
entities that cannot be hit by bullets.Here's some simple code (unrelated code omitted for simplicity).
Ship.gd
Building.gd
Tree.gd
Bullet.gd
There are a few problems with this duck-typing approach:
Bullet.gd
there is no code auto-completion or checking when callingbody.die()
so you need to implicitly know that that function exists.die
is not enough to determine what kind of object you're dealing with. In this example, we don't want bullets to hitTree
, even though a tree might happen to have a function nameddie
(the tree could die of natural causes for example).die
function we would have to do multiplehas_method
checks which can get ugly in code.Describe the feature / enhancement and how it helps to overcome the problem or limitation
My proposal is to introduce a language feature which allows us to explicitly define interfaces. This is a common feature in many modern languages and makes it easy to deal with complex object interactions without relying on inheritance.
See below for an implementation proposal.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
Let's take the same example code as above, and let's see what it could look like if GDScript had interface support.
Killable.gd
Ship.gd
Building.gd
Tree.gd
Bullet.gd
Note: Because we're nested in an
implements
check we should be able to auto-complete "die" and also throw an error if "die" is not part of the Killable interface.Alternatively, you can also reference the object via its interface by casting, for example:
Bullet.gd
This has a few advantages:
die()
fromShip
orBuilding
, the editor should show an error that you haven't fully implemented the interface.Bullet
we can now make our intentions explicit. The fact that there's a function calleddie
is an implementation detail, but the true intent is we want to know if we're dealing with aKillable
object. This way, classes likeTree
which happen to have adie
function won't be processed because they're not killable.die
is renamed in the interface, then there would be errors showing all the places where it is being called and implemented. Previously thehas_method("die")
would not be flagged as an error because it's referencing the function as a string.Bullet
, since we're now declared explicit intent that we want to deal with aKillable
object, we should be able to auto-completeKillable
functions, and also throw errors if we try to call a function that isn't defined inKillable
.If this enhancement will not be used often, can it be worked around with a few lines of script?
Technically it can be worked around with the current duck-typing approach. But I think it would make the code cleaner and safer to use explicit Interfaces. It's also easier and faster to write because we would have explicit checks and auto-completion. It's safer because it's more resilient to changes too (e.g. renaming a function wouldn't be flagged if the function is referenced as a string).
Is there a reason why this should be core and not an add-on in the asset library?
Interfaces are built-in to other languages, I think this would make sense to be a core part of GDScript and not some external tool. It's akin to classes, functions, variables, etc. so I think it deserved first-class citizenship.
The text was updated successfully, but these errors were encountered: