C'è una ragione per cui non possiamo iterare su "Reverse Range" in Ruby?


104

Ho provato a scorrere all'indietro utilizzando un intervallo e each:

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

L'iterazione attraverso 0..4scrive i numeri. D'altra Gamma r = 4..0sembra essere ok, r.first == 4, r.last == 0.

Mi sembra strano che il costrutto di cui sopra non produca il risultato atteso. Qual è la ragione per questo? Quali sono le situazioni in cui questo comportamento è ragionevole?


Non mi interessa solo come realizzare questa iterazione, che ovviamente non è supportata, ma piuttosto perché restituisce l'intervallo 4..0 stesso. Qual era l'intenzione dei progettisti del linguaggio? Perché, in quali situazioni va bene? Ho visto un comportamento simile anche in altri costrutti di rubino, e non è ancora pulito quando è utile.
fifigyuri

1
L'intervallo stesso viene restituito per convenzione. Poiché l' .eachistruzione non ha modificato nulla, non vi è alcun "risultato" calcolato da restituire. Quando questo è il caso, Ruby restituisce tipicamente l'oggetto originale in caso di successo e nilin caso di errore. Ciò consente di utilizzare espressioni come questa come condizioni su ifun'istruzione.
bta

Risposte:


99

Un intervallo è proprio questo: qualcosa definito dal suo inizio e dalla sua fine, non dal suo contenuto. "Iterare" su un intervallo non ha davvero senso in un caso generale. Considera, ad esempio, come "iterare" sull'intervallo prodotto da due date. Ripeteresti di giorno? per mese? per anno? di settimana? Non è ben definito. IMO, il fatto che sia consentito per gli intervalli in avanti dovrebbe essere visto solo come un metodo di convenienza.

Se vuoi scorrere all'indietro su un intervallo del genere, puoi sempre usare downto:

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

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

Ecco alcuni pensieri di altri sul perché è difficile sia consentire l'iterazione sia gestire in modo coerente gli intervalli inversi.


10
Penso che iterare su un intervallo da 1 a 100 o da 100 a 1 significhi intuitivamente utilizzare il passaggio 1. Se qualcuno desidera un passaggio diverso, cambia l'impostazione predefinita. Allo stesso modo, per me (almeno) iterare dal 1 gennaio al 16 agosto significa fare un passo di giorni. Penso che spesso ci sia qualcosa su cui possiamo essere comunemente d'accordo, perché intuitivamente lo intendiamo in questo modo. Grazie per la tua risposta, anche il link che hai fornito è stato utile.
fifigyuri

3
Continuo a pensare che definire iterazioni "intuitive" per molti intervalli sia difficile da fare in modo coerente e non sono d'accordo sul fatto che l'iterazione su date in questo modo implichi intuitivamente un passaggio pari a 1 giorno - dopotutto, un giorno stesso è già un intervallo di ora (da mezzanotte a mezzanotte). Ad esempio, chi può dire che "dal 1 gennaio al 18 agosto" (esattamente 20 settimane) non implica un'iterazione di settimane anziché di giorni? Perché non iterare per ora, minuto o secondo?
John Feminella

8
Il .eachlà è ridondante, 5.downto(1) { |n| puts n }funziona bene. Inoltre, invece di tutte quelle cose r.first r.last, fallo e basta (6..10).reverse_each.
mk12

@ Mk12: 100% d'accordo, stavo solo cercando di essere super esplicito per il bene dei nuovi Rubyists. Forse è troppo confuso, però.
John Feminella

Quando ho provato ad aggiungere anni a un modulo, ho usato:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross

92

Che ne dici di (0..1).reverse_eachquale itera la gamma all'indietro?



1
Questo ha funzionato per me ordinare una serie di date (Date.today.beginning_of_year..Date.today.yesterday).reverse_eachGrazie
Daniel

18

Iterando su un intervallo in Ruby con eachchiama il succmetodo sul primo oggetto nell'intervallo.

$ 4.succ
=> 5

E 5 è al di fuori dell'intervallo.

Puoi simulare l'iterazione inversa con questo hack:

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

John ha sottolineato che questo non funzionerà se si estende su 0. Ciò:

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

Non posso dire che mi piacciono davvero nessuno di loro perché in qualche modo oscurano l'intento.


2
No, ma moltiplicando per -1 invece di usare .abs puoi farlo.
Jonas Elfström

12

Secondo il libro "Programming Ruby", l'oggetto Range memorizza i due endpoint dell'intervallo e utilizza il .succmembro per generare i valori intermedi. A seconda del tipo di dati che stai utilizzando nel tuo intervallo, puoi sempre creare una sottoclasse Integere ridefinire il .succmembro in modo che agisca come un iteratore inverso (probabilmente vorrai anche ridefinirlo .next).

Puoi anche ottenere i risultati che stai cercando senza utilizzare una gamma. Prova questo:

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

Questo passerà da 4 a 0 con incrementi di -1. Tuttavia, non so se questo funzionerà per qualcosa tranne gli argomenti Integer.



5

Puoi anche usare un forciclo:

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

che stampa:

4
3
2
1
0

3

se l'elenco non è così grande. Penso che [*0..4].reverse.each { |i| puts i } sia il modo più semplice.


2
IMO è generalmente bene presumere che sia grande. Penso che sia la convinzione e l'abitudine giuste da seguire in generale. E poiché il diavolo non dorme mai, non mi fido di me stesso di ricordare dove ho ripetuto su un array. Ma hai ragione, se abbiamo 0 e 4 costanti, l'iterazione su array potrebbe non causare alcun problema.
fifigyuri

1

Come detto bta, il motivo è che Range#eachinvia succall'inizio, poi al risultato di quella succchiamata e così via fino a quando il risultato è maggiore del valore finale. Non puoi andare da 4 a 0 chiamando succ, e infatti inizi già più grande della fine.


1

Aggiungo un'altra possibilità su come realizzare l'iterazione sulla gamma inversa. Non lo uso, ma è una possibilità. È un po 'rischioso rattoppare oggetti con nucleo in rubino.

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

Questo ha funzionato per il mio caso d'uso pigro

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

0

L'OP ha scritto

Mi sembra strano che il costrutto di cui sopra non produca il risultato atteso. Qual è la ragione per questo? Quali sono le situazioni in cui questo comportamento è ragionevole?

non "Si può fare?" ma per rispondere alla domanda che non è stata posta prima di arrivare alla domanda che è stata effettivamente posta:

$ 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

Poiché si afferma che reverse_each crei un intero array, downto sarà chiaramente più efficiente. Il fatto che un progettista di linguaggi possa anche prendere in considerazione l'implementazione di cose del genere si lega alla risposta alla domanda effettiva come posta.

Per rispondere alla domanda come effettivamente posta ...

Il motivo è perché Ruby è un linguaggio infinitamente sorprendente. Alcune sorprese sono piacevoli, ma c'è molto comportamento che è decisamente rotto. Anche se alcuni di questi esempi seguenti vengono corretti dalle versioni più recenti, ce ne sono molti altri e rimangono come accuse nella mentalità del design originale:

nil.to_s
   .to_s
   .inspect

risultati in "" ma

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

risultati in

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

Probabilmente ti aspetteresti che << e push siano gli stessi per l'aggiunta agli array, ma

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

Probabilmente ti aspetteresti che 'grep' si comporti come il suo equivalente a riga di comando Unix, ma === non corrisponde a = ~, nonostante il suo nome.

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

Vari metodi sono inaspettatamente alias l'uno per l'altro, quindi devi imparare più nomi per la stessa cosa, ad esempio finde detect, anche se ti piacciono la maggior parte degli sviluppatori e usi solo l'uno o l'altro. Molto lo stesso vale per size, counte length, tranne per le classi che definiscono ogni diverso, o non definiscono uno o due affatto.

A meno che qualcuno non abbia implementato qualcos'altro, come il metodo principale tapè stato ridefinito in varie librerie di automazione per premere qualcosa sullo schermo. Buona fortuna per scoprire cosa sta succedendo, specialmente se qualche modulo richiesto da un altro modulo ha scimmiottato un altro modulo per fare qualcosa di non documentato.

L'oggetto della variabile d'ambiente ENV non supporta 'merge', quindi devi scrivere

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

Come bonus, puoi persino ridefinire le costanti tue o di qualcun altro se cambi idea su cosa dovrebbero essere.


Questo non risponde alla domanda in alcun modo, forma o forma. Non è altro che uno sfogo su cose che all'autore non piacciono di Ruby.
Jörg W Mittag

Aggiornato per rispondere alla domanda che non viene posta, oltre alla risposta effettivamente posta. rant: verbo 1. parlare o gridare a lungo in modo arrabbiato e appassionato. La risposta originale non era arrabbiata o appassionata: era una risposta ponderata con esempi.
android.weasel

@ JörgWMittag La domanda originale include anche: Mi sembra strano che il costrutto sopra non produca il risultato atteso. Qual è la ragione per questo? Quali sono le situazioni in cui questo comportamento è ragionevole? quindi cerca ragioni, non soluzioni di codice.
android.weasel

Ancora una volta, non riesco a vedere come il comportamento di grepè in qualche modo, forma o forma correlata al fatto che iterare su un intervallo vuoto è un no-op. Né vedo come il fatto che iterare su un intervallo vuoto sia un no-op sia in qualche modo "infinitamente sorprendente" e "decisamente rotto".
Jörg W Mittag

Perché un intervallo 4..0 ha l'ovvia intenzione di [4, 3, 2, 1, 0] ma sorprendentemente non solleva nemmeno un avvertimento. Ha sorpreso l'OP e mi ha sorpreso, e senza dubbio ha sorpreso molte altre persone. Ho elencato altri esempi di comportamento sorprendente. Posso citare di più, se vuoi. Una volta che qualcosa mostra più di una certa quantità di comportamento sorprendente, inizia a scivolare nel territorio del "rotto". Un po 'come il modo in cui le costanti generano un avviso quando vengono sovrascritte, specialmente quando i metodi non lo fanno.
android.weasel

0

Per quanto mi riguarda il modo più semplice è:

[*0..9].reverse

Un altro modo per iterare per l'enumerazione:

(1..3).reverse_each{|v| p v}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.