Flexible operations for a collection class

Discussion in 'Ruby' started by Edgardo Hames, Aug 30, 2004.

  1. 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

    --
    "Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible, you are,
    by definition, not smart enough to debug it." - Brian W. Kernighan
     
    Edgardo Hames, Aug 30, 2004
    #1
    1. Advertising

  2. Edgardo Hames

    George Ogata Guest

    Edgardo Hames <> writes:

    > 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.
     
    George Ogata, Aug 30, 2004
    #2
    1. Advertising

  3. "Edgardo Hames" <> schrieb im Newsbeitrag
    news:...
    > 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

    => #<Set: {}>
    >> s << 1 << 2 << 3 << 1

    => #<Set: {1, 2, 3}>
    >> 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:

    >> sortable_by = lambda {|a,b| b<=>a}

    => #<Proc:0x101becf0@(irb):6>
    >> s.sort &sortable_by

    => [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

    => #<SortedSet: {}>
    >> s << 1 << 2 << 3 << 1 << 2

    => #<SortedSet: {1, 2, 3}>
    >> s.sort

    => [1, 2, 3]
    >> s.each_sorted {|x| puts x}

    1
    2
    3
    => #<SortedSet: {1, 2, 3}>
    >> s.comp= lambda {|a,b| b<=>a}

    => #<Proc:0x101c5da0@(irb):26>
    >> s.each_sorted {|x| puts x}

    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
     
    Robert Klemme, Aug 30, 2004
    #3
  4. "Robert Klemme" <> schrieb im Newsbeitrag
    news:...

    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
     
    Robert Klemme, Aug 30, 2004
    #4
  5. On Tue, 31 Aug 2004 05:25:26 +0900, Robert Klemme <> wrote:
    >
    > >
    > > 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.



    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
    --
    "Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible, you are,
    by definition, not smart enough to debug it." - Brian W. Kernighan
     
    Edgardo Hames, Aug 30, 2004
    #5
  6. On Tue, 31 Aug 2004 05:30:25 +0900, Robert Klemme <> wrote:
    >
    > The example I presented is not complete: it lacks proper hash and equals
    > methods.
    >


    That's ok. I won't need them right now anyway.

    Regards,
    Ed



    --
    "Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible, you are,
    by definition, not smart enough to debug it." - Brian W. Kernighan
     
    Edgardo Hames, Aug 30, 2004
    #6
  7. "Edgardo Hames" <> schrieb im Newsbeitrag
    news:...
    > On Tue, 31 Aug 2004 05:25:26 +0900, Robert Klemme <>

    wrote:
    > >
    > > >
    > > > 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.

    >
    >
    > 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
     
    Robert Klemme, Aug 31, 2004
    #7
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.

Share This Page