Existe uma razão pela qual não podemos iterar no “intervalo reverso” no ruby?


104

Tentei iterar para trás usando um intervalo e each:

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

A iteração 0..4escreve os números. Por outro Faixa r = 4..0parece ser ok, r.first == 4, r.last == 0.

Parece-me estranho que a construção acima não produza o resultado esperado. Qual é a razão disso? Quais são as situações em que esse comportamento é razoável?


Não estou apenas interessado em como realizar essa iteração, que obviamente não é suportada, mas também por que ela retorna o próprio intervalo 4..0. Qual foi a intenção dos designers de linguagem? Por que, em quais situações isso é bom? Eu vi um comportamento semelhante em outras construções de rubi também, e ainda não está limpo quando é útil.
fifigyuri de

1
O próprio intervalo é retornado por convenção. Como a .eachinstrução não modificou nada, não há "resultado" calculado para retornar. Quando este for o caso, Ruby normalmente retorna o objeto original em caso de sucesso e nilerro. Isso permite que você use expressões como esta como condições em uma ifinstrução.
bta

Respostas:


99

Um intervalo é apenas isso: algo definido por seu início e fim, não por seu conteúdo. "Iterar" em um intervalo realmente não faz sentido em um caso geral. Considere, por exemplo, como você iria "iterar" no intervalo produzido por duas datas. Você iteraria por dia? por mês? por ano? por semana? Não está bem definido. IMO, o fato de que é permitido para intervalos futuros deve ser visto apenas como um método de conveniência.

Se quiser iterar para trás em um intervalo como esse, você sempre pode usar downto:

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

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

Aqui estão mais alguns pensamentos de outros sobre por que é difícil permitir a iteração e lidar consistentemente com intervalos reversos.


10
Acho que iterar em um intervalo de 1 a 100 ou de 100 a 1 significa intuitivamente usar a etapa 1. Se alguém quiser uma etapa diferente, altere o padrão. Da mesma forma, para mim (pelo menos) iterar de 1º de janeiro a 16 de agosto significa avançar por dias. Acho que geralmente há algo em que podemos concordar, porque intuitivamente queremos dizer isso. Obrigado pela sua resposta, também o link que você deu foi útil.
fifigyuri de

3
Ainda acho que definir iterações "intuitivas" para muitos intervalos é um desafio de fazer de forma consistente, e não concordo que iterar as datas dessa forma implique intuitivamente em uma etapa igual a 1 dia - afinal, um dia em si já é um intervalo de hora (da meia-noite à meia-noite). Por exemplo, quem pode dizer que "1 de janeiro a 18 de agosto" (exatamente 20 semanas) não implica uma iteração de semanas em vez de dias? Por que não iterar por hora, minuto ou segundo?
John Feminella

8
O .eachlá é redundante, 5.downto(1) { |n| puts n }funciona bem. Além disso, em vez de todas aquelas primeiras e últimas coisas, apenas faça (6..10).reverse_each.
mk12

@ Mk12: 100% concordo, eu só estava tentando ser super-explícito para o bem dos novos Rubistas. Talvez isso seja muito confuso, no entanto.
John Feminella,

Ao tentar adicionar anos a um formulário, usei:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross


18

Iterando em um intervalo em Ruby com eachchamadas o succmétodo no primeiro objeto no intervalo.

$ 4.succ
=> 5

E 5 está fora do intervalo.

Você pode simular a iteração reversa com este hack:

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

John apontou que isso não funcionará se abranger 0. Isso:

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

Não posso dizer que gosto mesmo de qualquer um deles porque meio que obscurecem a intenção.


2
Não, mas multiplicando por -1 em vez de usar .abs, você pode.
Jonas Elfström de

12

De acordo com o livro "Programming Ruby", o objeto Range armazena os dois pontos finais do intervalo e usa o .succmembro para gerar os valores intermediários. Dependendo do tipo de tipo de dados que você está usando em seu intervalo, você sempre pode criar uma subclasse de Integere redefinir o .succmembro para que ele atue como um iterador reverso (você provavelmente também desejaria redefinir .next).

Você também pode obter os resultados que procura sem usar um intervalo. Experimente isto:

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

Isso irá variar de 4 a 0 em etapas de -1. No entanto, não sei se isso funcionará para qualquer coisa, exceto argumentos inteiros.



5

Você pode até usar um forloop:

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

que imprime:

4
3
2
1
0

3

se a lista não for tão grande. eu acho que [*0..4].reverse.each { |i| puts i } é a maneira mais simples.


2
IMO geralmente é bom assumir que é grande. Acho que é a crença e o hábito corretos a seguir em geral. E como o diabo nunca dorme, não confio em mim mesmo, pois não me lembro de onde fiz uma iteração em um array. Mas você está certo, se tivermos 0 e 4 constantes, iterar sobre o array pode não causar nenhum problema.
fifigyuri

1

Como bta disse, o motivo é que Range#eachenvia succpara o seu início, depois para o resultado daquela succchamada, e assim por diante até que o resultado seja maior que o valor final. Você não pode passar de 4 a 0 chamando succe, na verdade, você já começa maior que o final.


1

Eu adiciono uma outra possibilidade de como realizar a iteração no intervalo reverso. Eu não uso, mas é uma possibilidade. É um pouco arriscado remendar objetos de núcleo de rubi.

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

Isso funcionou para meu caso de uso preguiçoso

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

0

O OP escreveu

Parece-me estranho que a construção acima não produza o resultado esperado. Qual é a razão disso? Quais são as situações em que esse comportamento é razoável?

não 'Isso pode ser feito?' mas para responder à pergunta que não foi feita antes de chegar à pergunta que realmente foi feita:

$ 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

Uma vez que é afirmado que reverse_each constrói um array inteiro, downto será claramente mais eficiente. O fato de que um designer de linguagem pode até mesmo considerar a implementação de coisas como essa meio que se vincula à resposta à pergunta real quando feita.

Para responder à pergunta como realmente feita ...

A razão é porque Ruby é uma linguagem infinitamente surpreendente. Algumas surpresas são agradáveis, mas há muitos comportamentos que estão completamente quebrados. Mesmo que alguns dos exemplos a seguir sejam corrigidos por versões mais recentes, há muitos outros e eles permanecem como acusações sobre a mentalidade do design original:

nil.to_s
   .to_s
   .inspect

resulta em "" mas

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

resulta em

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

Você provavelmente esperaria que << e push fossem iguais para anexar a matrizes, mas

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

Você provavelmente esperaria que 'grep' se comportasse como seu equivalente de linha de comando do Unix, mas faz === não correspondendo a = ~, apesar de seu nome.

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

Vários métodos são inesperadamente apelidos uns para os outros, então você precisa aprender vários nomes para a mesma coisa - por exemplo, finde detect- mesmo se você gosta da maioria dos desenvolvedores e usa apenas um ou outro. Praticamente o mesmo vale para size, counte length, exceto para classes que definem cada um de forma diferente, ou não definem um ou dois.

A menos que alguém tenha implementado algo diferente - como o método principal tapfoi redefinido em várias bibliotecas de automação para pressionar algo na tela. Boa sorte para descobrir o que está acontecendo, especialmente se algum módulo exigido por algum outro módulo manipulou outro módulo para fazer algo não documentado.

O objeto de variável de ambiente, ENV não suporta 'mesclar', então você tem que escrever

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

Como um bônus, você pode até mesmo redefinir suas constantes ou as de outra pessoa se mudar de ideia sobre como elas deveriam ser.


Isso não responde à pergunta de nenhuma maneira, forma ou forma. Não passa de um discurso retórico sobre coisas que o autor não gosta em Ruby.
Jörg W Mittag

Atualizado para responder à pergunta que não foi feita, além da resposta que foi realmente perguntada. rant: verbo 1. falar ou gritar longamente de uma forma irada e apaixonada. A resposta original não era zangada ou apaixonada: era uma resposta ponderada com exemplos.
android.weasel

@ JörgWMittag A pergunta original também inclui: Parece-me estranho que a construção acima não produza o resultado esperado. Qual é a razão disso? Quais são as situações em que esse comportamento é razoável? então ele está atrás de motivos, não soluções de código.
android.weasel

Mais uma vez, não consigo ver como o comportamento de grepestá de alguma forma, forma ou forma relacionada ao fato de que iterar em um intervalo vazio é um ambiente autônomo. Nem vejo como o fato de que a iteração em um intervalo vazio é um ambiente autônomo é de alguma forma ou forma "infinitamente surpreendente" e "totalmente quebrado".
Jörg W Mittag

Porque um intervalo 4..0 tem a intenção óbvia de [4, 3, 2, 1, 0], mas surpreendentemente nem mesmo emite um aviso. Isso surpreendeu a OP e me surpreendeu, e sem dúvida surpreendeu muitas outras pessoas. Listei outros exemplos de comportamento surpreendente. Posso citar mais, se quiser. Uma vez que algo exibe mais do que uma certa quantidade de comportamento surpreendente, ele começa a derivar para o território de 'quebrado'. Um pouco como as constantes emitem um aviso quando sobrescritas, especialmente quando os métodos não o fazem.
android.weasel

0

Para mim, a maneira mais simples é:

[*0..9].reverse

Outra maneira de iterar para enumeração:

(1..3).reverse_each{|v| p v}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.