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


Traté de iterar hacia atrás con el uso de un rango y each:

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

La iteración a través de 0..4 escribe los números. En el otro Rango r = 4..0 parece 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?

Author: the Tin Man, 2010-01-15

11 answers

Un rango es solo eso: algo definido por su inicio y final, no por su contenido. "Iterar" sobre un rango realmente 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. IMO, el hecho de que se permite para los rangos hacia adelante debe ser visto como un método de conveniencia solamente.

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

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

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

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

 83
Author: John Feminella,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-15 09:46:17

¿Qué tal (0..1).reverse_each que itera el rango hacia atrás?

 83
Author: Marko Taponen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-13 20:17:03

Iterar sobre un rango en Ruby con each llama al método succ en el primer objeto del rango.

$ 4.succ
=> 5

Y 5 está fuera del rango.

Puedes simular la iteración inversa con este hack:

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

Juan señaló que esto no funcionará si abarca 0. Esto sería:

>> (-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.

 17
Author: Jonas Elfström,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-07 02:07:17

De acuerdo con el libro "Programming Ruby", el objeto Range almacena los dos extremos del rango y utiliza el miembro .succ para generar los valores intermedios. Dependiendo del tipo de datos que esté utilizando en su rango, siempre puede crear una subclase de Integer y redefinir el miembro .succ para que actúe como un iterador inverso (probablemente también desee redefinir .next).

También puede lograr los resultados que está buscando sin usar un Rango. Tratar esto:

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

Esto pasará de 4 a 0 en pasos de -1. Sin embargo, no se si esto funcionará para cualquier cosa excepto argumentos Enteros.

 12
Author: bta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-15 23:45:40

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

 10
Author: Sergey Kishenin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-09 10:26:05

Si la lista no es tan grande. creo que [*0..4].reverse.each { |i| puts i } es la manera más simple.

 3
Author: marocchino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-15 10:59:45

Incluso puedes usar un bucle for:

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

Que imprime:

4
3
2
1
0
 3
Author: bakerstreet221b,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-09 23:21:00

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

 1
Author: Chuck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-01-16 00:53:38

Me añado otra posibilidad de cómo realizar iteración sobre el rango inverso. No lo uso, pero es una posibilidad. Es un poco arriesgado monkey patch ruby core objects.

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
 1
Author: fifigyuri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-25 19:20:28

Esto funcionó para mi caso de uso perezoso

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]
 0
Author: forforf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-22 13:36:41

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 los situaciones en las que este comportamiento es razonable?

'No se Puede hacer?'pero para responder a la pregunta que no se hizo antes de llegar a la pregunta que en realidad 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 construye una matriz completa, downto claramente va a ser más eficiente. El el hecho de que un diseñador de lenguaje incluso podría considerar implementar cosas como esa se relaciona con la respuesta a la pregunta real como se hizo.

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

La razón es porque Ruby es un lenguaje infinitamente sorprendente. Algunas sorpresas son agradables, pero hay mucho comportamiento que está francamente roto. Incluso si algunos de estos siguientes ejemplos son corregidos por nuevas versiones, hay muchos otros, y permanecen como acusaciones en la mentalidad del diseño original:

nil.to_s
   .to_s
   .inspect

Resulta en "" 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

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

Probablemente esperarías que 'grep' se comporte como su equivalente en la línea de comandos de Unix, pero lo hace === coincidiendo con not =~, a pesar de su nombre.

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

Varios métodos son inesperadamente alias el uno para el otro, por lo que tienes que aprender varios nombres para la misma cosa - p.ej. find y detect - incluso si te gustan la mayoría de los desarrolladores y solo usas uno u otro. Lo mismo ocurre con size, count, y length, excepto para las clases que definen cada una de manera diferente, o no definen una o dos en absoluto.

A menos que alguien haya implementado algo más, como el método core tap ha sido redefinido en varias bibliotecas de automatización para presionar algo en la pantalla. Buena suerte descubriendo lo que está pasando, especialmente si algún módulo requerido por algún otro módulo tiene monkeyed otro módulo para hacer algo indocumentado.

La variable de entorno object, ENV no admite 'merge', por lo que debe escribir

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

Como bono, incluso puedes redefinir tus constantes o las de otra persona si cambias de opinión sobre lo que deberían ser.

 0
Author: android.weasel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-09 11:08:15