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

Allow overriding a member variable's type in inherited classes #4686

Closed
filantus opened this issue Jun 17, 2022 · 10 comments
Closed

Allow overriding a member variable's type in inherited classes #4686

filantus opened this issue Jun 17, 2022 · 10 comments

Comments

@filantus
Copy link

Describe the project you are working on

Test project

Describe the problem or limitation you are having in your project

Lets assume there is base class with a variable value and some logic that utilize this variable.

class MyBaseClass:
	var value

If try to extends this class with another with type hinted value like this:

class MyIntClass extends MyBaseClass:
	var value: int

or

class MyFloatClass extends MyBaseClass:
	var value: float

it will brings an error:
The member "value" already exists in a parent class.


So the goal is to use already existed logic from BaseClass (that manipulate with var value) in another inherited classes but with type hinting that restrict to use distinct var type for particular inherited class.

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

Allow to override variables without type hinting from parent class with hinting some type in inherited classes.

Or add some ability to explictly specify variables type in inherited classes.

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

For example:

Variant 1 - just allow to override variables.

class MyBaseClass:
	var value

class MyIntClass extends MyBaseClass:
	var value: int

class MyFloatClass extends MyBaseClass:
	var value: float

Variant 2 - allow to specify variables type:

class MyBaseClass:
	var value

class MyIntClass extends MyBaseClass:
	value: int

class MyFloatClass extends MyBaseClass:
	value: float

Variant 3 - allow to specify variables type with additional syntax:

class MyBaseClass:
	var value

class MyIntClass extends MyBaseClass:
	@override value: int

class MyFloatClass extends MyBaseClass:
	@override value: float

Variant 4 - allow to override variables with additional syntax:

class MyBaseClass:
	var value

class MyIntClass extends MyBaseClass:
	@override_var(value, int)

class MyFloatClass extends MyBaseClass:
	@override_var(value, float)

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

I don't see a way to do this with class inheriting.

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

This is about improving GDScript

@Calinou Calinou changed the title Override variables type in inhereted classes Allow overriding a member variable's type in inherited classes Jun 17, 2022
@Calinou
Copy link
Member

Calinou commented Jun 17, 2022

Are there other programming languages allowing this? To me, it sounds like this breaks OOP principles.

@Mickeon
Copy link

Mickeon commented Jun 17, 2022

What would an actual use-case for this even be? It doesn't just break programming standards, but it could be avoided outright by not defining a member variable's type, keeping it a Variant.

@filantus
Copy link
Author

@Calinou in Python it's possible to reach that behaviour. Of course Python it is not a static typed language, but it's feasible.

@filantus
Copy link
Author

@Mickeon

For example, let's imagine we need some complex character int and float stat class.
Logic is same.
Difference only in type (int / float).

If try to do it now like this:

extends Node


class StatBase:
    var base
    var max
    var val

    func _init(params: Dictionary = {}):
        base = params.get('base', 0)
        max = params.get('max', 0)
        val = params.get('val', 0)

    func setv(value):
        val = clampf(value, 0, max)

    func add(value):
        val = clampf(val + value, 0, max)

    func sub(value):
        val = clampf(val - value, 0, max)

    func is_full():
        return val >= max


class IntStat:
    extends StatBase

    func setv(value: int):
        super.setv(value)

    func add(value: int):
        super.add(value)

    func sub(value: int):
        super.sub(value)


class FloatStat:
    extends StatBase

    func setv(value: float):
        super.setv(value)

    func add(value: float):
        super.add(value)

    func sub(value: float):
        super.sub(value)


func _ready():
    var int_stat: IntStat = IntStat.new()
    int_stat.max = 1.5

    var float_stat: FloatStat = FloatStat.new()
    float_stat.val = 'text'

It's possible even override functions with type hint for arguments.

But there is still remains some problems:

  1. it's allow to set float value to int_stat and not brigns up warning:
    Narrowing conversion (float is converted to int and loses precision).

  2. it's allow to set even not a numeric value (e.g. 'text' to float_stat.val).

@KoBeWi
Copy link
Member

KoBeWi commented Jun 17, 2022

This looks like something generics are for (which GDScript lacks).

@dalexeev
Copy link
Member

Related:

Overriding the type is a bad idea, it violates the Liskov substitution principle.

@filantus
Copy link
Author

filantus commented Jul 9, 2022

Related:

Overriding the type is a bad idea, it violates the Liskov substitution principle.

Okay. I think i was not clearly enough to convey my point of view. I don't need to change already defined types to a different. I just need ability to define variable without type in parent class and define type later in inherited classes.

The goal of it is:

  1. Parent class might have a some variables (without exact type) and contain some logic what work with these variables. Some logic that not dependent on variables type. Sort of impersonal logic.
  2. Inherited classes can additionally define types to variables which inherited from parent class and haven't types. And these inherited classes can as use impersonal logic from parent class as also add and use additional logic that involve these variables (that now already have type in inherited classes).

In this case inherited classes can change variables types only inside themselves. So i think it can't break anything in parent class and in logic inherited from parent class (because that logic don't rely on a variable type).

I mean it's not overriding one type to another. I consider it as extending existed functionality with specifying a type to a variables without type.

Yes, maybe it can break something if try to use that inherited class instead of parent class (substitution). But in my case that's will never be happened. Because in this case parent class is just only a base class that not designed to be used directly at all. So how about that?

Substitution principle can be violated even without this proposal. Just with methods overriding. But methods overriding is legit by itself. Therefore violation dependent on how technology are used, not on which technologies are available. In my case i think it's possible to use it without violation and with profit.

@dalexeev
Copy link
Member

dalexeev commented Jul 10, 2022

I don't need to change already defined types to a different. I just need ability to define variable without type in parent class and define type later in inherited classes.

There are no variables of undefined type. var x is var x: Variant, where Variant is the widest (generic) type.

Type narrowing (for example, Variant to int) is also a violation of the substitution principle, as well as a complete replacement (for example, int to String).

class A:
    var x: Variant

class B extends A:
    @override var x: int

# The static type of obj is A, and the actual value is type B,
# which inherits A. The code here doesn't need to know anything
# about subtypes of A, it could be func f(a: A) that was passed
# an object of class B.
var obj: A = B.new()

# According to the substitution principle, this is a legal operation,
# since A.x is Variant, the editor should mark this line as type safe,
# and the compiler cannot find an error here statically.
# However, with the proposal, there will be a runtime type error here!
obj.x = "test"

But in my case that's will never be happened.

This may sound harsh, but we're not just talking about your case here. We are thinking about how this proposal will affect the language as a whole.

Substitution principle can be violated even without this proposal.

Yes, because the meaning of a type is more than just int, float, etc. You can create f(x: int), and write in the documentation that x must be greater than zero (that is, the type is not int, but PositiveInt, which does not exist in GDScript). However, I have two objections to this argument:

  1. The ability to violate the substitution principle within a single type (e.g. int, as in the example above) does not mean that we should make it possible where GDScript distinguishes between types.
  2. Even where strict adherence to the substitution principle cannot be enforced with static types, it is still a bad idea to violate it, it leads to bugs, non-obvious behavior, bad architecture, code duplication, etc. The substitution principle is still better to follow (by checks at runtime, asserts or by conventions). In particular, it forbids type redefinition in descendant classes: both a GDScript type and an implied type that does not exist in GDScript (a type is a set of valid values).

Of course, in practice there are exceptions to any principle. They are caused 1. by the fact that the principle itself is not universal and in some cases, on the contrary, harms, 2. by the fact that we sometimes have no time to understand our mistakes and design an ideal system. Personally, I consider the substitution principle to be one of the most basic and important principles in programming. It comes from the laws of mathematics, not subjective human preferences. If I ever violated this principle, then I think all these cases relate to p. 2. In my opinion, this principle is too important for us to create tools that allow its violation where it was previously impossible.

Probably what you are asking can be solved with abstract/virtual properties (and abstract classes), with generic methods and/or parametric types. The last two are unlikely to appear due to GDScript's dynamic typing, but perhaps abstract/virtual will appear in future versions of GDScript.

@filantus
Copy link
Author

Ok. I just wanted to propose a some addition what can be useful in some cases if use it right. If you don't want it so be it.

Repository owner moved this from In Discussion to Implemented in Godot Proposal Metaverse Jul 10, 2022
@YuriSizov YuriSizov closed this as not planned Won't fix, can't repro, duplicate, stale Jul 10, 2022
@RichardEllicott
Copy link

RichardEllicott commented Jan 27, 2023

Liskov substitution principle.

according to this principle, it means a superclass can be replaced with a subclass... so it doesn't violate this principle as we are adding variables to the subclass or overriding them

obviously if you then break your program by adding the wrong variable that's your fault, no amount of child stabalisers is gonna stop that person falling off his bike... in fact you can already over-ride a function and break your "Liskov substitution principle" then

i'd like this to be a feature as it works in all other dynamic languages i've used... it's beginner freindly syntax which is the way GDScript should be... i understand some coders like to put all their vars in init because they get fed up with inconsistency so many purists want to pipe in and say it shouldn't be the case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Implemented
Development

No branches or pull requests

7 participants