godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add access levels for class method and variable in GDScript #11039

Open zjin123 opened 3 days ago

zjin123 commented 3 days ago

Describe the project you are working on

A cross-platform app

Describe the problem or limitation you are having in your project

I think hiding class method/variable from outside may reduce potential bugs and makes code more clean.

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

Allow access level modifier on class method and variable.

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

Implemented in https://github.com/godotengine/godot/pull/98606

UPDATE:
internal changed to protected. pub changed to public. Project setting for global default access level is removed.

The following should not break existing code.

Add keywordspublic protected private as access level modifier for class method and class variable and an additional keyword readonly for class variable.

public the method/variable can be accessed by anyone. protected the method/variable can be only accessed by the class defining it and its subclasses. private the method/variable can be only accessed by the class defining it. readonly the variable can be read by anyone but can be only assigned to by the class defining it.

All of the four keywords can be used as identifier to not break existing code.

class_name Foo
extends Node

public var a = 1
protected var b = 2
private var c = 3
readonly var d = 4

private func _init():
    pass

public func foo():
    pass

public static func Create() -> Foo:
    return Foo.new()

# the following are OK
public var public = 0
protected var protected = 0
private var private = 0
readonly var readonly = 0

For class method/variable without explicit access level modifier in one script, add two top-most annotations @private @protected. @private implicates private. @protected implicates protected. If no such annotation is applied, default to public to not break existing code.

@private
extends Node

var a = 1 # a is private
@protected
extends Node

var a = 1 # a is protected
extends Node

var a = 1 # a is public

Limitation

This feature works only at compiling time and rely on typed code. It has no effect on untyped code both at compiling time and at runtime. For example, the following untyped code will compile and run without triggering any access level check. It is up to the user to decide to opt-in this feature (by writing types and access level modifiers) or not.

extends Node
class Foo:
    private func foo():
        pass

func bar(obj):  # <-- Because obj is untyped, there is no access level check.
    obj.foo()

func test():
    bar(Foo.new())

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

No.

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

Need to modify GDScript's compiler.

Chaosus commented 3 days ago

Seems like a duplicate of https://github.com/godotengine/godot-proposals/issues/641

Lazy-Rabbit-2001 commented 3 days ago

I think this consolidates #641 with some extra usages

Flynsarmy commented 3 days ago

Personally I'd prefer protected keyword over internal. Looking at all the related PRs and proposals they too all seem to use protected.

Lazy-Rabbit-2001 commented 3 days ago

Personally I'd prefer protected keyword over internal. Looking at all the related PRs and proposals they too all seem to use protected.

The more ambiguous reason is that inner may lead new gdscript users to the concept of private, which only allows a member to be accessed from the same class.

This also mentioned me of what dalexeev tipped under that pr of mine. In some languages like Haxe, private = protected. So if the author insist using private and inner as modifiers, it's better to swap their functions.

dalexeev commented 3 days ago

Limitation

This feature relys on static typing. So the following code works (and it should not):

extends Node
class Foo:
    private func foo():
        pass

func bar(obj):  # <-- obj's type is unknown
    obj.foo()

func test():
    bar(Foo.new())

because the type of obj cannot be determined at compiling time.

GDScript is a gradually typed language, i.e. it combines the qualities of both dynamically and statically typed languages. Type hints are optional and help with static analysis and performance. However, typed code must easily interoperate with untyped code. We don't want to limit users of untyped GDScript and there are no plans to make GDScript a fully statically typed language, as far as I know. So this limitation raises serious concerns about whether we should include the feature in this form.

HolonProduction commented 3 days ago

For new project, add a project setting gdscript/default_access_level which is an enum of Public, Internal and Private. It will change the default access level for all class methods and class variables without modifier for the whole project.

I really don't think we should make GDScript behaviour configurable through project settings.

zjin123 commented 3 days ago

Limitation This feature relys on static typing. So the following code works (and it should not):

extends Node
class Foo:
    private func foo():
        pass

func bar(obj):  # <-- obj's type is unknown
    obj.foo()

func test():
    bar(Foo.new())

because the type of obj cannot be determined at compiling time.

GDScript is a gradually typed language, i.e. it combines the qualities of both dynamically and statically typed languages. Type hints are optional and help with static analysis and performance. However, typed code must easily interoperate with untyped code. We don't want to limit users of untyped GDScript and there are no plans to make GDScript a fully statically typed language, as far as I know. So this limitation raises serious concerns about whether we should include the feature in this form.

Thanks for the reply. The feature is indeed optional. It is a pure static checking for typed code without involving any runtime change. For untyped code, this feature actually has no effect and those codes will work like usual. Only when a user decides to opt-in this feature by explicitly marking types and applying access level modifier, the feature can provide additional check at compiling time as a bonus. If a user chooses to stay with untyped code, then the feature appears as it does not exist (like the code in the limitation section will compile and run as usual without triggering any access level check at all). Hence I think it does not limit user of untyped code or force them to make any change. Purely optional. It is all up to user's decision.

zjin123 commented 3 days ago

For new project, add a project setting gdscript/default_access_level which is an enum of Public, Internal and Private. It will change the default access level for all class methods and class variables without modifier for the whole project.

I really don't think we should make GDScript behaviour configurable through project settings.

Indeed. I will remove it.

zjin123 commented 3 days ago

Personally I'd prefer protected keyword over internal. Looking at all the related PRs and proposals they too all seem to use protected.

Sure. I will change internal to protected. Actually I have no preference over those two :)

Flynsarmy commented 3 days ago

One other change I'd like to see is pub changed to public. You're using the full word for private and protected, but abbreviating public to pub which is inconsistent and may cause confusion.

zjin123 commented 3 days ago

One other change I'd like to see is pub changed to public. You're using the full word for private and protected, but abbreviating public to pub which is inconsistent and may cause confusion.

I just pick pub from Rust as it is short and convenient... But you are right, I will change it.

nbaum commented 2 days ago

Thanks for the reply. The feature is indeed optional. It is a pure static checking for typed code without involving any runtime change. For untyped code, this feature actually has no effect and those codes will work like usual. Only when a user decides to opt-in this feature by explicitly marking types and applying access level modifier, the feature can provide additional check at compiling time as a bonus. If a user chooses to stay with untyped code, then the feature appears as it does not exist (like the code in the limitation section will compile and run as usual without triggering any access level check at all). Hence I think it does not limit user of untyped code or force them to make any change. Purely optional. It is all up to user's decision.

While developers can opt-in to typing, they cannot opt another developer out of typing by "forgetting" the type of a variable. e.g.

class Foo:
  var x := "hello"

func _ready() -> void:
  var y : Variant = Foo.new()
  y.x = 3.0 # valid at compile time, but fails at runtime because I can't make Foo.x ignore its own type

But this proposal and associated PR do trivially allow me to bypass somebody else's access restrictions. e.g.

class Foo:
  private var x := "hello"

func _ready() -> void:
  var y : Variant = Foo.new()
  y.x = "world" # valid at compile time, and doesn't fail at runtime: Foo.x ignores its own access restriction

This behaviour may be surprising, especially for the author of Foo.

While I like the feature, I worry about this limitation.

girng commented 2 days ago

reduz's initial core values for gdscript were centered around being easy to learn and fast prototyping.

even though gdscript has changed over the years, i still think those values are integral to godot. if the dev needs access to variables/functions that are private in c++, they can compile from source.

vice versa if they need to implement access specifiers for classes in gdscript, they can make a module or use gdnative. imo, piling on extra keywords in gdscript can make it overly complex for the developer

Flynsarmy commented 2 days ago

if they need to implement access specifiers for classes in gdscript, they can make a module or use gdnative

This is currently the 11th most requested feature in godot-proposals and that's not even including all the duplicates of the same request. Suffice to say, the desire by the community is high enough for it to be added to the engine IMO.

girng commented 2 days ago

if they need to implement access specifiers for classes in gdscript, they can make a module or use gdnative

This is currently the 11th most requested feature in godot-proposals and that's not even including all the duplicates of the same request. Suffice to say, the desire by the community is high enough for it to be added to the engine IMO.

https://www.mtfca.com/discus/messages/506218/541602.png

core functionality shouldn't be added just because it's popular. it should be gauged whether it's truly needed. there's already two solutions the developer can do: compile from source or use gdnative. the more stuff that gets piled in gdscript, the more bugs that arise from it (which can negatively effect godot's development).

i've seen numerous threads in this section where users are wanting to turn gdscript into some god-like c++ language with STL support, access specifiers, etc. imo it goes against the entire purpose the language was designed for..

TX256 commented 2 days ago

I've grown to like the simple and succinct Python-style underscore for private stuff, and I believe GDScript manages just fine with that.

As a bonus, I like how explicit the underscore is, I can see from the function call-site and variable usage, whether the element is private.

Personally, regarding GDScript, I would simply make a single underscore account for both private & protected, and not introduce any additional keywords.