GDScript: Add support for @abstract annotation with vars#110753
GDScript: Add support for @abstract annotation with vars#110753StarryWorm wants to merge 2 commits intogodotengine:masterfrom
Conversation
|
I don't get it. Is this abstract accessor? ( |
|
Dude, you should first realize that in most languages there is not such a concept called abstract class variables, and the keyword 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 |
|
@Delsin-Yu sorry it appears my PR description (and the feature proposal) aren't as clear as they ought to be. @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
I would disagree that
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. |
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. |
|
@Delsin-Yu 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:
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:
returnThis 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. @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 @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 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:
Please let me know if this answer is satisfactory to you, or what still remains unsatisfactory. |
|
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. 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:
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. The
This modification to the initializer, which is good practice, also differs from what can be achieved by abstract getters and setters.
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_varThis 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
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_varNow, 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. |
|
The example you provided makes me extra confused. What's the point of making the
From my point of view, a more reasonable implementation should look like this:
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
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 |
|
That implementation is not safely read-only though, since 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:
returnYou 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. # 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, |
This is precisely what I wish to point out: shouldn't 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; |
|
In this specific case, maybe it is. 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. |
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:
|
|
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). |
|
Firstly, I would like to point you to the following two feature proposals, which discuss the Second, here is my specific example. @abstract class_name TargetingStrategy extends Node
var current_target: Entity = null
@abstract func find_target() -> void
class_name SomeTargetingStrategy extends TargetingStrategy
func find_target() -> void:
# needs to be in the SceneTree
current_target = new_target
@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
class_name ArrowTowerComponent extends TowerComponent
@override var targeting_strategy:= FirstTargetingStrategy.new()
class_name CannonTowerComponent extends TowerComponent
@override var targeting_strategy:= StrongestTargetingStrategy.new()Why this needs
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. |
|
Perhaps we need to ask @vnen 's opinion on this and see what he will be like when he hears abstract variables. |
7611549 to
c4a532e
Compare
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
c4a532e to
a300e35
Compare
|
@Lazy-Rabbit-2001 as you were mentioning in the contributor's chat,
My question then is, what part of it is causing confusion? Is it the name or the concept? Some ideas: |
|
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 I'll continue my discussions on the proposal as they are not related to any implementation details here. |


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
@abstractannotation with variables within@abstractclasses.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:@overrideenables 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:
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: