Multiple Module Inclusion (#13)

Submitted by: Alex Solla

This revision by: Alex Solla

Date: Wed Jun 27 01:01:39 -0400 2007

ABSTRACT

If a class mixes in a module implementing a particular API, then another implementing the same API, it cannot recover the first implementation by including the first module. A solution to this problem would be to modify rb_mod_append_features function in Obj.c to append the methods in the inheritance chain even if they already exist.

PROBLEM

When writing a program utilizing a client-server architecture, one often wants to write server objects that respond to different “kinds” of client requests in a different manner, while still maintaining a stable API the client can use. Using mixins to solve this problem seems very natural. Consider the following code:


class Request
  attr_reader :kind, :data
  def initialize(kind, data)
    @kind = kind
    @data = data
  end
end

module Identity
  def process(data)
    return data
  end
end

module Double
  def process(data)
    return data + data
  end
end

class Service
  def initialize # Unimportant details here.
  end
  def run(request)
    self.class.class_eval("include #{request.kind}")
    process(request.data)
  end
end

service = Service.new()
request = Request.new("Identity", 1)
request2 = Request.new("Double", 1)
request3 = Request.new("Identity", 1)

service.run(request) # returns 1
service.run(request2) # returns 2
service.run(request3) # returns 2 instead of the intended 1.
—End of Code

As the last comment notes, service.run(request3) returns 2 instead of the intended 1. That is to say, this pattern cannot be implemented using the current semantics for Module.include.

PROPOSAL

This proposal does not affect Ruby’s syntax. The proposal is to re-implement Module::include so that the last included module in a class is appended to the inheritance chain, even if it has been included in the class previously. This will only affect Ruby code that relies on the behavior that a module can override another module’s methods, and spuriously attempts to load the original module.

ANALYSIS

A different solution for this problem already exists, but it is not as obvious or elegant. Instead of the class Service definition in the Problem section, we can write

class Service
  def run(request)
    self.instance_eval("#{request.kind}::process(#{request.data})")
  end
end
Indeed, we are no longer even using mixins. The semantics of the instance_eval method and variable interpolation make this a confusing hack. This would also require running instance_eval on every invocation of run, since the complete method named invoked by run must be passed to a different context each time. If my suggestion is implemented, the original class Service can be modified to

class Service
  def initialize # Unimportant details here.
    @last = "" 
  end
  def run(request)
    unless @last == request.kind
      self.class.class_eval("include #{request.kind}")
      @last = request.kind
    end
  process(request.data)
  end
end

effictively caching service’s role. Depending on how often service’s role changes, this can be significantly faster than the version using instance_eval. On the other hand, if service’s role changes frequently, the modified version would likely be much slower. because of the overheard of virtual class creation.

I have experimented with Object::Send, but was unable to construct a run method that worked as intended.

Several other thoughts have occured to me. If my suggesting is implemented, duplicate Modules names should likely be removed the inheritance chain. Otherwise, the chain can grow arbitrarily long during a service objects lifetime. Indeed, this can occur even if they are removed, should an arbitrarily long list of roles be demanded of the service. But that would be poor design, not something we should really concern ourselves about. However, a mechanism to explicitly remove a module from a class’s inheritance chain can be implemented as well.

Mixins that vary through time seem like a natural extension of the mixin functionality. However, perhaps a new Module instance method could be implemented to automate module loading. implemented_by :symbol would be my suggestion.

IMPLEMENTATION

It is my understanding that this proposal would require changes to Ruby’s interpreter. I am not good with C, so I shall leave this to the professionals.

Comments

from Trans Onoma, Wed Jun 27 06:41:42 -0400 2007

I concur with the over all effort of this RCR.

However, I think a more complete solution should be had, in which the
programmer can remove mixins or swap one mixin for another, and
perhaps even control the mixin chain as one can manipulate an array.

T.

from Robert Klemme, Wed Jun 27 07:16:26 -0400 2007

2007/6/27, rcrchive+13@rcrchive.net <rcrchive+13@rcrchive.net>:
> This is a digest of all the comments, so far, on RCR 13,
> "Multiple Module Inclusion".  You'll be on the mailing list for all future
> comments.  You can also add a comment by replying to this digest,
> but please trim it down!
>
>
> On Wed Jun 27 06:41:42 EDT 2007, Trans  Onoma said:
>
> I concur with the over all effort of this RCR.
>
> However, I think a more complete solution should be had, in which the
> programmer can remove mixins or swap one mixin for another, and
> perhaps even control the mixin chain as one can manipulate an array.
>
> T.
>
> ---------------------------------------------

The sample code does not convince me of the usefulness of this
suggestion. There are other solutions than the one noted, for example
using a Delegator per invocation which will extend with the proper
module. Generally I consider it a bad idea to continuously change the
inheritance hierarchy of a class at runtime - especially since there
seems to be a design flaw here: method Service#run is an instance
method and so if at all the type of the particular instance executing
the method should change, not the type of all the instances of this
class. But that is exactly what happens with the class_eval.

Being able to manipulate the inheritance chain with meta programming
(as T. mentions) is different story.  That's probably useful although
I do not have a use case for this yet. I suggest to open another RCR
for this if that use case can be found.

robert

from Robert Dober, Wed Jun 27 16:21:09 -0400 2007

On 6/27/07, rcrchive+13@rcrchive.net <rcrchive+13@rcrchive.net> wrote:
> Welcome to the comment list for:
>
>   Multiple Module Inclusion (RCR #13)
>
> This RCR was submitted by Alex  Solla,
> at Wed Jun 27 01:01:39 EDT 2007.
>
>
> To comment on this RCR, just reply to this message, using the Reply-To
> address.  All commenting will be done via this email list.
>
> Please trim away this introductory message when you reply!
>
>
> This is a digest of all the comments, so far, on RCR 13,
> "Multiple Module Inclusion".  You'll be on the mailing list for all future
> comments.  You can also add a comment by replying to this digest,
> but please trim it down!
>
>
> On Wed Jun 27 06:41:42 EDT 2007, Trans  Onoma said:
>
> I concur with the over all effort of this RCR.
>
> However, I think a more complete solution should be had, in which the
> programmer can remove mixins or swap one mixin for another, and
> perhaps even control the mixin chain as one can manipulate an array.
>
> T.
>
>
> ---------------------------------------------
>
> On Wed Jun 27 07:16:26 EDT 2007, Robert  Klemme said:
>
> 2007/6/27, rcrchive+13@rcrchive.net <rcrchive+13@rcrchive.net>:
> > This is a digest of all the comments, so far, on RCR 13,
> > "Multiple Module Inclusion".  You'll be on the mailing list for all future
> > comments.  You can also add a comment by replying to this digest,
> > but please trim it down!
> >
> >
> > On Wed Jun 27 06:41:42 EDT 2007, Trans  Onoma said:
> >
> > I concur with the over all effort of this RCR.
> >
> > However, I think a more complete solution should be had, in which the
> > programmer can remove mixins or swap one mixin for another, and
> > perhaps even control the mixin chain as one can manipulate an array.
> >
> > T.
> >
> > ---------------------------------------------
>
> The sample code does not convince me of the usefulness of this
> suggestion. There are other solutions than the one noted, for example
> using a Delegator per invocation which will extend with the proper
> module. Generally I consider it a bad idea to continuously change the
> inheritance hierarchy of a class at runtime - especially since there
> seems to be a design flaw here: method Service#run is an instance
> method and so if at all the type of the particular instance executing
> the method should change, not the type of all the instances of this
> class. But that is exactly what happens with the class_eval.
>
> Being able to manipulate the inheritance chain with meta programming
> (as T. mentions) is different story.  That's probably useful although
> I do not have a use case for this yet. I suggest to open another RCR
> for this if that use case can be found.
>
> robert
>
>
> ---------------------------------------------
I am rather going with Robert on this, not that I think that this is
nonsense, far from that, but I feel that the base of this RCR is not
right.
Module inclusion does just not seem the right tool for what Alex wants
to achieve.
Furthermore it would be nice if the standard procedure for RCRs were
followed, i.e. discuss your idea on the mailing list before.
I have refrained from making two RCRs thanks to the opinions of
learned members of that list.
I will change my vote if discussing this topic brings new insights,
but I feel that such a discussion is an absolute must.

Cheers
Robert




Return to top

Copyright © 2006, Ruby Power and Light, LLC