¿Hay alguna razón por la que no podamos iterar en "rango inverso" en ruby?

104

Intenté iterar hacia atrás con el uso de un Range y each:

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

La iteración 0..4escribe los números. Por otro rango r = 4..0parece estar bien, r.first == 4, r.last == 0.

Me parece extraño que la construcción anterior no produzca el resultado esperado. ¿Cuál es la razón de eso? ¿Cuáles son las situaciones en las que este comportamiento es razonable?

Fifigyuri
fuente
No solo estoy interesado en cómo realizar esta iteración, que obviamente no es compatible, sino por qué devuelve el rango 4..0 en sí. ¿Cuál fue la intención de los diseñadores de lenguajes? ¿Por qué, en qué situaciones es bueno? También vi un comportamiento similar en otras construcciones ruby, y todavía no está limpio cuando es útil.
Fifigyuri
1
El rango en sí se devuelve por convención. Dado que la .eachdeclaración no modificó nada, no hay ningún "resultado" calculado para devolver. Cuando este es el caso, Ruby normalmente devuelve el objeto original en caso de éxito y nilerror. Esto le permite usar expresiones como esta como condiciones en una ifdeclaración.
bta

Respuestas:

99

Un rango es solo eso: algo definido por su inicio y final, no por su contenido. "Iterar" en un rango no tiene sentido en un caso general. Considere, por ejemplo, cómo "iteraría" sobre el rango producido por dos fechas. ¿Repetirías de día? ¿Por mes? ¿por año? ¿por semana? No está bien definido. En mi opinión, el hecho de que esté permitido para rangos de avance debe verse solo como un método de conveniencia.

Si desea iterar hacia atrás en un rango como ese, siempre puede usar downto:

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

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

Aquí hay algunos pensamientos más de otros sobre por qué es difícil permitir la iteración y tratar consistentemente con rangos inversos.

John Feminella
fuente
10
Creo que iterar sobre un rango de 1 a 100 o de 100 a 1 significa intuitivamente usar el paso 1. Si alguien quiere un paso diferente, cambia el predeterminado. Del mismo modo, para mí (al menos) iterar del 1 de enero al 16 de agosto significa ir paso a paso. Creo que a menudo hay algo en lo que podemos estar de acuerdo, porque intuitivamente lo decimos en serio. Gracias por tu respuesta, también el enlace que proporcionaste fue útil.
Fifigyuri
3
Sigo pensando que definir iteraciones "intuitivas" para muchos rangos es un desafío para hacerlo de manera consistente, y no estoy de acuerdo en que iterar sobre fechas de esa manera implique intuitivamente un paso igual a 1 día; después de todo, un día en sí ya es un rango de tiempo (de medianoche a medianoche). Por ejemplo, ¿quién puede decir que "del 1 de enero al 18 de agosto" (exactamente 20 semanas) no implica una iteración de semanas en lugar de días? ¿Por qué no iterar por hora, minuto o segundo?
John Feminella
8
El .eachno es redundante, 5.downto(1) { |n| puts n }funciona bien. Además, en lugar de todo lo primero, lo último, hazlo (6..10).reverse_each.
mk12
@ Mk12: 100% de acuerdo, solo estaba tratando de ser súper explícito por el bien de los nuevos Rubyists. Sin embargo, quizás eso sea demasiado confuso.
John Feminella
Al intentar agregar años a un formulario, utilicé:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross
92

¿Qué tal (0..1).reverse_eachque itera el rango al revés?

Marko Taponen
fuente
1
Crea
kolen
1
Esto funcionó para mí ordenando un rango de fechas (Date.today.beginning_of_year..Date.today.yesterday).reverse_eachGracias
daniel
18

Iterando sobre un rango en Ruby con eachllamadas al succmétodo en el primer objeto en el rango.

$ 4.succ
=> 5

Y 5 está fuera del rango.

Puede simular la iteración inversa con este truco:

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

John señaló que esto no funcionará si se extiende por 0. Esto:

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

No puedo decir que realmente me guste ninguno de ellos porque oscurecen la intención.

Jonas Elfström
fuente
2
No, pero multiplicando por -1 en lugar de usar .abs puedes hacerlo.
Jonas Elfström
12

Según el libro "Programming Ruby", el objeto Range almacena los dos puntos finales del rango y usa el .succmiembro para generar los valores intermedios. Dependiendo del tipo de tipo de datos que esté usando en su rango, siempre puede crear una subclase Integery volver a definir el .succmiembro para que actúe como un iterador inverso (probablemente también desee volver a definirlo .next).

También puede lograr los resultados que busca sin utilizar un rango. Prueba esto:

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

Esto pasará de 4 a 0 en pasos de -1. Sin embargo, no sé si esto funcionará para algo excepto para los argumentos Integer.

bta
fuente
11

Otra forma es (1..10).to_a.reverse

Sergey Kishenin
fuente
5

Incluso puedes usar un forbucle:

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

que imprime:

4
3
2
1
0
bakerstreet221b
fuente
3

si la lista no es tan grande. Creo que [*0..4].reverse.each { |i| puts i } es la forma más sencilla.

marocchino
fuente
2
En mi opinión, en general es bueno asumir que es grande. Creo que es la creencia y el hábito correctos a seguir en general. Y como el diablo nunca duerme, no confío en mí mismo al recordar dónde repetí una matriz. Pero tiene razón, si tenemos 0 y 4 constantes, iterar sobre la matriz podría no causar ningún problema.
Fifigyuri
1

Como dijo bta, la razón es que Range#eachenvía succa su inicio, luego al resultado de esa succllamada, y así sucesivamente hasta que el resultado sea mayor que el valor final. No puedes pasar de 4 a 0 llamando succ, y de hecho ya comienzas más que el final.

Arrojar
fuente
1

Agrego otra posibilidad de cómo realizar la iteración sobre el rango inverso. No lo uso, pero es una posibilidad. Es un poco arriesgado parchear objetos con núcleo de rubí.

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
fuente
0

Esto funcionó para mi caso de uso perezoso

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

El OP escribió

Me parece extraño que la construcción anterior no produzca el resultado esperado. ¿Cuál es la razón de eso? ¿Cuáles son las situaciones en las que este comportamiento es razonable?

no '¿Se puede hacer?' pero para responder la pregunta que no se hizo antes de llegar a la pregunta que realmente se hizo:

$ 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

Dado que se afirma que reverse_each crea una matriz completa, es evidente que downto será más eficiente. El hecho de que un diseñador de idiomas pueda incluso considerar implementar cosas como esa se relaciona un poco con la respuesta a la pregunta real tal como se le preguntó.

Para responder a la pregunta como se hizo realmente ...

La razón es que Ruby es un lenguaje infinitamente sorprendente. Algunas sorpresas son agradables, pero hay muchos comportamientos que están rotos. Incluso si algunos de estos ejemplos siguientes se corrigen con versiones más recientes, hay muchos otros y permanecen como acusaciones en la mentalidad del diseño original:

nil.to_s
   .to_s
   .inspect

da como resultado "" pero

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

resultados en

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

Probablemente esperaría que << y push sea lo mismo para agregar a matrices, pero

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

Probablemente esperaría que 'grep' se comporte como su equivalente en la línea de comandos de Unix, pero no coincide === con = ~, a pesar de su nombre.

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

Varios métodos tienen alias inesperadamente entre sí, por lo que debe aprender varios nombres para lo mismo, por ejemplo, findy detect, incluso si le gusta la mayoría de los desarrolladores y solo usa uno u otro. Lo mismo ocurre con size, county length, excepto para las clases que definen cada una de manera diferente, o que no definen una o dos en absoluto.

A menos que alguien haya implementado algo más, como que el método principal tapse haya redefinido en varias bibliotecas de automatización para presionar algo en la pantalla. Buena suerte averiguando lo que está pasando, especialmente si algún módulo requerido por otro módulo ha engañado a otro módulo para hacer algo indocumentado.

El objeto de la variable de entorno, ENV no admite 'fusionar', por lo que debe escribir

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

Como beneficio adicional, incluso puede redefinir sus constantes o las de otra persona si cambia de opinión sobre lo que deberían ser.

android.weasel
fuente
Esto no responde a la pregunta de ninguna manera, forma o forma. No es más que una perorata sobre cosas que al autor no le gustan de Ruby.
Jörg W Mittag
Actualizado para responder la pregunta que no se hace, además de la respuesta que realmente se hizo. despotricar: verbo 1. hablar o gritar largamente de una manera enojada y apasionada. La respuesta original no fue enojada ni apasionada: fue una respuesta considerada con ejemplos.
android.weasel
@ JörgWMittag La pregunta original también incluye: Me parece extraño que la construcción anterior no produzca el resultado esperado. ¿Cuál es la razón de eso? ¿Cuáles son las situaciones en las que este comportamiento es razonable? por lo que busca razones, no soluciones de código.
android.weasel
Una vez más, no veo cómo el comportamiento de grepestá relacionado de alguna manera, forma o forma con el hecho de que iterar sobre un rango vacío es una no operación. Tampoco veo cómo el hecho de que iterar sobre un rango vacío es una operación no operativa es de alguna manera "infinitamente sorprendente" y "completamente roto".
Jörg W Mittag
Porque un rango 4..0 tiene la intención obvia de [4, 3, 2, 1, 0] pero sorprendentemente ni siquiera genera una advertencia. Sorprendió al OP y me sorprendió, y sin duda ha sorprendido a muchas otras personas. Enumeré otros ejemplos de comportamiento sorprendente. Puedo citar más, si lo desea. Una vez que algo exhibe más de una cierta cantidad de comportamiento sorprendente, comienza a derivar hacia el territorio de lo 'roto'. Un poco como cómo las constantes generan una advertencia cuando se sobrescriben, especialmente cuando los métodos no lo hacen.
android.weasel
0

En cuanto a mí, la forma más sencilla es:

[*0..9].reverse

Otra forma de iterar para la enumeración:

(1..3).reverse_each{|v| p v}
AgenteIvan
fuente