Есть ли причина, по которой мы не можем перебрать «обратный диапазон» в ruby? - PullRequest
95 голосов
/ 15 января 2010

Я пытался выполнить итерацию в обратном направлении, используя Range и each:

(4..0).each do |i|
  puts i
end
==> 4..0

Итерация по 0..4 записывает числа. На другом диапазоне r = 4..0 вроде бы нормально, r.first == 4, r.last == 0.

Мне кажется странным, что приведенная выше конструкция не дает ожидаемого результата. В чем причина этого? В каких ситуациях такое поведение разумно?

Ответы [ 11 ]

89 голосов
/ 14 января 2014

Как насчет (0..1).reverse_each, который повторяет диапазон в обратном направлении?

88 голосов
/ 15 января 2010

Диапазон - это просто что-то, определяемое его началом и концом, а не содержимым. «Итерация» по диапазону не имеет смысла в общем случае. Рассмотрим, например, как бы вы «перебрали» диапазон, созданный двумя датами. Будете ли вы повторять по дням? по месяцам? по году? неделей? Это не четко определено. ИМО, тот факт, что это разрешено для прямых диапазонов, следует рассматривать только как удобный метод.

Если вы хотите выполнить итерацию в обратном направлении в таком диапазоне, вы всегда можете использовать downto:

$ r = 10..6
=> 10..6

$ (r.first).downto(r.last).each { |i| puts i }
10
9
8
7
6

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

18 голосов
/ 15 января 2010

Перебор диапазона в Ruby с помощью each вызывает метод succ для первого объекта в диапазоне.

$ 4.succ
=> 5

А 5 находится вне диапазона.

Вы можете смоделировать обратную итерацию с помощью этого хака:

(-4..0).each { |n| puts n.abs }

Джон указал, что это не сработает, если охватит 0. Это будет:

>> (-2..2).each { |n| puts -n }
2
1
0
-1
-2
=> -2..2

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

12 голосов
/ 16 января 2010

Согласно книге "Программирование Ruby", объект Range хранит две конечные точки диапазона и использует элемент .succ для генерации промежуточных значений. В зависимости от того, какой тип данных вы используете в своем диапазоне, вы всегда можете создать подкласс Integer и переопределить член .succ, чтобы он действовал как обратный итератор (вы, вероятно, также захотите повторно определить также .next).

Вы также можете достичь желаемых результатов, не используя Range. Попробуйте это:

4.step(0, -1) do |i|
    puts i
end

Это будет шаг от 4 до 0 с шагом -1. Однако я не знаю, сработает ли это для чего-либо, кроме целочисленных аргументов.

10 голосов
/ 09 июня 2014

Другой способ - (1..10).to_a.reverse

4 голосов
/ 10 ноября 2013

Вы даже можете использовать цикл for:

for n in 4.downto(0) do
  print n
end

который печатает:

4
3
2
1
0
3 голосов
/ 15 января 2010

если список не такой большой. Я думаю [*0..4].reverse.each { |i| puts i } это самый простой способ.

1 голос
/ 16 января 2010

Как говорит bta, причина в том, что Range#each отправляет succ в его начало, затем в результат этого вызова succ и так далее, пока результат не станет больше конечного значения. Вы не можете получить от 4 до 0, позвонив succ, и на самом деле вы уже начинаете больше конца.

1 голос
/ 15 января 2010

Я добавляю еще одну возможность, как реализовать итерацию по обратному диапазону. Я им не пользуюсь, но это возможно. Это немного рискованно, чтобы обезьяна исправить патч объекты рубинового ядра.

class Range

  def each(&block)
    direction = (first<=last ? 1 : -1)
    i = first
    not_reached_the_end = if first<=last
                            lambda {|i| i<=last}
                          else
                            lambda {|i| i>=last}
                          end
    while not_reached_the_end.call(i)
      yield i
      i += direction
    end
  end
end
0 голосов
/ 26 апреля 2017

ОП написал

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

не "Это может быть сделано?" но чтобы ответить на вопрос, который не задавался, прежде чем перейти к вопросу, который был фактически задан:

$ irb
2.1.5 :001 > (0..4)
 => 0..4
2.1.5 :002 > (0..4).each { |i| puts i }
0
1
2
3
4
 => 0..4
2.1.5 :003 > (4..0).each { |i| puts i }
 => 4..0
2.1.5 :007 > (0..4).reverse_each { |i| puts i }
4
3
2
1
0
 => 0..4
2.1.5 :009 > 4.downto(0).each { |i| puts i }
4
3
2
1
0
 => 4

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

Чтобы ответить на вопрос в том виде, в котором он был задан ...

Причина в том, что Ruby - это бесконечно удивительный язык. Некоторые сюрпризы приятны, но есть много поведения, которое прямо нарушено. Даже если некоторые из этих следующих примеров будут исправлены более новыми выпусками, есть много других, и они остаются в качестве обвинительного заключения на образ мышления оригинального дизайна:

nil.to_s
   .to_s
   .inspect

приводит к "", но

nil.to_s
#  .to_s   # Don't want this one for now
   .inspect

Результаты в

 syntax error, unexpected '.', expecting end-of-input
 .inspect
 ^

Вы, вероятно, ожидаете, что << и push будут одинаковыми для добавления в массивы, но </p>

a = []
a << *[:A, :B]    # is illegal but
a.push *[:A, :B]  # isn't.

Вы, вероятно, ожидаете, что 'grep' будет вести себя как эквивалент командной строки Unix, но он === не соответствует = ~, несмотря на его имя.

$ echo foo | grep .
foo
$ ruby -le 'p ["foo"].grep(".")'
[]

Различные методы неожиданно являются псевдонимами друг для друга, поэтому вам нужно выучить несколько имен для одной и той же вещи - например, find и detect - даже если вам нравится большинство разработчиков и когда-либо используете только один или другой. То же самое касается size, count и length, за исключением классов, которые определяют каждый из них по-разному или не определяют один или два вообще.

Если кто-то не реализовал что-то еще - например, основной метод, tap был переопределен в различных библиотеках автоматизации для нажатия чего-либо на экране. Удачи в выяснении того, что происходит, особенно если какой-то модуль, требуемый другим модулем, подключил еще один модуль для выполнения каких-либо документов.

Объект переменной среды, ENV не поддерживает слияние, поэтому вы должны написать

 ENV.to_h.merge('a': '1')

В качестве бонуса вы можете даже переопределить свои или чужие константы, если передумаете, какими они должны быть.

...