back2dos / tinkerbell

MIT License
83 stars 8 forks source link

Feature request: Automatic generation of interface for delegation member/class #32

Closed MetaChrome closed 11 years ago

MetaChrome commented 11 years ago

Automatic generation of the interface of the delegation object would eliminate the manual creation of the interface delineating the composed functionality:

class Human implements MouthI { @:forward mouth:Mouth; } class Mouth implements MouthI { }

This is extremely relevant imo as it is often the case that one needs to specify a function parameter as mouth or whatever other interface.

In terms of implementing this:

In a @:forward(i) (would by my preferred declaration, but @:forward("interface") may be more sane) declaration, have an option to specify that you want the forwarding interface appended. Add the delegation class to be interfaced to a nonduplicate set datastructure and add all classes requiring the interface. Generate interfaces and append as specified.

I will most likely implement this this week, but if you could do it that would be amazing as you know the framework and the relevant code I don't imagine would be particularly extensive.

MetaChrome commented 11 years ago

Ugh, on the same note, why do the generated accesors for the delegated property not the same as the accessors' names in the delegation class? This is necessary for specifying interface. One could specify get_prop interface... but I personally use prop_get/prop_set for my accessors and this flexibility would be appreciated.

back2dos commented 11 years ago

As for your second question:

A couple of years back I've decided to standardize my property names and to use that standard in tink. The reason between get_prop and set_prop is simply, because that's what var prop(dynamic, dynamic) will translate to.

Now half a year ago Nicolas announced that these will be the enforced accessor names in Haxe 3. So I suggest you see this as an invitation to start making your code Haxe 3 compatible ;)

On a side note, these are the accessor names @:read and @:prop will generate, which also work on interfaces. If you use those instead, then you'll be able to transition to Haxe 3 without having to fiddle with all your property generations, because tink_lang will abstract the changes away.

back2dos commented 11 years ago

As for your actual request, for now I'd like to share my thoughts on this.

From the Haxe group I understand you're using syntactic delegation as traits. At the bottom line, traits are just a subtle flavor of inheritance, while the syntactic delegation is the mere act of making composition easier.

As a matter of fact, prior to publishing tink did support traits (before the idea of syntactic delegation came to my mind). I would rather not go into a lengthy discussion on their conceptual flaws. But you should take the concerns voiced in the Haxe group as an indicator that there's something to that stance ;)

The thing is @:forward(i) means "forward to the member i". You could also see @:forward as a shortcut to @:forward("*").

Regarding your example, a couple of questions arise:

  1. Should a human really expose all the functions of a mouth?
  2. Even then, does that actually make a human a mouth?

I would answer both questions with "no", because I think doing this would clutter the interface of Human and would lead to weird type relationships and abstractions in general.

What one could do is cook up a macro, that generates an interface definition from a class. While practical at first sight, I seriously doubt it is actually a good idea. Interfaces should be clear and specific, with high cohesion, rather then being a derivation of a class. If you add further members to the class, that the class might need that don't actually lie within the abstraction of the derived interface, all other implementors break. Ultimately, an interface should not be determined by implementors, but by consumers, i.e. the classes operating on the abstraction in presents.

So at the bottom line, I think it encourages bad style and I am very hesitant to implement it.

MetaChrome commented 11 years ago

I'm not sure if you understood so here is a clarification:

I have the class Unit. It is delegating to Archer. I want ArcherI automatically generated from the definition of Archer, and specified that Unit implements ArcherI. That way I can pass different variables that are in fact ArcherIs to functions dealing with ArcherIs. I mean tink's delegation is basically squishing components into traits as opposed to entity components, thus eliminating the delineated class/interface. With entities you operate on the component class(or interface). I use entity composition on all models basically. But sometimes it's a bit of overkill. With tink, you're going to want to operate on the delegation interface by definition.

Human is a bad contextual example because computer science classes always use behavior examples as opposed to physical examples. A more computer sciencey example would be Human>Eater. In our universe though lol, you are in fact material and not behavior, one's behavior is a manifestation of it's material. So yes ;) I would definitely argue, I am in fact a mouth among many other things. Anyway this is off topic/in jest.

By definition if I am delegating, as implemented by tink, I am that thing because I have its methods/interface.

The obvious example is EventDispatcher. However, almost every use of delegation requires the interface. Every use I am making requires the interface.

The point is that function arguments require interfaces identical to the delegation class. People are going to create these interfaces regardless.

Re bad style: I don't think this is an exotic feature, but rather a fundamentally missing piece of a complete implementation.

Re declaration: Yah I forgot that the arguments in the @:forward macro are mappings. One could:

  1. Specify that arguments to the metadata with a dash are options (ie: @:forward(-i) if you're a big fan of executables
  2. Url parameter format.

Personally for complicated annotations I use url parameter format, ie @:relation("type=manyToMany","owner=inverse","bi_prop=friends"). Essentially the same thing as options except the input isn't inferenced from the lack of a dash. Heck, executables should run on url parameters. Now that I think about it, in general, one could use a single string of url parameters as an argument: @:forward("mapping=prop1,regex&i"). Encoding/delimiting is a problem though, especially with regex. Off topic/overkill: I have even considering json as a meta argument but I didn't want to write a non-quoted json parser but with super complicated meta arguments, json does make sense. Anyway, I think that just about covers everything in terms baking it however you want.

  1. Have a second annotation namely @:forward_i. lol i hope not

Anyway I think this is literally a maximum of 500 lines of code, not counting the change to declaration, so I definitely think it should be included and will definitely implement it shortly.

Re traits: Delegation with interface generation is sufficient in relation to having traits declared in haxe language syntax. I couldn't care less either way. In general, arguments against traits are incorrect and blindly ignore the fact that manually writing boilerplate to accomplish the same thing is not better by any parameters, including but not limited to:

  1. Functionality
  2. Decoupling
  3. Comprehension of code
  4. Not wasting time and grinding my gears

Certainly any user, and certainly any developer, of this fantastic library, wishing to address the inconveniences of composition, feels the same way. :)

MetaChrome commented 11 years ago

The correct implemention and revised feature request goes further, by transplanting all interfaces of the delegation class, and an interface of the class definition, if it has members excessive to the members defined in it's interfaces, to the delegating class.

Hero, our delegating class Archer implements EventDispatcherI, SoldierI, UnitI

Archer, has members excess to those defined by the set of interfaces it implements so:

Human implements ArcherI, EventDispatcherI, SoldierI, UnitI

When there is a mapping, one could have an additional parameter specifying this mapping's interface name, as there is no reasonable way to automatically specify a name for a mapping of a class. If the mapping matches any implemented interfaces, they would be transplanted as well.

back2dos commented 11 years ago

Firstly, macros cannot transplant interfaces, simply because there's no API to add an interface to a class. (You can get the local class and push a new interface onto its interfaces, but it will have no effect. My guess is, that's because you modify the converted macro runtime representation and not the underlying data as used by the compiler.)

Secondly, I am not in favor of some implicit implementation. If a class happens to forward to some member then so be it, but the implementation of an interface is an explicit contract, the visibility of which is important. If you like implicitness, I suggest you express your type relationships through anonymous types and use structural instead of nominal subtyping.

Thirdly, the point of syntactic delegation is to selectively forward specific calls without effort (full delegation being a mere edge case), not to use it as a substitute for multiple inheritance. I don't consider multiple inheritance (or flavors of it such as traits and mixins) to be an exotic feature. It has been around for over 30 years, and as a matter of fact, a lot of languages coming after C++ decided not to support it. Including Haxe.

Lastly, I know this isn't much code. In fact tink_lang itself is less than 500 lines of code (built on top of 1.8KLOC dependencies) . Each of those lines is carefully considered. I could probably implement actual traits in the amount of time arguing here has consumed. But it's my genuine belief that it's not a good thing to do. I will give this some more thought, but you really shouldn't count on it.

MetaChrome commented 11 years ago

Re: no api to append interfaces. I thought so, ie Nicolas mentioned that at macro execution the built class has already been typed. I guess one must go deeper.

Re: I sincerely hope you don't think I'm "arguing". My lack of prepending statements with "I think" may be the cause of this. :) My attempt was to clarify. I have specified my request in detail and hope you consider it's superior value. I don't believe the functionality in question is implicit. It is also optional.

Re your traits implementation (which are basically similar to this request, but were presumably implemented differently):

If you have any work (even if its unfinished) on a traits implementation, it's release will be appreciated.

The functionality in question, traits, is finite, functional and well defined and guarding its use when an implementation exists (yours), makes no sense as that logically only causes the pointless inefficiency, of another implementation being created. (presuming that it isn't withheld because of licensing considerations)

tldr, withholding implemented functionality makes no sense. "So at the bottom line, I think it encourages bad style" That's reasonable for non-existent code.

back2dos commented 11 years ago

I've now added support for default implementations in interfaces, which permits any interface to act similarly to a trait.

To do so, the interface must implement tink.lang.Cls. Any implementation and initialization found there will act as a default for all implementors. Here's an example: https://gist.github.com/3958864

This is still at an early stage, so feedback is very welcome.