Flexible operations for a collection class

E

Edgardo Hames

Hi you all.

I'm writing a collection class, which looks like this (by heart, don't
have the code here)

class Collection
def initialize(elements=nil)
@elements=elements
end
def add(elements)
elements.each{ |o| @elements << o unless @elements.include? o}
end
def sortable_by=(element_attribute)
@sortable_by=element_attribute
end
end

I need some help with the following issues:

0) I would like to be able to pass either an array or an single object
to the constructor, but (a) @elements should be a list of objects (not
an array containing arrays). I tried this

def initialize(o, *array)
@elements << o
array.each{ |o| @elements << o unless @elements.include? o}
end

but (a) is not satisfied when I do

c = Collection.new([1,2,3,4])

The same goes for the #add method.

1) I would like to use the #sortable_by method to indicate which
attribute of the collection elements should be compared when sorting
it. Right now, I'm sorting it like this

def sort(&block)
if block_given?
@elements.sort(&block)
else
@elements.sort{|x,y| x.send(@sortable_by) <=> y.send(@sortable_by)}
end
end

but I would like to implement the #<=> method in my objects, so I can
delegate the #sort method to @elements. I came up with this,

def sortable_by=(element_attribute)
@sortable_by=element_attribute
@elements.each{|o| o.sortable_by=@sortable_by}
end

and I update every new object I add. If I had a Java Comparator like
class, that would be trivial.

Can anybody point out some suggestions on how to do this in a better way.
Thanks for reading such a long message.

Kind regards,
Ed
 
G

George Ogata

Edgardo Hames said:
Hi you all.

I'm writing a collection class, which looks like this (by heart, don't
have the code here)

class Collection
def initialize(elements=nil)
@elements=elements
end
def add(elements)
elements.each{ |o| @elements << o unless @elements.include? o}
end
def sortable_by=(element_attribute)
@sortable_by=element_attribute
end
end

I need some help with the following issues:

0) I would like to be able to pass either an array or an single object
to the constructor, but (a) @elements should be a list of objects (not
an array containing arrays). I tried this

def initialize(o, *array)
@elements << o
array.each{ |o| @elements << o unless @elements.include? o}
end

but (a) is not satisfied when I do

c = Collection.new([1,2,3,4])

The same goes for the #add method.

It seems like a bad idea to me to have the constructor take _either_ n
element-args, _or_ 1 list-of-elements-arg, since, as you discovered,
the "argument spaces" overlap.

I'd suggest doing it like Array:

-- Collection#initialize may take an Enumerable, in which case its
elements become the elements of the new Collection
-- Collection.[] takes n element args, and returns the n-element
Collection containing those elements

Thus, Collection.new(list) is the same as Collection[*list].
1) I would like to use the #sortable_by method to indicate which
attribute of the collection elements should be compared when sorting
it. Right now, I'm sorting it like this

def sort(&block)
if block_given?
@elements.sort(&block)
else
@elements.sort{|x,y| x.send(@sortable_by) <=> y.send(@sortable_by)}
end
end

but I would like to implement the #<=> method in my objects, so I can
delegate the #sort method to @elements. I came up with this,

def sortable_by=(element_attribute)
@sortable_by=element_attribute
@elements.each{|o| o.sortable_by=@sortable_by}
end

and I update every new object I add. If I had a Java Comparator like
class, that would be trivial.

My suggestion:

If the ordering is part of the elements' nature, then implement the
elements' class's #<=> accordingly.

If the ordering is something that belongs to the container
(Collection), then have Collection#sort_by (just like Array#sort_by).

If you want the sort proc to be part of the container's state, then do
something like (untested):

class Collection
attr_accessor :default_sort_proc

def sort &blk
blk ||= default_sort_proc
@elements.sort(&blk)
end

## optional
def default_sort_key= proc
self.default_sort_proc = lambda{|x, y| proc.call(x) <=> proc.call(y)}
end
end

HTH.
 
R

Robert Klemme

Edgardo Hames said:
Hi you all.

I'm writing a collection class, which looks like this (by heart, don't
have the code here)

class Collection
def initialize(elements=nil)
@elements=elements
end
def add(elements)
elements.each{ |o| @elements << o unless @elements.include? o}
end
def sortable_by=(element_attribute)
@sortable_by=element_attribute
end
end

I need some help with the following issues:

0) I would like to be able to pass either an array or an single object
to the constructor, but (a) @elements should be a list of objects (not
an array containing arrays). I tried this

def initialize(o, *array)
@elements << o
array.each{ |o| @elements << o unless @elements.include? o}
end

but (a) is not satisfied when I do

c = Collection.new([1,2,3,4])

The same goes for the #add method.

1) I would like to use the #sortable_by method to indicate which
attribute of the collection elements should be compared when sorting
it. Right now, I'm sorting it like this

def sort(&block)
if block_given?
@elements.sort(&block)
else
@elements.sort{|x,y| x.send(@sortable_by) <=> y.send(@sortable_by)}
end
end

but I would like to implement the #<=> method in my objects, so I can
delegate the #sort method to @elements. I came up with this,

def sortable_by=(element_attribute)
@sortable_by=element_attribute
@elements.each{|o| o.sortable_by=@sortable_by}
end

Putting the comparison criteria into the elements is very bad design. What
will you do if your elements are in two different collections at the same
time? The sorting criterium belongs into the collection.
and I update every new object I add. If I had a Java Comparator like
class, that would be trivial.

Can anybody point out some suggestions on how to do this in a better way.
Thanks for reading such a long message.

Apparently you are trying to implement a sortable set.

About the set part: They are already implemented in Ruby:
require 'set' => true
s=Set.new
=> # said:
s << 1 << 2 << 3 << 1
=> # said:
s.sort => [1, 2, 3]
s.sort {|a,b| b<=>a} => [3, 2, 1]

About the sort part:

Either use the built in functionality (like above) or make @sortable_by a
lambda with two arguments like this:
=> [3, 2, 1]

The lambda is essentially the Comparator you know from Java.

I'd probably do this if I wanted a sorted set class:

require 'set'

class SortedSet < Set
DEF_SORT = lambda {|a,b| a <=> b}

attr_accessor :comp

def initialize(*args, &b)
super(*args)
@comp = b || DEF_SORT
end

def sort(&b)
super( &( b || @comp ) )
end

def each_sorted(&b)
sort.each(&b)
self
end
end

Then you can do this:
s = SortedSet.new
=> # said:
s << 1 << 2 << 3 << 1 << 2
=> # said:
s.sort => [1, 2, 3]
s.each_sorted {|x| puts x}
1
2
3
3
2
1
=> #<SortedSet: {1, 2, 3}>

However, if you need to sort often compared to insert and delete operations,
then a sorted data structure (like an ordered tree) is more efficient.

Kind regards

robert
 
R

Robert Klemme

The example I presented is not complete: it lacks proper hash and equals
methods.
However, if you need to sort often compared to insert and delete operations,
then a sorted data structure (like an ordered tree) is more efficient.

An additional note: if you change the sorting criterium often, then a
collection that maintains order is likely to be not so efficient since you
need to resort the whole thing on every sort criterium change.

Kind regards

robert
 
E

Edgardo Hames

Putting the comparison criteria into the elements is very bad design. What
will you do if your elements are in two different collections at the same
time? The sorting criterium belongs into the collection.


From The Pickaxe Book, in the Enumerable Module

"Mix in Enumerable, and suddenly your class supports things such as
map, include?, and find_all?. If the objects in your collection
implement meaningful ordering semantics using the <=> method, you'll
also get min, max, and sort."

That's the reason behind my design. But, the Set solution satisfies
most of my needs. I just need to add a few methods to it.

Thanks for your clear response.
Ed
 
R

Robert Klemme

Edgardo Hames said:
From The Pickaxe Book, in the Enumerable Module

"Mix in Enumerable, and suddenly your class supports things such as
map, include?, and find_all?. If the objects in your collection
implement meaningful ordering semantics using the <=> method, you'll
also get min, max, and sort."

That's the reason behind my design.

Yeah, but <=> just gives you the default ordering (e.g. lexical for strings
and numeric for fixnum). As far as I can see, that's not what you wanted:
you wanted to be able to define the order yourself. Changing <=> all the
time is not a good idea because of the issue with multiple collections as
well as other pieces of code relying on the natural ordering.

Btw, see also Comparable.
http://www.rubycentral.com/book/ref_m_comparable.html
But, the Set solution satisfies
most of my needs. I just need to add a few methods to it.
Fine!

Thanks for your clear response.

You're welcome.

robert
 

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
473,764
Messages
2,569,567
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top