2010-01-15 10 views
79

me trataron de una iteración hacia atrás con el uso de un rango y each:¿Hay alguna razón por la que no podamos iterar en "Rango inverso" en ruby?

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

iteración a través 0..4 escribe los números. Por otro lado, el rango r = 4..0 parece estar bien, r.first == 4, r.last == 0.

Parece extraño para mí que la construcción anterior no produzca el resultado esperado. ¿Cuál es el motivo para eso? ¿Cuáles son las situaciones cuando este comportamiento es razonable?

+0

No solo estoy interesado en cómo realizar esta iteración, que obviamente no es compatible, sino más bien por qué devuelve el rango 4..0. ¿Cuál fue la intención de los diseñadores de idiomas? ¿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í es devuelto por convención. Como la declaración '.each' no modificó nada, no hay un 'resultado' calculado para devolver. Cuando este es el caso, Ruby típicamente devuelve el objeto original en caso de éxito y 'nil' en error. Esto le permite usar expresiones como esta como condiciones en una instrucción 'if'. – bta

Respuesta

79

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

Si desea recorrer hacia atrás sobre un rango de esa manera, siempre se puede utilizar downto:

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

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

Éstos son some more thoughts de los demás acerca de por qué es difícil tanto permitir la iteración y trate a fondo inversas rangos.

+7

Creo que iterar en 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 desde el 1 de enero hasta el 16 de agosto significa pasar por días. Creo que a menudo hay algo en lo que comúnmente podemos estar de acuerdo, porque intuitivamente lo decimos de esa manera. Gracias por su respuesta, también el enlace que dio fue útil. – fifigyuri

+3

Todavía creo que la definición de iteraciones "intuitivas" para muchos rangos es un reto hacerlo constantemente, y no estoy de acuerdo con que repetir las fechas de esa manera implique intuitivamente un paso igual a 1 día; después de todo, un día en sí mismo es un rango de tiempo (desde la medianoche hasta la 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? –

+8

El '.each' no es redundante,' 5.downto (1) {| n | puts n} 'funciona bien. Además, en lugar de todas las primeras cosas r.last, simplemente haz '(6..10) .reverse_each'. – mk12

17

Iterar 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 intervalo.

Puede simular 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 haría:

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

¿No decir que me gusta cualquiera de ellos porque oscurecen la intención.

+3

¡Hack inteligente! Sin embargo, no funcionará para rangos que abarquen 0. –

+1

No, pero al multiplicar por -1 en lugar de usar .abs puedes. –

3

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

+2

IMO generalmente es bueno suponer que es grande. Creo que es la creencia correcta y el hábito de seguir en general. Y como el diablo nunca duerme, no confío en mí mismo que recuerdo dónde repetí una serie. Pero tiene razón, si tenemos 0 y 4 constantes, iterar sobre la matriz podría no causar ningún problema. – fifigyuri

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 para los objetos de núcleo de rubí mono parche.

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 
10

Según el libro "Programación Ruby", el objeto Range almacena los dos puntos extremos de la gama y utiliza el miembro de .succ para generar los valores intermedios. Dependiendo del tipo de 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 (también es probable que desee redefinir .next también)

También puede obtener los resultados que busca sin utilizar un Rango.Pruebe 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 nada, excepto los argumentos enteros.

1

Como dijo BTA, la razón es que Range#each envía succ a su comienzo, a continuación, con el resultado de que succ llamada, y así sucesivamente hasta que el resultado es mayor que el valor final. No puede obtener de 4 a 0 llamando al succ, y de hecho ya comienza más que al final.

2

incluso se puede utilizar un bucle for:

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

que imprime:

4 
3 
2 
1 
0 
78

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

+0

Construye una matriz temporal, al menos en 2.4.1 https://ruby-doc.org/core-2.4.1/Enumerable.html#method-i-reverse_each – kolen

+0

Esto funcionó para mí ordenando un rango de fechas '(Fecha .today.beginning_of_year..Date.today.ayer) .reverse_each' Gracias – daniel

10

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

0

Esto funcionó para mi caso de uso perezoso

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

La OP escribió

Parece ser extraño para mí que la construcción anterior no produce el resultado esperado. ¿Cuál es el motivo para eso? ¿Cuáles son las situaciones cuando este comportamiento es razonable?

no '¿Se puede hacer?' pero para responder a la pregunta que no se pidió antes de llegar a la pregunta que en realidad se le preguntó:

$ 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 

Desde reverse_each se demanda para construir toda una matriz, downto está claro que va a ser más eficiente. El hecho de que un diseñador de lenguaje incluso podría considerar la implementación de cosas así se relaciona con la respuesta a la pregunta real tal como se le preguntó.

Para responder a la pregunta en realidad se le preguntó ...

La razón se debe a que Ruby es un lenguaje infinitamente sorprendente. Algunas sorpresas son agradables, pero hay un montón de comportamientos que se rompen por completo. Incluso si algunos de estos ejemplos siguientes se corrigen por versiones más recientes, hay un montón de otros, y que permanecen como acusaciones en el modo de pensar del diseño original:

nil.to_s 
    .to_s 
    .inspect 

resultados 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 
^ 

es probable que esperar < < y empuje a ser el mismo para anexar a las matrices, pero

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

Es probable que esperar 'grep' a comportarse como su equivalente de línea de comandos de Unix, pero sí === juego no = ~, a pesar de su nombre.

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

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

A menos que alguien haya implementado otra cosa, como el método central tap se ha redefinido en varias bibliotecas de automatización para presionar algo en la pantalla. Buena suerte para descubrir qué está pasando, especialmente si algún módulo requerido por algún otro módulo ha monkeado otro módulo para hacer algo no documentado.

El objeto variable de entorno, ENV no apoyar 'fusión', por lo que tiene que escribir

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

Como beneficio adicional, incluso se puede redefinir sus o de alguien más constantes si cambia de opinión acerca de lo que debiera ser.

+0

Esto no responde a la pregunta de ninguna manera, forma o forma. No es más que una diatriba sobre cosas que el autor no le gusta de Ruby. –

+0

Se actualizó para responder a la pregunta que no se formuló, además de la respuesta que se hizo realmente. * rant: verbo 1. hablar o gritar largamente de una manera enojada, apasionada.* La respuesta original no fue enojado o apasionado: fue una respuesta considerada con ejemplos. –

+0

@ JörgWMittag La pregunta original también incluye: * Parece extraño para mí que la construcción anterior no produzca el resultado esperado. ¿Cuál es el motivo para eso? ¿Cuáles son las situaciones en las que este comportamiento es razonable? * Por lo que busca las razones, no las soluciones de código. –

Cuestiones relacionadas