Czy jest jakiś powód, dla którego nie możemy iterować „odwróconego zakresu” w rubinie?

104

Próbowałem wykonać iterację wstecz za pomocą Range i each:

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

Iteracja poprzez 0..4zapisuje liczby. Z drugiej strony zakres r = 4..0wydaje się być ok, r.first == 4, r.last == 0.

Wydaje mi się dziwne, że powyższa konstrukcja nie daje oczekiwanego rezultatu. Jaki jest tego powód? W jakich sytuacjach takie zachowanie jest uzasadnione?

fifigyuri
źródło
Nie tylko interesuje mnie, jak zrealizować tę iterację, która oczywiście nie jest obsługiwana, ale raczej dlaczego zwraca ona sam zakres 4..0. Jaki był zamiar projektantów języka? Dlaczego, w jakich sytuacjach jest to dobre? Widziałem podobne zachowanie w innych konstrukcjach ruby ​​i nadal nie jest czysty, gdy jest przydatny.
fifigyuri
1
Sam zakres jest zwracany zgodnie z konwencją. Ponieważ .eachinstrukcja niczego nie modyfikowała, nie ma obliczonego „wyniku” do zwrócenia. W takim przypadku Ruby zazwyczaj zwraca oryginalny obiekt w przypadku sukcesu i nilbłędu. Umożliwia to używanie takich wyrażeń jako warunków w ifinstrukcji.
bta

Odpowiedzi:

99

Zakres jest po prostu tym: czymś określonym przez początek i koniec, a nie przez zawartość. „Iterowanie” po zakresie nie ma sensu w ogólnym przypadku. Zastanów się na przykład, jak „iterować” zakres utworzony przez dwie daty. Czy chciałbyś iterować za dnia? według miesiąca? przez rok? za tydzień? Nie jest dobrze zdefiniowana. IMO, fakt, że jest to dozwolone dla zakresów forward, należy traktować tylko jako wygodną metodę.

Jeśli chcesz iterować wstecz w takim zakresie, zawsze możesz użyć downto:

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

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

Oto kilka przemyśleń innych osób na temat tego, dlaczego trudno jest zarówno pozwolić na iterację, jak i konsekwentnie radzić sobie z odwrotnymi zakresami.

John Feminella
źródło
10
Myślę, że iterowanie w zakresie od 1 do 100 lub od 100 do 1 intuicyjnie oznacza użycie kroku 1. Jeśli ktoś chce inny krok, zmienia domyślny. Podobnie dla mnie (przynajmniej) iteracja od 1 stycznia do 16 sierpnia oznacza krok po kroku. Myślę, że często jest coś, z czym możemy się powszechnie zgodzić, ponieważ intuicyjnie tak to rozumiemy. Dziękuję za odpowiedź, również podany przez Ciebie link był przydatny.
fifigyuri
3
Nadal uważam, że definiowanie „intuicyjnych” iteracji dla wielu zakresów jest trudne do wykonania konsekwentnie i nie zgadzam się, że iterowanie po datach w ten sposób intuicyjnie implikuje krok równy 1 dniu - w końcu sam dzień jest już zakresem czas (od północy do północy). Na przykład, kto powie, że „od 1 stycznia do 18 sierpnia” (dokładnie 20 tygodni) nie oznacza iteracji tygodni zamiast dni? Dlaczego nie iterować według godziny, minuty lub sekundy?
John Feminella,
8
.eachJest zbędny, 5.downto(1) { |n| puts n }działa bez zarzutu. Poza tym, zamiast tego wszystkiego r. Pierwszy r. Ostatni, po prostu zrób (6..10).reverse_each.
mk12
@ Mk12: W 100% zgadzam się, po prostu starałem się być bardzo wyraźny ze względu na nowszych Rubyistów. Może to jednak zbyt zagmatwane.
John Feminella
Próbując dodać lata do formularza, użyłem:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross
18

Iterowanie po zakresie w Rubim za pomocą eachwywołuje succmetodę na pierwszym obiekcie w zakresie.

$ 4.succ
=> 5

A 5 jest poza zakresem.

Możesz zasymulować odwrotną iterację za pomocą tego hacka:

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

John zwrócił uwagę, że to nie zadziała, jeśli będzie obejmowało 0. To:

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

Nie mogę powiedzieć, że naprawdę lubię którekolwiek z nich, ponieważ w pewnym sensie przesłaniają intencję.

Jonas Elfström
źródło
2
Nie, ale mnożąc przez -1 zamiast używać .abs możesz.
Jonas Elfström
12

Zgodnie z książką „Programming Ruby”, obiekt Range przechowuje dwa punkty końcowe zakresu i używa elementu .succczłonkowskiego do wygenerowania wartości pośrednich. W zależności od tego, jakiego typu danych używasz w swoim zakresie, zawsze możesz utworzyć podklasę Integeri ponownie zdefiniować element .succczłonkowski, tak aby działał jak iterator odwrotny (prawdopodobnie chciałbyś również ponownie zdefiniować .next).

Możesz także osiągnąć oczekiwane wyniki bez korzystania z Range. Spróbuj tego:

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

Spowoduje to przejście od 4 do 0 w krokach co -1. Jednak nie wiem, czy to zadziała w przypadku czegokolwiek poza argumentami typu Integer.

bta
źródło
11

Innym sposobem jest (1..10).to_a.reverse

Sergey Kishenin
źródło
5

Możesz nawet użyć forpętli:

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

który drukuje:

4
3
2
1
0
bakerstreet221b
źródło
3

jeśli lista nie jest tak duża. myślę, że [*0..4].reverse.each { |i| puts i } to najprostszy sposób.

marocchino
źródło
2
IMO ogólnie dobrze jest założyć, że jest duży. Myślę, że ogólnie rzecz biorąc, jest to właściwa wiara i nawyk. A ponieważ diabeł nigdy nie śpi, nie ufam sobie, że pamiętam, gdzie iterowałem po tablicy. Ale masz rację, jeśli mamy stałą 0 i 4, iteracja po tablicy może nie powodować żadnego problemu.
fifigyuri
1

Jak powiedział bta, powodem jest to, że Range#eachwysyła succna początek, następnie do wyniku tego succwywołania i tak dalej, aż wynik będzie większy niż wartość końcowa. Nie możesz dostać się od 4 do 0, dzwoniąc succ, a tak naprawdę już zaczynasz lepiej niż koniec.

Głaskanie pod brodę
źródło
1

Dodam jeszcze jedną możliwość realizacji iteracji w odwrotnym zakresie. Nie korzystam, ale jest taka możliwość. Łatanie obiektów z rdzeniem rubinowym jest nieco ryzykowne.

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
fifigyuri
źródło
0

To zadziałało dla mojego leniwego przypadku użycia

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]
forforf
źródło
0

OP napisał

Wydaje mi się dziwne, że powyższa konstrukcja nie daje oczekiwanego rezultatu. Jaki jest tego powód? W jakich sytuacjach takie zachowanie jest uzasadnione?

nie „Czy można to zrobić?” ale aby odpowiedzieć na pytanie, które nie zostało zadane przed przejściem do pytania, które zostało zadane:

$ 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

Ponieważ twierdzi się, że reverse_each buduje całą tablicę, downto z pewnością będzie bardziej wydajne. Fakt, że projektant języka mógłby nawet rozważyć zastosowanie takich rzeczy, wiąże się z odpowiedzią na zadane pytanie.

Aby odpowiedzieć na pytanie tak, jak faktycznie zadano ...

Powodem jest to, że Ruby to nieskończenie zaskakujący język. Niektóre niespodzianki są przyjemne, ale jest wiele zachowań, które są wręcz zepsute. Nawet jeśli niektóre z poniższych przykładów zostaną poprawione w nowszych wersjach, istnieje wiele innych i pozostają one jako akty oskarżenia w myśleniu pierwotnego projektu:

nil.to_s
   .to_s
   .inspect

daje w wyniku „” ale

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

prowadzi do

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

Prawdopodobnie spodziewałbyś się, że << i push będzie takie samo przy dołączaniu do tablic, ale

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

Prawdopodobnie spodziewałbyś się, że „grep” będzie zachowywał się jak jego odpowiednik w linii poleceń systemu Unix, ale mimo swojej nazwy === dopasowanie not = ~.

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

Różne metody są dla siebie nieoczekiwanie aliasami, więc musisz nauczyć się wielu nazw dla tej samej rzeczy - np. findI detect- nawet jeśli lubisz większość programistów i zawsze używasz tylko jednej lub drugiej. Dużo to samo dotyczy size, countoraz length, z wyjątkiem klas, które określają każdy inaczej, albo nie definiują jeden lub dwa w ogóle.

Chyba że ktoś zaimplementował coś innego - tak jak podstawowa metoda tapzostała na nowo zdefiniowana w różnych bibliotekach automatyzacji, aby nacisnąć coś na ekranie. Powodzenia w ustalaniu, co się dzieje, zwłaszcza jeśli jakiś moduł wymagany przez inny moduł zmusił kolejny moduł do zrobienia czegoś nieudokumentowanego.

Obiekt zmiennej środowiskowej ENV nie obsługuje funkcji „scalania”, więc musisz pisać

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

Jako bonus możesz nawet przedefiniować swoje lub czyjeś stałe stałe, jeśli zmienisz zdanie na temat tego, jakie powinny być.

android.weasel
źródło
To nie odpowiada na pytanie w żaden sposób, kształt ani forma. To nic innego jak tyradka o rzeczach, których autor nie lubi w Rubim.
Jörg W Mittag
Zaktualizowano, aby odpowiedzieć na pytanie, które nie zostało zadane, oprócz odpowiedzi, która została faktycznie zadana. rant: czasownik 1. długo mów lub krzycz w gniewny, namiętny sposób. Oryginalna odpowiedź nie była zła ani namiętna: była to przemyślana odpowiedź z przykładami.
android.weasel
@ JörgWMittag Pierwotne pytanie zawiera również: Wydaje mi się dziwne, że powyższa konstrukcja nie daje oczekiwanych rezultatów. Jaki jest tego powód? W jakich sytuacjach takie zachowanie jest uzasadnione? więc szuka powodów, a nie rozwiązań kodu.
android.weasel
Ponownie, nie widzę, w jaki sposób zachowanie grepjest w jakikolwiek sposób, kształt lub forma związane z faktem, że iteracja po pustym zakresie nie działa. Nie widzę też, jak fakt, że iteracja po pustym zakresie jest niczym nie działa, jest w jakikolwiek sposób „nieskończenie zaskakująca” i „wręcz zepsuta”.
Jörg W Mittag,
Ponieważ zakres 4..0 ma oczywisty zamiar [4, 3, 2, 1, 0], ale, co zaskakujące, nawet nie powoduje ostrzeżenia. Zaskoczyło to OP, zaskoczyło mnie i niewątpliwie zaskoczyło wiele innych osób. Podałem inne przykłady zaskakujących zachowań. Jeśli chcesz, mogę zacytować więcej. Kiedy coś wykazuje więcej niż pewną ilość zaskakującego zachowania, zaczyna dryfować na terytorium „zepsutego”. Trochę tak, jak stałe powodują ostrzeżenie, gdy są nadpisywane, zwłaszcza gdy metody tego nie robią.
android.weasel
0

Jak dla mnie najprostszy sposób to:

[*0..9].reverse

Inny sposób na iterację w celu wyliczenia:

(1..3).reverse_each{|v| p v}
AgentIvan
źródło