Чи є причина, що ми не можемо повторити «зворотний діапазон» в рубіні?


104

Я спробував повторити назад за допомогою діапазону та each:

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

Ітерація через 0..4записування цифр. З іншого діапазон , r = 4..0здається, добре, r.first == 4, r.last == 0.

Мені здається дивним, що вищезгадана конструкція не дає очікуваного результату. У чому причина цього? Які ситуації, коли така поведінка розумна?


Мене цікавить не тільки те, як реалізувати цю ітерацію, яка, очевидно, не підтримується, а навпаки, чому вона сама повертає діапазон 4..0. Який був задум мовних дизайнерів? Чому, в яких ситуаціях це добре? Я бачив подібну поведінку і в інших рубінових конструкціях, і вона все ще не є чистою, коли це корисно.
п’ятігюрі

1
Сам діапазон повертається умовно. Оскільки .eachоператор нічого не змінив, обчисленого "результату" повернення не існує. У такому випадку, Ruby, як правило, повертає початковий об'єкт на успіх та nilна помилку. Це дозволяє використовувати такі вирази як умови для ifзаяви.
бта

Відповіді:


99

Діапазон - це саме те, що визначається його початком і кінцем, а не змістом. "Ітерація" за діапазоном насправді не має сенсу в загальному випадку. Розглянемо, наприклад, як би ви "повторили" діапазон, створений двома датами. Ви хочете повторити день? по місяці? за роком? за тиждень? Це не досить чітко визначено. IMO, той факт, що він дозволений для прямих діапазонів, слід розглядати лише як метод зручності.

Якщо ви хочете повторити назад за такий діапазон, ви завжди можете використовувати 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 серпня означає крокувати по днях. Я думаю, що часто ми можемо домовитись про те, що ми інтуїтивно маємо на увазі саме так. Дякуємо за вашу відповідь, також посилання, яке ви дали, було корисним.
п’ятігюрі

3
Я все ще думаю, що визначати "інтуїтивні" ітерації для багатьох діапазонів складно робити послідовно, і я не погоджуюся, що повторення дат таким чином інтуїтивно передбачає крок, рівний 1 день - адже сам день вже є діапазоном час (з півночі до півночі). Наприклад, хто скаже, що "1 січня по 18 серпня" (рівно 20 тижнів) не означає повторення тижнів, а не днів? Чому б не повторити годину, хвилину чи секунду?
Джон Фемінелла

8
.eachЄ зайвий, 5.downto(1) { |n| puts n }працює відмінно. Крім того, замість усього цього, що потрібно зробити, просто зробіть (6..10).reverse_each.
mk12

@ Mk12: 100% згоден, я просто намагався бути надто очевидним заради нових рубістів. Можливо, це занадто заплутано.
Джон Фемінелла

Намагаючись додати роки до форми, я використав:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Ерік Норкросс


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

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

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

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

Це стане кроком від 4 до 0 кроками -1. Однак я не знаю, чи це спрацює для чогось, крім аргументів Integer.



5

Можна навіть використовувати forцикл:

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

який друкує:

4
3
2
1
0

3

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


2
ІМО, як правило, добре вважати, що він великий. Я думаю, це правильне переконання і звичка дотримуватися загалом. І як диявол ніколи не спить, я не вірю собі, що пам’ятаю, куди я перебрався через масив. Але ви маєте рацію, якщо у нас постійні 0 і 4, ітерація над масивом може не спричинити жодних проблем.
п’ятігюрі

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]

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 створює цілий масив, downto очевидно буде більш ефективним. Той факт, що мовний дизайнер навіть міг розглянути можливість реалізувати такі речі, як такі, що є відповіддю на відповідь на власне запитання.

Щоб відповісти на запитання, як насправді задали ...

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

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, але він, незважаючи на свою назву, === відповідає не = ~.

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

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

Якщо хтось не реалізував щось інше - як основний метод tap був перероблений у різних бібліотеках автоматизації, щоб щось натиснути на екрані. Успіхів дізнатися, що відбувається, особливо якщо якийсь модуль, який вимагає якийсь інший модуль, маніпулює ще одним модулем, щоб зробити щось без документації.

Об'єкт змінної середовища, ENV не підтримує "злиття", тому вам доведеться писати

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

Як бонус ви навіть можете переосмислити свої чи чужі константи, якщо передумаєте, якими вони повинні бути.


Це жодним чином, не відповідає, не відповідає на питання. Це не що інше, як шахрайство щодо того, що автор не любить про Рубі.
Йорг W Міттаг

Оновлено, щоб відповісти на запитання, яке не задається, на додаток до відповіді, яка насправді була задана. rant: дієслово 1. говорити чи кричати в довжину сердито, безстрашно. Оригінальна відповідь не гнівалась і не жаліла: це була врахована відповідь із прикладами.
android.weasel

@ JörgWMittag Оригінальне запитання також включає: Мені здається дивним, що вищезгадана конструкція не дає очікуваного результату. У чому причина цього? Які ситуації, коли така поведінка розумна? тож він має причини, а не кодові рішення.
android.weasel

Знову ж таки, я не бачу, як поведінка grepбудь-якого способу, форми чи форми пов'язана з тим, що повторення над порожнім діапазоном є неоперативним. Я також не бачу, як той факт, що повторення над порожнім діапазоном є жодним, не має жодної форми або форми "нескінченно дивно" та "прямо".
Йорг W Міттаг

Оскільки діапазон 4..0 має очевидний намір [4, 3, 2, 1, 0], але дивно навіть не викликає попередження. Це здивувало ОП, і це здивувало мене, і, безперечно, здивувало багатьох інших людей. Я перерахував інші приклади дивовижної поведінки. Я можу навести більше, якщо вам подобається. Після того, як щось проявляє більше певної кількості дивовижної поведінки, воно починає плисти на територію "зламаного". Трохи на зразок того, як константи піднімають попередження при перезаписі, особливо коли методи не роблять.
android.weasel

0

Щодо мене найпростіший спосіб:

[*0..9].reverse

Ще один спосіб перерахування для перерахунку:

(1..3).reverse_each{|v| p v}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.