the dark side of inherited methods

J

Joel VanderWerf

A = Struct.new:)x, :y, :z) do
def length
Math.sqrt(x**2 + y**2 + z**2)
end
end

How do you fix this?

A = Struct.new :x, :y, :z do
def length
Math.sqrt(x**2 + y**2 + z**2)
end

def x=(new_x)
raise ArgumentError if new_x > 100
super
end
end

a = A.new
a.x = 1 # NoMethodError
 
J

James Edward Gray II

How do you fix this?

A = Struct.new :x, :y, :z do
def length
Math.sqrt(x**2 + y**2 + z**2)
end

def x=(new_x)
raise ArgumentError if new_x > 100
super
end
end

a = A.new
a.x = 1 # NoMethodError

Good question. It works to replace super with:

self[:x] = new_x

I realize that's not an ideal solution though.

James Edward Gray II
 
J

James Edward Gray II

=20
Can you give some examples of where it has its place?

I have to lay down the law, because there will always be exceptions, but =
I thought Robert Klemme describes it pretty good a few messages back =
with the "is a" check.
I realized the only time I ever subclass anything
is ActiveRecord::Base (and I'm not really sure what that offers over
modules, DataMapper uses modules, for example).

ActiveRecord is an interesting case of Rubyish inheritance too. It's =
really a focused DSL, I think. You inherit from ActiveRecord::Base to =
get a bunch of macros (has_many(), validates_presence_of(), etc.). =
Using this mini-DSL you describe your model and that changes how the =
objects produced from that class will behave. It's a =
build-your-on-class-with-shortcuts system, I think.

James Edward Gray II
 
I

Ian Hobson

I would not subscribe to that. Although Bertrand Meyer is a big fan
of implementation inheritance I am not yet convinced that it is such a
good idea so often. Of course, there are always uses for any
technique present, but I find the "is a" relationship interpretation
of inheritance so clear and obvious that I somehow feel bad about
polluting inheritance with other uses. I cannot really put forward a
more concrete argument or even back this up by some hard (business)
numbers, but a world in which "A inherits B" <=> "A is a B" is so much
simpler. And in Ruby, whenever you need implementation inheritance you
can use a mixin module. Enumerable is a very good example for that.
There are, IIRC, two places where is a, is not well implemented by
inheritance.

One is the classic square and rectangle problem. In a graphics library,
both square and rectangle are graphic objects that can be drawn on the
screen, moved and resized etc. But what is the relationship between
square and rectangle? Is a square a rectangle with height = width, or is
a rectangle a square without the constraint of height = width, or does
is-a not apply at all? Whatever the author of the package chooses the
users have to know if they have a square or a rectangle, or some very
reasonable code sequences will fail to do what is expected.

x.width *= 2;
x.height *= 2;
If x is a rectangle this doubles both dimensions.
if x is a square, this will quadruple the dimensions, or fail.

The other is where the inheriting object has to learn a new skill at run
time. A teachable foo is not a foo.
Well, there are other forms of code reuse - even in procedural
languages. It's not that you _need_ inheritance to have code reuse.
I would even go as far as to claim that it is more difficult to
implement classes intended solely for implementation inheritance than
classes which implement a particular real world concept and which are
only inherited if "is a" is needed. I say this because a class
intended for implementation inheritance needs other criteria for
including functionality than a class which implements a particular
concept.


That does not sound like a, um, proper application of inheritance to
me. It might be useful to do it that way in some rare conditions but
I would not promote this as a big feature of inheritance.
IIRC, (I started back in the day also), it was promoted as a way to
achieve data migration. You altered the schema, and altered the class to
match, but only converted the data if it was edited. It was helpful in
situations where just *linking* the OLTP took 36 hours, and updating all
the data would have meant the service was down for days - too expensive
to contemplate.

Regards
Ian
 
R

Robert Klemme

There are, IIRC, two places where is a, is not well implemented by
inheritance.

One is the classic square and rectangle problem. In a graphics library, b= oth
square and rectangle are graphic objects that can be drawn on the screen,
moved and resized etc. But what is the relationship between square and
rectangle? Is a square a rectangle with height =3D width, or is a rectang= le a
square without the constraint of height =3D width, or does is-a not apply= at
all?

The rule is pretty easy: state of subclasses can be separated in two groups=
:

1. state present in superclass
2. everything not in 1

Now the rule to maintain "is a" relationship is simple: the set of
allowed states in group 1 must be a subset of the set of allowed
states in the superclass (not a proper subset, so both may be
identical). Allowed state in group 2 is totally governed by the
subclass's invariant.

From this it follows immediately that, if inheritance is used here,
Square can only be a subclass of Rectangle and not the other way
round.
=A0Whatever the author of the package chooses the users have to know if
they have a square or a rectangle, or some very reasonable code sequences
will fail to do what is expected.

x.width *=3D 2;
x.height *=3D 2;
If x is a rectangle this doubles both dimensions.
if x is a square, this will quadruple the dimensions, or fail.

A square _is a_ rectangle where width =3D=3D height. That's the
definition in math and that's how I would implement it usually. The
simplest version would be

class Rectangle
attr_reader :width, :height

def initialize(width, height)
@width =3D width
@height =3D height
end

def outline
2 * ( width + height )
end
end

class Square < Rectangle

def initialize(x)
@width =3D x
end

def height; width; end
end

Note that in Ruby, different than in other programming languages,
@height is not wasted in a Square because it is never allocated. In
other languages you would simply ensure that both members had the same
value all the time in class Square.

Now if you need mutable squares and rectangles you need to devise a
method to manipulate Rectangle which can be used by subclasses as
well, e.g.

class Rectangle
def resize(width, height)
raise ArgumentError if width < 0 || height < 0
@width =3D width
@height =3D height
self
end
end

class Square
def resize(width, height)
raise ArgumentError if width < 0 || height < 0 || width !=3D height
@width =3D width
self
end
end

In a more realistic example you would probably make Rectangle inherit
Polygon and have completely different code for outline calculation and
manipulation. Then also there would be a generalized storage of side
lenghts and angles which would cope with arbitrary many sides thus not
wasting space for a member that is not used.
 
R

Robert Klemme

How do you fix this?

A =3D Struct.new :x, :y, :z do
=A0def length
=A0 =A0Math.sqrt(x**2 + y**2 + z**2)
=A0end

=A0def x=3D(new_x)
=A0 =A0raise ArgumentError if new_x > 100
=A0 =A0super
=A0end
end

a =3D A.new
a.x =3D 1 # NoMethodError

Good question. =A0It works to replace super with:

=A0self[:x] =3D new_x

I realize that's not an ideal solution though.

... as is wasting a class because one wants to spare the typing IMHO. :)

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
Y

Yossef Mendelssohn

class Rectangle
=A0 attr_reader :width, :height

=A0 def initialize(width, height)
=A0 =A0 @width =3D width
=A0 =A0 @height =3D height
=A0 end

=A0 def outline
=A0 =A0 2 * ( width + height )
=A0 end
end

class Square < Rectangle

=A0 def initialize(x)
=A0 =A0 @width =3D x
=A0 end

=A0 def height; width; end
end
class Rectangle
=A0 def resize(width, height)
=A0 =A0 raise ArgumentError if width < 0 || height < 0
=A0 =A0 @width =3D width
=A0 =A0 @height =3D height
=A0 =A0 self
=A0 end
end

class Square
=A0 def resize(width, height)
=A0 =A0 raise ArgumentError if width < 0 || height < 0 || width !=3D heig= ht
=A0 =A0 @width =3D width
=A0 =A0 self
=A0 end
end

I find it interesting that you change a two-argument Rectangle
initialization method to take one argument in Square, but you keep the
resize method as two arguments.
 
J

Joel VanderWerf

... as is wasting a class because one wants to spare the typing IMHO. :)

My suggestion is not just for reducing typing: the super construct gives
you a clean, idiomatic way to wrap the methods provided by Struct.

Anyway, it's not as if Class.new were depleting the ozone layer or
something :)
 
I

Intransition

I would not subscribe to that. =A0Although Bertrand Meyer is a big fan of
implementation inheritance I am not yet convinced that it is such a good
idea so often. =A0Of course, there are always uses for any technique
present, but I find the "is a" relationship interpretation of
inheritance so clear and obvious that I somehow feel bad about polluting
inheritance with other uses. =A0I cannot really put forward a more
concrete argument or even back this up by some hard (business) numbers,
but a world in which "A inherits B" <=3D> "A is a B" is so much simpler.
And in Ruby, whenever you need implementation inheritance you can use a
mixin module. =A0Enumerable is a very good example for that.

Mixins are just another way to do inheritance. It has the same issues.
The only difference is the module level --and that's just an arbitrary
deviation often circumvented by "ClassMethods" hacks..

To the main point of Ruby's poor handling of inheritance, I should
give an an example. Consider creating a subclass of Numeric. We have
no access to the `Numeric.new`, so a subclass of Numeric is actually a
trick. Such a class would (by necessity) work just as well without the
numeric superclass. The only reason for the superclass is so that
`is_a?(Numeric)` will work.
Well, there are other forms of code reuse - even in procedural
languages. =A0It's not that you _need_ inheritance to have code reuse. = =A0I
would even go as far as to claim that it is more difficult to implement
classes intended solely for implementation inheritance than classes
which implement a particular real world concept and which are only
inherited if "is a" is needed. =A0I say this because a class intended for
implementation inheritance needs other criteria for including
functionality than a class which implements a particular concept.

I think implementation inheritance becomes easier if classes are kept
small with simple APIs and focused on a limited goal. Then larger
"real world" classes are built up by inheriting from these smaller
abstractions. But to have a really robust system to do this, one needs
some tools that Ruby doesn't provide, such a private inheritance,
probably class private instance variables and methods, and better
multiple inheritance than Ruby's mixin system.

I think there is a place for both inheritance and composition. It's
not an all or nothing deal. I think Enumerable is actually a great
example of the power of inheritance when used well. And I think Ruby
would be even better if it took that idea and ran with it a bit
further.
Btw, I find that we have this type of discussion far too seldom here
nowadays and I imagine that we did that more often earlier on. =A0But tha= t
is totally subjective.

Agreed.
 
R

Robert Klemme

My suggestion is not just for reducing typing: the super construct gives you
a clean, idiomatic way to wrap the methods provided by Struct.

That's true. It's just that it feels itchy to me to use a class this way.
Anyway, it's not as if Class.new were depleting the ozone layer or something
:)

Hmm... What about global warming? ;-)

Cheers

robert
 
K

Klaus Stein

Intransition said:
Mixins are just another way to do inheritance. It has the same issues.
The only difference is the module level --and that's just an arbitrary
deviation often circumvented by "ClassMethods" hacks..
What I would like to have are Mixins (especially Enumerable) behaving some
more clever.

Asume I have some MyCollection class. I implement #each and include
Enumerable. Now I can do:

mc = MyCollection.new('some', 'data', 'here', 'and', 'there')
mc4 = mc.select { |s| s.length != 4 }

But, unfortunately, mc4.class is Array and not MyCollection. I have to
implement select, reject and others by myself, loosing some of the nice help
Enumerable gives me.

I often want to chain things and call some mapping method on some collection
class without falling back to Array.

This could be solved if Enumerable looks for some MyCollection.newEnum()
class method and calls it if present to create a new object for collecting
the results of the select (or some other mechanism, not sure how to go
best. Any way to tell Enumerable how to collect the results of the mapping
method in a new MyCollection object will do).

Klaus
 
R

Robert Klemme

Mixins are just another way to do inheritance. It has the same issues.
The only difference is the module level --and that's just an arbitrary
deviation often circumvented by "ClassMethods" hacks..

To the main point of Ruby's poor handling of inheritance, I should
give an an example. Consider creating a subclass of Numeric. We have
no access to the `Numeric.new`, so a subclass of Numeric is actually a
trick. Such a class would (by necessity) work just as well without the
numeric superclass. The only reason for the superclass is so that
`is_a?(Numeric)` will work.

I am not sure I get your point. What do you mean by access to
Numeric.new? Method new is defined in Class and Numeric does not have
a method #initialize:

irb(main):028:0> Numeric.ancestors.map {|cl| cl.method:)new) rescue cl}
=3D> [#<Method: Class#new>, Comparable, #<Method: Class#new>, Kernel,
#<Method: Class#new>]

irb(main):029:0> Numeric.ancestors.map {|cl|
cl.instance_method:)initialize) rescue cl}
=3D> [#<UnboundMethod: Numeric(BasicObject)#initialize>, Comparable,
#<UnboundMethod: Object(BasicObject)#initialize>, Kernel,
#<UnboundMethod: BasicObject#initialize>]

Did you mean Integer? I describe the issue here
http://blog.rubybestpractices.com/posts/rklemme/019-Complete_Numeric_Class.=
html

Even that would only be a case of a specific class and not something
about inheritance in Ruby in general.

What is it that you find "poor" about inheritance in Ruby?
I think implementation inheritance becomes easier if classes are kept
small with simple APIs and focused on a limited goal. Then larger
"real world" classes are built up by inheriting from these smaller
abstractions.

Makes me think of traits. Scala has some nice features in that area IIRC.

http://www.scala-lang.org/node/126
But to have a really robust system to do this, one needs
some tools that Ruby doesn't provide, such a private inheritance,
probably class private instance variables and methods, and better
multiple inheritance than Ruby's mixin system.
Exactly.

I think there is a place for both inheritance and composition. It's
not an all or nothing deal. I think Enumerable is actually a great
example of the power of inheritance when used well. And I think Ruby
would be even better if it took that idea and ran with it a bit
further.

What exactly would you like to have changed in the language? So far I
always liked the simplicity and cleanness of Ruby. But maybe
evolutions of the language are possible that will maintain the balance
yet get more out of inheritance. I am really curios what you are
imagining.

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
R

Robert Klemme

What I would like to have are Mixins (especially Enumerable) behaving some
more clever.

Asume I have some MyCollection class. I implement #each and include
Enumerable. Now I can do:

mc = MyCollection.new('some', 'data', 'here', 'and', 'there')
mc4 = mc.select { |s| s.length != 4 }

But, unfortunately, mc4.class is Array and not MyCollection. I have to
implement select, reject and others by myself, loosing some of the nice help
Enumerable gives me.

I often want to chain things and call some mapping method on some collection
class without falling back to Array.

This could be solved if Enumerable looks for some MyCollection.newEnum()
class method and calls it if present to create a new object for collecting
the results of the select (or some other mechanism, not sure how to go
best. Any way to tell Enumerable how to collect the results of the mapping
method in a new MyCollection object will do).

Of course there are ways that could be used to implement what you want, e.g.

module Enumerable
# default: new behavior
def select_2(cl = self.class)
r = (cl.new rescue [])
each {|x| r << x if yield x}
r
end
end

But this does not solve the major issue that you change the interface.
Currently there is a ton of code around that works with the
assumption (explicit or implicit) that #select returns an Array. I
would not dare judge how much code will be broken if we change the
default behavior.

A solution would be this:

module Enumerable
# default: old behavior
def select_2(cl = Array)
r = cl.new
each {|x| r << x if yield x}
r
end
end

But then you loose the convenience to not have to provide the type.

Kind regards

robert
 
K

Klaus Stein

Robert Klemme said:
Of course there are ways that could be used to implement what you want, e.g.

module Enumerable
# default: new behavior
def select_2(cl = self.class)
r = (cl.new rescue [])
each {|x| r << x if yield x}
r
end
end

But this does not solve the major issue that you change the interface.
Currently there is a ton of code around that works with the
assumption (explicit or implicit) that #select returns an Array. I
would not dare judge how much code will be broken if we change the
default behavior.

This is why I would not want this as _default_ behavior. I would expect
select, reject etc return an Array as it is now. But if the class including
Enumerable provides a _special_ MyCollection::enumNew method it would be
used instead.

So something like

module Enumerable
def select
r = (self.class.enumNew(self) rescue [])
# [ rest of the select code ]
end
end

would do. This should not break old code.
I am not sure how to implement this best, as creating an empty collection
and adding elements by << ma be not the most efficient way to go.

(please note that MyCollection.enumNew takes the original object as
parameter to be able to copy other instance variables etc)

kind regards,

Klaus
 
I

Intransition

I am not sure I get your point. =A0What do you mean by access to
Numeric.new? =A0Method new is defined in Class and Numeric does not have
a method #initialize:

That is what I am referring.
irb(main):029:0> Numeric.ancestors.map {|cl|
cl.instance_method:)initialize) rescue cl}
=3D> [#<UnboundMethod: Numeric(BasicObject)#initialize>, Comparable,
#<UnboundMethod: Object(BasicObject)#initialize>, Kernel,
#<UnboundMethod: BasicObject#initialize>]

Did you mean Integer? =A0I describe the issue herehttp://blog.rubybestpra=
ctices.com/posts/rklemme/019-Complete_Numeric_...

No not Integer, although this holds for it too. Your HexNum is a
perfect example. Notice you never use #super in it's implementation.
Because you can't. Also try calling a Numeric method that you haven't
defined for HexNum, like #ceil.
Even that would only be a case of a specific class and not something
about inheritance in Ruby in general.

What is it that you find "poor" about inheritance in Ruby?

There are different things. For one I don't think the core classes are
well honed for inheritance, hence the original topic of this thread.
Makes me think of traits. =A0Scala has some nice features in that area II= RC.

What exactly would you like to have changed in the language? =A0So far I
always liked the simplicity and cleanness of Ruby. =A0But maybe
evolutions of the language are possible that will maintain the balance
yet get more out of inheritance. =A0I am really curios what you are
imagining.

* Getting rid of class vs. module distinction.
* Have #include (or another method) provide "class level" methods in
inheritance chain.
* Private inclusion, such that all methods of class/module being
included are private to the including class/module.
* (Maybe) class private methods and variables, i.e. methods not
visible to the inheritance chain.
* Built-in method to mean `self.class.new()` --I think people forget
to do it that way.
* Built-in support for parametric mixins.
* Work on improving modularity of core library (e.g. Facet's Indexable
and Hashery's Ostructable).

I'm sure there are others too. And it's not an all or nothing deal.
Any one of them would improve upon things.

I agree about the simplicity. In fact, in some ways I want it to be
even more so.
 
R

Robert Klemme

But this does not solve the major issue that you change the interface.
=A0Currently there is a ton of code around that works with the
assumption (explicit or implicit) that #select returns an Array. =A0I
would not dare judge how much code will be broken if we change the
default behavior.

This is why I would not want this as _default_ behavior. I would expect
select, reject etc return an Array as it is now. But if the class includi= ng
Enumerable provides a _special_ MyCollection::enumNew method it would be
used instead.

So something like

module Enumerable
=A0def select
=A0 =A0r =3D (self.class.enumNew(self) rescue [])
=A0 =A0# [ rest of the select code ]
=A0end
end

would do. This should not break old code.

It will, because classes can be modified in Ruby.

require 'set'

class Set
def self.enum_new(other_set)
new
end
end

require 'a_library_that_uses_set'

...

x =3D a_set.select {|x| x > 0}
assert { Array =3D=3D=3D x } # boom!

Again, global state (in this case a method in class Set) bytes. My
suggested approach with providing the class as argument is better IMHO
because

- It is more flexible (at one time you can map an Array to a Set,
another time to an Array etc.).

- It does not break old code (see above).

Actually you do not want to determine the return type of Set#map
*globally* but rather per *use*.
I am not sure how to implement this best, as creating an empty collection
and adding elements by << ma be not the most efficient way to go.

There is no other way because you do not know beforehand whether all
or only part of the elements of the current collection go into the new
one.
(please note that MyCollection.enumNew takes the original object as
=A0parameter to be able to copy other instance variables etc)

I noticed that but this is useless without knowing what the copy will
be used for. Enumerable#map and #select have vastly differing
semantics!

Cheers

robert


--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
R

Robert Dober

Mixins are just another way to do inheritance. It has the same issues.
The only difference is the module level --and that's just an arbitrary
deviation often circumvented by "ClassMethods" hacks..

To the main point of Ruby's poor handling of inheritance, I should
give an an example. Consider creating a subclass of Numeric. We have
no access to the `Numeric.new`, so a subclass of Numeric is actually a
trick. Such a class would (by necessity) work just as well without the
numeric superclass. The only reason for the superclass is so that
`is_a?(Numeric)` will work.


I think implementation inheritance becomes easier if classes are kept
small with simple APIs and focused on a limited goal. Then larger
"real world" classes are built up by inheriting from these smaller
abstractions. But to have a really robust system to do this, one needs
some tools that Ruby doesn't provide, such a private inheritance,
probably class private instance variables and methods, and better
multiple inheritance than Ruby's mixin system.

I think there is a place for both inheritance and composition. It's
not an all or nothing deal. I think Enumerable is actually a great
example of the power of inheritance when used well. And I think Ruby
would be even better if it took that idea and ran with it a bit
further.


Agreed.
Completely share this impression and I agree that it is a pity.
I am not sure I completely agree with any opinion stated here but most
of them have made some points I was not completely aware of.

Cheers
R.



--=20
There are worse things than having people misunderstand your work. A
worse danger is that you will yourself misunderstand your work.
-- Paul Graham
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,777
Messages
2,569,604
Members
45,219
Latest member
KristieKoh

Latest Threads

Top