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

104

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

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

Итерация 0..4записывает числа. С другой диапазон , r = 4..0кажется, хорошо, r.first == 4, r.last == 0.

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

фифигюри
источник
Меня интересует не только то, как реализовать эту итерацию, которая явно не поддерживается, но и то, почему она возвращает сам диапазон 4..0. В чем заключались намерения разработчиков языка? Почему, в каких ситуациях это хорошо? Я видел подобное поведение и в других конструкциях Ruby, и они все еще не чисты, когда это полезно.
fifigyuri
1
Сам диапазон возвращается по соглашению. Поскольку .eachоператор ничего не изменил, вычисленного «результата» для возврата нет. В этом случае Ruby обычно возвращает исходный объект в случае успеха или nilошибки. Это позволяет использовать подобные выражения в качестве условий ifоператора.
bta

Ответы:

100

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

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

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

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

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

Джон Феминелла
источник
10
Я думаю, что повторение диапазона от 1 до 100 или от 100 до 1 интуитивно означает использование шага 1. Если кто-то хочет другой шаг, меняет значение по умолчанию. Точно так же для меня (по крайней мере) итерация с 1 января по 16 августа означает переход по дням. Я думаю, что часто есть что-то, в чем мы можем прийти к общему мнению, потому что интуитивно мы это подразумеваем именно так. Спасибо за ответ, и ссылка, которую вы дали, оказалась полезной.
fifigyuri
3
Я по-прежнему считаю, что определение "интуитивных" итераций для многих диапазонов является сложной задачей, и я не согласен с тем, что повторение дат таким образом интуитивно подразумевает шаг, равный 1 дню - в конце концов, сам день уже представляет собой диапазон время (с полуночи до полуночи). Например, кто сказал, что «с 1 января по 18 августа» (ровно 20 недель) не подразумевает итерацию недель вместо дней? Почему бы не перебирать по часам, минутам или секундам?
Джон Феминелла
8
.eachЕсть лишний, 5.downto(1) { |n| puts n }работает отлично. Кроме того, вместо всего этого r.first r.last просто сделайте (6..10).reverse_each.
mk12
@ Mk12: на 100% согласен, я просто пытался быть предельно ясным ради новых рубистов. Хотя, может быть, это слишком сбивает с толку.
Джон Феминелла
Пытаясь добавить годы к форме, я использовал:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Эрик Норкросс
92

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

Марко Тапонен
источник
1
Создает временный массив, по крайней мере, в 2.4.1 ruby-doc.org/core-2.4.1/Enumerable.html#method-i-reverse_each
kolen
1
Это работает для меня сортировку диапазона дат (Date.today.beginning_of_year..Date.today.yesterday).reverse_eachСпасибо
Даниель
18

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

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

Йонас Эльфстрём
источник
2
Нет, но можно умножить на -1 вместо использования .abs.
Йонас Эльфстрём
12

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

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

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

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

bta
источник
12

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

Сергей Кишенин
источник
5

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

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

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

4
3
2
1
0
Bakerstreet221b
источник
4

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

Marocchino
источник
2
ИМО, как правило, хорошо предполагать, что он большой. Я думаю, что это правильное убеждение и привычка в целом. И поскольку дьявол никогда не спит, я не верю себе, что помню, где я перебирал массив. Но вы правы, если у нас есть константы 0 и 4, итерация по массиву может не вызвать никаких проблем.
fifigyuri
1

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

Чак
источник
1

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

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

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

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]
forforf
источник
0

ОП написал

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

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

$ 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 строит весь массив, очевидно, что метод downo будет более эффективным. Тот факт, что разработчик языка может даже подумать о реализации таких вещей, как бы связан с ответом на фактический вопрос, который задается.

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

Причина в том, что 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 будут такими же для добавления в массивы, но

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, не поддерживает «слияние», поэтому вам нужно написать

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

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

android.weasel
источник
Это не отвечает на вопрос ни в каком виде, ни в какой форме. Это не что иное, как напыщенная речь о том, что автору не нравится в Ruby.
Jörg W Mittag
Обновлено, чтобы ответить на вопрос, который не задан, в дополнение к ответу, который был задан на самом деле. rant: глагол 1. долго говорить или кричать сердито, страстно. Первоначальный ответ не был злым или страстным: это был продуманный ответ с примерами.
android.weasel
@ JörgWMittag Исходный вопрос также включает: Мне кажется странным, что приведенная выше конструкция не дает ожидаемого результата. В чем причина этого? В каких ситуациях такое поведение разумно? так что он ищет причины, а не решения кода.
android.weasel
Опять же, я не вижу, как поведение grepкаким-либо образом, формой или формой связано с тем фактом, что итерация по пустому диапазону не работает. Я также не вижу, как тот факт, что итерация по пустому диапазону является запретной операцией, каким-либо образом придает форму или форму «бесконечно удивительному» и «совершенно нарушенному».
Jörg W Mittag
Потому что диапазон 4..0 имеет очевидное намерение [4, 3, 2, 1, 0], но, что удивительно, даже не вызывает предупреждения. Это удивило ОП, и это удивило меня, и, несомненно, удивило многих других. Я перечислил другие примеры удивительного поведения. Я могу привести еще, если хотите. Как только что-то демонстрирует более чем определенное количество неожиданного поведения, оно начинает смещаться на территорию «сломанного». Это немного похоже на то, как константы вызывают предупреждение при перезаписи, особенно когда методы этого не делают.
android.weasel
0

Как по мне, самый простой способ:

[*0..9].reverse

Другой способ перебора для перечисления:

(1..3).reverse_each{|v| p v}
Агент Иван
источник