Ruby: манипулировать итераторами? - PullRequest
6 голосов
/ 08 августа 2011

У меня проблемы с прорезыванием зубов в Ruby, связанные с созданием однонаправленных, лениво оцениваемых, потенциально бесконечных итераторов.По сути, я пытаюсь использовать Ruby, как если бы я использовал списки на Haskell и, в меньшей степени, генераторы Python.

Не то чтобы я их не понимал сам по себе;Я просто не знаю, как использовать их так же небрежно, как и другие языки, и я также не уверен, какие методы в Ruby превратят их в массивы за моей спиной, без необходимости выгружая всю последовательность в память.

И да, я изучал справочное руководство по Ruby.На самом деле полчаса, внимательно.Или, возможно, очевидно, что нет.

Например, если бы я реализовал колоду карт, в Python это выглядело бы примерно так (не проверено):

# Python 3

from itertools import chain, count

face_ranks =
    dict(
        zip(
            ('jack', 'queen', 'king', 'ace'),
            count(11)))

sorted_deck =
    map(
        lambda suit:
            map(
                lambda rank:
                    {
                        'rank' : rank,
                        'suit' : suit
                    },
                chain(
                    range(2, 11),
                    face_ranks.keys())),
        ('clubs', 'diamonds', 'hearts', 'spades'))

Итак, как бы я это сделалэто в Ruby, полностью избегая массивов?Обратите внимание, что в приведенном выше коде, насколько мне известно, используются только кортежи и генераторы: ни в коем случае вся последовательность не выгружается в память, как если бы я использовал массив.Я могу ошибаться в приведенном выше коде, но вы получаете то, что я хочу.

Как мне объединить итераторы (например, Python's chain ())?Как мне сгенерировать итератор бесконечного диапазона (например, счетчик Python ())?Как я могу добавить массив в итератор (например, передать кортеж в цепочку Python ()), не преобразовывая все это в массив в процессе?

Я видел решения, но они включают в себя массивы или ненужныесложности, такие как волокна.

В Python я могу манипулировать и разбрасывать итераторы с той же простотой, что и массивы.Я почти могу относиться к ним как к спискам на Haskell, что мне больше всего нравится, и это действительно то, о чем я думаю, когда пишу код.Меня не устраивают массивы Ruby, поэтому я обращаюсь за помощью с его альтернативой (ами).

Мне удалось собрать в интернете кучу информации об этом, но я не смог найтилюбой, который охватывает основные манипуляции с такими структурами данных в Ruby?Любая помощь?

Ответы [ 3 ]

4 голосов
/ 08 августа 2011

В Ruby, похоже, не так много встроенных методов для того, чтобы делать разные вещи, которые вы хотели делать с перечислителями, но вы можете создавать свои собственные методы. Вот что я сделал здесь, используя Ruby 1.9:

iter.rb

def get_enums_from_args(args)
  args.collect { |e| e.is_a?(Enumerator) ? e.dup : e.to_enum }
end

def build(y, &block)
  while true
    y << (begin yield; rescue StopIteration; break; end)
  end
end

def zip(*args)
  enums = get_enums_from_args args
  Enumerator.new do |y|
    build y do
      enums.collect { |e| e.next }
    end
  end
end

def chain(*args)
  enums = get_enums_from_args args
  Enumerator.new do |y|
    enums.each do |e|
      build y do
        e.next
      end
    end
  end
end

def multiply(*args)
  enums = get_enums_from_args args
  duped_enums = enums.collect { |e| e.dup }
  Enumerator.new do |y|
    begin
      while true
        y << (begin; enums.collect { |e| e.peek }; rescue StopIteration; break; end )

        index = enums.length - 1
        while true
          begin
            enums[index].next
            enums[index].peek
            break
          rescue StopIteration
            # Some iterator ran out of items.

            # If it was the first iterator, we are done,
            raise if index == 0

            # If it was a different iterator, reset it
            # and then look at the iterator before it.
            enums[index] = duped_enums[index].dup
            index -= 1
          end
        end
      end
    rescue StopIteration
    end
  end
end

И я написал спецификацию, используя rspec для тестирования функций и демонстрации того, что они делают:

iter_spec.rb:

require_relative 'iter'

describe "zip" do
  it "zips together enumerators" do
    e1 = "Louis".chars
    e2 = "198".chars
    zip(e1,e2).to_a.should == [ ['L','1'], ['o','9'], ['u','8'] ]
  end

  it "works with arrays too" do
    zip([1,2], [:a, nil]).to_a.should == [ [1,:a], [2,nil] ]
  end
end

describe "chain" do
  it "chains enumerators" do
    e1 = "Jon".chars
    e2 = 0..99999999999
    e = chain(e1, e2)
    e.next.should == "J"
    e.next.should == "o"
    e.next.should == "n"
    e.next.should == 0
    e.next.should == 1
  end
end

describe "multiply" do
  it "multiplies enumerators" do
    e1 = "ABC".chars
    e2 = 1..3
    multiply(e1, e2).to_a.should == [["A", 1], ["A", 2], ["A", 3], ["B", 1], ["B", 2], ["B", 3], ["C", 1], ["C", 2], ["C", 3]]
  end

  it "is lazily evalutated" do
    e1 = 0..999999999
    e2 = 1..3
    e = multiply(e1, e2)
    e.next.should == [0, 1]
    e.next.should == [0, 2]
    e.next.should == [0, 3]
    e.next.should == [1, 1]
    e.next.should == [1, 2]
  end

  it "resulting enumerator can not be cloned effectively" do
    ranks = chain(2..10, [:jack, :queen, :king, :ace])
    suits = [:clubs, :diamonds, :hearts, :spades]
    cards = multiply(suits, ranks)
    c2 = cards.clone
    cards.next.should == [:clubs, 2]
    c2.next.should == [:clubs, 2]
    c2.next.should == [:clubs, 3]
    c2.next.should == [:clubs, 4]
    c2.next.should == [:clubs, 5]
    cards.next.should == [:clubs, 6]
  end

  it "resulting enumerator can not be duplicated after first item is evaluated" do
    ranks = chain(2..10, [:jack, :queen, :king, :ace])
    suits = [:clubs, :diamonds, :hearts, :spades]
    cards = multiply(ranks, suits)
    cards.peek
    lambda { cards.dup }.should raise_error TypeError
  end
end

Как показано в спецификации выше, эти методы используют ленивую оценку.

Кроме того, основным недостатком функций zip, chain и multiply, определенных здесь, является то, что результирующий перечислитель не может быть легко дублирован или клонирован, потому что мы не написали никакого кода для дублирования аргументов перечисления на что полагаются эти новые счетчики. Вам, вероятно, понадобится создать подкласс Enumerator или класс, включающий модуль Enumerable или что-то в этом роде, чтобы dup работал хорошо.

2 голосов
/ 08 августа 2011

Ближайшим эквивалентом в Ruby является Enumerator . Это позволяет вам создавать ленивые генераторы.

2 голосов
/ 08 августа 2011

Кажется, что вы избегаете массивов Ruby из-за проблем с производительностью, возможно, из-за опыта работы с массивами на других языках. Вам не нужно избегать массивов Ruby - они являются ближайшим к кортежу в Ruby.

foo = 1, 2, 3, 4
foo.class       #=> Array

Похоже, вы ищете Range вместо генератора:

range = 1..4
range.class     #=> Range
range.count     #=> 4

('a'..'z').each { |letter| letter.do_something }

Диапазон не преобразуется в массив, но он включает Enumerable, поэтому вы можете использовать все свои обычные перечислители. Что касается цикла / итерации - собственный цикл в Ruby осуществляется через Enumerable. for i in group на самом деле является синтаксическим сахаром для циклов перечислителя (например, .each). Перечисляемые методы обычно возвращают отправителя, поэтому вы можете связать их:

(1..10).map { |n| n * 2 }.each { |n| print "##{n}" }
# outputs #2#4#6#8#10#12#14#16#18#20
# returns an array:
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Я бы хотел дать вам более конкретные ответы о вашем Python »Ruby-эквивалентах, но я незнаком с Python.

Обновление

Вы можете объединить диапазоны во вложенный массив следующим образом:

(1..26).zip('a'..'z') #=> [[1, 'a'], [2, 'b'], ...]

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

allowed = 'a'..'z', 1..100
input = # whatever
allowed.each do |range|
  return false unless range.cover? input
end

Конечно, вы всегда можете использовать счетчики с диапазонами для «генерации» значений на лету.

...