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

Automate or reduce the boilerplate when forwarding script properties in GDScript #6750

Open
nlupugla opened this issue Apr 24, 2023 · 27 comments · May be fixed by godotengine/godot#78991
Open

Comments

@nlupugla
Copy link

nlupugla commented Apr 24, 2023

Describe the project you are working on

A jrpg with stats-heavy combat and visual novel elements.

Describe the problem or limitation you are having in your project

I often find myself having to write a lot of tedious boilerplate code to gracefully navigate through complicated objects.

As a simple example, suppose I have a Character script that contains, among its many variables, a Stats resource (which defines attributes like hp, attack, defense, etc). One way to access the hp of a character would be character.stats.hp. However, best practices (eg: The Law of Demeter) dictate that it would be better to write character.get_hp() or simply character.hp so that the Character script doesn't need to know anything about the internal structure of the Stats resource. The most straightforward approach I can think of would be to write the Character script like so:

var stats : Stats
var hp : int :
    get: return stats.hp
    set(val): stats.hp = val
var defense : int :
    get: return stats.defense
    set(val): stats.defense = val

and so on for all the attributes I want Character to have direct access to. My problems with this approach are that

  1. The code is tedious to write and feels like it could be automated easily
  2. It adds a lot of visual noise to the beginning of a script file that makes it difficult to parse what variables a script has access to

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


Edit 28 April 2023:
After discussion, the proposed solution is to upgrade the inline set/get syntax to allow for an extra property:StringName argument. For example,

var stats : Stats #inherits from Reference
var hp : int : set = set_stats_property, get = get_stats_property
var defense : int : set = set_stats_property, get = get_stats_property


func set_stats_property(property:StringName, value:Variant) -> void:
    stats.set(property, value)


func get_stats_property(property:StringName) -> Variant:
    return stats.get(property)

I see two approaches for overcoming this problem.

  1. Adding some syntactic sugar (possibly by way of custom annotations or a new built-in annotation) that stands in for the boilerplate.
  2. Let the editor auto-generate the boilerplate, perhaps via a custom plug-in or a new built-in editor feature.

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

Syntactic Sugar Approach

Something along the lines of

var stats : Stats
@forward_to(stats) var hp : int
@forward_to(stats) var defense : int

@forward_to could be a new built-in annotation or a custom annotation if #1316 is ever implemented.

Auto-generation Approach

When the user writes

var stats: Stats

there is some UI within the editor which allows the user to auto-generate code such as

# auto-generated setters and getters --------------------
func get_hp() -> int:
    return stats.hp


func set_hp(value : int) -> void:
    stats.hp = value

This code could be inserted towards the end of the script to avoid cluttering the script's header with visual noise.

The UI for achieving this could either be built in to the editor or provided by custom tooling/a plug-in.

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

While a single property can be forwarded in 2-3 lines of code using getters/setters, these lines quickly add up over many properties.

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

Better built-in support for forwarding encourage the use of good design principles like Composition over Inheritance, Single Responsibility, and the Façade pattern.

The Character script in my example respects these principles because it is composed of a Stats object (among other variables), instead of inheriting (from something like CharacterWithoutStats or EntityWithStats) or directly including the members of Stats (which could make Character have multiple responsibilities).

@Calinou
Copy link
Member

Calinou commented Apr 24, 2023

@YuriSizov
Copy link
Contributor

This looks like a good use case for custom annotations, when we implement them. Adding an engine built-in for something that appears pretty specific to a particular project architecture doesn't look like a good idea to me. And a feature to auto-generate setters and getters doesn't solve the problem of visual noise that you've outlined.

@nlupugla
Copy link
Author

This looks like a good use case for custom annotations, when we implement them. Adding an engine built-in for something that appears pretty specific to a particular project architecture doesn't look like a good idea to me. And a feature to auto-generate setters and getters doesn't solve the problem of visual noise that you've outlined.

Is there any active work on custom annotations? It feels like it could be a very powerful feature. As far as I can tell from a quick glance through #1316, there hasn't been much development on it.

If the auto-generated setters and getters showed up towards the bottom of the script, it would at least reduce the noise at the top of the script where all the variables are defined.

Finally, I'm surprised that this is considered a specific/niche problem. You're not the first person to have mentioned something like that. To me, it seems like you'll run in to this problem whenever you want to group multiple low-level objects into a single high-level scene/object. In particular, isn't it considered good practice to keep the logical state of a game entity separate from the entity's visual appearance?

In some ways, accessing nodes by their Unique Scene Name solved this problem for me at the node/scene level. For example, in a complicated scene tree, I can get any node I want by calling %NodeIWant instead of $ParentOfNodeIWant/NodeIWant. This is great because I can get NodeIWant without having to understand the structure of ParentOfNodeIWant. However, this approach only goes so far because it doesn't let me reach into the variables of those nodes. For example, if the following syntax worked (borrowed from the syntax used in AnimationPlayer), it would solve my issue in almost all cases:
@onready var variable_i_want = %NodeIWant:variable_i_want
By "worked" what I mean is that accessing variable_i_want behaves essentially as a reference/pointer to NodeIWant.variable_i_want.

Coming at things from the opposite direction, if the proposed @forward_to annotation also worked for nodes, do you think there would be additional interest in the feature? In other words, one could use

@forward_to(%my_node) var property_of_my_node

as syntactic sugar for

var property_of_my_node:
    get: return %my_node.property_of_my_node
    set(value): %my_node.property_of_my_node = value

@dalexeev
Copy link
Member

dalexeev commented Apr 25, 2023

First we need to understand what features we expect from custom annotations. Should it just be "machine-readable metadata information on declarations in code" or we want custom annotations to allow you to modify the behavior just like the standard ones. Annotations are not Python decorators, they are more like PHP attributes, although the standard annotations have hardcoded behavior.

The following things deeply affect the language itself, they cannot be implemented if custom annotations are just static metadata:

  1. Modify the default value of an uninitialized variable (for example, @export_node_path, but this is not implemented yet).
  2. Postpone the evaluation and assignment of the value of the initializer (@onready).
  3. Setters and getters suggested here (no standard annotations). The question also arises of what should happen if a variable has both a setter/getter and a custom annotation that implements the setter/getter.

Also note that annotation arguments are currently required to be constant values, the following code does not work:

@export_range(0, 100) var a: int
@export_range(0, a) var b: int

Thus, custom annotations are quite a difficult problem.

As for the problem of boilerplate, I have three more ideas.

1. @alias annotation (close to your suggestion).

@alias var a = x.a

Syntactic sugar for

var a:
    get:
        return x.a
    set(value):
        x.a = value

A variable with the @alias annotation applied cannot have a setter and/or getter and must have an initializer that is an "lvalue", i.e. another variable or subscript chain (e.g. x, x.a, x.a.a1, x["a"], x["a"]["a1"], f().x are valid initializers, but f(), 42 and x + y are not).

2. nset and nget properties.

In addition to the get: set(value): syntax, there is also the following option:

var a: int: get = get_a, set = set_a

func get_a() -> int:
    return a

func set_a(value: int) -> void:
    a = value

The problem is that you can't reuse the setter and getter because they don't take a variable name as an argument. But we could add new nget and nset properties:

var a: int: nget = get_prop, nset = set_prop

func get_prop(property: StringName) -> int:
    return _data[property]

func set_prop(property: StringName, value: int) -> void:
    _data[property] = value

You cannot use get with nget and set with nset.

Or make it so that get and set recognize these signatures (if the getter has no arguments, this is a regular getter, if there is 1 argument, then the variable name is passed to it).

3. I don't think this is a good idea in terms of compatibility and performance, but perhaps _get and _set should not only be called when accessing non-existent properties, but always. Or maybe we could make it configurable. See also #2837.

@nlupugla
Copy link
Author

Thanks for the ideas! I had no idea how annotations worked under the hood. I can see why even just determining the scope for what custom annotations should be able to do is a tricky question. I was imagining annotations to be implemented as functional wrappers like in Python.

As for your suggestions, I really like the syntax for @alias.

I'm confused about nset and nget. What do the names mean? I do like the idea of being able to alias multiple properties at once though. Would the editor be able to auto complete/intilsense correctly though? In addition, it seems like you would loose some type safety. Unless all the properties you want to forward are the same type, it seems like you'd have to have the types be Variants.

@dalexeev
Copy link
Member

dalexeev commented Apr 25, 2023

I'm confused about nset and nget. What do the names mean?

"nset" is short for "setter with name" or "setter for N properties". I agree that the name is not very good, but we could just use set and recognize the number of arguments (I added a note to my comment).

Would the editor be able to auto complete/intilsense correctly though? In addition, it seems like you would loose some type safety. Unless all the properties you want to forward are the same type, it seems like you'd have to have the types be Variants.

If you want to use a single function as a setter/getter for heterogeneous properties, then its value argument or return type must be Variant. Yes, you will lose type safety and autocompletion, but only inside the setter/getter. When accessing properties, their types will be correct.

var a: int: set = set_property, get = get_property
var b: String: set = set_property, get = get_property

var _data := { a = 1, b = "test" }

func _ready() -> void:
    var x: int = a # Type safe.
    var y: String = b # Type safe.

func set_property(property: StringName, value: Variant) -> void:
    _data[property] = value

func get_property(property: StringName) -> Variant:
    return _data[property]

@alias is type safe too:

@alias var a: int = x.a # x.a must be type compatible with int.

Note that @alias has a more limited use, it does not allow you to add logic, unlike setter/getter.

@nlupugla
Copy link
Author

Or make it so that get and set recognize these signatures (if the getter has no arguments, this is a regular getter, if there is 1 argument, then the variable name is passed to it).

I'm definitely more a fan of this approach than introducing new keywords.

Note that @alias has a more limited use, it does not allow you to add logic, unlike setter/getter.

I agree that @alias is much more limited in comparison. One thing I like about your approach is that it also makes it convenient to emit signals when one of the properties changes. Coming back to my original example with Character and Stats for the sake of concreteness how does the following look?

signal stats_changed(property : StringName)

var stats : Stats #inherits from Resource
var hp : int : set=set_stats_property, get=get_stats_property
var attack : int : set=set_stats_property, get=get_stats_property
var defense : int : set=set_stats_property, get=get_stats_property
var job : String : set=set_stats_property, get=get_stats_property #added this for the sake of type heterogeneity

func set_stats_property(property : StringName, value : Variant) -> void:
    if stats.get(property) != value: stats_changed.emit(property)
    stats.set(property, value)


func get_stats_property(property : StringName) -> Variant:
    return stats.get(property)

I suppose if one were incredibly serious about type safety, it should be possible to do something this as well right?

var defense : int : set=set_stats_int, get=get_stats_int
var job : String : set=set_stats_string, get=get_stats_string

This is starting to sound like a pretty natural extension to the existing setter/getter system. What do people think?

@nlupugla
Copy link
Author

Another thought I had is about whether this approach would be compatible with Godot's C# API. I don't use C# much, but since Godot "officially" supports it to some extent, it would be great if this feature could be designed in such a way that it works in C# as well.

@nlupugla
Copy link
Author

Finally (and this might be veering pretty far off topic), it would be great if the set/get syntax had better support for read only properties. I've experimented with the following two approaches using the existing syntax, which each have their own drawbacks.

Approach 1: don't write a set method

var hp : int :
    get: return stats.hp

The problem with this approach is that if I write a line like character.hp = 0 somewhere else in the code, there are no clear indications that this code won't do anything. This kind of thing is ripe for hard to track down bugs.

Approach 2: assert(false) in the set method

var hp : int :
    get: return stats.hp
    set(value): assert(false)

This approach at least aborts execution if the program encounters character.hp = 0, but it would be great if the editor could flag this line as an error. On the contrary, the editor gives me a warning about my assert(false) statement, which is not very helpful in this circumstance.

This is probably better suited for its own issue/proposal, but I mention it here because it's worth considering while we are already discussing upgrades to set/get.

@nlupugla
Copy link
Author

How difficult do you think the expanded set/get functionality would be to implement? Would it be doable for a first-time contributor like me?

@dalexeev
Copy link
Member

Here are some snippets you will need:

1. You probably won't need to make changes to the parser, but you'll find gdscript_parser.h very useful (node structures, DataType class).

https://github.com/godotengine/godot/blob/26fb911f79d7b16c46ca476923fe1f32ab5d27ed/modules/gdscript/gdscript_parser.h#L1183-L1199

2. In the analyzer, you need to check the function signature (number of arguments and their types).

https://github.com/godotengine/godot/blob/26fb911f79d7b16c46ca476923fe1f32ab5d27ed/modules/gdscript/gdscript_analyzer.cpp#L1280-L1303

3. Also, some changes in the compiler will be required. I'm not familiar with the later stages, but here's what I found.

https://github.com/godotengine/godot/blob/26fb911f79d7b16c46ca476923fe1f32ab5d27ed/modules/gdscript/gdscript_compiler.cpp#L1182-L1194

4. It is also possible that changes to other files (gdscript_byte_codegen.cpp, gdscript_vm.cpp, etc.) will be needed, but at first glance I think this is unlikely.

5. At the end, you need to add tests.

@dalexeev
Copy link
Member

Finally (and this might be veering pretty far off topic), it would be great if the set/get syntax had better support for read only properties.

Related:

@nlupugla
Copy link
Author

Thanks for pointing me to the relevant source code!

As I started thinking about the implementation, a noticed a potential problem with the two-argument set/get approach we discussed. Because the name of the variable would be passed as an argument to the set/get method, the name of the variable becomes more important than usual.

For example, consider the code below, which we had discussed previously:

var stats : Stats #inherits from Resource
var hp : int : set=set_stats_property, get=get_stats_property

This is great if Stats has a property called "hp" AND I want to name my variable "hp", but there are cases where both might not be true.

Imagine I want to keep track of a character's stats over time. I would like to be able to write something like

var current_stats : Stats
var previous_stats : Stats
var current_hp : int : set_current_stats_property, get=get_current_stats_property
var previous_hp : int : set_previous_stats_property, get=get_previous_stats_property

but this might lead to very unexpected results.

If Stats only has a property called "hp", not "previous_hp" or "current_hp", the editor could highlight the line as an error. However, if Stats does have a property called "previous_hp", then the user will actually be accessing the previous_hp of previous_stats, whereas they might expect to access hp of previous_stats. If the user understands this, they could in principle modify their set/get with a match statement, eg:

func get_previous_stats_property(property : StringName) -> Variant:
    match property:
        "previous_hp": return previous_stats.get(hp)
        _: return previous_stats.get(property)

The weird thing about the code above is that property refers to the user's name for the variable in the script. I can't think of any other places where the user has programmatic access to the names of their script variables. I'm not really sure how big of a problem that is.

What do others think?

@dalexeev
Copy link
Member

I don't think this is a problem. The name passed to the setter/getter is needed in order to distinguish which property this setter/getter was called for. And since there are no pointers/references in GDScript, the only way to do this is to pass a string with the name of the property.

If the object/dictionary has different names, then of course you have to use if/match/dictionary to get the target name, but that has nothing to do with the name passed to the setter/getter.

Another option is to allow binding arguments when specifying a setter/getter:

var a: int: set = set_property.bind(&"a"), get = get_property.bind(&"a")
var b: int: set = set_property.bind(&"b1"), get = get_property.bind(&"b1")

func set_property(value: Variant, property: StringName) -> void:
    _data[property] = value

func get_property(property: StringName) -> Variant:
    return _data[property]

But personally, I think this is an unnecessary complication compared to the above solution. As a last resort, you can use existing inline setters/getters, but you end up with 3 or 5 lines instead of one (depending on the style).

@nlupugla
Copy link
Author

I'm glad to hear you don't think it's a problem!

I also thought about a functional approach using bind. I was even curious if it would work with GDScript as is. It turns out it doesn't (probably because bind only evaluates at run time). The syntax for this approach also isn't as amenable to copy and pasting. In your above example, you would have to replace a1 with b1 twice after copy and pasting.

I'll go ahead with the implementation we discussed (and edit my original post when I get the chance).

If the variable name thing ever becomes confusing for users, we can always combine it with something like the proposed @alias decorator. For example,

@alias("hp") var previous_hp : int : set=set_previous_stats_property, get=get_previous_stats_property

could be the syntax for accessing previous_stats.hp and calling it previous_hp.

PS:

The bind inside the set/get line only evaluating at runtime could be addressed with something like a @bind decorator. For example,

var previous_hp : int : set=@bind("hp") set_previous_stats_property, get=@bind("hp") get_previous_stats_property

I would say this is overkill for this particular example (it also doesn't fix the copy-and-pastability), but I mention it because I imagine an @bind decorator could be useful in many different circumstances.

@dalexeev
Copy link
Member

var previous_hp : int : set=@bind("hp") set_previous_stats_property, get=@bind("hp") get_previous_stats_property

Annotations are not decorators and not expressions, so this syntax would never be valid I guess.

I also thought about a functional approach using bind. I was even curious if it would work with GDScript as is. It turns out it doesn't (probably because bind only evaluates at run time).

Yes, that's right. We could make the argument binding part of the syntax, but that would restrict them to constant values and therefore also a bad option.

@alias("hp") var previous_hp : int : set=set_previous_stats_property, get=get_previous_stats_property

In my opinion, this is non-obvious, "magic" behavior, which does not correspond to my original idea of @alias. You could use it like this, but the limitation would still be that you can't add custom logic (and separate setter and getter):

@alias var a = x.a
@alias var b = x.b1

I'll go ahead with the implementation we discussed (and edit my original post when I get the chance).

Good luck! If you need help, don't hesitate to ask.

@vnen
Copy link
Member

vnen commented Apr 29, 2023

Note that just because this is being discussed it does not mean the proposal was approved. Not to discourage you, but to avoid having you working on something that won't be merged.

Honestly, none of the approaches sound like a good idea to me. Not because they don't solve the problem in a good way but because the problem itself is very specific to the way you're structuring your code. I think it's not even a problem: if you want to follow this pattern you will have to provide some boilerplate, that's how those things commonly go. Usually code is written once and read many times, so while it might be tedious to write at first, you only have to do it a handful times.

there is some UI within the editor which allows the user to auto-generate code such as

It might be possible already to write a custom plugin that does this. The only issue is that you don't have access to internals of the GDScript compiler to help, so you would need some manual parsing to make it work. Given your specific use case, I don't think anything general would be helpful for you. Usually generating getter/setter from IDE would just give you the standard:

var stats: Stats:
	get: return stats
	set(value): stats = value

Instead of doing what you want.

Regarding custom annotations, what @dalexeev said holds: we don't have a good picture yet of what is expected of custom annotations. There's many questions to answers, not everything is fleshed out in the proposal. But it is still on my mind and I want to work on it at some point, as it was part of the original idea for annotations.

I'm against any syntactic sugar that is not standard or used a lot (e.g., the $ notation is used pretty much everywhere, so it definitely brings benefits). If we add this sugar because of one specific use, what else won't we add to GDScript because of other use cases? I prefer offering the ability to do it yourself when you needed and if we see a lot of people using we can consider integrating to the language. While annotations to relieve the burden of some new keywords or syntax sugar, it does not mean we'll add tons of different annotations for specific cases.

Just to be clear: C# is unrelated. This is a language feature so it's only relevant for GDScript and can be solved within the GDScript implementation. If you needed it in C# you would have to ask Microsoft to implement the feature in the language.


The setter/getter that receives the name of the property may have some merit to allow reusing the same function with many properties. Still, I'm not sure if those are warranted if they only solve this problem. Are there other actual use cases that would benefit from this? Perhaps it would be the solution to other proposals? This might be "easy" to implement, so maybe it's worth it for the sake of it.

Type safety is something to consider as well. If the same setter/getter is used for properties of different types, the type of the parameter must be compatible (or Variant). The input value for the setter and the return value for the getter must be properly converted beforehand, and this needs to be guaranteed by the compiler.

Binding arbitrary arguments sounds useful, but it's probably complex to implement. Functions are not Callables, they just create Callables at runtime when referenced. So to properly call the setter/getter with binds those would need to be stored in the script and passed as arguments at runtime. If you want non-constant values it becomes more difficult because it would need to store expressions to be executed when calling. I don't think this is worth the hassle.

@dalexeev
Copy link
Member

Note that just because this is being discussed it does not mean the proposal was approved. Not to discourage you, but to avoid having you working on something that won't be merged.

Thanks for the addition. I wanted to advise @nlupugla to first ask for confirmation of this design on the #gdscript channel in Rocket Chat, but at some point I forgot about it. I always take the risk that my PR may not be merged, but that doesn't stop me if I believe the feature is good enough. But you are absolutely right that I should have reminded this for a first time contributor.

The setter/getter that receives the name of the property may have some merit to allow reusing the same function with many properties. Still, I'm not sure if those are warranted if they only solve this problem. Are there other actual use cases that would benefit from this? Perhaps it would be the solution to other proposals?

In my opinion, the option is the best and most compromise of those proposed. It reduces the boilerplate, is quite versatile (it solves not only @nlupugla's specific problem, you can add custom logic in the setter/getter), relatively easy to implement, logically continues an existing feature, rather than introducing a new one (if you want to delegate a setter/getter function with set =/get =, it's logical to assume that you'll want to reuse it for multiple variables and need to differentiate between them).

Type safety is something to consider as well. If the same setter/getter is used for properties of different types, the type of the parameter must be compatible (or Variant). The input value for the setter and the return value for the getter must be properly converted beforehand, and this needs to be guaranteed by the compiler.

Yes, it should be handled correctly in the analyzer and compiler, but I think it's relatively easy to implement and shouldn't cause any problems.

The only other thing we need to think about is whether we want to use some other setter/getter signature in the future. For example, there is a suggestion (#6107) to allow functions with optional extra arguments to be used as setter/getter (for example, my_var = 1 and set_my_var(1, "optional_arg1", "optional_arg2"). But I don't think this is a problem, since you can always use a function with a different name. Or if we want to implement this, then we can only take into account the required arguments.

@dalexeev
Copy link
Member

@nlupugla
Copy link
Author

nlupugla commented Apr 29, 2023

Note that just because this is being discussed it does not mean the proposal was approved. Not to discourage you, but to avoid having you working on something that won't be merged.

Thanks for the clarification! That's kind of what I figured anyway. I'm having fun working on it, even if it doesn't get approved. I'll be sure to discuss the feature a bit on the chat. Plus there's a few details that I need some help with anyway. Is it okay to @ either of you with specific questions about the source code or is that considered bothersome?

Are there other actual use cases that would benefit from this? Perhaps it would be the solution to other proposals?

I vaguely remember seeing a proposal related to making it easier to make your variables to emit a signal when they change. This change would solve that issue I think.

@vnen
Copy link
Member

vnen commented Apr 29, 2023

Thanks for the clarification! That's kind of what I figured anyway. I'm having fun working on it, even if it doesn't get approved. I'll be sure to discuss the feature a bit on the chat. Plus there's a few details that I need some help with anyway. Is it okay to @ either of you with specific questions about the source code or is that considered bothersome?

You can @ me in the chat if you need to ask something.

I vaguely remember seeing a proposal related to making it easier to make your variables to emit a signal when they change. This change would solve that issue I think.

True. I feel that the name argument in setter/getter is a good overall addition.

@dalexeev

The only other thing we need to think about is whether we want to use some other setter/getter signature in the future. For example, there is a suggestion (#6107) to allow functions with optional extra arguments to be used as setter/getter (for example, my_var = 1 and set_my_var(1, "optional_arg1", "optional_arg2"). But I don't think this is a problem, since you can always use a function with a different name. Or if we want to implement this, then we can only take into account the required arguments.

I thought this was already allowed. For what I see now it is supported in 3.x and dropped in 4.0, but this was an oversight. I was already considering this case, the difference between optional and required arguments would be enough for this.

@nlupugla
Copy link
Author

I thought this was already allowed. For what I see now it is supported in 3.x and dropped in 4.0, but this was an oversight. I was already considering this case, the difference between optional and required arguments would be enough for this.

Could you clarify what you mean here? In 4.0, the only allowed getter signature is

my_getter_function() -> some_type

With the variable getters we've proposed in this issue, we would allow one additional getter signature:

my_getter_function(property:StringName) -> some_type

If we allow for optional arguments, what signatures would be supported? Something like

my_getter_function(property:StringName, option_1 := default_1, option_2 := default_2) -> some_type

looks pretty compatible with variable getter proposal, but what about the following signature?

my_getter_function(option_1=default_1)

If the compiler determines the getter behavior by simple argument counting, this signature would get confused with the (property:StringName) signature. It could count how many non-default arguments are in the signature I suppose. Is that what you meant?

@nlupugla
Copy link
Author

PS: I was looking through some of the issues linked to this one and noticed that #3133 looks very related as well.

@dalexeev
Copy link
Member

looks pretty compatible with variable getter proposal, but what about the following signature?

my_getter_function(option_1=default_1)

If the compiler determines the getter behavior by simple argument counting, this signature would get confused with the (property:StringName) signature. It could count how many non-default arguments are in the signature I suppose. Is that what you meant?

When a variable has an "outer" setter/getter (with the PROP_SETGET style), the analyzer only checks the number of required arguments (and their types, if specified), ignoring the optional arguments.

For a regular setter and getter, there must be 1 and 0 required arguments, for setter and getter proposed here there must be 2 and 1 required arguments, respectively. Any other options for the number of required arguments will result in an analyzer error.

Thus, the following signatures are different and there will be no conflict, because if you use an "outer" setter/getter, then the function must satisfy these requirements.

func set_prop(name, value) # A setter with property name.
func set_prop(value, opt_arg = 1) # A regular setter with one optional argument.
func get_prop(name) # A getter with property name.
func get_prop(opt_arg = 1) # A regular getter with one optional argument.

You can't have a regular setter with two required arguments or a regular getter with one requred argument (neither in 4.0 nor 3.x), it doesn't make sense. Therefore, it does not break compatibility.

@nlupugla
Copy link
Author

nlupugla commented May 3, 2023

Should the compiler ensure that the first argument of a 1-argument getter or 2-argument setter is a StringName? Maybe it should only enforce this if the first argument isn't optional, so as not to conflict with any future plans to allow for setters and getters with optional arguments?

@dalexeev
Copy link
Member

dalexeev commented May 3, 2023

Maybe it should only enforce this if the first argument isn't optional

If the first argument is optional, it should be a regular getter/setter, not a getter/setter with name.

Should the compiler ensure that the first argument of a 1-argument getter or 2-argument setter is a StringName?

If the argument is required and has a specified type (that is, it can be either a weak type or a hard String/StringName).

@dalexeev
Copy link
Member

dalexeev commented Sep 4, 2023

An interesting point is that the core also supports setters/getters with additional arguments, there is a macro ADD_PROPERTYI(). But in the core the argument must be an integer (index), not a string. In GDScript String/StringName makes more sense, of course.

This is not a change request, just an additional argument in favor of an additional argument. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants