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

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

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

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

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

Ответ 1

Диапазон таков: что-то определено его началом и концом, а не его содержимым. "Итерация" по диапазону в общем случае не имеет смысла. Рассмотрим, например, как вы будете "перебирать" диапазон, созданный двумя датами. Повторяли бы вы по дням? по месяцам? по годам? по неделям? Он не определен. IMO, тот факт, что он допускал дальнейшие диапазоны, следует рассматривать только как метод удобства.

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

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

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

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

Ответ 2

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

Ответ 3

Итерация по диапазону в 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

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

Ответ 4

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

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

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

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

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

4
3
2
1
0

Ответ 8

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

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

Ответ 9

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

Ответ 10

Это работало для моего ленивого варианта использования

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]

Ответ 11

OP писал

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

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

$ 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
 ^

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

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

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

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

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

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

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

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

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