r-lib / R6

Encapsulated object-oriented programming for R
https://R6.r-lib.org
Other
407 stars 56 forks source link

Multiple inheritance #9

Closed gaborcsardi closed 10 years ago

gaborcsardi commented 10 years ago

Just an idea for a cool feature. Probably not needed much, but sometimes handy, especially if it is possible to implement it in a simple and useful way. And not too many languages have done this properly, so R6 would be a standout. :)

A possible use-case is a class that implements a feature that can be useful in many other classes as well. E.g. you could have a pretty_printer class that has some fancy print method. This is potentially useful for any class. So in a project, your class foo just inherits from pretty_printer, in addition to its normal inheritance hierarchy, and then foo has a nice print method. There can be many similar features if course.

There could be of course several other examples. E.g. think about a class hierarchy of visualization types. You can have dynamic_plots, and also line_plots, but of course it should be possible to inherit from both of these to create a dynamic_line_plot. An alternative would be to have a dynamic_plot and a line_plot as members, but that is less expressive.

I personally like the implementation of multiple inheritance in the Eiffel language:

wch commented 10 years ago

I'm wary of multiple inheritance - I think I'd need to know the problem space better before I'd really consider adding it. Coincidentally, I saw this recently: http://arstechnica.com/information-technology/2014/08/why-is-multiple-inheritance-so-abhorred/

gaborcsardi commented 10 years ago

I agree it is a difficult problem. That's probably the reason why many languages screwed it up, and now people are wary. :) Anyway, imho that link and the original SE question list a lot of problems that do not come up it you do things "right".

Some of the problems simply don't come up, because R has dynamic typing. Others can be handled easily. E.g. somebody says that the order in which the superclasses are listed matters. Well, no, it does not matter if you do it properly. (Yes, it might for C++, Java or whatever your language is.) Also, nobody seems to be aware of the nice MI implementation in Eiffel.

This said, I think

  1. It is probably not the thing to implement in R6, surely not now.
  2. It might not fit into R and/or at all. E.g. in which order would you list superclasses in the class attribute?

So I think I'll close this now, sorry for the noise.

jdeboer commented 9 years ago

The need for multiple inheritance has arisen for a package I'm developing that using R6 classes for the Google Analytics Management and Google Tag Manager APIs. I'm working around the limitation of R6 not currently supporting multiple inheritance, by copying shared fields and methods. This is achieved by accessing class definitions via the elements of the class generators, e.g. my_class_gen$private_fields and my_class_gen$private_methods, but I would prefer to not to use this technique as it feels like a hack.

Here are links to the source code to see what this is looking like:

I've found it much easier to get where I've wanted to with R6, but now that I have a working solution I'm considering translating this to S4 so that I can attempt to redefine the solution in a way that supports multiple inheritance - this will be a challenge though. Any suggestions would be appreciated.

jankowtf commented 8 years ago

@jdeboer: I've ran into a similar requirement (multiple inheritance). I tried to follow your hack, but it seems that the underlying scripts have changed at least for your pointer to Ressource and collection super classes. Would you mind updating these reference? I'm really eager to find out how you made it happen.

@wch: +1 for @gaborcsardi's request. As my R prototypes need to be as close as possible to design patterns and dependency injection containers used in our production language (C#), I've come to like to use Interface (or abstract) classes to decouple my code and follow the D of the SOLID principles of OOD: simplified explanation, detailed explanation. Eventhough R/R6 does not explicitly support interfaces, I can nevertheless perfectly mimick them with R6:

Context

As my R prototypes for web-app-like stuff need to be as close as possible to design patterns and dependency injection containers used in our production language (C#), I've come to like to use interface (or abstract) classes to decouple my code and comply with the D of the SOLID principles of OOD:

Even though R6 does not explicitly support interfaces, I can nevertheless perfectly mimick them with R6 (see example below). This helps me a lot with communicate my software designs to our OO-programmers.

Example

Before inversion of dependency:

Foo depends on concrete class Bar. From an OOD principles' view, this is pretty bad as it leads to code being tightly coupled.

Bar <- R6Class("Bar",
  public = list(doSomething = function(n) private$x[1:n]),
  private = list(x = letters)
)
Foo <- R6Class("Foo",
  public = list(bar = Bar$new())
)
inst <- Foo$new()
> class(inst)
> class(inst$bar)
[1] "Bar" "R6" 

After inversion of dependency:

Foo and Bar are decoupled now. Both depend on an interface which is mimicked by class IBar. I can decide which implementation of that interface I would like to plug in to instances of Foo at runtime (realized via Property Injection: field bar of Foo)

IBar <- R6Class("IBar",
  public = list(doSomething = function(n = 1) stop("I'm the inferace method"))
)
Bar <- R6Class("Bar", inherit = IBar,
  public = list(doSomething = function(n = 1) private$x[1:n]),
  private = list(x = letters)
)
Baz <- R6Class("Baz", inherit = IBar,
  public = list(doSomething = function(n = 1) private$x[1:n]),
  private = list(x = 1:24)
)
Foo <- R6Class("Foo",
  public = list(bar = IBar$new())
)

inst <- Foo$new()
inst$bar <- Bar$new()
> class(inst$bar)
[1] "Bar"  "IBar" "R6"  
> inst$bar$doSomething(5)
[1] "a" "b" "c" "d" "e"

inst$bar <- Baz$new()
[1] "Baz"  "IBar" "R6"  
> inst$bar$doSomething(5)
[1] 1 2 3 4 5

A bit mor on why this makes sense with regard to OOD: Foo should be completely agnostic of the the way the object stored in field bar is implemented. All it needs to know is which methods it can call on that object. And in order to know that, it's enough to know the interface that the object in field bar implements (IBar with method doSomething(), in our case).

Using inheritance from base classes to simplify design:

So far, so good. However, I'd also like to simplify my design by definining certain concrete base classes that some of my other concrete classes can inherit from.

BaseClass <- R6Class("BaseClass",
  public = list(doSomething = function(n = 1) private$x[1:n])
)
Bar <- R6Class("Bar", inherit = BaseClass,
  private = list(x = letters)
)
Baz <- R6Class("Bar", inherit = BaseClass,
  private = list(x = 1:24)
)

inst <- Foo$new()
inst$bar <- Bar$new()
> class(inst$bar)
[1] "Bar"       "BaseClass" "R6"   
> inst$bar$doSomething(5)
[1] "a" "b" "c" "d" "e"

inst$bar <- Baz$new()
> class(inst$bar)
[1] "Baz"       "BaseClass" "R6"       
> inst$bar$doSomething(5)
[1] 1 2 3 4 5

Combining "interface implementation" and base clases inheritance:

This is where I would need multiple inheritance so something like this would work (PSEUDO CODE):

IBar <- R6Class("IBar",
  public = list(doSomething = function() stop("I'm the inferace method"))
)
BaseClass <- R6Class("BaseClass",
  public = list(doSomething = function(n = 1) private$x[1:n])
)
Bar <- R6Class("Bar", inherit = c(IBar, BaseClass),
  private = list(x = letters)
)
inst <- Foo$new()
inst$bar <- Bar$new()
class(inst$bar)
[1] "Bar"       "BaseClass" "IBar" "R6"

Currently, my value for inherit is already being used up "just" for mimicking interfaces and so I lose the benefits of inheritance for my actual concrete classes.

Alternatively, it would be great to explicitly support a differentiation between interface and concrete classes somehow. For example:

Bar <- R6Class("Bar", implements = IBar, inherit = BaseClass,
  private = list(x = letters)
)
jankowtf commented 8 years ago

I gave it a second thought and realized that's it's not really multiple inheritance per se that I want/need, but rather some sort of better mimicking the use of interfaces/abstract classes without giving up inherit for that purpose.

So I went ahead and had a go at tweaking R6 a bit so it would allow me to distinguish between inheritand implement.

Probably tons of reasons why this is a bad idea ;-) But for now, it gets the job done.

jdeboer commented 8 years ago

Hi @rappster Here are the source file in ganalytics where you can see what I did to overcome the limitation of single inheritance in R6. I'm not sure its the solution you're looking for though: https://github.com/jdeboer/ganalytics/blob/dev/R/ga-api-classes.R https://github.com/jdeboer/ganalytics/blob/dev/R/management-api-classes.R

I've actually started working on a new approach, which is currently on hold and not yet functioning at the moment, where I intend on using R6 and S4 in combination: https://github.com/jdeboer/ganalytics/blob/dev/R/WIP/management-api-classes-V3.R

Wish I had more time to work on this at the moment, but I've enrolled myself in full time study while also working, so a bit stretched. My apologies for taking a while to get back to you.

petermeissner commented 8 years ago

Maybe my use case is no multiple inheritance at all, maybe it is to short sighted ... but let's see.

Some background info:

Playing around with R6 I developed a class binding together text and data (rtext). On my way I split up parts of the class because they are separate blocks of functionality and reading code and maintainig code should be better this way.

So what I end up is somthing like a class dependecy like that

                               | - rtext_extension_a   |
R6 - R6_extension - rtext_base | - rtext_extension_2   | - rtext
                               | - rtext_extension_foo |

Which I have to write like this ...

R6_extension      <- R6::R6Class( classname = "R6_extension",                                   ... )
rtext_base        <- R6::R6Class( classname = "rtext_base",        inherit = R6_extension,      ... )
rtext_extension_1 <- R6::R6Class( classname = "rtext_extension_1", inherit = rtext_base,        ... )
rtext_extension_2 <- R6::R6Class( classname = "rtext_extension_2", inherit = rtext_extension_1, ... )
rtext_extension_3 <- R6::R6Class( classname = "rtext_extension_3", inherit = rtext_extension_2, ... )
rtext             <- R6::R6Class( classname = "rtext",             inherit = rtext_extension_3, ... )

... which is (possible but) tedious whenever I extend the functionality by another 'module' (sub-class).

Now would it not be nice to have something like that:

rtext             <- 
  R6::R6Class( 
    classname = "rtext",             
    inherit = 
     list(
       R6_extension,
       rtext_base, 
       rtext_extension_1,
       rtext_extension_2,
       rtext_extension_3
      ), 
    ... 
  )

... which works like just a series of inherits?

lrnv commented 7 years ago

Why is this issue closed since 3 years ? It seems like multiple inheritences are still not availiable in R6.

Will you reconsider implementing the possibility of multiple inheritance ? Did you decide not to ever do it ?

If so, why ? Thx for your responce.

nhamilton1980 commented 6 years ago

So how do you propose the diamond problem is handled?

lrnv commented 6 years ago

Well there is a lot of ways to handle a diamond issue in a multiple inheritance framework. My english is a little bad so i'm not gonna sketch them, i refer you to some exemples there : https://en.wikipedia.org/wiki/Multiple_inheritance#Mitigation

My thoughts : there are 2 easy way and a harder one of handling the issue : Quick and dirty : Just default the inheritance to the first declared parent. Quick and not-so-dirty : Refuse to inherit methods if a diamond issue is founded. -- the user would have to re-define the problematic method in the child, making him aware of the problem. Hard and Neat : Copy the Eiffel way of doing it. Eiffel explicitely asks the user wich methods and attributes should be inherited from wich parent when the child is declared, and therefore avoid any diamond problem.

I'll sketch a little more the second solution :

Supose the Parent class has 2 child, Son and Daughter wich both inerits from Parent. Supose a IncestuousChild class inherit from Son and Daughter.

The methods of each are as follows : Parent has two atribute name and familyname and a method great Son and Daughter both inherits name, both re-implement great but only Daughter re-implements familyname. IncestuousChild will them inherit name normaly because neither Son or Daughter re-implemented it. IncestuousChild WONT inherit great because there are 2 possibilities : either inehrit Son's or Daughter's. IncestuousChild WONT inherit familyname because there are also 2 possibilities : inherit directly from Son or from Parent through Daughter.

For the last 2, user will have to redefine the methods.

I'm not enough aware of the implementation choices in R6 to be able to choose the best option, since there are a lot of them -- sorry -- , but there is definitely a solution to the problem.

Glad to explain more if needed :)

nhamilton1980 commented 6 years ago

Yep that makes sense. What about the initializers, would they have to have the same (or closely-related) signatures? super would need to return a list, rather than a single object.

lrnv commented 6 years ago

Well taking super as a list could make a lot of sence if we choose the ordered parrent way of dispatching methods.

But the goal is to keep things as they are and not to break everyone's code by changing the definition of super. Keep in mind that the system will have to stay the same if no multiple inheritance are defined. keep also in mind that if no diamond problems are found, everything should behave the same.

nhamilton1980 commented 6 years ago

Wouldn't have to be necessarily ordered, a named list would be handy, where the names are the classes respectively, something like this perhaps for example:

A = R6::A('A')
B1 = R6::R6Class('B1',inherit=A)
B2 = R6::R6Class('B2',inherit=A)
C = R6::R6Class('C',inherit=c(B1,B2),
public = list(
  initialize = function(){
     super$B1$initialize()
     super$B2$initialize()
     ## So forth
  }
)
)
lrnv commented 6 years ago

You'l loose the

super$method()

syntax, and have to replace it by

super$NameOfParent$method() 

?

I'll prefere tweaking the super object such that :

super$methodFromDad()
super$methodFromMom()

still works.

nhamilton1980 commented 6 years ago

Yeah you would probably lose first syntax. I am still trying to decide whether that is a good or a bad thing.