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

this is unintuitive #115

Closed
Mindena opened this issue Dec 3, 2021 · 2 comments
Closed

this is unintuitive #115

Mindena opened this issue Dec 3, 2021 · 2 comments

Comments

@Mindena
Copy link

Mindena commented Dec 3, 2021

Unlike other object-oriented languages that allow the elision of this when accessing class members (cf. Java), the resulting expressions exhibit differing behavior in TScript. I found two specific instances of this.

this can only access public members

class A
{
private:
	var x;
protected:
	var y;
public:
	var z;
	constructor()
	{
		this.x; # error
		this.y; # error
		this.z; # works
	}
}
A();

This seems to be a known "feature". I understand that it might make the . operator easier to handle for the interpreter (although super.y on a protected member works just fine for some reason???), but from a beginner's perspective this communicates some questionable ideas about encapsulation and obfuscates how the heck non-static members even work in the first place. (I have found that to be a big hurdle in my own learning process when first starting out in C#, and reminding myself that I could always prepend an explicit this. has made it a lot easier to grasp.)
Furthermore, the lack of a way to disambiguate between constructor parameters and non-public members means that different sets of names have to be chosen for each, whether through Hungarian notation or a more irregular naming scheme. Not a big issue, but it has always bugged me from an aesthetic perspective.

Eliding this breaks overriding

class A
{
public:
	var x = "A";
	function foo() { print(x); }
	function bar() { print(this.x); }
}

class B : A
{
public:
	var x = "B";
}

B().foo(); # prints "A"
B().bar(); # prints "B"

Same thing happens for methods. This effectively makes anything non-overridable unless you make sure to write out the this. everywhere, and as stated above this isn't always possible.


I believe that making the notations x and this.x interchangeable in all places would make the language more conceptually sound and in tune with object-oriented design principles. Would be glad to see this fixed or to be enlightened on why it shouldn't or can't be fixed.

@Mindena Mindena added the bug Something isn't working label Dec 3, 2021
@TGlas
Copy link
Owner

TGlas commented Dec 4, 2021

Thanks for bringing this up. Indeed, the use of x vs. this.x was a design decision I was not sure about.

In the end, I don't care so much for whether x and this.x are semantically interchangeable, but I do care that the mechanisms are not confusing. Your learning process was apparently driven by the assumption that they are interchangeable, which makes sense for some languages, but not for all. If nobody would ever have told you that you can use this.x instead of x everywhere, maybe the learning path would have been different. I'd therefore not call this a bug, but rather a mismatch of expectations. Anyway, let's see what we can do to make this less confusing.

The topic touches two aspects, related to static vs. dynamic binding:

  • name lookup rules
  • virtual methods in inheritance chains

Compared with statically typed languages, additional complications arise from the fact that dynamic name lookup ("duck typing") is the default.

In the current design, there are three ways to access members from within a class:

  • x - static name resolution within class and superclass,
  • super.x - static name resolution restricted to the inheritance chain (upwards),
  • this.x - dynamic name resolution, hence taking overrides in sub-classes into account.

You can think of it as follows:

  • x - start lookup in the class itself
  • super.x - start lookup in the superclass
  • this.x - start lookup in the (lowest) subclass

In all cases, lookup goes up the inheritance chain until a (visible) member is found. One possible solution may be to introduce a new syntax for the last case, maybe something along the lines of sub.x or dynamic.x. That would be compatible with super.x and make the purpose of using this.x instead of simply x more clear. One could also argue that the syntax super.x is confusing, since it is the only case in which the dot-operator does not mean "dynamic name lookup".

It should be noted that in TScript, this and super are very different concepts. For example, print(this); works, since this refers to the current object (and actually not to the object restricted to the class, but to the subclass object). In contrast, print(super); does not work. super is not an object, it is only a special syntax to restrict name lookup to the superclass inheritance chain. The reason is that in a dynamically typed language, casting an object to its super class simply does not make any sense. That's because name resolution with the dot-operator is anyway dynamic. Casting is used for changing representations (e.g., string to number), but not within the inheritance chain. I see that this can be confusing for programmers coming from statically typed languages.

In terms of functionality, I'd like to cover the following:

  • Ability to access members without the need for excessive use of this.x; that's (to me) a major pain point of Python. I do know that some folks seem to explicitly like the self.x syntax.
  • Ability to call overrides in subclasses, i.e., dynamic name resolution.
  • Ability to call a super class method of the same name, i.e., static name resolution restricted to the inheritance chain.

Now, different languages came up with different solutions. C++ has the concept of virtual methods, and it seems that C# does the same. In contrast, Java simply makes everything virtual, or in other words, it defaults to dynamic binding. Python relies on duck typing, which makes it superfluous to even define the interface at all. However, people obviously find explicit interfaces useful, see abstractmethod.

TScript is somewhere in between. It is dynamically typed (like Python and Javascript), but it enforces static object layout though classes (because I want to teach basics of object oriented design, therefore objects are not dictionaries, extensible at runtime). TScript is already capable of duck typing, and therefore it does not have a need for a second mechanism for dynamic binding. That's why x and this.x are not interchangeable, and why there is no need for declaring methods as virtual. There is also the aspect of efficient implementations (hash lookup vs. virtual function tables), but I don't think that such a consideration should drive the design of a teaching-oriented language.

One advantage of virtual functions is that they make the interface between classes more explicit. While a protected member is an interface for the subclass to interact with the superclass, a virtual method is an interface through which the superclass can call its subclass. Right now, the latter is not solved at the level of the interface definition, but instead inside of the method body, where a call like this.someMethod() is made instead of someMethod(). Nice thing is that an existing language mechanism is reused (dynamic name lookup), and the not so nice thing is that the interface remains implicit.

I see the following ways of moving forward:

  1. Introduce a virtual keyword and make interfaces within the inheritance chain explicit. Then a call like someMethod() would be dynamically resolved, even without leading this..
  2. Introduce a new keyword like sub or dynamic to make the intent of calling a subclass method more explicit.
  3. Change super.x into something else, so as to avoid the use of the dot operator. The same should probably apply to sub or dynamic. Maybe simply drop the dot, e.g., write super x? Or introduce something along the lines of the C++ scope operator super::x? Or the arrow operator super->x? Maybe even super(x)?

Finally, I hesitate to alter the meaning of this.x, since dynamic name lookup is the default, and I don't want that to become inconsistent.

@TGlas TGlas removed the bug Something isn't working label Dec 4, 2021
@Mindena
Copy link
Author

Mindena commented Dec 4, 2021

Thank you very much for your detailed reply. Due to my lack of experience with dynamic OOP languages, I had failed to consider the ramifications of dynamic name lookup. Looks like Ruby behaves the same way. I apologize for wasting your time :)

I like your suggestions overall. The one thing I would oppose is the syntax super(x), since it looks like a function call and is already used for selecting the superclass constructor in the subclass's constructor signature, which I fear might lead to even more confusion than the dot operator. Whitespace or :: would perhaps be better.

@TGlas TGlas closed this as completed Jan 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants