dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

An object wrapping/mutating/decorating shortcut related to #40 #414

Open icatalud opened 5 years ago

icatalud commented 5 years ago

This is highly related with #40 which is being implemented now as far as I understood. There are use cases that are not covered by the presented solutions though.

Sometimes you want to "mutate" objects of classes from external libraries and then send them back to the library, causing #41 to not be enough (it's scoped). To do so, you have to wrap the object like this. There are quite a few nuisances with wrapping an object when you only want to mutate one or a few functions and the class has 30+ members. It is also not enough, since the object could have been promoted to a super class when you receive it and later it gets downcasted back (your wrapper fails in that case). You would like the wrapped object to retain all its properties and not only the visible ones, while only having a few overridden members.

Direct mutation might be forbidden, but the language could help to make it easy to create wrapped versions of the object. For example:

class A {
  foo() { }
  jaz() {}
}

wrapper W {
  @override
  foo() { wrapped.foo(); }  // super keyword perhaps instead of wrapped
  bar() {}
}

A a = A();
A a2 = W(a);
a.foo(); // calls original A foo
a2.foo(); // calls wrapped W foo
// a2.bar() compile error since type A has no bar
W w = W(a); //
w.foo(); // calls W foo
w.bar(); // calls W foo
// w.baz() compile error since type A has no bar

class B extends A { }

A b = B();
A b2 = W(b);
B b3 = b2;  // possible

So essentially the wrapper would be an object that defers all its function calls, type checks, etc to the wrapped object. Perhaps the wrapped object could be a special type that defers all calls except for a reserved word wrapped property which points to the var that got wrapped.

In any case this could be an addition to the Static extension types of #41, where besides the casting to the extension type, the extension could have a default wrap function, which creates a wrapped object. Something like extension.Wrap(object).

In the end, all of this is currently possible, but it requires a huge amount of work, which makes the code less readable and more prone to errors. What do you think?

I ask this and access to private members in #409 because those have been essentially the stones on the road that stopped me when I began developing in flutter a few weeks ago.

ds84182 commented 5 years ago

I feel this is... very unnecessary and both the semantics and implementation are complicated. It is completely untyped (discards type information), and forces the program to lose a lot of useful optimizations. For example:

wrapper FooString {
  @override
  String toString() => "foo";
}

final int x =  123;
final int /* very confusing */ y = FooString(x);
print(x); // 123
print(y); // foo
print(x + y); // 246

void printInt(int x) {
  print(x); // Now forces dynamic dispatch to `toString`, everywhere where an int is printed suffers performance-wise? May also break some optimizations from Dart2JS.
}

To put it in perspective, this is like creating VTables at runtime (for C++) just to patch a method. Actually, worse: this is like monkey patching every method in the program (at runtime!) so you can get special behavior out of any random object.

icatalud commented 5 years ago

The wrapper notation was just an example to illustrate the idea of the expected functionality, before going into the depths. Like I said, probably the same static extension methods designed for #41 can be used, by extending the definition and adding a default .wrap method (that's one option).

I agree it's not a desired pattern. If the functionality existed and the language transformed from object oriented to wrapping oriented programming, I also wouldn't like it. It would be quite confusing, like the example you give. Generally you do not want to wrap an object, but you need to do it when you are dealing with objects that come from external libraries. It's the same reason they gave to create static extension types. If the fear is that it could become overused, perhaps you could limit the usage to package imported library classes only.

If you only illustrate a poor usage of course it will look like a bad idea! Challenge me and I can give you an example where OO programming looks like a terrible pattern. Any tool can be misused. Having a variety of tools is cool when you use the right tool for the right task. You really need to wrap external objects sometimes, it's a well known pattern. But yes, if it exists it probably shouldn't be super highlighted as a language feature (could get misinterpreted as a cool thing), but rather let people that is searching for this to find it.

Currently it is possible to implement the example you give, it's just that it is very verbose an annoying to do it. For example you could do:

class FooString implements int {
  int wrapped;
  FooString(this.wrapped);

  @override
  String toString() => "foo";
  ceil() => wrapped.ceil();
  // ... and so forth with 30 extra methods calling wrapped.method_name
}

And you get the same result.

ds84182 commented 5 years ago

It is _not_possible to implement int. Furthermore, that is way more explicit, and makes sense from the type system's point of view, which is the entire point. Instead of sidestepping the entire type system, just... use the tools already provided.

icatalud commented 5 years ago

@ds84182 If it is not possible to implement int, then it also shouldn't be possible to wrap it. There are several options for such feature, the least intrusive being purely syntactic sugar to shortcut the pattern I described in the previous post, like (my first preference goes with something more intrusive than this though):

// this format is used to display the feature, it doesn't have to be implemented like a wrapper class
wrapper Foo on Bar {   
  @override
  jaz() { }
}

which is essentially syntactic sugar for the pattern:

class Foo implements Bar {
  Bar wrapped;
  Foo(this.wrapped);
  jaz() { }
  // implement all other methods as a single call wrapper.method
}

I should note that if Bar implementation was directly reachable I would add a new function jazBoosted() {} to the original Bar class. I only want to do this when I do not have access to the implementation. This makes me think that wrappers could be allowed to be used only on external library classes, preventing that people abuses of this undesired pattern.

Furthermore, that is way more explicit, and makes sense from the type system's point of view, which is the entire point. Instead of sidestepping the entire type system, just... use the tools already provided.

I think of every word of a language as shortcut to a common coding pattern. 'for' and 'if' are some of the most basic shortcuts and from there languages add more and more sophisticated syntax depending on the usages of the language. If you saw these patterns for the first time you would probably question why people want a feature for something they can already do. Reason being that they do not want to write the same repeating pattern again and again, they want to do it in one line when your are adding one line of information and the rest is the same thing (just like the reason you write functions). Languages have syntactic shortcuts for patterns I have never used and therefore I would never implement myself if I were implementing the language. It is therefore very important to notice that what we believe is worth adding is based on the narrow perspective we have as mere individuals. The best thing we could get to improve a language is information about how it is being used in the outside world. We should be happy then when people comments about their needs. As the owner of a product I would be very happy to hear what people wants the most, so I can take rational decisions taking into account supply and demand instead of doing what I just feel is best. If every user comes to comment what they want, you could take some statistics and use them as information to choose what direction to take next. I guess that's the idea of this forum, since you may upvote-downvote requirements.

I do not know what the users want, perhaps very few people are requiring this pattern so is not worth doing. The idea is posted here now, someone might like this too. I shared my requirement to help the language (knowing that they are developing new features) by saying how I'm using it and what I would love to have as a feature in the same way I criticize food when I go the restaurant.