Ruby #to_enum: как лучше всего извлечь исходный объект из перечислителя?

Предположим, у меня есть объект:

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

И я конвертирую его в перечислитель:

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

Теперь я хочу вернуть свой объект из перечислителя. Я нашел способ сделать это, но он кажется излишне заумным (не говоря уже о довольно хрупком):

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

Если это неясно, я проверяю объект Enumerator, используя регулярное выражение для извлечения идентификатора исходного объекта из результирующей строки, преобразования его в целое число (и деления на 2) и использования ObjectSpace._id2ref для преобразования идентификатора в ссылка на мой объект. Уродливые вещи.

Мне трудно поверить, что это самый простой способ выполнить эту работу, но несколько часов гугления ничего мне не открыли. Есть ли простой способ извлечь объект после обертывания перечислителя вокруг него с помощью #to_enum, или это в значительной степени способ сделать это?

Изменить:

Как говорит Амадан ниже (и очень признателен, Амадан), это может быть проблема XY, и мне, возможно, придется переосмыслить свое решение. Я немного объясню, как я сюда попал.

Вариант использования (мета): у меня есть (переменное) количество объектов в массиве. Каждый объект предоставляет массив целых чисел (все одинакового размера) в качестве атрибута, отсортированного от большего к меньшему. Я хочу одновременно перебирать массивы каждого из объектов, находя объект или объекты с наибольшим целым числом, не совпадающим в массиве другого объекта.

Казалось, что внешняя итерация была хорошим способом сделать это, так как одновременная внутренняя итерация нескольких объектов, которые должны знать о промежуточных результатах итераций друг друга, также довольно быстро распространяется. Но когда я нашел перечислитель, содержащий объект с массивом с наибольшим значением, мне нужно вернуть объект, который он обертывает.

Там вполне может быть лучший способ, чем использование счетчиков, когда это является требованием. Однако фактический процесс итерации и выбора при их использовании довольно тривиален.

Так. Прикладной вариант использования: количество покерных комбинаций без какой-либо руки лучше, чем старшая карта. Найдите выигрышную руку. «Выигрышная рука» - это рука, в которой старшая карта не соответствует по рангу другой руке. (Масти не имеют значения.) Если все карты совпадают в двух или более руках, верните эти руки в виде массива.

«Минимально воспроизводимый пример»:

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"]]]

Это «работает», но я, конечно, согласен с Амаданом, что это хак. Интересный и поучительный, может быть, но все же халтура. TIA для любых предложений.


person BobRodes    schedule 05.02.2020    source источник
comment
Я думаю, вы правы, я не вижу, чтобы Enumerator каким-либо образом раскрывал обернутый объект, кроме найденного вами хака. Однако даже необходимость сделать это является хаком — вы уверены, что нет другого способа, кроме как получить объект от Enumerator? Подозрение на проблему XY.   -  person Amadan    schedule 05.02.2020
comment
Перечислителям не нужен базовый объект. Enumerator.new { |y| y << 1 << 2 << 3 } создает превосходный перечислитель, который не привязан к конкретному объекту.   -  person Stefan    schedule 05.02.2020
comment
В дополнение к точке @Stefan, 1.step является еще одним примером.   -  person Cary Swoveland    schedule 05.02.2020
comment
@Amadan Возможно, это действительно так. Я отступил назад и отредактировал свой мыслительный процесс и соответствующий код.   -  person BobRodes    schedule 05.02.2020
comment
@Stefan Ну, это не совсем так; ваш код фактически оборачивает объект Enumerator::Generator. (Документация по объекту Enumerator::Generator кстати состоит из одного слова: Generator, которое так же полезно показывает ссылку на самого себя.) Но я лукавлю. Более того, тот факт, что перечислители могут быть созданы без какой-либо внешней ссылки на объект (т. е. ссылки, которую вы и я можем видеть в нашем коде) на объект, не означает, что это невозможно. И вопрос в том, когда это произойдет, используя #to_enum для переноса существующего объекта в перечислитель.   -  person BobRodes    schedule 05.02.2020
comment
@CarySwoveland Думаю, я не понимаю вас, ребята. Не могли бы вы немного уточнить?   -  person BobRodes    schedule 05.02.2020
comment
@Stefan просто указал на то, что не все перечислители являются сопоставлениями из базового объекта. Это не отменяет вашего вопроса; вы ищете базовый объект для перечислителя, у которого он есть. Для тех, я не верю, что есть альтернатива вашему взлому.   -  person Cary Swoveland    schedule 05.02.2020
comment
@CarySwoveland Хорошо, имеет смысл. Спасибо. И я был бы признателен за любой вклад в другие способы выполнения работы.   -  person BobRodes    schedule 05.02.2020
comment
Почему вы не можете передать экземпляр руки вместе с перечислением рук дальше и извлечь его в конце   -  person Fabio    schedule 05.02.2020
comment
@ Фабио, я думал о том, как это сделать, да. Я надеюсь найти более простой путь.   -  person BobRodes    schedule 05.02.2020
comment
Извините, я не могу понять логику метода high_cards: означает ли это, что если в одной руке окажется старшая карта на первой итерации, то другие руки будут удалены. Если да, то почему вам нужно продолжать зацикливать другие карты одной оставшейся руки? Но что, если старшая карта находится в другой руке, но последней в порядке?   -  person Fabio    schedule 05.02.2020
comment
@Fabio В массиве может быть любое количество рук, поэтому одну можно удалить, оставив любое число. Я мог бы добавить логику для прерывания после того, как осталась только одна, но поскольку всего итераций всего пять (и, действительно, в конце может быть несколько равных рук), добавление этой проверки на каждой итерации показалось менее эффективным.   -  person BobRodes    schedule 05.02.2020
comment
@Stefan Мой комментарий звучит немного пренебрежительно, если посмотреть на него еще раз. Пожалуйста, извините меня, если это относится и к вам; Я не это имел в виду.   -  person BobRodes    schedule 05.02.2020
comment
Извините еще раз, всегда ли коллекция карт в наличии упорядочена по убыванию?   -  person Fabio    schedule 05.02.2020
comment
@ Фабио Да, я так настроил.   -  person BobRodes    schedule 05.02.2020
comment
Как насчет такого метода, как этот get_highest_hand(hands), и внутри этого метода вы бы перебирали все карты, находящиеся в соответствующих руках?   -  person BKSpurgeon    schedule 06.02.2020
comment
@BobRodes Я просто хотел указать, что перечислители не обязательно поддерживаются коллекцией. Точно так же, как объекты, подобные IO, не обязательно основаны на файлах. Думайте о счетчике как о чем-то, что выдает объекты в определенном порядке, ни больше, ни меньше. Допустим, эти объекты являются персонажами. Их источником может быть строка в коде или файл на жестком диске или интерактивный пользовательский ввод. Наш фиктивный счетчик символов добавляет слой абстракции и намеренно скрывает эти детали.   -  person Stefan    schedule 06.02.2020
comment
@Stefan Если вы возьмете свой код и проверите полученный перечислитель, может показаться, что он поддерживается объектом Enumerator::Generator. Это не коллекция? Я вижу объект Enumerator как оболочку для другого объекта, который выполняет итерацию по внутреннему объекту в соответствии с тем, что этот объект дает для итерации.   -  person BobRodes    schedule 06.02.2020
comment
@BobRodes Enumerator::Generator кажется оболочкой вокруг блока, переданного Enumerator.new, так что Enumerator может иметь дело с объектом, имеющим метод each. Но это детали реализации. Счетчики не раскрывают свои источники, они просто выдают значения.   -  person Stefan    schedule 06.02.2020
comment
@Стефан Да, я это вижу. Если вы хотите, чтобы счетчик открывал свой источник, вы должны взломать его, а это хрупкая вещь.   -  person BobRodes    schedule 06.02.2020
comment
@BKSpurgeon Не уверен в том различии, которое вы проводите. Разве не этим сейчас занимается high_cards?   -  person BobRodes    schedule 06.02.2020


Ответы (2)


Я не уверен, почему все эти разговоры о перечислениях.

Я предполагаю, что руки имеют формат, подобный этому:

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"]])]

и вы хотите получить 0-й и 2-й элементы. Я также предполагаю, что руки отсортированы, согласно вашему утверждению. Насколько я вижу, это все, что нужно, так как массивы сравниваются лексикографически:

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

В качестве альтернативы используйте group_by (немного лучше, так как не нужно вычислять ранги дважды):

hands_by_ranks = hands.group_by { |hand| hand.cards.map(&:first) }
max_hands = hands_by_ranks[hands_by_ranks.keys.max]
person Amadan    schedule 06.02.2020
comment
Извинения. Сейчас очень поздно, поэтому я подожду, чтобы опубликовать воспроизводимый пример. Но это не совсем правильно. В приведенном вами примере первая итерация устранит 1-й элемент, а затем вторая итерация устранит (теперь) 1-й элемент, поскольку 9 меньше 10. То же, что и в покере, если все ранги в каждой руке были разными. (В вашем примере, конечно, во второй раздаче пара, а в третьей тройка.) - person BobRodes; 06.02.2020
comment
Вот почему я сказал, что у вас нет минимально воспроизводимого примера. Если вы сделаете один, я пересмотрю. Предположим, я ничего не знаю о покере: объясните все. - person Amadan; 06.02.2020
comment
и вы хотите получить 0-й и 2-й элемент, потому что 12 является самым высоким - не совсем так, вы хотите получить только 0-й элемент, потому что рука (12, 10, 8, 3, 2) имеет более высокий ранг, чем 2-й элемент (12, 9, 8, 8, 8). 12 == 12, но 10 > 9 - person Fabio; 07.02.2020
comment
Исправлено пояснение @Fabio и определение класса OP. - person Amadan; 07.02.2020

Как уже упоминалось в комментариях, нет способа надежно получить базовый объект, потому что он не всегда есть в перечислителе.
Хотя ваше решение будет работать в этом конкретном случае, я бы предложил придумать другой подход, который не будет зависеть от деталей реализации объекта перечислителя.

Одним из возможных решений было бы передать экземпляр Hand вместе с перечислителем.

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

Другой, более объектно-ориентированный подход, состоит в том, чтобы ввести пользовательский Enumerator и предоставить базовый объект более явным образом:

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
person Fabio    schedule 05.02.2020
comment
Мне это нравится. Я думал о том, чтобы сделать что-то подобное, но в то время был сильно измотан и не видел способа сделать это легко. Я также понимаю вашу точку зрения о зависимости от деталей реализации перечислителя; это то, что беспокоило меня об этом в первую очередь. (ps в чисто техническом смысле перечислитель действительно всегда имеет базовый объект, который он обертывает, в чем вы можете убедиться сами, если создадите несколько различных способов и используете #inspect, чтобы увидеть результаты , В этом я не согласен с другими комментариями, хотя могу согласиться с ними в практическом смысле.) - person BobRodes; 06.02.2020
comment
Ваша дополнительная (ОО) идея мне нравится даже больше. :) Очень хорошо. Спасибо, что поделились. Прежде чем я приму это как ответ, я хотел бы покопаться в вашем Hand#to_enum, чтобы я полностью его понял. У меня нет времени этим заниматься сегодня, но я сделаю это завтра. (p.s. Я исправил #for_enum на #enum_for — после просмотра, чтобы быть уверенным.) - person BobRodes; 06.02.2020
comment
Фабио, я изучил ваш код и теперь лучше его понимаю. Я никогда не видел ничего подобного вашему yielder << card.first, и у меня есть несколько вопросов по этому поводу. Почему вы ставите это вместо yielder.yield card.first? Они эквивалентны? Если это так, это наводит меня на мысль, что (недокументированный) объект Enumerator::Yielder является массивом. Вы знаете что-нибудь об этом? - person BobRodes; 06.02.2020
comment
yield и << оба делают одно и то же, разница только в том, что << вернет self (yielder) обратно вызывающей стороне. У меня нет предпочтений между этими двумя. << проще использовать в методе reduce, поскольку он возвращает yielder к следующей итерации. Yielder — это не массив, а простой класс, который позволяет построить Enumerator из блока (из комментариев к исходному коду :)) - person Fabio; 07.02.2020