2
\$\begingroup\$

I am trying to implement the Wikipedia definition of a MultiSet and need feedback on the implementation.

class MultiSet
 include Enumerable
 attr_reader :members
 def initialize enum={}
 @members = {}
 raise_error unless enum.class.include? Enumerable
 enum.each do |item|
 if @members.include?(item) 
 @members[item] += 1
 else
 @members[item] = 1
 end 
 end 
 end
 def each(&blk)
 @members.each(&blk)
 end
 def == other
 members.to_h == other.to_h 
 end
 def eql? other
 self == other
 end
 def to_h
 members.dup
 end 
 def to_set
 Set.new @members.keys
 end 
 def remove item
 if @members.include?(item) 
 @members[item] = @members[item] - 1
 if @members[item] < 1
 @members.delete(item)
 end
 end 
 self
 end 
 def add item
 if @members.include?(item) 
 @members[item] += 1 
 else
 @members[item] = 1
 end 
 self
 end 
 def empty! 
 @members.clear 
 end 
 def multiplicity item 
 @members[item] == 0 ? nil : @members[item]
 end
 def include? item
 @members.include? item 
 end
 def cardinality
 return 0 if @members.empty?
 @members.values.reduce(:+)
 end 
 def | other
 other.each do |k,v|
 if members.include? k
 members[k] = multiplicity(k) + other.multiplicity(k)
 else
 add k
 end 
 end
 self
 end 
 def & other
 members.each do |k,v|
 if other.include? k
 members[k] = [multiplicity(k), other.multiplicity(k)].min
 else
 remove k
 end 
 end 
 self
 end 
end
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Oct 19, 2014 at 2:03
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
  • Sometimes you're skipping parentheses, sometimes you're not. Please be consistent

  • Sometimes you're accessing @members directly, sometimes you're using the members reader method. In some cases that might make sense to do one or the other, but here it's random. Again: Be consistent.
    However, be careful about providing a reader. Right now, I could say a_multiset.members[some_item] = -100 or something, and things would get weird because I've messed with an internal data structure. A #members accessor might be better as simply an alias for #to_h

  • What is raise_error? I don't see it defined anywhere. Which, funnily enough, means that it does raise an error - an error telling you that raise_error isn't defined.
    If you're going to raise an error, then raise the appropriate one:

    raise ArgumentError, "enum must include the 'Enumerable' module"
    
  • enum.class.include?(Enumerable) is a roundabout way of writing enum.kind_of?(Enumerable)

  • Your initializer duplicates logic from you #add method; just call #add instead.

  • #remove can be cleaned up a little by postfixing the if members[item] < 1, just to get rid of the pyramid indentation.

  • Your #empty! method should probably be called #clear. That's the conventional name used by Hash, Array and Set (as you can tell, since that's what your method calls on the @members hash)

  • Pretty sure that your #multiplicity method is exactly backwards. You're returning nil if an item's count is zero. I think you want the exact opposite: Return zero if the item doesn't exist:

    include?(item) ? members[item] : 0
    
  • Use your own methods when you can. You have an #include? method already, so there's no need to use @members.include? all over the place.

  • #cardinality can be written as just

    members.values.reduce(0, :+)
    

    If you provide an initial value to #reduce (the zero), you don't need the extra emptiness-check you have now

  • Pretty sure your union method is incorrect too. If a key doesn't exist, you simply add it - meaning it'll have a count of 1. But in fact, it should have the same count as it has in the other set.
    And you can simplify the method, too:

    def |(other)
     other.each do |key, count|
     members[key] = multiplicity(key) + count
     end
     self
    end
    
  • Both the union and intersection methods should probably return a new MultiSet instance, rather than modify the receiver.

answered Oct 19, 2014 at 14:49
\$\endgroup\$
5
  • \$\begingroup\$ 1. How would the #members accessor be made alias to to_h, if I made members private \$\endgroup\$ Commented Oct 19, 2014 at 18:28
  • \$\begingroup\$ @gitarchie I don't know what you're asking there. I'm just saying that a members accessor should return a dup of @members, like to_h does (in other words, it should be an alias) - it should not return the instance variable itself. \$\endgroup\$ Commented Oct 19, 2014 at 18:34
  • \$\begingroup\$ Ok, I fixed all the issues you mentioned and submitted a new version, I realize my union and intersection were wrong so I changed them and leveraging the add to actually create the multiset. \$\endgroup\$ Commented Oct 19, 2014 at 19:15
  • \$\begingroup\$ @gitarchie Please don't update your question with new code - but feel free to post a brand new question, if you want! Please see this meta post: "What you can and cannot do after receiving answers". I'll take the liberty of rolling back your edit \$\endgroup\$ Commented Oct 19, 2014 at 19:18
  • 1
    \$\begingroup\$ @gitarchie I know what you mean about versioning, but questions must stay in lock-step with their answers - otherwise the Q&A format falls apart. However, if you simply include a link to/from a new question, it's works out just fine. You can also title a follow-up question with "follow-up", "take 2", "updated" or similar to make the distinction clear. Your new code certainly looks better, by the way. Can't really review it fully here in the comments, of course, but again feel free to post a new question, if you'd like (also just to get other opinions than mine) \$\endgroup\$ Commented Oct 19, 2014 at 19:32

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.