Ruby #to_enum: what's the best way to extract the original object from the enumerator?

北慕城南 提交于 2020-07-22 22:16:53

问题


Suppose I have an object:

obj = Object.new  #<Object:0x00007fbe36b4db28>

And I convert it to an Enumerator:

obj_enum = obj.to_enum  #<Enumerator: #<Object:0x00007fbe36b4db28>:each>

Now I want to get my object back from the enumerator. I found a way to do it, but it seems unnecessarily abstruse (not to mention pretty fragile):

extracted_obj = ObjectSpace._id2ref(
  obj_enum.inspect.match(/0x[0-9a-f]*/).values_at(0)[0].to_i(16)/2
)
p obj.equal? extracted_obj # => true

In case it isn't clear, I'm inspecting the Enumerator object, using regex to pull the original object's id from the resulting string, converting it to an integer (and dividing by 2), and using ObjectSpace._id2ref to convert the id to a reference to my object. Ugly stuff.

I have trouble believing that this is the easiest way to get this job done, but some hours of googling haven't revealed anything to me. Is there a simple way to extract an object after wrapping an Enumerator around it with #to_enum, or is this pretty much the way to do it?

Edit:

As Amadan says below (and much appreciated, Amadan), this may be an XY problem, and I may have to rethink my solution. I'll explain a bit about how I got here.

The (meta) use case: I have a (variable) quantity of objects in an array. The objects each expose an array of integers (all of the same size) as an attribute, sorted high to low. I want to iterate the arrays of each of the objects simultaneously, finding the object or objects with the highest integer not matched in another object's array.

It seemed like external iteration was a good way to go about doing this, since simultaneous internal iteration of multiple objects that have to know about the intermediate results of one another's iterations gets out there pretty quickly as well. But when I have found the enumerator that contains the object with the array with the highest value, I need to return the object that it wraps.

There very well may be a better way to go than using enumerators when this is a requirement. However, the actual iteration and selection process is pretty trivial when using them.

So. The applied use case: a quantity of poker hands with no hand better than a high card. Find the winning hand. The "winning hand" is the hand with the highest card not matched in rank by another hand. (Suits are irrelevant.) If all the cards match in two or more hands, return those hands in an array.

"Minimal reproducible example":

class Hand
  attr_reader :cards

  def initialize(cards)
    @cards = cards.sort.reverse
  end

  def each
    @cards.each { |card| yield(card.first) }
  end
end

class Poker
  def initialize(hands)
    @hands = hands.map { |hand| Hand.new(hand) }
  end

  def high_cards
    hand_enums = @hands.map(&:to_enum)
    loop do
      max_rank = hand_enums.map(&:peek).max
      hand_enums.delete_if { |enum| enum.peek != max_rank }
      hand_enums.each(&:next)
    end
    hand_enums.map { |e| from_enum(e).cards }
  end

  def from_enum(enum)
    ObjectSpace._id2ref(
      enum.inspect.match(/0x[0-9a-f]*/).values_at(0)[0].to_i(16) / 2
    )
  end
end

hands = [
  [[10, "D"], [3, "C"], [8, "C"], [7, "C"], [9, "D"]],
  [[10, "D"], [8, "S"], [7, "S"], [9, "H"], [2, "H"]],
  [[9, "C"], [8, "H"], [9, "S"], [4, "C"], [7, "D"]]
]

game = Poker.new(hands)
p game.high_cards # => [[[10, "D"], [9, "D"], [8, "C"], [7, "C"], [3, "C"]]]

This "works," but I certainly agree with Amadan that it's a hack. An interesting and instructive one, maybe, but a hack all the same. TIA for any suggestions.


回答1:


I am not sure why all this talk of enums.

I'll presume hands are in a format similar to this:

hands = [Hand.new([[12, "H"], [10, "H"], [8, "D"], [3, "D"], [2, "C"]]),
         Hand.new([[10, "D"], [9, "H"], [3, "C"], [2, "D"], [2, "H"]]),
         Hand.new([[12, "D"], [10, "S"], [8, "C"], [3, "S"], [2, "H"]]),
         Hand.new([[12, "C"], [9, "S"], [8, "C"], [8, "S"], [8, "S"]])]

and you want to get the 0th and 2nd element. I'll also assume the hands are sorted, per your assert. As far as I can see, this is all that is needed, since arrays compare lexicographically:

max_ranks = hands.map { |hand| hand.cards.map(&:first) }.max
max_hands = hands.select { |hand| hand.cards.map(&:first) == max_ranks }

Alternately, use group_by (a bit better, as it doesn't need to calculate ranks twice):

hands_by_ranks = hands.group_by { |hand| hand.cards.map(&:first) }
max_hands = hands_by_ranks[hands_by_ranks.keys.max]



回答2:


As already mentioned in the comments, there is no way to reliably retrieve the underlying object, because the enumerator doesn't always have one.
While your solution will work for this specific case, I would suggest coming up with another approach that will not depend on the implementation details of the enumerator object.

One of the possible solutions would be to pass an instance of Hand along with the enumerator.

class Poker
  def high_cards(hands)
    hand_enums = hands.map { |hand| [hand, hand.to_enum] }

    loop do
      max_rank = hand_enums.map(&:last).map(&:peek).max
      hand_enums.delete_if {|enum| enum.last.peek != max_rank }

      hand_enums.each {|pair| pair.last.next}
    end

    hand_enums.map(&:first)
  end
end

Another more object-oriented approach is to introduce a custom Enumerator and expose the underlying object in a more explicit way:

class HandEnumerator < Enumerator
  attr_reader :hand

  def initialize(hand, &block)
    @hand = hand
    super(block)
  end
end

class Hand
  def to_enum
    HandEnumerator.new(self) { |yielder| @cards.each { |card| yielder << card.first }}
  end

  # To satisfy interface of enumerator creation
  def enum_for 
    to_enum
  end
end

class Poker
  def high_cards(hands)
    hand_enums = hands.map(&:to_enum)

    loop do
      max_rank = hand_enums.map(&:peek).max
      hand_enums.delete_if {|enum| enum.peek != max_rank }

      hand_enums.each(&:next)
    end

    hand_enums.map(&:hand)
  end
end


来源:https://stackoverflow.com/questions/60073714/ruby-to-enum-whats-the-best-way-to-extract-the-original-object-from-the-enume

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!