Inheriting Array and slice() behaviour

R

Robert Klemme

Patrick said:
[...]

Great, that works fine with all my tests :) Somehow I was always
afraid of delegate.rb. Don't ask me why, perhaps mostly because it is
not shipped with lots of examples where it shows its advantage over
other approaches (in my case: subclassing Array directly).
Or do it selectively (what I'd prefer).

I use my class to build a library. So I don't know in advance what
methods the users would like use. So is there a warning attached to
the above?

Well, you as the lib provider have to decide on the public interface of
your lib. If you just unconditionally make internals available you might
discover that someone did something to your object that neither foresaw
nor supported. I know too few about your case but I'm inclined to do the
forwarding selectively or even manually as a general rule of thumb. You
create the type so it's up to you to decide on the interface. Extending
it later is usually easier than removing public methods.

Kind regards

robert
 
D

Daniel Brockman

Robert Klemme said:
class Module
def delegate(member, method)
class_eval "def #{method}(*a,&b) @#{member}.#{method}(*a,&b) end"
end
end
[...]
class Foo
def initialize() @s = "foo" end

delegate :s, :size
end

Maybe change that to not assume an instance variable and accept
multiple method names:

class Module
def delegate target, *methods
for method in methods do
module_eval %{def #{method} *a, &b
(#{target}).#{method} *a, &b end}
end
end
end

class Buffer
delegate :mad:content, :size, :length
end

That way you could do stuff like this:

class Balancer
delegate '@workers.random', :process
end

(Sorry about the lack of a better example.)
 
J

James Edward Gray II

Yep. Or do it selectively (what I'd prefer). This can help:

class Module
def delegate(member, method)
class_eval "def #{method}(*a,&b) @#{member}.#{method}(*a,&b) end"
end
end


?> delegate :s, :size

"def size(*a,&b) @s.size(*a,&b) end"
=> nil

=> 3

Btw, IMHO this would be a good addition to delegate.rb. What do
others
think?

This is what the standard Forwardable library does:

class Queue
extend Forwardable

def initialize
@q = [ ] # prepare delegate object
end

# setup prefered interface, enq() and deq()...
def_delegator :mad:q, :push, :enq
def_delegator :mad:q, :shift, :deq

# support some general Array methods that fit Queues well
def_delegators :mad:q, :clear, :first, :push, :shift, :size
end

q = Queue.new
q.enq 1, 2, 3, 4, 5
q.push 6

q.shift # => 1
while q.size > 0
puts q.deq
end

q.enq "Ruby", "Perl", "Python"
puts q.first
q.clear
puts q.first

Hope that helps.

James Edward Gray II
 
R

Robert Klemme

James said:
Yep. Or do it selectively (what I'd prefer). This can help:

class Module
def delegate(member, method)
class_eval "def #{method}(*a,&b) @#{member}.#{method}(*a,&b) end"
end
end



"def size(*a,&b) @s.size(*a,&b) end"
=> nil

=> 3

Btw, IMHO this would be a good addition to delegate.rb. What do
others
think?

This is what the standard Forwardable library does:

class Queue
extend Forwardable

def initialize
@q = [ ] # prepare delegate object
end

# setup prefered interface, enq() and deq()...
def_delegator :mad:q, :push, :enq
def_delegator :mad:q, :shift, :deq

# support some general Array methods that fit Queues well
def_delegators :mad:q, :clear, :first, :push, :shift, :size
end

q = Queue.new
q.enq 1, 2, 3, 4, 5
q.push 6

q.shift # => 1
while q.size > 0
puts q.deq
end

q.enq "Ruby", "Perl", "Python"
puts q.first
q.clear
puts q.first

Hope that helps.

James Edward Gray II

Yuck! Didn't new this one. That covers pretty much everything one needs
(apart from Daniel's suggestion). Thanks for the heads up!

Kind regards

robert
 
W

William Morgan

Excerpts from Robert Klemme's mail of 6 Jul 2005 (EDT):
I wouldn't be so certain of this. It's normal for operations like
#dup and #clone to behave that way. Also, part of the smartness of
ruby is that you can simply do self.class.new to get a new instance of
the same class. And often you need to create an instance of the same
class. That's not too abnormal.

Dup and clone work on objects that have already been initialized.
Self.class.new is done explicitly, so that's fine too---if the
constructor has required arguments, you supply them at that point.

My problem with Array#slice is that the objects created will never have
#initialize called. Indeed, they never *can* have it called, because who
knows what arguments need to be supplied.
IMHO if there is a problem then with proper initialization of the new
instance. OTOH, inheriting classes like Array is seldom really good.
There are too many methods that return plain arrays and you end up
with Arrays anyway. Delegation, ad hoc modification or extension with
a Module is often better.

I agree that it's seldom a good idea (and it certainly wasn't in this
case). But sometimes it could be the right thing to do.
 
W

William Morgan

Excerpts from Yukihiro Matsumoto's mail of 6 Jul 2005 (EDT):
|Like the OP, I think it's weird and special-cased to have #slice
|magically return something with the receiver's class. Especially since
|the return value is going to be broken (not initialized) except in
|trivial cases.

What if it can be initialized properly?

But I don't think it can be. To be initialized, #initialize must be
called, but #initialize can have required arguments. That's the essence
of my trouble with Array#slice: returned objects can't be initialized
properly.

I think the subclass should override #slice if they want this behavior,
and then they can explicitly call #new with the correct arguments.
They're probably going to have to do this for #+, anyways.

But I guess I'm the lone gunman by now, so I will stop pressing the
issue. Thanks for your comments!
 
A

Ara.T.Howard

Excerpts from Robert Klemme's mail of 6 Jul 2005 (EDT):

Dup and clone work on objects that have already been initialized.
Self.class.new is done explicitly, so that's fine too---if the
constructor has required arguments, you supply them at that point.

My problem with Array#slice is that the objects created will never have
#initialize called. Indeed, they never *can* have it called, because who
knows what arguments need to be supplied.

it's this simple:

harp:~ > cat a.rb
class Expression < ::Array
def initialize(*a, &b)
init(*a, &b)
end
def init(*a, &b)
@a, @b = a, b
end
def inspect
{'a' => @a, 'b' => @b}.inspect
end
inited_return_methods = %w(
slice
)
inited_return_methods.each do |m|
eval <<-definition
def #{ m }(*a, &b)
ret = super
ret.init(*@a, &@b)
ret
end
definition
end
end

exp = Expression::new 'an arg', 'another arg'

subexp = exp.slice 0,2

p subexp


harp:~ > ruby a.rb
{"a"=>["an arg", "another arg"], "b"=>nil}


note that the subexp has the same values given to the ctor of the original exp.


this will not be exactly what you want, but the approach is generic:

* remember what args the object was initialized with
* make a public initializer
* use meta-programming to re-construct the methods you want such that return
values are, in fact, initialized.

if you remember what an object was initialized with it's always possible to
re-initialize it or to initialize another object with those values - you need
only decide the rules for doing so.

hth.

-a
--
===============================================================================
| email :: ara [dot] t [dot] howard [at] noaa [dot] gov
| phone :: 303.497.6469
| My religion is very simple. My religion is kindness.
| --Tenzin Gyatso
===============================================================================
 
A

Austin Ziegler

This is a lisp-like language, so expressions are basically array of atoms
and expressions. When I do a slice() this is to get a subset of the list,
but a subset is not an expression itself.
=20
Expression is-a Array, but Expression.slice is just an Array

I disagree. In an OO language, a slice of an Expression should be an
Expression if Expression is a descendant of Array.

-austin
--=20
Austin Ziegler * (e-mail address removed)
* Alternate: (e-mail address removed)
 
S

Sylvain Joyeux

I disagree. In an OO language, a slice of an Expression should be an
Expression if Expression is a descendant of Array.
I don't. We're on the side of dogmas here, so no need to argue about it.

Sylvain
 
W

William Morgan

Excerpts from Ara.T.Howard's mail of 7 Jul 2005 (EDT):
it's this simple:

Actually, it's even simpler---the author of Expression can simply
override #slice to do the right thing. The point is that the *author*
must do this explicitly. If Ruby tries to do this automatically, it will
return invalid objects.

In short, the current #slice behavior is a) wrong in non-trivial cases,
b) magic, and c) inconsistent with #+ and other methods.
 
W

William Morgan

Excerpts from Austin Ziegler's mail of 7 Jul 2005 (EDT):
In an OO language, a slice of an Expression should be an Expression if
Expression is a descendant of Array.

I'd say that's probably a requirement for a well-written OO Expression
*class*, depending on what exactly it's supposed to be doing. The
question is whether the language should---or even can---enforce that.

In particular, what if Expression#initialize takes parameters that, for
sub-expressions, need to be determined from some combination of the
receiver's state and the arguments to #slice? That's the situation here,
and it needs to be explicitly handled by the author. Anything Ruby does
automatically will be wrong.
 
P

Pit Capitain

William said:
Excerpts from Ara.T.Howard's mail of 7 Jul 2005 (EDT):



Actually, it's even simpler---the author of Expression can simply
override #slice to do the right thing. The point is that the *author*
must do this explicitly. If Ruby tries to do this automatically, it will
return invalid objects.

In short, the current #slice behavior is a) wrong in non-trivial cases,
b) magic, and c) inconsistent with #+ and other methods.

The LSP has been mentioned before in this thread. According to that
principle, if someone creates a subclass of Array, you should be able to
use the subclass like the original Array class. Array has a well defined
#initialize method that can take zero arguments. So should the subclass,
otherwise you're breaking the LSP. In other words, it's perfectly legal
to call self.class.new in #slice. If Expression can't provide an
#initialize method without arguments, it shouldn't be a subclass of Array.

Regards,
Pit
 
W

William Morgan

Excerpts from Pit Capitain's mail of 7 Jul 2005 (EDT):
The LSP has been mentioned before in this thread. According to that
principle, if someone creates a subclass of Array, you should be able
to use the subclass like the original Array class. Array has a well
defined #initialize method that can take zero arguments. So should the
subclass, otherwise you're breaking the LSP. In other words, it's
perfectly legal to call self.class.new in #slice. If Expression can't
provide an #initialize method without arguments, it shouldn't be a
subclass of Array.

So you're suggesting that #initialize be called (automatically) by
Array#slice, with no arguments? And, presumably, an ArgumentError raised
if it has required arguments?

What about #+, #reverse, #select, etc.?
 
A

Austin Ziegler

Excerpts from Pit Capitain's mail of 7 Jul 2005 (EDT):
So you're suggesting that #initialize be called (automatically) by
Array#slice, with no arguments? And, presumably, an ArgumentError raised
if it has required arguments?

No, from a design level, Expression isn't an Array if it doesn't
accept #initialize with no arguments.

-austin
--=20
Austin Ziegler * (e-mail address removed)
* Alternate: (e-mail address removed)
 
S

Sylvain Joyeux

Are you suggesting that everywhere I write a subclass I should have the
same initialize than the parent class ? Sounds not very useful to me.

Sylvain
 
P

Pit Capitain

Sylvain said:
Are you suggesting that everywhere I write a subclass I should have the
same initialize than the parent class ? Sounds not very useful to me.

See http://en.wikipedia.org/wiki/Liskov_substitution_principle for a
short definition. According to this principle, the subclass doesn't have
to have exactly the same initialize as the parent class, but it has to
support at least the same interface. For example, if the parent class
defines an initialize with two parameters

def ParentClass.initialize( a, b )

then the following definitions wouldn't be allowed according to the LSP:

def ClientClass.initialize( a )
def ClientClass.initialize( a, b, c )

because they don't allow to call initialize with two parameters. The
following definitions would be legal though, as they still accept the
original form with two parameters:

def ClientClass.initialize( a, b = nil )
def ClientClass.initialize( *args )

Of course you're not forced to comply with the LSP, but I think it can
help to decide when to use inheritance and when not. You can also look
at http://c2.com/cgi/wiki?LiskovSubstitutionPrinciple for a more
controversial discussion.

Regards,
Pit
 
P

Pit Capitain

William said:
So you're suggesting that #initialize be called (automatically) by
Array#slice, with no arguments?

I was thinking that that was the actual behaviour, but now I realized it
isn't. After looking at the source code and thinking about Matz' reply,
I'm not so sure anymore.
And, presumably, an ArgumentError raised if it has required
arguments?

See Austin's answer to that.
What about #+, #reverse, #select, etc.?

Quite some time ago, there have been some discussions on ruby-talk about
this very subject. Maybe you can get more information about the pros and
cons from there.

Regards,
Pit
 
D

Devin Mullins

Pit said:
See http://en.wikipedia.org/wiki/Liskov_substitution_principle for a
short definition. According to this principle, the subclass doesn't
have to have exactly the same initialize as the parent class, but it
has to support at least the same interface.

Wrong. The LSP says that /objects/ of a subtype have to support the same
interface. That is, it specifically ignores the constructor. That's how
Java gets away with not inheriting the constructor.

Of course, Ruby *does* inherit the constructor, so you can just tell
people that want to derive from Array not to define initialize, or to do
it in a way that doesn't suck, as you mentioned in the rest of your email.

In that case, I suggest what I suggested previous:
Array has a "copy constructor" (Array.new can take another Array), so you could call klass.new(theNewSubArray) unless klass == Array, instead of just massaging it into a klass...
But in truth, I agree most with William Morgan:
In short, the current #slice behavior is a) wrong in non-trivial cases,
b) magic, and c) inconsistent with #+ and other methods.
Devin
Sorry to shout "Wrong," like that. It's just I get so few opportunities
to be absolutely confident in my answer. :)
 
A

Adam P. Jenkins

Pit said:
The LSP has been mentioned before in this thread. According to that
principle, if someone creates a subclass of Array, you should be able to
use the subclass like the original Array class. Array has a well defined
#initialize method that can take zero arguments. So should the subclass,
otherwise you're breaking the LSP. In other words, it's perfectly legal
to call self.class.new in #slice. If Expression can't provide an
#initialize method without arguments, it shouldn't be a subclass of Array.

The LSP is defined in terms of particular interfaces. Given an
interface IF, according to the LSP I should be able to use any object
which claims to implement IF interchangably, without having to be aware
of the object's specific type, as long as I'm only using the
functionality specified by IF. When it comes to is-a relationships,
I've never seen the LSP interpreted to mean subclasses must have the
same constructor signatures as their superclasses. The constructors are
generally considered to be outside the scope of the interface, unless
explicitly stated otherwise.

Adam
 
W

William Morgan

Excerpts from Austin Ziegler's mail of 7 Jul 2005 (EDT):
No, from a design level, Expression isn't an Array if it doesn't
accept #initialize with no arguments.

I'm sorry, that simply isn't true, under any kind of reasonable
philosophy.

class InducingArray < Array
def initialize(klass)
@klass = klass
end

def [] *a
if a.length == 1
@klass.induced_from super
else
super
end
end
end
 

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

No members online now.

Forum statistics

Threads
474,175
Messages
2,570,942
Members
47,491
Latest member
mohitk

Latest Threads

Top