12

Actual question

What are my options to workaround the fact that R6 does not support multiple inheritance?

Disclaimer

I know that R is primarily a functional language. However, it does also have very powerful object-orientation built in. Plus: I don't see what's wrong with mimicking OOD principles/behavior when you

  1. know you're prototyping for an object-oriented language such as C#, Java, etc.

  2. your prototypes of apps need to be self-sufficient ("full stack" including DB-backends, business logic and frontends/UI)

  3. you have such great "prototyping technology" like R6 and shiny at your disposal

Context

My R prototypes for web apps need to be both "full stack"/self sufficient and as close as possible to design patterns/principles and dependency injection containers (proof of concept of simple DI in R) used in our production language (C#/.NET).

In that regard, I came to like the use of interfaces (or abstract classes) very much in order to decouple code modules and to comply with the D (dependency inversion principle) of the SOLID principles of OOD (detailed explanation by "Uncle Bob").

Even though R6 does not explicitly support interfaces, I can nevertheless perfectly mimick them with R6 classes that define nothing but "abstract methods" (see example below). This helps me a lot with communicating my software designs to our OO-programmers that aren't very familiar with R. I strive for as little "conceptional conversion effort" on their part.

However, I need to give up my value for inherit in R6Class for that which becomes a bit of a problem when I actually want to inherit from other concrete (as opposed to "abstract-like" mimicked interface classes) because this would mean to define not one but two classes in inherit.

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 an interface implementation and so I lose the "actual" benefits of inheritance for my actual concrete classes.

Alternative thought:

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

Bar <- R6Class("Bar", implement = IBar, inherit = BaseClass,
  private = list(x = letters)
)
Community
  • 1
  • 1
Rappster
  • 12,762
  • 7
  • 71
  • 120
  • I see plenty wrong with it, even (especially) for prototyping: just don’t use an inadequate tool to prototype an OOD system that heavily relies on inheritance and interfaces. – Konrad Rudolph Feb 15 '16 at 17:18
  • @KonradRudolph: could you elaborate a bit on the reasons? – Rappster Feb 15 '16 at 17:19
  • I’m not sure what to elaborate on. So let me instead ask a question: **why** are you using a clearly inadequate tool for prototyping? – Konrad Rudolph Feb 15 '16 at 17:20
  • IMO, R really shines in two domains: data analysis and prototyping. So let me return the question: **why wouldn't** you use R if you need to prototype functionality related to data analyis even though it happens to be for a language that relies more on OOD than R? I don't understand why it always has to be so "black and white" with regard to R being either functional or object-oriented. Maybe I'm just not getting it, but R **does have** strong object-oriented features (S3, S4, Ref Classes, R6) - so what's wrong with making the best use of them? – Rappster Feb 15 '16 at 17:25
  • See my answer. But I’m still puzzled by your insistence that R is well-suited here when the existence of your question alone is sufficient to demonstrate that this clearly isn’t the case. You’re trying, almost literally, to shove a square peg into a round hole, all the while insisting that this is a good fit “because the colour of the material matches”. – Konrad Rudolph Feb 15 '16 at 17:35
  • Is there some reason you don't just use reference classes here, since they do support multiple inheritance? – Nick Ulle Oct 12 '17 at 01:01
  • 1
    @NickUlle yes, several indeed: 1) I found reference classes to be terribly slow compared to R6. 2) They have a much more complicated architecture. 3) They're "S4-only" while with R6 you can flexibly switch between the S3 and S4 world (with the trick of making an R6 class a formal S4 class whenever you really need to) – Rappster Oct 12 '17 at 09:36

2 Answers2

8

For those interested:

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.

So I tried tweaking R6 a bit so it would allow me to distinguish between inherit and implement in a call to R6Class.

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

You can install the tweaked version from my forked branch.

Example

devtools::install_github("rappster/R6", ref = "feat_interface")
library(R6)

Correct implementation of interface and "standard inheritance":

IFoo <- R6Class("IFoo",
  public = list(foo = function() stop("I'm the inferace method"))
)
BaseClass <- R6Class("BaseClass",
  public = list(foo = function(n = 1) private$x[1:n])
)
Foo <- R6Class("Foo", implement = IFoo, inherit = BaseClass,
  private = list(x = letters)
)

> Foo$new()
<Foo>
  Implements interface: <IFoo>
  Inherits from: <BaseClass>
  Public:
    clone: function (deep = FALSE) 
    foo: function (n = 1) 
  Private:
    x: a b c d e f g h i j k l m n o p q r s t u v w x y z

When an interface is not implemented correctly (i.e. method not implemented):

 Bar <- R6Class("Bar", implement = IFoo,
    private = list(x = letters)
  )
> Bar$new()
Error in Bar$new() : 

Non-implemented interface method: foo

Proof of concept for dependency injection

This is a little draft that elaborates a bit on the motivation and possible implementation approaches for interfaces and inversion of dependency in R6.

Rappster
  • 12,762
  • 7
  • 71
  • 120
  • I found this, and I really like what you did with this. However: would it be better to introduce a new thing: R6Interface where, unlike R6 class which specifies a lists of public, private, active members an R6Interface would accept a list of lists indicating the necessary public functions/fields and signatures of each field. Then the 'interface implemetnation' check can ensure that all methods are implemetned with the correct sigs. – Chris Knoll May 03 '22 at 04:32
6

Plus: I don't see what's wrong with mimicking OOD principles/behavior when you know you're prototyping for an object-oriented language such as C#, Java, etc.

What’s wrong with it is that you needed to ask this question because R is simply an inadequate tool to prototype an OOD system, because it doesn’t support what you need.

Or just prototype those aspects of your solution which rely on data analysis, and don’t prototype those aspects of the API which don’t fit into the paradigm.

That said, the strength of R is that you can write your own object system; after all, that’s what R6 is. R6 just so happens to be inadequate for your purposes, but nothing stops you from implementing your own system. In particular, S3 already allows multiple inheritance, it just doesn’t support codified interfaces (instead, they happen ad-hoc).

But nothing stops you from providing a wrapper function that performs this codification. For instance, you could implement a set of functions interface and class (beware name clashes though) that can be used as follows:

interface(Printable,
    print = prototype(x, ...))

interface(Comparable,
    compare_to = prototype(x, y))

class(Foo,
    implements = c(Printable, Comparable),
    private = list(x = 1),
    print = function (x, ...) base::print(x$x, ...),
    compare_to = function (x, y) sign(x$x - y$x))

This would then generate (for instance):

print.Foo = function (x, ...) base::print(x$x, ...)

compare_to = function (x, y) UseMethod('compare_to')

compare_to.foo = function (x, y) sign(x$x - y$x)

Foo = function ()
    structure(list(x = 1), class = c('Foo', 'Printable', 'Comparable'))

… and so on. In fact, S4 does something similar (but badly, in my opinion).

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 1
    +1 for taking the time, stating your opinion on why what I want to do is a bad idea and sketching your approach. But I don't see anything related to my actual question regarding R6 in particular. For everything OO-prototyping-related, R6 did a great job for me so far. Multiple inheritance is the only feature I'm really missing. – Rappster Feb 15 '16 at 17:41
  • @Rappster I agree that R6 is a kick-ass OO system. But as you rightly observed, it simply doesn’t support multiple inheritance, and I don’t think there’s a way of adding it without changing the R6 internals. And while that wouldn’t be a bad option per se, I don’t know enough about its internals to comment on it. – Konrad Rudolph Feb 15 '16 at 17:43
  • Fair enough, thanks anyway! There's someone that hacked into the layers of environments that make up a R6 class and got multiple inheritance working somehow, but that seems like a lot of code to reverse engineer/understand and indeed to be a really dirty hack: https://github.com/wch/R6/issues/9 – Rappster Feb 15 '16 at 17:46