daokoder / dao

Dao Programming Language
http://daoscript.org
Other
199 stars 19 forks source link

Discussion: simplifying the OOP #434

Open Night-walker opened 9 years ago

Night-walker commented 9 years ago

One thing attractive in Go and Rust are their simplicity when it comes to OOP. Even Rust, being a rather tricky language to deal with, bases its OOP on several simple concepts. Sometimes that leads to a more verbose, boilerplate code, but the gain in maintainability is worth attention.

The ability to easily read, understand and change existing code, in my opinion, is usually more important then being able to concoct some working piece of code with minimal efforts (aka write-only programming). Simplicity and transparency play crucial role here, and that is what classic OOP often lacks.

Hence I open this abstract issue to discuss ways of making Dao OOP simpler. It won't come without sacrifices, but we should better decide what is really important and what is just a fancy toy or a legacy cruft. Looking at Go, you can clearly ascertain that it is not the abundance of features which makes the language successful.

Here are few simplest ideas with short synopses. Note that they are not proposals, just some thoughts to reflect upon.

1) Throw async classes overboard (#393). There is a dedicated issue in which the arguments are presented, so I don't stop here.

2) Always use explicit self to refer to class members. + clear and readable (mostly), prevents name collisions - happens to be verbose

3) Get rid of static variables (at least do not expose them to the users). + no loss of functionality since they can always be substituted by global variables and namespace usage - tricky wrapping of C++ classes with public static variable members (albeit highly unlikely)

4) Switch to explicit self parameter in class methods. + syntax consistency with wrapped types, no need for static and invar methods syntax sugar - somewhat cumbersome

5) Remove in-class interface methods in favor of separate interfaces. + clarity and predictability of class behavior - boilerplate code to emulate abstract classes:

# currently
class Abstract {
    interface routine virtual(){}
    routine invokeVirtual(){
        virtual()
    }
}

class Derived: Abstract {
    interface routine virtual(){...}
}

# after the change
interface HasVirtual {
    routine virtual()
}

class Abstract {
    var slot: HasVirtual
    routine Abstract(slot: HasVirtual){
        self.slot = slot
    }
    routine invokeVirtual(){
        slot.virtual()
    }
}

class Derived: Abstract {
    routine Derived(): Abstract(self){}
    routine virtual(){...}
}

6) Exclude private visibility of class members. + no need to choose between it and protected each time when writing a class - may expose sensitive data (albeit it can nevertheless be achieved by modifying the source)

dumblob commented 9 years ago

ad 1) I already commented in the referenced issue.

ad 2) I don't see it necessary. It's quite clear to me without explicit self. With explicit self there'll be too much boilerplate code - just take a look at Python modules and try to find name collision - there are basically none, so there is really no objective reason to require writing self all the time. The clarity is quite subjective, but for me it's clear enough without explicit self.

ad 3) I agree, although only in case there is a reasonable solution for efficient handling of static variables in wrapped classes (not necessarily only C++ ones)

ad 4) see ad 2) (I would leave this duality, because the difference between a class and a wrapped type in the use of self is apparent and justified)

ad 5) I'm strongly opposed to this proposal - it feels like getting back to eightees with the goal to bloat all the code around without any additional value (no clarity, no concisness)

ad 6) I totally agree with this, even though I'm again afraid of wrapping. So again, I won't support this idea unless there is a good solution for wrapped classes (not necessarily only C++ ones).

It also seems, that nearly none of the proposals would simplify the Dao VM implementation, so even from this point of view I'm slightly more reluctant to them.

Night-walker commented 9 years ago

ad 3) I agree, although only in case there is a reasonable solution for efficient handling of static variables in wrapped classes (not necessarily only C++ ones)

Wrapped types may just be exceptions to this rule, as they already are in certain cases.

ad 6) I totally agree with this, even though I'm again afraid of wrapping. So again, I won't support this idea unless there is a good solution for wrapped classes (not necessarily only C++ ones).

And how do you imagine wrapping of private C++ class members? :) No solution is needed here.

It also seems, that nearly none of the proposals would simplify the Dao VM implementation, so even from this point of view I'm slightly more reluctant to them.

The goal is to simplify the language, not the VM.

dumblob commented 9 years ago

Wrapped types may just be exceptions to this rule, as they already are in certain cases.

That would create another inconsistency. If there is no efficient way how to put static variables into a namespace to fake a global variable, I'm opposed to introducing another inconsistency.

And how do you imagine wrapping of private C++ class members? :) No solution is needed here.

You're right, I completely forgot to mention my vision :) In the best case, I don't want to get rid of private completely, but shift it's meaning from forbidden (i.e. error) to potentially dangerous (i.e. compile-time warning saying "hey, you're touching something you shouldn't").

The goal is to simplify the language, not the VM.

I thought, that the goal is to simplify language only up to a point when the VM becomes unnecessarily complicated (that's subjective and therefore I mentioned, that I'm slightly reluctant to the proposed changes).

Night-walker commented 9 years ago

If there is no efficient way how to put static variables into a namespace to fake a global variable, I'm opposed to introducing another inconsistency.

Well, static variable can always be replaced by a static method (or two methods).

You're right, I completely forgot to mention my vision :) In the best case, I don't want to get rid of private completely, but shift it's meaning from forbidden (i.e. error) to potentially dangerous (i.e. compile-time warning saying "hey, you're touching something you shouldn't").

Then it's not real private. I see no reason for half-measures: either there is private or there isn't.

Night-walker commented 9 years ago

ad 2) I don't see it necessary. It's quite clear to me without explicit self. With explicit self there'll be too much boilerplate code - just take a look at Python modules and try to find name collision - there are basically none, so there is really no objective reason to require writing self all the time. The clarity is quite subjective, but for me it's clear enough without explicit self.

Of course, Python modules don't have name collisions between class members and other stuff -- exactly because they use explicit self :)

ad 4) see ad 2) (I would leave this duality, because the difference between a class and a wrapped type in the use of self is apparent and justified)

It is not apparent. Classes can be used with interfaces with explicit self in methods, and vice versa -- wrapped types can satisfy interfaces without explicit self.

ad 5) I'm strongly opposed to this proposal - it feels like getting back to eightees with the goal to bloat all the code around without any additional value (no clarity, no concisness)

That depends on how often abstract classes are anticipated to be seen in Dao (as of now, there ins't a single one in the standard library) and how wrapped C++ abstract classes are handled in this case (that's the main question). The gain would be in explicit and clear polymorphism backed by interfaces only, but the cost may be too great.

dumblob commented 9 years ago

Well, static variable can always be replaced by a static method (or two methods).

With static property?

Then it's not real private. I see no reason for half-measures: either there is private or there isn't.

Correct. From this point of view of accepting only yes/no, there is no private thing in the whole programming world :) (sometimes something seems private, but time will always show, that there is a non-private use for it). So yes, I agree with private removal. The same actually holds for protected.

The only use case for these two keywords was always "the initial orientation in a new code" - i.e. initial decision what should I use and what rather not even without reading the code of methods and understanding the logic. This brings me to a more general problem of OOP. The internal state of an object and dependencies between methods. Nowadays I've seen so many objects which have weird dependencies between method calls and the internal state is so ad-hoc, that I've myself decided, that i'll promote OOP only in two cases:

1) the object/class is designed so, that any method can be called at any time and it will always make sense and it won't cause any harm 2) if there is any depency (i.e. required order) between method calls, the object must include a full-featured FSM, which'll ensure, that in as many cases as possible, the whole dependency tree will be called once the user (programmer) calls some method depending on something else; this case is usually very tricky and I'd rather discourage using OOP for such problems; unfortunately, such problems are very often "solved" by OOP and the result is just horrible bloatware :(

dumblob commented 9 years ago

Of course, Python modules don't have name collisions between class members and other stuff -- exactly because they use explicit self :)

No, I meant only variable names, not the whole expression referring to them. There are no collisions. self is really just a result of the window method (look out of a window and decide whether we will use self or not).

It is not apparent. Classes can be used with interfaces with explicit self in methods, and vice versa -- wrapped types can satisfy interfaces without explicit self.

I can't imagine any unclear or even disputable example. If I see class, then it's a class. If I see interface then it's an interface. What's so unclear on that?

Night-walker commented 9 years ago

Well, static variable can always be replaced by a static method (or two methods). With static property?

Something like that.

So yes, I agree with private removal. The same actually holds for protected.

Nope. Exposing encapsulated data in a sub-class is not the same as exposing it in any class instance. At least minimal safeguarding should exist.

I can't imagine any unclear or even disputable example. If I see class, then it's a class. If I see interface then it's an interface. What's so unclear on that?

It's not unclear. It's just that there is little point in dividing classes and wrapped types since they are intermixed anyway. That's why using the same notation would make things nicer and simpler, but at a cost of extra verbosity.

dumblob commented 9 years ago

It's not unclear. It's just that there is little point in dividing classes and wrapped types since they are intermixed anyway. That's why using the same notation would make things nicer and simpler, but at a cost of extra verbosity.

Ok, then again - it's subjective and I myself count low verbosity into nicer and simpler ;)

daokoder commented 9 years ago

1) Throw async classes overboard (#393). There is a dedicated issue in which the arguments are presented, so I don't stop here.

We will discuss that there.

2) Always use explicit self to refer to class members.

  • clear and readable (mostly), prevents name collisions
  • happens to be verbose

This should not be removed, but fixed. The way I am considering to fix it is to raise warning or even error, if any parameter or local/global variable bear the same name as one of the class member and any class member is not accessed through self. Namely, you can have parameters or local/global variables with the same names as members, but then you will have to access all members through self; or you can access members directly if none of the names are the same.

3) Get rid of static variables (at least do not expose them to the users).

  • no loss of functionality since they can always be substituted by global variables and namespace usage
  • tricky wrapping of C++ classes with public static variable members (albeit highly unlikely)

It would be quite tricky to define states/variables shared across class instances, and quite awkward to emulate them if static members are removed. Besides, it does not really simplify the language much.

4) Switch to explicit self parameter in class methods.

  • syntax consistency with wrapped types, no need for static and invar methods syntax sugar
  • somewhat cumbersome

You already mentioned it would become cumbersome. If you want consistency with wrapped types, I would go with changing the parsing of signatures of wrapped methods by supporting something like { fpter, "static meth(...)" }. But then it is also cumbersome and less elegant, and requires a lot of changes.

5) Remove in-class interface methods in favor of separate interfaces.

  • clarity and predictability of class behavior
  • boilerplate code to emulate abstract classes:

I think we had once removed virtual method and use interface to emulate it before, but brought it back precisely because it was too cumbersome to use. Also the emulation with interface does not improve clarity nor predictability of class behavior, your example hardly proves this point.

6) Exclude private visibility of class members.

  • no need to choose between it and protected each time when writing a class
  • may expose sensitive data (albeit it can nevertheless be achieved by modifying the source)

I more or less support the removal of private visibility, protected seems to be sufficient most of the time. How about removing protected and use private as the current protected? I feel private matches better with public.

It's not unclear. It's just that there is little point in dividing classes and wrapped types since they are intermixed anyway.

There is always a gap between classes and wrapped types. To really close the gap, we will have to remove or disable several important things such as multi-inheritance and template-like type from wrapped types, or bring them to Dao classes and add a lot more complexity.

dumblob commented 9 years ago

How about removing protected and use private as the current protected? I feel private matches better with public.

Sounds logical and feels good. I'm though very curious about the confusion when mixing dao classes and e.g. C++ wrapped classes (having opened two manuals side by side - the Dao one and the particular C++ library one - and trying to match all the three sections private protected public). Sounds like a challenge to me, but maybe I'm exaggerating.

daokoder commented 9 years ago

I'm though very curious about the confusion when mixing dao classes and e.g. C++ wrapped classes

I don't see the confusion, because wrapped C++ classes has no private members, because the private members of the original C++ classes are not accessible by Dao and cannot be wrapped.

Night-walker commented 9 years ago

This should not be removed, but fixed. The way I am considering to fix it is to raise warning or even error, if any parameter or local/global variable bear the same name as one of the class member and any class member is not accessed through self. Namely, you can have parameters or local/global variables with the same names as members, but then you will have to access all members through self; or you can access members directly if none of the names are the same.

That seems like yet more rules to follow, no? Not very intuitive if I got it correctly.

I've just got an absolutely crazy idea. Use mandatory self to refer to class members by default, but allow import self in methods to obtain self-less access. Probably too abnormal to adopt, but fancy :)

It would be quite tricky to define states/variables shared across class instances, and quite awkward to emulate them if static members are removed. Besides, it does not really simplify the language much.

Why not simply use a global variable instead of a static one?

I think we had once removed virtual method and use interface to emulate it before, but brought it back precisely because it was too cumbersome to use. Also the emulation with interface does not improve clarity nor predictability of class behavior, your example hardly proves this point.

Not that I put much hopes in that example. Without any alternative work-around, it does look too clunky. It's just that the idea of having neither virtual functions nor abstract classes doesn't seem non-viable after getting acquainted with Rust.

I more or less support the removal of private visibility, protected seems to be sufficient most of the time. How about removing protected and use private as the current protected? I feel private matches better with public.

I would have proposed that too.

There is always a gap between classes and wrapped types. To really close the gap, we will have to remove or disable several important things such as multi-inheritance and template-like type from wrapped types, or bring them to Dao classes and add a lot more complexity.

Yes, but I didn't mean to close the whole gap. Just unify the method notation which is the most eye-striking difference. Won't argue that it would be better, because I myself am used to C++-like syntax.

Night-walker commented 9 years ago

Sounds logical and feels good. I'm though very curious about the confusion when mixing dao classes and e.g. C++ wrapped classes (having opened two manuals side by side - the Dao one and the particular C++ library one - and trying to match all the three sections private protected public). Sounds like a challenge to me, but maybe I'm exaggerating.

You just won't see a private section in case of C++, so there is little difficulty in matching the other two with their corresponding analogues in Dao.

daokoder commented 9 years ago

That seems like yet more rules to follow, no? Not very intuitive if I got it correctly.

The rule is actually very simple and logical: there should be no name shadowing in the code. You can use class members directly, as long as none of them is shadowed by parameters or variables. If the shadowing happens, the parser will raise an error. Then the user can choose to change parameter/variable name(s), or choose to access all members through self.

Users don't really need pay attention to the rule, it is the parser to apply this rule.

Why not simply use a global variable instead of a static one?

Then you will have to make the global variable private, name it certain way to indicate which class it is intended for. And when you use them, you will have to make sure you are using the correct one. This way, you have most of the inherent issues of global variables.

It's just that the idea of having neither virtual functions nor abstract classes doesn't seem non-viable after getting acquainted with Rust.

Rust may have done something right, but it is no golden standard. So I won't try too hard to make things similar to Rust.

Just unify the method notation which is the most eye-striking difference. Won't argue that it would be better, because I myself am used to C++-like syntax.

That difference might be too apparent, but the C++ like syntax is clearly more suitable for Dao classes, and the explicit self syntax is also clearly more suitable for wrapped types. Trying to close this particular gap, will inevitably worsen one of the two sides.

Night-walker commented 9 years ago

Then you will have to make the global variable private, name it certain way to indicate which class it is intended for. And when you use them, you will have to make sure you are using the correct one. This way, you have most of the inherent issues of global variables.

Placing global variable together with the related class into a separate namespace while exporting only the class should resolve all the issues you mentioned.

Rust may have done something right, but it is no golden standard. So I won't try too hard to make things similar to Rust.

I wound't want Dao to resemble Rust. I just threw in some non-conventional ideas which might be interesting to reflect upon.

daokoder commented 9 years ago

Placing global variable together with the related class into a separate namespace while exporting only the class should resolve all the issues you mentioned.

What's the advantage of doing this?

I think it is more consistent to support static class members, consider the following scoping scheme:

Without class static variable, there will be a gap in the scheme.

dumblob commented 9 years ago

Maybe @Night-walker is talking more about the implementation - i.e. make the class smaller by putting thestatic data to a namespace. But I doubt there will be some performance gain.

daokoder commented 9 years ago

Maybe @Night-walker is talking more about the implementation - i.e. make the class smaller by putting thestatic data to a namespace.

It will make little difference.

Night-walker commented 9 years ago

Placing global variable together with the related class into a separate namespace while exporting only the class should resolve all the issues you mentioned.

What's the advantage of doing this?

And what's the advantage of static class variables? Ability to place them inside class rather then outside?

I think it is more consistent to support static class members, consider the following scoping scheme:

instance variable: owned/scoped by instances; class static variable: owned/scoped by classes; global variable: owned/scoped by namespaces; Without class static variable, there will be a gap in the scheme.

I don't think we should support a feature just because there is an empty place which it can fit. Static variables (both in routines and classes) are very seldom used and usually may only make the code more tricky.

If you see a global variable, you immediately see that a global state is present. Static variables hidden within functions and classes are easier to overlook.

Static variables do look natural in the presence of static methods, but it does not make them necessary. At least not when global variables can be used to accomplish the same tasks just as fine.

Night-walker commented 9 years ago

Maybe @Night-walker is talking more about the implementation - i.e. make the class smaller by putting thestatic data to a namespace. But I doubt there will be some performance gain.

I don't care about the implementation. I'm talking about the language, not the VM.

dumblob commented 9 years ago

I checked Go and there are no static variables (but I agree, that it's not directly comparable to the situation in Dao).

I imagined myself deciding whether to put a static variable inside of a class or to a namespace as a global one and I realized, that it would push me to not do that at all. I think abandoning static class variables is actually a good unobtrusive way to discourage people from using global/static data (which makes sense in a language with built-in concurrency).

daokoder commented 9 years ago

Static variables hidden within functions and classes are easier to overlook.

That can be easily fixed. We can require explicit class name prefix for using such variables anywhere.

I think abandoning static class variables is actually a good unobtrusive way to discourage people from using global/static data (which makes sense in a language with built-in concurrency).

This is a good point. In terms of accidental use, class statics is safer than global variables.

Night-walker commented 9 years ago

Static variables hidden within functions and classes are easier to overlook.

That can be easily fixed. We can require explicit class name prefix for using such variables anywhere.

The use of global variables in specific namespaces would essentially boil down to the same end result.

In terms of accidental use, class statics is safer than global variables.

And absence of global state is even better :)

Overall, I think that static variables are just too seldom needed to give them any significant advantage over global variables. They are just an additional way to introduce a mutable global state and make things more convoluted then necessary. Newer languages like Go, Ceylon and Rust abandon this concept completely.

Night-walker commented 9 years ago

7) Make inherited/mixed-in members accessible only as Parent::member + prevents subtle mistakes caused by misunderstanding and name clashing - obvious extra verbosity

This one would be really helpful. There are so many ways to create a defective mutant when composing classes together, that the only sure way of avoiding it is keeping the staff strictly separated. Given that inheritance is required relatively seldom (unless you're an OOP maniac), the added verbosity is nothing comparing to the confidence that you made it right.

daokoder commented 8 years ago

And absence of global state is even better :)

We cannot avoid global state, otherwise, the interactive mode will become useless.

7) Make inherited/mixed-in members accessible only as Parent::member

This is useful when there is name clashing. I think the language can support it, but should not enforce it by default. It should be enforced only when name clashing is detected.

Night-walker commented 8 years ago

7) Make inherited/mixed-in members accessible only as Parent::member

This is useful when there is name clashing. I think the language can support it, but should not enforce it by default. It should be enforced only when name clashing is detected.

Given the infrequent use of inheritance in Dao, having to specify fully-qualified names won't be a burden. And you (along with those reading your code) will always know what exactly you're working with, which is way more helping then insignificant savings in typing.

As for static variables, I am still confident they do more harm then good. I don't use them at all in my code because declaring global data as actually global and putting it in one place help a lot at keeping an eye on shared mutual state and timely avoid race conditions.