Skip to content

GDScript: Add support for @abstract annotation with vars#110753

Closed
StarryWorm wants to merge 2 commits intogodotengine:masterfrom
StarryWorm:gdscript-abstract-vars
Closed

GDScript: Add support for @abstract annotation with vars#110753
StarryWorm wants to merge 2 commits intogodotengine:masterfrom
StarryWorm:gdscript-abstract-vars

Conversation

@StarryWorm
Copy link
Copy Markdown
Contributor

@StarryWorm StarryWorm commented Sep 21, 2025

Draft for now as dicussion is still underway on the proposal, but should be ready for merge if/when approved.

Adds support for using the @abstract annotation with variables within @abstract classes.
This annotation requires that the variable it is attached to gets given an initial value by child classes using @override.
For more details on @override, please check #93787. tldr: @override enables the user to specialize an initial value for a variable which is not impacted by the setter.

This does not automatically make the setter and getter abstract.

Basic usage example:

@abstract class Animal:
  @abstract var legs: int # Every class that extends animal must give this an initial value

class Cat extends Animal:
  @override var legs: int = 4 # Sets the initial value for Animal.legs to 4

# Stork will be considered incomplete since it doesn't give a value for legs, 
# and will throw an analyzer error (won't let you compile/run). 
class Stork extends Animal:
  var feathers: int = 10000

Technically speaking, this requires that the child class defines the initializer for the field, as opposed to the current behavior where the base class is responsible for defining this initializer (which is also optional in GDScript).
This has two benefits:

  • The initializer does not go through the setter, meaning its value stored as-is without any chance of being modified
  • Makes giving an initial value to the variable a requirement for child class completeness, which is a development safety feature to ensure all child classes define an initial value that is appropriate for their use

@Delsin-Yu
Copy link
Copy Markdown
Contributor

I don't get it. Is this abstract accessor? (get, set?)

@Lazy-Rabbit-2001
Copy link
Copy Markdown
Contributor

Lazy-Rabbit-2001 commented Sep 22, 2025

Dude, you should first realize that in most languages there is not such a concept called abstract class variables, and the keyword abstract defines what it should do instead of how it should do. The class variables, or we can say fields, are concrete implementation, which belongs to the part of how, and assigning a variable is to assign a piece of memory for the data. Therefore it is conflict with the nature of being abstract.

Or try to think this question: if a variable doesn't have any instantiation, what it should be assigned with? Imagine there were a variable of int type and you declare it as abstract, what's the default value of it? Obviously it is not the default value of its type since it is abstract and should not have any instantiation, but avariable must have a value for instantiation, so that's a paradox.

@StarryWorm
Copy link
Copy Markdown
Contributor Author

StarryWorm commented Sep 22, 2025

@Delsin-Yu sorry it appears my PR description (and the feature proposal) aren't as clear as they ought to be.
The @abstract annotation would create a contract that says the variable must be initialized by inheritors of the abstract class, akin to how @abstract methods must be implemented in the inheritors.
This is separate from abstract accessors, which are already possible in GDScript.
I've updated the PR to hopefully be more clear, please let me know if anything still isn't clear, so I can fix that.

@Lazy-Rabbit-2001 why the agression? If that is because this is my second Draft PR for this, the reason for that is that the implementation is completely different from the first one, and requires the @override implementation to work, so I closed the old one and opened this new one with that commit upstream.

abstract defines what it should do instead of how it should do.

I would disagree that abstract defines what instead of how, and much rather define it as a contract. It defines that this must exist, but not what it will be. It does tell you what it can be, i.e. a method that takes in a float and returns an int, or a variable that is an int, but not what it is.

if a variable doesn't have any instantiation, what it should be assigned with

This doesn't make that possible. The analyzer will stop you if you don't override the variable and provide it with an initializer. Just like it would for methods. Check the tests in the PR.

@Delsin-Yu
Copy link
Copy Markdown
Contributor

@StarryWorm

please let me know if anything still isn't clear, so I can fix that.

I would like to express my concerns regarding the proposed feature, particularly in relation to its intended use case. I have not identified a clear application for this feature, as I believe that the existing functionalities provided by accessor abstractions sufficiently address the needs that abstract variables aim to fulfill.

From an application design perspective, variables typically represent the local state of an instance and are usually kept private within a type. In this context, methods and accessors serve as the appropriate means for external communication (including child types). Therefore, I question the practicality and utility of abstract variables in this framework. Your insights on this matter would be appreciated.

@StarryWorm
Copy link
Copy Markdown
Contributor Author

@Delsin-Yu
Firstly, I think the issue you may be having with this feature isn't specific to the @abstract annotation, but mostly for the @override annotation. @abstract does nothing more than making @override required.

Regardless, I think the best way to approach answering this is by explaining what accessors can and cannot do at the moment in GDScript in the context of OOP. The core issue is the lack of truly private variables.

So let's take the following example:

  • Every Entity must inherit from a base Entity Class
  • All Entities must have a PositionComponent-type Component
  • The Component is an object, which has an internal state
  • The Component should be initialized once, and read-only, such that the same instance is always guaranteed to be returned

Currently, you can make it work like this:

@abstract class Entity:
  @abstract func get_position_component() -> PositionComponent

class StaticEntity extends Entity:
  var _position_component:= StaticPositionComponent.new(): set = _no_set
  # StaticPositionComponent inherits PositionComponent. 
  # Other inheritors can also be defined for other types of behaviors. 

  func get_position_component() -> PositionComponent:
    return _position_component

  func _no_set(val) -> void:
    return

This works, but it looks pretty clunky. Every child class must implement the getter, store its own component in a var that it also makes read-only through a _no_set.
I would like to also take this opportunity to point out that the following would not work due to the setter making it read-only:

@abstract class Entity:
  var position_component: PositionComponent = null: set = _no_set # Any default value would do

  func _no_set(val) -> void:
    return

class StaticEntity extends Entity
  func _init():
    position_component = StaticPositionComponent.new() 

With @override you can do the following

@abstract class Entity:
  var position_component: PositionComponent = null: set = _no_set # Any default value would do

  func _no_set(val) -> void:
    return

class StaticEntity extends Entity
  @override position_component: PositionComponent = StaticPositionComponent.new()

This accomplishes the same thing but with a lot less boilerplate. This feature is also not what this PR is about, and seems to be very well received by the community (see #93787)

This however, is optional. You don't have to override it. With @abstract you would have to. You couldn't have an Entity that doesn't set its own type of PositionComponent

@abstract class Entity:
  @abstract var position_component: PositionComponent: set = _no_set # No default

  func _no_set(val) -> void:
    return

class StaticEntity extends Entity
  @override position_component: PositionComponent = StaticPositionComponent.new()

In summary this PR creates the following split:

  • Non-abstract var which has a default value and can be overriden
  • Abstract var which can't have a default value and must be overriden
    The override mechanism is not the subject of this PR.

Please let me know if this answer is satisfactory to you, or what still remains unsatisfactory.

@StarryWorm
Copy link
Copy Markdown
Contributor Author

Disclaimer: I was not properly taught Computer Science in Higher Education, but rather learned how code works and is designed as I went and used various programming languages. I am much more comfortable with what different features can and can't achieve, rather than their formal definition.
On the other hand, it appears that all of you have some degree of formal education in computer science, and I have been thinking over the past few hours about how to explain this concept in a way that would be more formal.

I think the best way I can describe it is by decomposition of the concept of a property. A property is made up of four optional components:

  • a backing field - which stores the value. (not found in calculated properties)
  • an initializer - which defines what value to store in the field at initialization. (not found in calculated properties, most languages allow you to omit it, instead setting the field to some form of 0/null)
  • a getter - which defines how the value is retrieved (not found in write-only properties, which are usually not recommended)
  • a setter - which defines how the value is modified (not found in read-only properties)

Each of these components has a different role in the implementation of the property. Keeping those roles separate is crucial, as binding one role to another reduces what is possible with the property.
Ideally, a user should be able to define the initializer, getter, and setter, as being responsibilities of either the base class or the child class (in the event that there is one). By default, these are all responsibilities of the base class, which can be changed using abstraction.

The @abstract annotation as well as the @override annotation, allow the user to pass the responsaility over the initializer to child classes:

  • @override enables a child class to redefine the initializer, regardless of whether the base class provided one
  • @abstract requires all child classes to define the initializer through overriding. This also comes with the initializer being unable to be specified at the base class level - which would be useless either way, as it would always be overriden.

This modification to the initializer, which is good practice, also differs from what can be achieved by abstract getters and setters.

  • Abstract getters only modify how the value is retrieved (which can also be worded as what is returned). To try and emulate what an abstract property would do would be akin to reducing it to a computed property, which lacks the full capabilities of a property.
class BaseClass:
  var some_var: SomeType: get = _get_some_var()
  @abstract func _get_some_var() -> SomeType

class ChildClass extends BaseClass:
  func _get_some_var() -> SomeType:
    return some_other_var

This loses two features: the value can no longer be modified, as the getter ignores that, and the getter will always return a new instance of some_computed_var, which is undesired when working with objects

  • Abstract setters only modify how the value is modified. In this case, emulating an abstract property would reduce it to a constant.
class BaseClass:
  var some_var: SomeType: set = _set_some_var()
  @abstract func set_some_var(val: SomeType) -> void

class ChildClass extends BaseClass:
  func _set_some_var(val: SomeType) -> void:
    if not some_var: # To avoid redefining it if it is already defined
      some_var = some_other_var

Now, the value can no longer be changed, losing some capabilities of a full property.

Defining an abstract property (or overriding an existing one) allows you to achieve this outcome, i.e. defining the initial value at the child level, without sacrificing any functionality of your property. You can still define how it is retrieved, and how it is changed, because all you modified is how it is initialized.

Hopefully this answer, in conjunction with my above example-driven answer, is enough to properly convey what this would be, and why it is so valuable.
If you have any remaining questions, or if my attempt at formalizing this didn't make much sense, please let me know, and I would be glad to work through those concerns with you (assuming they are resolvable).

@Delsin-Yu
Copy link
Copy Markdown
Contributor

The example you provided makes me extra confused. What's the point of making the _position_component an accessor in the first place?

  • Every Entity must inherit from a base Entity Class
  • All Entities must have a PositionComponent-type Component
  • The Component is an object which has an internal state
  • The Component should be initialized once, and read-only, such that the same instance is always guaranteed to be returned

From my point of view, a more reasonable implementation should look like this:

Other scripts

Details
# Entity.gd
@abstract class_name Entity
@abstract func get_position_component() -> PositionComponent

# PositionComponent.gd
class_name PositionComponent extends RefCounted

# StaticPositionComponent.gd
class_name StaticPositionComponent extends PositionComponent

StaticEntity.gd

class_name StaticEntity extends Entity

var _position_component := StaticPositionComponent.new();

func get_position_component() -> PositionComponent:
	return _position_component;

In the example above, there are only three lines of code that implement the get_position_component, which is the minimum verbosity required to ensure code clarity (in other words, they are stripped to the bare minimum and are not verbose to programmers).

@StarryWorm
Copy link
Copy Markdown
Contributor Author

StarryWorm commented Sep 23, 2025

That implementation is not safely read-only though, since _position_component can still be modified (since we don't have properly private variables in GDScript). To make it safely read-only you would need to do the following:
StaticEntity.gd

class_name StaticEntity extends Entity

var _position_component := StaticPositionComponent.new(): set = _void_set

func get_position_component() -> PositionComponent:
  return _position_component

func _void_set(comp: PositionComponent) -> void:
  return

You now have to add a setter in every single child class to make it read-only, and if you forget, it's not safely read-only. Your implementation cost is: 1 abstract getter in base class & 1 pseudo-private variable, 1 concrete getter, and 1 read-only setter in child class.
On the other hand, if we use an abstract variable in parent class, we can do the following:

# Entity.gd
@abstract class_name Entity
@abstract var position_component: PositionComponent: set = _void_set()

func _void_set(comp: PositionComponent) -> void:
  return

# StaticEntity.gd
class_name StaticEntity extends Entity
@override var position_component:= StaticPositionComponent.new()

This implementation only needs 1 abstract variable and 1 read-only setter in base class & 1 concrete (override) variable in child class. This is less verbose, and (maybe only in my opinion) just as clear.

The issue of ownership also comes up. To me, position_component is a property of Entity, not a property of StaticEntity. So it should live at the Entity level, not at the StaticEntity level. That is purely subjective though.

@Delsin-Yu
Copy link
Copy Markdown
Contributor

Delsin-Yu commented Sep 23, 2025

@StarryWorm

That implementation is not safely read-only though, since _position_component can still be modified (since we don't have properly private variables in GDScript). To make it safely read-only you would need to do the following...

This is precisely what I wish to point out: shouldn't @private and @init_only(readonly) be the better alternative compared to the abstract field in your example?

class_name StaticEntity extends Entity

@private
@init_only
var _position_component := StaticPositionComponent.new();

func get_position_component() -> PositionComponent:
	return _position_component;

Alternatively, the get/init-only accessor is also embraced by many languages; it partially resolves your need (they are still visible to the outside world though):

class_name StaticEntity extends Entity

var _position_component : StaticPositionComponent:
	init: StaticPositionComponent.new();

func get_position_component() -> PositionComponent:
	return _position_component;

@StarryWorm
Copy link
Copy Markdown
Contributor Author

StarryWorm commented Sep 23, 2025

In this specific case, maybe it is.
Although I would still argue that it isn't due to the ownership problem.
Edit: using both would actually solve all of this, i.e. @abstract @init_only position_component in the base class.

Also, as explained in my - seemingly not that understandable - formal explanation, abstract enables specific behaviors, namely having a complete property (with a setter and getter) that is required to have an initial value provided by child classes. You cannot have such a requirement without the abstract concept.

@Delsin-Yu
Copy link
Copy Markdown
Contributor

Delsin-Yu commented Sep 23, 2025

Although I would still argue that it isn't due to the ownership problem... You cannot have such a requirement without the abstract concept.

Then you should give another real-world use case to demonstrate what your proposal is capable of. Please see this page of the contributor documentation that specifically states the correct perspective for a new feature, and I quote:

1: The problem always comes first

Many contributors are extremely creative and just enjoy the process of designing abstract data structures, creating nice user interfaces, or simply love programming. Whatever the case may be, they come up with cool ideas, which may or may not solve real problems.

image
These are usually called solutions in search of a problem. In an ideal world, they would not be harmful but, in reality, code takes time to write, takes up space and requires maintenance once it exists. Avoiding the addition of anything unnecessary is always considered a good practice in software development.

2: To solve the problem, it has to exist in the first place

This is a variation of the previous practice. Adding anything unnecessary is not a good idea, but what constitutes what is necessary and what isn’t?
image
The answer to this question is that the problem needs to exist before it can be actually solved. It must not be speculation or a belief. The user must be using the software as intended to create something they need. In this process, the user may stumble upon a problem that requires a solution to proceed, or in order to achieve greater productivity. In this case, a solution is needed.

Believing that problems may arise in the future and that the software needs to be ready to solve them by the time they appear is called “Future proofing” and it’s characterized by lines of thought such as:

  • I think it would be useful for users to…

  • I think users will eventually need to…

This is generally considered a bad habit because trying to solve problems that don’t actually exist in the present will often lead to code that will be written but never used, or that is considerably more complex to use and maintain than it needs to be.

@StarryWorm
Copy link
Copy Markdown
Contributor Author

I will definitely work on providing a concrete example tomorrow of what this accomplishes that cannot be accomplished in any other way. This was just the first one that came off the top of my head (since I was working on it at the time).
To me, this example was already an apt demonstration, but it seems that is not a universal opinion.

@StarryWorm
Copy link
Copy Markdown
Contributor Author

StarryWorm commented Sep 23, 2025

Firstly, I would like to point you to the following two feature proposals, which discuss the @override aspect. godotengine/godot-proposals#338 godotengine/godot-proposals#10060. Both contain examples of what could be achieved with @override, even though those things are already possible without it, just in complicated/unsafe ways.
The BrickParent example especially would benefit from @abstract since the dev would want the user to be forced to specify a points value in child scripts.
I would like to point out that @abstract for variables does not permit anything new compared to @override but rather requires it. This is similar to how @abstract for methods is technically unneeded in GDScript, since they are all virtual-by-default, but it was still added for convenience and safety (see godotengine/godot-proposals#1631, #106409.

Second, here is my specific example.
TargetingStrategy.gd

@abstract class_name TargetingStrategy extends Node

var current_target: Entity = null

@abstract func find_target() -> void

FirstTargetingStrategy.gd, StrongTargetingStrategy.gd, etc (using SomeTargetingStrategy as representative)

class_name SomeTargetingStrategy extends TargetingStrategy

func find_target() -> void:
  # needs to be in the SceneTree
  current_target = new_target

TowerComponent.gd

@abstract class_name TowerComponent extends Node

signal strategy_changed(new_strategy: TargetingStrategy)

@abstract var targeting_strategy: TargetingStrategy: set = _set_targeting_strategy()

func set(new_strategy: TargetingStrategy) -> void:
  if targeting_strategy == new_strategy:
    return

  targeting_strategy = new_strategy
  targeting_strategy.find_target()
  strategy_changed.emit(targeting_strategy)

func _process() -> void:
  targeting_strategy.find_target()
  # ... do stuff

ArrowTowerComponent.gd

class_name ArrowTowerComponent extends TowerComponent

@override var targeting_strategy:= FirstTargetingStrategy.new()

CannonTowerComponent.gd

class_name CannonTowerComponent extends TowerComponent

@override var targeting_strategy:= StrongestTargetingStrategy.new()

Why this needs @override:

  • The initial value needs to bypass the setter. It shouldn't emit a strategy_changed signal, and the find_target() method is unsafe to call during initialization, should only be called after tower is in SceneTree.
  • This uses an object, so using a computed property doesn't work
    Why this needs @abstract:
  • The initial value matters. Giving null would be unsafe, and providing any non-null initial value would cause unwanted behavior if the user forgets to override it. With abstract that is impossible.

Hopefully this example is enough to convince you that this isn't a solution looking for a problem, but a solution to a problem.

@Delsin-Yu please let me know if you find this example convincing enough. If so, I will update the proposal and the PR to reflect this, and reshare it in the appropriate channels, now that it would be more fleshed out and rigorous.

Also @Shadows-of-Fire and @Lazy-Rabbit-2001 , I'd like your feedback too on this - hopefully - more fleshed out proposal, since you two were in disagreement with it, and I would like to know if that disagreement remains. I expect some of the initial disagreement wasn't necessarily because of the feature itself, but rather my poor explanation of it.
If you still disagree, please do let me know if it is the kind of disagreement that can be resolved or not.

@Lazy-Rabbit-2001
Copy link
Copy Markdown
Contributor

Lazy-Rabbit-2001 commented Sep 25, 2025

Perhaps we need to ask @vnen 's opinion on this and see what he will be like when he hears abstract variables.

@StarryWorm StarryWorm force-pushed the gdscript-abstract-vars branch 3 times, most recently from 7611549 to c4a532e Compare September 25, 2025 14:12
dalexeev and others added 2 commits September 25, 2025 10:44
Co-Authored-By: Danil Alexeev <dalexeev12@yandex.ru>
Add support for `@abstract` annotation with vars

Adds support for using the `@abstract` annotation with variables.
Uses existing `@override` annotation
@StarryWorm StarryWorm force-pushed the gdscript-abstract-vars branch from c4a532e to a300e35 Compare September 25, 2025 14:45
@StarryWorm
Copy link
Copy Markdown
Contributor Author

@Lazy-Rabbit-2001 as you were mentioning in the contributor's chat,

it's quite confusing when a member variable, or property sometimes, can become abstract

My question then is, what part of it is causing confusion? Is it the name or the concept?
I am in no way attached to calling it @abstract, all that I care about is the behavior (i.e. making overriding the base class variable's initializer a completion requirement for child classes).
So if the name is the issue, I am 100% open to changing it, especially if others agree (including @Delsin-Yu and @Shadows-of-Fire since both of your issues with the PR might just be because of the name), and please feel free to suggest other nomenclature, I am completely open to any ideas.

Some ideas:
@abstract (the original idea, inspired by C# and Kotlin, based on the fact it is an inheritance behavior)
@must-override (seems a bit verbose but does a good job explaining)
@child-init (decent job explaining, not the biggest fan but eh, throwing out ideas)

@Shadows-of-Fire
Copy link
Copy Markdown
Contributor

No; renaming it doesn't resolve my concern. I do not believe we should add additional syntax that allows child classes to inline new initializer code for a given member variable. This should be handled by constructors, which control object initialization.

The main lacking feature from constructors at the current moment is that constructors do not mandate you call a superconstructor, which is a design flaw that should be addressed in the future.

The ability to make well-informed assumptions about the state of a variable (i.e. if we want to add @final variables, and "trust" that those variables are final for optimization) are incompatible with a model where child classes can arbitrarily provide new initializer code that cannot be known in advance.

I'll continue my discussions on the proposal as they are not related to any implementation details here.

@StarryWorm StarryWorm closed this Oct 2, 2025
@AThousandShips AThousandShips removed this from the 4.x milestone Oct 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add abstract class variables to GDScript

6 participants