New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(ChildClass < ::Array).dup Broken #2015
Comments
|
The problem here is the following: " develops methods which alter the behavior". Basically if you do that you don't have guarantees that code actually keeps working. With inheritance you should only extend behavior and not modify it. This principle is know as the Liskov Substitution Principle. The better way to solve problems is usually composition over inheritance, so you don't have problems like this. Looking at the example class here, I see no reason why it needs to be an Array, and cannot use an Array internally for storage (or just instance variables). This also provides a bunch of information on this topic: http://www.engineyard.com/blog/2010/rubinius-wants-to-help-you-make-ruby-better |
|
BTW, the code you provided does not show this failure. First of all it throws a Superclass mismatch: Object != Array (TypeError) exception because Range is already a built in type. If I change the name of Range it works fine. Probably in your original code you're setting a @start instance variable, which is also a variable that is used internally in Array. |
|
I cant take credit, ts not my code - just my attempts to get Metasploit to run properly on RBX with a slightly simplified copy of Rex::Socket::Range as the example. Its namespaced, so if you run it in :: then you'll collide with ::Range and you will see a different error. My apologies for overlooking that. Here's what's ive' done to get the class to work properly in MSF: module Rex::Socket
class Range < Array # :nodoc: all
def start; self[0]; end
def stop; self[1]; end
def ipv6; self[2]; end
def options; self[3]; end
def start=(val); self[0] = val; end
def stop=(val); self[1] = val; end
def ipv6=(val); self[2] = val; end
def options=(val); self[3] = val; end
# RBX doesnt handle descendants of Array as MRI does
# Issue opened with RBX team at:
# https://github.com/rubinius/rubinius/issues/2015
if RUBY_ENGINE == 'rbx'
def dup
r = self.class.new
r.start = self.start
r.stop = self.stop
r.ipv6 = self.ipv6
r.options = self.options ? self.options.dup : nil
return r
end
end
end
endThere's a lot more work there: |
|
The thing is that this is not something that we should fix in Rubinius, but that this class in MSF then needs to be refactored. Please don't go adding workarounds like described here to the code, but properly use composition here for this class instead of inheritance. |
|
@dbussink I love reading what you write on GH issues. |
|
It's scary to me that there are Rubies that will run that code without an error. It's certainly an interesting problem though; when is it a 'feature' of MRI that something deliberately hides its internals from people trying to change them. |
|
@dbussink I'm confused by your first comment on this issue. Are you suggesting that inheriting from a class and overridding a method from it is a) bad and b) a Liskov violation? Sorry, I know this is a tangent, I just don't know what forum would be more appropriate for the question. |
|
trololo |
|
@mortice I agree with @dbussink that overriding a method in a child class is indeed bad. This is especially true if that method is a dependency of other methods in the superclass. Every time you modify a dependency, you end up changing things in ways you may not have intended. To answer your second question, Liskov is violated if While I agree that this isn't the place to discuss code quality, my point is that the best way to resolve this particular issue isn't to "fix" Rubinius to accommodate bad code. |
|
@jgaskins oh yea, I totally agree that in this case there's a Liskov violation, and I'm not convinced either that Rubinius should be fixed to accommodate it. I just don't think method overriding is bad. It's a key enabler of polymorphism when a type hierarchy exists, and overriding methods which are "[dependencies] of other methods in the superclass" is precisely the mechanism used in implementing the Template Method pattern (http://en.wikipedia.org/wiki/Template_method_pattern), which I happen to find useful on occasion. I would accept the modification: inheriting from core classes like Anyway, sorry for the tangent. Agreed on the proposed approach and proposed solution to the problem of using composition. |
|
My very short version is that you shouldn't change the behavior in a child in such a way that the new implementation is incompatible with the original behavior. Templating is fine in this context, if you have an abstract method expecting to return a string, returning something different would be breaking it. |
|
@mortice I had a feeling you might go there. :-) I suppose I should've clarified that in most cases it's a bad idea. Template methods are a bit of a special case, though. |
|
There's only one thing rubinius should be concerned about here, "Does this code work on other implementations of Ruby?" In fact, it does. Using this simplified example: a = Array.new
puts 'Array instance'
p :start => a.respond_to?(:start)
p :stop => a.respond_to?(:stop)
class R < ::Array
def start() self[0] end
def stop() self[1] end
end
r = R.new
puts 'R instance'
p :start => r.respond_to?(:start)
p :stop => r.respond_to?(:stop)
r[0] = 123456
r[1] = 123457
p r
p r.dupIt works on CRuby version 1.8.7: It works on CRuby 1.9.3: It works on CRuby trunk (an indicator it will work on the 2.0.0 release): It works on JRuby 1.7.0: It works on MacRuby 0.12: It doesn't work on rubinius master: It doesn't matter how impure the code is, how ugly it is, how many principles of good software it violates or if it calls on evil creations from forgotten dark places in order to operate. If it doesn't run in rubinius and it runs under other implementations then rubinius must fix it. Of particular importance, you'll note that rubinius defines Array#start and uses it to duplicate its instances. In Ruby (the language, not the implementation) duplication of an Array does not depend on any methods to work. If a user defines a method and Array#dup breaks then rubinius must change. |
|
So do you argue that in Rubinius we cannot add any method that is not in MRI? So we cannot implement things by extracting shared functionality in methods like normal OO code? So basically we cannot implement things in Ruby but have to do it in another language / dialect so we can hide stuff from users? |
|
@drbrain Sorry, Rubinius cannot support your argument that we must be able to run any code, no matter how many MRI implementation assumptions it relies on. In Rubinius, the core classes are implemented in Ruby, using Ruby semantics. The same integration issues exist when running on Rubinius as exist when using any library. You cannot blindly override methods in a libraries class. And you cannot blindly override methods on Ruby core classes. @sempervictus This code can easily be written better using OO principles. We're happy to provide a patch if you would like for the code. We won't be changing Rubinius to accommodate poorly written code. |
|
@dbussink @brixen I find it bizarre that you think the only way to fix this bug is to step away from good design principles in rubinius. I can think of one very simple way to fix this bug. A way that is already used many times in the rubinius core class implementation. A way that already hides the internals of rubinius from the user. Simply prefix |
|
We're not adding under_under methods all over the core library. We mitigate brokenness from poor code in a few select cases. |
|
@drbrain you would not make such an argument for your own libraries, so making one for Rubinius is disingenuous. Rubinius implements the core classes and gets priority. You would not take seriously someone suggesting that your library classes should allow having implementation methods overridden in a way that breaks your semantics. Your argument that Rubinius should cannot be taken seriously. |
|
If I were writing a compatible implementation of someone else's library and my implementation broke someone's Rakefile I would consider it a bug and would make an effort to fix it. When I am writing adding new features to my libraries and I break someone else's bad code I make an effort to fix it. Often, I am successful. So yes, I would make such an argument for my own libraries, just as I did here. |
Honestly guys, I think @drbrain is right and you are just delaying the inevitable. Mind you, we're not talking about stupid stuff like this: class Stupid < Array
def size
42 # that'll show 'em!
end
endCode like this: class R < Array
def start
end
enddoesn't really violate Liskov Substitution Principle. Adding internal |
|
We're not delaying the inevitable anything. We are not adding There is no code that must add code to core classes, or subclass core classes, in a way that breaks Rubinius. If a project wants to be so recalcitrant about this, they are welcome to run on JRuby or MRI. |
Why is this not true for Rubinius? Why must Rubinius add code to core classes in a way that breaks other libraries? It seems to me you are saying other Ruby code is not allowed to extend Ruby core classes, because you like extending core classes. |
|
Rubinius is implementing the Ruby core classes, using Ruby and object-oriented methods. The fact that MRI is implemented in C and does not use good object-oriented methods is the reason that poorly written code runs on MRI. In every case we have seen, it's both possible and easy to use the core classes properly. |
|
The question is, how far will you go with compatibility. This code works on all other Rubies but probably messes with Rubinius: Rubinius = 42 |
|
I'm all for implementing Ruby core classes in Ruby. What I have problem with is the way you chose to do it, as it clashes with a Ruby feature/property that allows for extension (via monkey patching or inheritance) of Ruby core classes. Yes, I can avoid any issues, provided I am very careful not to overwrite any of the Rubinius's undocumented internal methods. And yes... you, coming from a position of a very important Ruby open source project, can bully me into doing so (if I want my library to work across all the important Ruby VMs). Yet I think it is very wrong that we allow Ruby VMs to break Ruby core classes extendability. Today only Rubinius proudly does so, what if the other current or future VMs follow your way? Writing a cross-VM code is going to get much harder as I'm now going to be forced to consult multiple VM code bases checking for any conflicting internal methods... I do hope you change your mind. PS: have you considered what would it be like if the Ruby/Rubinius community was 50x bigger than it is today and you'd have 50x the amount of stupid, time wasting bug reports like this one? |
|
We have to draw the line somewhere. Like @rkh pointed out, if someone defined Rubinius and then reported "hey, my code doesn't work on Rubinius because I need it to be value X" should we always bow and change our stuff? If we do, we compromise part of the point of Rubinius which is to use ruby as ruby. The question is where is that line and that is what is being debated. We feel the need to push back because otherwise we can't establish where we want the line to be. It's always going to be a debate, that's how the line is set. Another question is what role does Rubinius play within the Ruby community. It's totally valid for Rubinius to play a role of working to improve practices by drawing lines about what we support. @drbrain's feeling that if MRI allows it then Rubinius must allow it is too much of a blanket statement and I'm sure he'd agree. For instance, must Rubinius support RHASH() in the C-API? It doesn't and never will so if we being 100% compatible with MRI is an unattainable case. But it's also not a case that we must strive for! Rubinius is an alternative and doing things different to provide different abilities is part of the point of being an alternative. But thats contrasted with wanting to be a drop in replacement for MRI. This is a constant tention and because it's on OSS project that tension is played out in public. Thusly, I hope you all understand that Rubinius and it's developers must have an opinion about this and please honor our decisions. |
|
@evanphx thanks for chiming in! Not sure I agree with your slippery slope argument about us users wanting you to remove the I'm also hoping next time you have some gripes with the MRI dev team you don't get back the response that reads like this:
|
|
Actually, we get that often from the MRI team. Same with JRuby. |
|
Rubinius should change to allow this code to work. Here's why:
There's really not that many cases of this in Rubinius anymore, ever since Evan started requiring people to put non-standard methods on Rubinius:: scoped modules. Why not just fix this case too? It would not require much work to alias these methods to __ versions (a standard way in Ruby to say "don't override me") and use those, or use instance_variable_get to avoid calling methods at all. |
|
Looks like this will get fixed after all by #2077. Excellent! |
|
+1 On Sun, Nov 18, 2012 at 12:58 PM, Jamie Gaskins notifications@github.comwrote:
by Jose Narvaez |
The original ticket (#2015) that lead to the PR in #2077 contains a fair amount of bullshit, so some clarification is warranted. Mirrors are an elegant and useful architectural tool for separating meta-level and base-level functionality. The problem here is only partially of this nature so use of mirrors provides only a partial solution. The meta, base separation permits re-classifying some methods as "meta" when they are more accurately just a set of primitive operations omitted from the object's API. Ruby's core classes are generally poorly designed, being more a random bag of operations than a well-designed interface. Two concepts are essential to a well-designed interface: stratification and composition. Stratification is the clear separation of methods into primitives and compositions. Composition is the combining of two or more primitive operations to provide a more complex operation. All compositions should clearly state what they compose and the constraints they impose on the composition. This permits subclasses to accurately know how refining methods can impact other behaviors of the class. Nothing even remotely like this is specified for Ruby core classes. The Ruby methods of a class are a thin veneer of names over a jumble of C functions. Primitive operations and their constraints are unspecified and may not even be exposed to Ruby. Given this situation, attempting to implement the core Ruby classes like Array in Ruby, and attempting to use good OO design principles, is extremely hard. There are essentially three options: 1. Duplicate primitive operations in literal code in numerous methods that are the exposed Ruby API. 2. Factor these methods into "helper" methods that pollute the class namespace relative to the MRI namespace, where the helper functions are in C and invisible to Ruby. 3. Use an abstraction like mirrors to segregate the implementation of primitive operations or more complex arrangements of operations to implement some aspect of the exposed Ruby API. A Ruby object's method table is logically flat and has a single namespace. In other words, you can build a simple list of all methods that an object responds to. There is no segmentation in this list. The use of mirrors, as done here, provides a second, separate namespace of methods that the exposed API methods can access but users of the object won't access (however, nothing prohibits a user from accessing them at this point). This solves the problem of polluting the object's namespace while still permitting the implementation of the exposed API to factor out common functionality. However, that's not the full story. The state of the object (i.e. what instance variables it has) is also a flat namespace. Nothing prevents a user from overriding that state. In the case at issue here, Array has the following attributes: @start, @ToTal, and @tuple. There is no way to implement Array in normal Ruby without some state. So the only options are: 1. have state that a user can interfere with (as here), or 2. use some implementation method that interposes a rigid boundary (like MRI does by implementing things in C). The use of mirrors here does not prevent code like the following: class A < Array attr_accessor :state # Some method sets state to values incompatible with the expectations of # the methods implementing Array end None of the suggested solutions in #2015 actually solve these problems. Naming methods with __some_name__ convention does not prevent them from being overridden, even if it *may* prevent them from being inadvertently overridden (we have delt with cases is DataMapper and BlankSlate where these implementations *did not* properly ovoid modifying under under methods). So that is an ineffective hack that significantly reduces the quality of the core class code. Neither does only accessing the instance variables through reflection prevent them from being over-written or misused. The only solutions that completely address the situation are frozen core classes (i.e. not permitting code to modify core classes once they are loaded) or implementing Ruby as a thin veneer of names on top of some language like C or Java. Both of these options are unacceptable for Rubinius. We are attempting to build a more useful Ruby system. One that permits good use of OO principles to compose programs. We also want to make the system easier to develop and maintain by Ruby developers through use of Ruby code instead of some foreign language. Just as important, there are significant benefits to writing the core classes in Ruby: 1. The JIT compiler can inline core class code into application code, or application code into core class code, to produce very fast machine code. By using Ruby, arbitrary barriers in the system that defeat optimization are removed. 2. Writing the core classes in Ruby enables tools for comprehending program operation without an arbitrary opaque barrier between the application code and the core class code. 3. The core classes are available for inspection, learning, and improvement by ordinary Ruby programmers. 4. Improvements to the system that make Ruby execute faster improve the core classes and the application code. 5. Debugging application code is not inhibited by an opaque boundary at the core class level where a debugger would need to be able to step through Ruby code and then into C code. These and other benefits weigh heavily in favor of the architecture Rubinius is pursuing and against the idea of a system that is a bag of names over some opaque and foreign language. This is a decision that Rubinius is free to make. Finally, Rubinius is not "reimplementing someone's library". Rubinius would not exist if MRI wasn't a technical disaster. Rubinius is both a competing project and specifically guided by the goal of improving Ruby as a language. Rubinius is the fundamental system underlying and providing services to a Ruby application. Rubinius gets priority. The core classes are completely visible in Ruby. No programmer has to wonder if their code is using some "undocumented* feature. They can, *and should*, read the code that implements the APIs they use. It's their choice to write code that does not run on Rubinius. It's not Rubinius' responsibility to make any random code work. If someone doesn't like that, they are welcome to and encouraged to use MRI instead.
When a class inherits from ::Array, develops methods which alter the behavior, and is then duplicated, a Tuple::copy_from error is generated.
Example:
Fix - had to instantiate a dup method for the subclass to prevent failing up to Array.dup:
Unfortunately this seems to happen with lots of classes exhibiting this behavior. Maybe all of them. Would it make sense to avoid parsing out the array if the object inherits from the ::Array class?
Stack trace for r.dup:
The text was updated successfully, but these errors were encountered: