Gibt es einen Grund, warum wir in Ruby nicht auf "Reverse Range" iterieren können?


104

Ich habe versucht, mit einem Bereich rückwärts zu iterieren und each:

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

Iteration durch 0..4schreibt die Zahlen. Auf der anderen Strecke r = 4..0scheint in Ordnung zu sein r.first == 4, r.last == 0.

Es scheint mir seltsam, dass das obige Konstrukt nicht das erwartete Ergebnis liefert. Was ist der Grund dafür? In welchen Situationen ist dieses Verhalten angemessen?


Ich bin nicht nur daran interessiert, wie diese Iteration realisiert werden kann, die offensichtlich nicht unterstützt wird, sondern warum sie den Bereich 4..0 selbst zurückgibt. Was war die Absicht der Sprachdesigner? Warum, in welchen Situationen ist es gut? Ich habe ein ähnliches Verhalten auch bei anderen Rubinkonstrukten gesehen, und es ist immer noch nicht sauber, wenn es nützlich ist.
Fifigyuri

1
Der Bereich selbst wird gemäß Konvention zurückgegeben. Da die .eachAnweisung nichts geändert hat, kann kein berechnetes "Ergebnis" zurückgegeben werden. In diesem Fall gibt Ruby normalerweise das ursprüngliche Objekt bei Erfolg und nilFehler zurück. Auf diese Weise können Sie solche Ausdrücke als Bedingungen für eine ifAnweisung verwenden.
bta

Antworten:


99

Ein Bereich ist genau das: etwas, das durch seinen Anfang und sein Ende definiert wird, nicht durch seinen Inhalt. Das "Iterieren" über einen Bereich ist im Allgemeinen nicht wirklich sinnvoll. Stellen Sie sich zum Beispiel vor, wie Sie über den Bereich "iterieren" würden, der durch zwei Daten erzeugt wird. Würden Sie tagsüber iterieren? nach Monaten? Pro Jahr? pro Woche? Es ist nicht gut definiert. IMO, die Tatsache, dass es für Vorwärtsbereiche erlaubt ist, sollte nur als bequeme Methode angesehen werden.

Wenn Sie über einen solchen Bereich rückwärts iterieren möchten, können Sie immer Folgendes verwenden downto:

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

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

Hier sind einige weitere Gedanken von anderen darüber, warum es schwierig ist, sowohl Iteration zuzulassen als auch konsequent mit umgekehrten Bereichen umzugehen.


10
Ich denke, das Iterieren über einen Bereich von 1 bis 100 oder von 100 bis 1 bedeutet intuitiv die Verwendung von Schritt 1. Wenn jemand einen anderen Schritt wünscht, ändert sich die Standardeinstellung. In ähnlicher Weise bedeutet für mich (zumindest) das Iterieren vom 1. Januar bis 16. August, schrittweise um Tage zu gehen. Ich denke, es gibt oft etwas, worüber wir uns einig sein können, weil wir es intuitiv so meinen. Vielen Dank für Ihre Antwort, auch der Link, den Sie gegeben haben, war nützlich.
Fifigyuri

3
Ich denke immer noch, dass es schwierig ist, "intuitive" Iterationen für viele Bereiche konsequent zu definieren, und ich stimme nicht zu, dass das intuitive Iterieren über Daten einen Schritt von 1 Tag impliziert - schließlich ist ein Tag selbst bereits ein Bereich von Zeit (von Mitternacht bis Mitternacht). Wer sagt zum Beispiel, dass "1. Januar bis 18. August" (genau 20 Wochen) keine Wiederholung von Wochen statt Tagen bedeutet? Warum nicht nach Stunde, Minute oder Sekunde iterieren?
John Feminella

8
Das .eachgibt es überflüssig, 5.downto(1) { |n| puts n }funktioniert gut. Anstelle all dieser ersten Dinge tun Sie es einfach (6..10).reverse_each.
mk12

@ Mk12: 100% stimmen zu, ich habe nur versucht, für neuere Rubyisten super-explizit zu sein. Vielleicht ist das aber zu verwirrend.
John Feminella

Als ich versuchte, einem Formular Jahre hinzuzufügen, verwendete ich:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross


18

Durch Iterieren über einen Bereich in Ruby mit eachruft die succMethode für das erste Objekt im Bereich auf.

$ 4.succ
=> 5

Und 5 liegt außerhalb des Bereichs.

Mit diesem Hack können Sie die umgekehrte Iteration simulieren:

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

John wies darauf hin, dass dies nicht funktionieren wird, wenn es sich über 0 erstreckt. Dies würde:

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

Ich kann nicht sagen, dass ich einen von ihnen wirklich mag, weil sie die Absicht irgendwie verdunkeln.


2
Nein, aber durch Multiplizieren mit -1 anstelle von .abs können Sie.
Jonas Elfström

12

Gemäß dem Buch "Programming Ruby" speichert das Range-Objekt die beiden Endpunkte des Bereichs und .succgeneriert mit dem Member die Zwischenwerte. Abhängig davon, welche Art von Datentyp Sie in Ihrem Bereich verwenden, können Sie jederzeit eine Unterklasse von erstellen und das Element Integerneu definieren, .succsodass es sich wie ein umgekehrter Iterator verhält (wahrscheinlich möchten Sie es auch neu definieren .next).

Sie können die gewünschten Ergebnisse auch erzielen, ohne einen Bereich zu verwenden. Versuche dies:

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

Dies wird in Schritten von -1 von 4 auf 0 erhöht. Ich weiß jedoch nicht, ob dies für etwas anderes als Integer-Argumente funktioniert.



5

Sie können sogar eine forSchleife verwenden:

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

welche druckt:

4
3
2
1
0

3

wenn die Liste nicht so groß ist. Ich denke, [*0..4].reverse.each { |i| puts i } ist der einfachste Weg.


2
IMO ist es im Allgemeinen gut anzunehmen, dass es groß ist. Ich denke, es ist der richtige Glaube und die richtige Gewohnheit, allgemein zu folgen. Und da der Teufel niemals schläft, vertraue ich mir nicht, dass ich mich daran erinnere, wo ich über ein Array iteriert habe. Aber Sie haben Recht, wenn wir die Konstanten 0 und 4 haben, kann das Iterieren über das Array kein Problem verursachen.
Fifigyuri

1

Wie BTA gesagt, ist der Grund dafür , dass Range#eachsendet succan seinem Anfang, dann auf das Ergebnis dieses succAnrufs, und so weiter , bis das Ergebnis größer als der Endwert. Sie können nicht von 4 auf 0 kommen, indem Sie anrufen succ, und tatsächlich beginnen Sie bereits größer als das Ende.


1

Ich füge einander eine Möglichkeit hinzu, wie man eine Iteration über den umgekehrten Bereich realisiert. Ich benutze es nicht, aber es ist eine Möglichkeit. Es ist ein bisschen riskant, Rubinkernobjekte mit Affenflecken zu versehen.

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

Dies funktionierte für meinen faulen Anwendungsfall

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

0

Das OP schrieb

Es scheint mir seltsam, dass das obige Konstrukt nicht das erwartete Ergebnis liefert. Was ist der Grund dafür? In welchen Situationen ist dieses Verhalten angemessen?

nicht 'Kann es gemacht werden?' aber um die Frage zu beantworten, die nicht gestellt wurde, bevor man zu der Frage kam, die tatsächlich gestellt wurde:

$ 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

Da reverse_each angeblich ein ganzes Array erstellt, wird Downto eindeutig effizienter. Die Tatsache, dass ein Sprachdesigner sogar in Betracht ziehen könnte, solche Dinge zu implementieren, hängt mit der Antwort auf die eigentliche Frage zusammen.

Um die tatsächlich gestellte Frage zu beantworten ...

Der Grund ist, dass Ruby eine unendlich überraschende Sprache ist. Einige Überraschungen sind angenehm, aber es gibt viele Verhaltensweisen, die regelrecht gebrochen sind. Auch wenn einige dieser folgenden Beispiele durch neuere Versionen korrigiert wurden, gibt es viele andere, und sie bleiben als Anklage gegen die Denkweise des ursprünglichen Designs:

nil.to_s
   .to_s
   .inspect

führt zu "" aber

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

führt zu

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

Sie würden wahrscheinlich erwarten, dass << und push beim Anhängen an Arrays gleich sind, aber

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

Sie würden wahrscheinlich erwarten, dass sich 'grep' wie sein Unix-Befehlszeilenäquivalent verhält, aber es stimmt trotz seines Namens nicht = ~ überein.

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

Verschiedene Methoden sind unerwartet Aliase für einander, daher müssen Sie mehrere Namen für dasselbe lernen - z. B. findund detect- auch wenn Sie die meisten Entwickler mögen und immer nur den einen oder anderen verwenden. Ähnliches gilt für size,, countund length, mit Ausnahme von Klassen, die jeweils unterschiedlich oder gar nicht ein oder zwei definieren.

Es sei denn, jemand hat etwas anderes implementiert - wie die tapKernmethode in verschiedenen Automatisierungsbibliotheken neu definiert wurde, um etwas auf den Bildschirm zu drücken. Viel Glück beim Herausfinden, was los ist, insbesondere wenn ein Modul, das von einem anderen Modul benötigt wird, ein weiteres Modul aktiviert hat, um etwas Undokumentiertes zu tun.

Das Umgebungsvariablenobjekt ENV unterstützt 'Zusammenführen' nicht, daher müssen Sie schreiben

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

Als Bonus können Sie sogar die Konstanten Ihrer oder einer anderen Person neu definieren, wenn Sie Ihre Meinung darüber ändern, was sie sein sollen.


Dies beantwortet die Frage in keiner Weise, in keiner Form oder in keiner Form. Es ist nichts anderes als ein Schimpfen über Dinge, die der Autor an Ruby nicht mag.
Jörg W Mittag

Aktualisiert, um die Frage zu beantworten, die nicht gestellt wird, zusätzlich zu der Antwort, die tatsächlich gestellt wurde. rant: verb 1. spreche oder schreie ausführlich auf wütende, leidenschaftliche Weise. Die ursprüngliche Antwort war nicht böse oder leidenschaftlich: Es war eine überlegte Antwort mit Beispielen.
android.weasel

@ JörgWMittag Die ursprüngliche Frage beinhaltet auch: Es scheint mir seltsam, dass das obige Konstrukt nicht das erwartete Ergebnis liefert. Was ist der Grund dafür? In welchen Situationen ist dieses Verhalten angemessen? Also ist er nach Gründen, nicht nach Codelösungen.
android.weasel

Auch hier kann ich nicht erkennen, wie das Verhalten von grepin irgendeiner Weise, Form oder Gestalt mit der Tatsache zusammenhängt, dass das Iterieren über einen leeren Bereich ein No-Op ist. Ich sehe auch nicht, wie die Tatsache, dass das Iterieren über einen leeren Bereich ein No-Op ist, in irgendeiner Form "endlos überraschend" und "geradezu gebrochen" ist.
Jörg W Mittag

Denn ein Bereich 4..0 hat die offensichtliche Absicht [4, 3, 2, 1, 0], löst aber überraschenderweise nicht einmal eine Warnung aus. Es hat das OP und mich überrascht und zweifellos viele andere Menschen überrascht. Ich habe andere Beispiele für überraschendes Verhalten aufgelistet. Ich kann mehr zitieren, wenn Sie möchten. Sobald etwas mehr als ein gewisses Maß an überraschendem Verhalten zeigt, beginnt es, in das Gebiet der "gebrochenen" zu driften. Ein bisschen wie Konstanten eine Warnung auslösen, wenn sie überschrieben werden, besonders wenn Methoden dies nicht tun.
android.weasel

0

Für mich ist der einfachste Weg:

[*0..9].reverse

Eine andere Möglichkeit, die Aufzählung zu wiederholen:

(1..3).reverse_each{|v| p v}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.