Y a-t-il une raison pour laquelle nous ne pouvons pas itérer sur «Reverse Range» en rubis?


104

J'ai essayé d'itérer en arrière en utilisant une plage et each:

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

L'itération par 0..4écrit les nombres. D'autre plage r = 4..0semble être ok, r.first == 4, r.last == 0.

Il me semble étrange que la construction ci-dessus ne donne pas le résultat attendu. Quelle en est la raison? Quelles sont les situations où ce comportement est raisonnable?


Je ne suis pas seulement intéressé par la façon de réaliser cette itération, qui n'est évidemment pas prise en charge, mais plutôt par pourquoi elle renvoie la plage 4..0 elle-même. Quelle était l'intention des concepteurs de langage? Pourquoi, dans quelles situations est-ce bon? J'ai également vu un comportement similaire dans d'autres constructions ruby, et ce n'est toujours pas propre quand c'est utile.
fifigyuri

1
La plage elle-même est renvoyée par convention. Puisque l' .eachinstruction n'a rien modifié, il n'y a aucun "résultat" calculé à renvoyer. Dans ce cas, Ruby renvoie généralement l'objet d'origine en cas de succès et nilen cas d'erreur. Cela vous permet d'utiliser des expressions comme celle-ci comme conditions sur une ifinstruction.
bta

Réponses:


99

Une plage n'est que cela: quelque chose défini par son début et sa fin, pas par son contenu. «Itérer» sur une plage n'a pas vraiment de sens dans un cas général. Considérez, par exemple, comment vous «itéreriez» sur la plage produite par deux dates. Souhaitez-vous itérer par jour? par mois? par année? par semaine? Ce n'est pas bien défini. OMI, le fait qu'il soit autorisé pour les plages avant doit être considéré comme une méthode de commodité uniquement.

Si vous souhaitez effectuer une itération en arrière sur une plage comme celle-ci, vous pouvez toujours utiliser downto:

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

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

Voici quelques réflexions d'autres personnes sur les raisons pour lesquelles il est difficile à la fois d'autoriser l'itération et de gérer systématiquement les plages inversées.


10
Je pense qu'itérer sur une plage de 1 à 100 ou de 100 à 1 signifie intuitivement utiliser l'étape 1. Si quelqu'un veut une étape différente, change la valeur par défaut. De même, pour moi (au moins) itérer du 1er janvier au 16 août signifie passer par jours. Je pense qu'il y a souvent quelque chose sur lequel nous pouvons nous entendre, parce que nous l'entendons intuitivement de cette façon. Merci pour votre réponse, le lien que vous avez donné était également utile.
fifigyuri

3
Je pense toujours que définir des itérations "intuitives" pour de nombreuses plages est difficile à faire de manière cohérente, et je ne suis pas d'accord pour dire que l'itération sur des dates de cette manière implique intuitivement un pas égal à 1 jour - après tout, un jour lui-même est déjà une plage de heure (de minuit à minuit). Par exemple, qui peut dire que «du 1er janvier au 18 août» (exactement 20 semaines) n'implique pas une itération de semaines au lieu de jours? Pourquoi ne pas itérer par heure, minute ou seconde?
John Feminella

8
Le .eachlà est redondant, 5.downto(1) { |n| puts n }fonctionne très bien. Aussi, au lieu de tous ces trucs r.first r.last, faites-le (6..10).reverse_each.
mk12

@ Mk12: 100% d'accord, j'essayais juste d'être super-explicite pour les nouveaux Rubyistes. C'est peut-être trop déroutant, cependant.
John Feminella

En essayant d'ajouter des années à un formulaire, j'ai utilisé:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross


18

Itérer sur une plage en Ruby avec eachappelle la succméthode sur le premier objet de la plage.

$ 4.succ
=> 5

Et 5 est en dehors de la plage.

Vous pouvez simuler une itération inverse avec ce hack:

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

John a souligné que cela ne fonctionnera pas s'il s'étend sur 0. Cela:

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

Je ne peux pas dire que j'aime vraiment l'un d'entre eux parce qu'ils obscurcissent en quelque sorte l'intention.


2
Non, mais en multipliant par -1 au lieu d'utiliser .abs, vous pouvez.
Jonas Elfström

12

Selon le livre "Programming Ruby", l'objet Range stocke les deux extrémités de la plage et utilise le .succmembre pour générer les valeurs intermédiaires. Selon le type de type de données que vous utilisez dans votre plage, vous pouvez toujours créer une sous-classe Integeret redéfinir le .succmembre afin qu'il agisse comme un itérateur inversé (vous voudrez probablement également redéfinir .next).

Vous pouvez également obtenir les résultats que vous recherchez sans utiliser de plage. Essaye ça:

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

Cela passera de 4 à 0 par incréments de -1. Cependant, je ne sais pas si cela fonctionnera pour autre chose que les arguments Integer.



5

Vous pouvez même utiliser une forboucle:

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

qui imprime:

4
3
2
1
0

3

si la liste n'est pas si grande. je pense que [*0..4].reverse.each { |i| puts i } c'est le moyen le plus simple.


2
OMI, il est généralement bon de supposer qu'il est grand. Je pense que c'est la bonne croyance et la bonne habitude à suivre en général. Et comme le diable ne dort jamais, je ne me fais pas confiance dans le fait que je me souviens où j'ai parcouru un tableau. Mais vous avez raison, si nous avons une constante 0 et 4, une itération sur un tableau ne pose aucun problème.
fifigyuri

1

Comme bta l'a dit, la raison est que cela Range#eachenvoie succà son début, puis au résultat de cet succappel, et ainsi de suite jusqu'à ce que le résultat soit supérieur à la valeur finale. Vous ne pouvez pas passer de 4 à 0 en appelant succ, et en fait vous commencez déjà plus grand que la fin.


1

J'ajoute une autre possibilité de réaliser une itération sur une plage inversée. Je ne l'utilise pas, mais c'est une possibilité. Il est un peu risqué de 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

0

Cela a fonctionné pour mon cas d'utilisation paresseux

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

0

L'OP a écrit

Il me semble étrange que la construction ci-dessus ne donne pas le résultat attendu. Quelle en est la raison? Quelles sont les situations où ce comportement est raisonnable?

pas «Peut-il être fait? mais pour répondre à la question qui n'a pas été posée avant d'arriver à la question qui a été réellement posée:

$ 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

Puisque reverse_each est censé créer un tableau entier, downto sera clairement plus efficace. Le fait qu'un concepteur de langage puisse même envisager de mettre en œuvre des choses comme celles-là est un peu lié à la réponse à la question réelle posée.

Pour répondre à la question telle qu'elle est réellement posée ...

La raison en est que Ruby est un langage infiniment surprenant. Certaines surprises sont agréables, mais il y a beaucoup de comportements qui sont carrément cassés. Même si certains de ces exemples suivants sont corrigés par des versions plus récentes, il y en a beaucoup d'autres, et ils restent comme des accusations sur l'état d'esprit de la conception originale:

nil.to_s
   .to_s
   .inspect

donne "" mais

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

résulte en

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

Vous vous attendriez probablement à ce que << et push soient les mêmes pour l'ajout aux tableaux, mais

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

Vous vous attendriez probablement à ce que «grep» se comporte comme son équivalent en ligne de commande Unix, mais il correspond === non = ~, malgré son nom.

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

Diverses méthodes sont inopinément des alias l'une pour l'autre, vous devez donc apprendre plusieurs noms pour la même chose - par exemple findet detect- même si vous aimez la plupart des développeurs et n'utilisez jamais que l'un ou l'autre. Il en va de même pour size, countet length, à l' exception des classes qui définissent différemment chacun, ou ne définissent pas un ou deux du tout.

À moins que quelqu'un n'ait implémenté autre chose - comme la méthode principale tapa été redéfinie dans diverses bibliothèques d'automatisation pour appuyer sur quelque chose à l'écran. Bonne chance pour découvrir ce qui se passe, surtout si un module requis par un autre module a monkey encore un autre module pour faire quelque chose de non documenté.

L'objet de variable d'environnement, ENV ne prend pas en charge 'merge', vous devez donc écrire

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

En prime, vous pouvez même redéfinir vos constantes ou celles de quelqu'un d'autre si vous changez d'avis sur ce qu'elles devraient être.


Cela ne répond en aucune manière à la question, forme ou forme. Ce n'est rien d'autre qu'une diatribe sur des choses que l'auteur n'aime pas à propos de Ruby.
Jörg W Mittag

Mis à jour pour répondre à la question qui n'est pas posée, en plus de la réponse qui a été réellement posée. diatribe: verbe 1. Parlez ou criez longuement avec colère et passion. La réponse originale n'était ni fâchée ni passionnée: c'était une réponse réfléchie avec des exemples.
android.weasel

@ JörgWMittag La question initiale comprend également: Il me semble étrange que la construction ci-dessus ne produit pas le résultat attendu. Quelle en est la raison? Quelles sont les situations où ce comportement est raisonnable? il cherche donc des raisons, pas des solutions de code.
android.weasel

Encore une fois, je ne vois pas en quoi le comportement de grepest de quelque manière que ce soit, forme ou forme lié au fait que l'itération sur une plage vide est un non-op. Je ne vois pas non plus en quoi le fait que l'itération sur une plage vide soit un non-op est en aucune façon une forme ou une forme "infiniment surprenante" et "carrément cassée".
Jörg W Mittag

Parce qu'une plage 4..0 a l'intention évidente de [4, 3, 2, 1, 0] mais, étonnamment, ne déclenche même pas d'avertissement. Cela a surpris l'OP et cela m'a surpris, et a sans doute surpris beaucoup d'autres personnes. J'ai énuméré d'autres exemples de comportements surprenants. Je peux en citer plus, si vous le souhaitez. Une fois que quelque chose présente plus qu'un certain nombre de comportements surprenants, il commence à dériver sur le territoire du «brisé». Un peu comme la façon dont les constantes déclenchent un avertissement lorsqu'elles sont écrasées, surtout lorsque les méthodes ne le font pas.
android.weasel

0

Quant à moi, le moyen le plus simple est:

[*0..9].reverse

Une autre façon d'itérer pour l'énumération:

(1..3).reverse_each{|v| p v}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.