Submitted by minam (Fri Jan 23 14:01:21 UTC 2004)
This RCR supercedes #189, proposing an improved syntax and implementation of this approach to method visibility.
The following example implements the State design pattern (trivially). Consider a system that implements a sequence of integers as a class. Calls to its next
method return subsequent values from the sequence. The sequence also allows its state to be saved and restored, via an opaque SequenceState object, thereby allowing the sequence to be restarted from some previous value.
Here is where the selective export is useful. The SequenceState object should be opaque to everyone except the Sequence itself (or to other classes that want to mimic the behavior of a Sequence). The Sequence object must be able to query and modify the values in the SequenceState object that represent the Sequence's internal state.
Here is the implementation of the SequenceState class:
class SequenceState def get_counter @counter end def set_counter( counter ) @counter = counter end publish :StateAccess, :get_counter, :set_counter end
Notice the last line; this publishes a new profile, called "StateAccess", and indicates that both "get_counter" and "set_counter" should be in this profile. Putting those methods in the profile also implicitly makes those methods private.
The Sequence class is implemented thus:
class Sequence def initialize( start=0 ) @counter = start-1 end def next @counter += 1 @counter end def get_state state = SequenceState.new.subscribe( :StateAccess ) do |access| access.set_counter @counter end return state end def restore_state( state ) state.subscribe( :StateAccess ) do |access| @counter = access.get_counter end end end
Notice the "get_state" and "restore_state" methods. In "get_state", a SequenceState object is instantiated. Then, the caller subscribes to the StateAccess profile of the new object. A proxy object, which has access to methods of that profile (including "set_counter"), is then passed to the corresponding block.
In restore_state, the caller subscribes to the StateAccess profile of the state object, which gives them access to the "get_counter" method of the state, thus allowing them to restore the value of the counter.
The subscription process and use of a proxy object do incur performance penalties, which should be understood by developers that wish to use this feature. However, the performance hit on calling through a proxy is not too bad (about 2-3 times slower than a direct method invocation). The subscription hit is more significant (about 20x slower than doing without a subscription). The following table shows the results of some benchmarks. Each test involved 1,000,000 method invocations.
Direct: 1.024s Subscribed: 2.769s 1,000,000 subscriptions: 24.26s
In an attempt to improve performance, I wrote a C version of the pubsub
module. Although it did not improve the performance of the call through the proxy significantly (2.5s for 1,000,000 method invocations), it did improve the subscription performance (7.5s for 1,000,000 subscriptions).
However, it should be stated that use of this feature is intended for those relatively infrequent occassions when an object needs access to the internal state of another object. In general, this shouldn't happen in tight iterations, where speed is necessary, and so the lower performance of this feature should not be a concern.
It may be argued that this kind of visibility limitation is no limitation at all, since any object at all may subscribe to a service, if it knows about the service. However, the point (in this case) is not to limit visibility. Designers still have access to private and protected access modes, which prevent outside access to methods so annotated. Selective export is not about preventing access, but controlling access. It ensures that the clients must know the proper "protocol" for communicating with an object.
Given that the reference implementation is functional, it may be asked why this RCR is necessary (since the same functionality is now available as a module). There are two reasons:
<li>Although it may be implemented completely in Ruby, there are limitations to such an implementation (among them, speed, since the existance of a proxy object inside of each subscription block introduces overhead for every method call made on that proxy). It may be necessary to implement part, if not all, of the functionality of this in C--perhaps even making minor modifications to the interpreter itself.</li> <li>Making such functionality a standard part of the Ruby distribution--whether it is implemented purely in Ruby or not--would encourage developers to make use of it. If it must be distributed with every project that uses it, or if it must be installed before projects that use it may be installed, it makes it more difficult for it to be properly adopted.</li>
The module referenced above is implemented by reopening Object and adding functionality to it, thereby granting both publish
and subscribe
to every class. It might also be implemented as a module which is included by those classes that need it; this approach might be more desirable if the Object class needs to be kept "lean".
Comments | Current voting | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
|
This RCR supersedes RCR 189.
RCRchive copyright © David Alan Black, 2003-2005.
Powered by .
You know, it's one thing to be strongly opposed to an RCR. Everyone has that right. But if you are going to vote "strongly opposed" on an RCR, don't you think it would be best if you also posted a comment explaining *why* you are strongly opposed to it? It would at least give the creator(s) of the RCR a chance to clarify areas of the proposal that might have been vague or unclear, or to understand what aspects of it some might find distasteful. Please, if you're going to vote in opposition to an RCR, at least give a sentence or two saying why. Thanks.
- Jamis Buck
I'm not the one who voted against this RCR. But I'm still not sure whether this proposal is useful or not. I know selective export is good thing in static typed language, but in a language like Ruby, its complexity and performance penenalty attracts me less, especially with the syntax described in this RCR. If we have accepted the selective export, it should be built in the syntax to gain expressiveness and performance
-matz.
If "published methods" was to become a part of the language, what would it look like?
One idea that might work is to have access granted just for the immediate scope. In the runtime, the scope would keep track of the subscriptions that have been made, and allow access to methods as required directly from object references. If the scope is converted to a proc (or other related activities) subscriptions would be "unsubscribed" in that scope.
Also, defining subscribe/unsubscribe as language features could avoid having to use symbol syntax when specifying what profiles to subscribe to (not sure if that's a good idea though).
Maybe we should refer to this topic as "selective import" of published methods, because the form of publish/subscribe being discussed in this RCR publishes the methods to everyone who wants to subscribe. It is when subscribing that we are being selective. :)
-corey.
Another thing to consider is to simplify things and not have multiple profiles. That is, to have all published features being accessible when a call to subscribe is made. This would then be more like the other visibility classifications, and would simplify the syntax -- also allowing the "published" keyword to be used to specify a section of the class definition as being "published access".
All published features would be lumped together, and made accessible by calling an object's subscribe method. Calling the object's unsubscribe method would again hide those features, as would moving out of the scope where subscribe was called. Transporting the scope (e.g. converting to a Proc, etc) in any way would also unsubscribe.
-corey.
Thanks for your comments, Matz. I admit that there is complexity involved in doing this, but do you really feel it is too complex? Admittedly, the initial reference implementation had some needless complexity in it, but Corey and I have since polished it off and tightened it up nicely.
As for performance, I can appreciate your concerns there. I have added to the Analysis section, above, and posted some performance benchmarks. Pubsub did not perform nearly as poorly as I would have feared--being only about 2.5 times slower than direct method invocations. As I mentioned in that section, a C version was also written, which did not significanly improve proxy performance, but that was largely due to the fact that I am not very familiar with the Ruby API and probably did some things the hard way.
I disagree that this needs to be built into the syntax, any more than "public" and "private" are currently built into Ruby's syntax. Putting it into the syntax will only complicate the grammar further, and for what? It is a feature that will not be used for small scripts, which is what most people use Ruby for, I think. However, it will be invaluable for larger, more complex projects--and in those cases, the syntax I proposed (as a method of Object) is sufficient.
(I should state, though, that if you plan to incorporate "public", "private", and "protected" into the sytnax of Rite, then perhaps selective export should be incorporated into Rite's syntax, as well, for consistency's sake.)
Because this is a feature that wont' be used for the vast majority of Ruby programs (like callcc, for instance), and because it's existence will never impact the performance of scripts that don't use it, I don't know that performance is a significant concern, either. It performs quite well already, and if it were to be reimplemented in C, I think the performance would improve even further (as I have stated above). Thus, although the pubsub approach I have proposed will not perform as well as a direct method invocation, I don't think it needs to, any more than 'Continuation#call' needs to perform on par with a standard method invocation.
Lastly, I've already communicated with Corey about this, but I don't think restricting each Object to one published profile is useful. It would restrict the expressiveness of this feature, and would make it so that methods that should otherwise not be in the same profile, MUST be declared in the same profile. I would much rather see Objects able to publish multiple profiles, and thus be able to categorize their selective exports by functionality.
Thanks!
Jamis Buck
public, private, and protected are method calls. They are not built into the syntax any more than puts is.
-- Paul Brannan
That was my point exactly.
-- Jamis Buck
This proposal would add selective export to Ruby, thus exporting set of methods to the particular block. Fine. But what for? I mean, what is the problem you want to solve by this selective export? Selective export is a good thing, I know. But in Ruby, is this really affordable for the users to pay the price of explictly calling "publish" in the class body, and "subscribe" in the method body, to just control visibility of methods? That's the point. I feel like many amoung Ruby users are too lazy to do this.
-- matz.
I agree--most people are not going to use this feature, whether because they are "too lazy", or because they won't be writing sufficiently complex programs. The fact is, this feature is only going to be useful when you start having some complex interactions between objects.
Here's a "real life" example of where it would be valuable, taken straight from the Ruby 1.8.1. distribution. Looking in lib/cgi/session.rb, on lines 265-281, the Session class accesses the internal state of the 'request' object (CGI class) directly, using "instance_eval". I think you'll agree that this is dangerous--a change in CGI that affects how @output_hidden and @output_cookies are implemented would cause this code to break. However, it may be that the interface that modifies those values should remain hidden to everyone except the Session class. Using selective export, you could do this easily: just implement a couple of private methods on CGI that allow modification of those values, and then publish them in a profile. The 'initialize' method of Session could then subscribe to that profile and invoke those methods as required. This would be a cleaner (and safer) solution than using instance_eval to directly modify another object's state.
The performance hit, in this case, is negligable, since an application should never instantiate a Session object over and over, in a tight loop. Thus, the penalty for executing a subscription and proxy call will be negligable compared to the cost of executing the rest of the program.
As I said, though, I agree that this will not be a commonly used feature--and that's one reason why I don't think it should be made part of the syntax of Ruby. However, the feature itself is valuable enough for more complex Ruby programs that I think it has merit as a standard part of the Ruby distribution (especially if it is used to "fix" direct internal access issues that exist in the current Ruby distribution--just do a search for "instance_eval" in the lib directory and you'll see there are a handful of other methods that do this, in addition to Session#initialize).
However, it is your call, matz, and I understand that. If you decide that it isn't what you want for your language, I can live with using this library as it is, and distributing it explicitly with my own deliverables.
-- Jamis
It looks like from matz' last response that the *real* reason is that he doesn't see much need for this feature, that Ruby programmers are too lazy to use it, and "what is the problem you(we) want to solve"?
I believe that there is a need for such a feature, as Jamis cleverly pointed out in Ruby's own library. Is this the style of programming you wish to encourage? Has anyone else seen the 'instance_eval's in Ruby's library? Is this the "Ruby way?". Currently it is. Is this a problem worth solving?
Good method and variable "hiding" is a fundamental principle of OOP. I believe that Ruby would be the appropriate language to introduce a better way of expressing selective export. I don't know of any language yet that handles selective export gracefully. This feature would introduce a new flexible and concise way of solving a common problem. At least I can say that the "cost" of specifying 'publish'and 'subscribe' for those specialized situations is quite minimal, and a heck of alot safer and cleaner than the current 'instance_eval' hack.
But, as Jamis pointed out, this is matz' call.
- Danny
As long as these features don't introduce performance penalties to developers who don't use them, I think it's a good thing to have. Maybe the getters/setters example is not the best, and that misguided other people to believe this isn't a needed tool. For a real world scenario I will refer to Borland Delphi's IDE. Delphi has always had protected, public, private *and published* keywords, the most frequent example of "published" happens when used for objects that need to be manipulated by plug-ins and property editors. One developer writes the object (vcl component, in delphi jargon) and provides a set of methods that will be used by the IDE, the IDE can manipulate visually the properties of the objects (ie: a special editor for a rich text widget, that shows a preview while dinamically setting the object's attributes) without knowing beforehand of the existence of the objects or what they actually do, this allows for loosely coupled, pluggable objects. Not only it is a desirable feature for consistency, cleanness and formal correctness, but also for practical reasons. Best, ---vruz