Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Brandon Weaver
Brandon Weaver

Posted on • Edited on

     

Understanding Ruby - For vs Each

Introduction

For those coming from other languages withfor loops the concept ofeach, anonymous functions, blocks, and all of those new terms feels very foreign.

Why is it that Ruby doesn't usefor loops? Well we're going to cover that one today.

Difficulty

Foundational

Some knowledge required of functions in Ruby. This post focuses on foundational and fundamental knowledge for Ruby programmers.

Prerequisite Reading:

None

Suggested to readUnderstanding Ruby - Blocks, Procs, and Lambdas after this article if you haven't already.

For vs Each – High Level Overview

Ruby does have afor loop

Let's start with an acknowledgement: Rubydoes have afor loop:

foritemin[1,2,3]putsitem+1end# 2# 3# 4
Enter fullscreen modeExit fullscreen mode

...but you're not going to see it in common use. You're going to seeeach far more frequently.

Introducingeach

each in Ruby is the de facto way of iterating through a collection:

[1,2,3].eachdo|item|putsitem+1end# 2# 3# 4
Enter fullscreen modeExit fullscreen mode

There are a few things here which may not be familiar, which are covered in more detail inthat article mentioned above, but for now the important parts:

do ... end starts what we call a block function, or anonymous function in other languages, and|item| represents the arguments to that function. For each iteration of the loop each item will be fed into that function as an argument.

Ruby uses blocksheavily, and you'll find them commonly both in reading and writing code in the language. Their usage goes far beyondeach, and we'll mention that in a bit, but first let's cover a few concerns aboutfor in Ruby.

Concerns with For

There are more than a few legitimate issues withfor loops in Ruby, and we'll quickly cover a few of them.

They're Implemented with Each

Yep. Thefor loop in Ruby is usingeach behind the scenes, so even if you're not using it you're still using it. That also means that it's slower:

require'benchmark/ips'# => truecollection=(1..100).to_a# => [1, 2, 3, 4, 5, 6, 7, 8, 9, ...Benchmark.ipsdo|bench|bench.report("for loop")dosum=0foritemincollectionsum+=itemendsumendbench.report("each loop")dosum=0collection.eachdo|item|sum+=itemendsumendend# Warming up --------------------------------------#             for loop    22.017k i/100ms#            each loop    23.543k i/100ms# Calculating -------------------------------------#             for loop    218.466k (± 2.6%) i/s - 1.101M in 5.042495s#            each loop    231.274k (± 2.1%) i/s - 1.177M in 5.092110s
Enter fullscreen modeExit fullscreen mode

Granted this is not asignificant difference, but it is something to keep in mind.

Shadowing and Scoping

for loops leak variables into their outer scope:

foritemincollectionsum||=0sum+=itemenditem# => 100sum# => 5050
Enter fullscreen modeExit fullscreen mode

That means if the code around it has anitem it'll be overwritten. Same withsum. Contrast witheach here:

collection.eachdo|item2|sum2||=0sum2+=item2enditem2# => nilsum2# NameError (undefined local variable or method `sum2' for main:Object)
Enter fullscreen modeExit fullscreen mode

We'll get into that in a moment, but for this moment know that block functions are isolated in that outside code cannot see inside of them, but they can certainly see outside code around them.

The Case for Each

So why would one want to use anonymous functions,each, and related methods in Ruby rather than afor loop? This section will look into that.

Closures

Going back to the above section, let's clarify what we mean by what the function can "see" or "not see".

A block function is what's called a closure, meaning it captures the outside context (think variables) inside the function, but the outside code cannot see inside, hencesum2 being undefined here. Believe it or not that's quite useful later on, but has been known as a stumbling block to some.

Consider this code:

sum=0[1,2,3].eachdo|item|sum+=itemendsum# => 6
Enter fullscreen modeExit fullscreen mode

We can "see"sum as it's in the context of the block function, or what's immediately around it when it runs. This can be really useful for more advanced code, as that means functions effectively have memory, and in Ruby you can even redefine where it finds its memory by changing its context, but that's considerably more advanced.

The outside code, however, cannot seeitem as it's only visible inside the block function. This can present some headaches, and early on in my Ruby career this confused me to no end:

require'net/ssh'# Don't actually use passwords if you do this, use keysNet::SSH.start('hostname','username',password:'password')do|ssh|config=ssh.exec!"cat /tmp/running.cfg"enddefined?(config)# => nil
Enter fullscreen modeExit fullscreen mode

For those cases I used global variables back then, which I would not recommend, instead prefer this pattern:

config=nil# Don't actually use passwords if you do this, use keysNet::SSH.start('hostname','username',password:'password')do|ssh|config=ssh.exec!"cat /tmp/running.cfg"enddefined?(config)# => local-variable
Enter fullscreen modeExit fullscreen mode

...or if you read theNet::SSH docs you might find that the block isn't even entirely necessary for this and get around the issue entirely. Anyways, point being there are some traps there potentially for the unaware, so be careful on what isolated block function scopes mean.

Enumerable

Ruby has a collections library calledEnumerable which is one of the most powerful features of the language.

Let's say I wanted to get the sum of every even number greater than 4 in a collection, but double them as well. With a for loop that might look like this:

sum=0foritemin1..100sum+=item*2ifitem>4&&item.even?endsum# => 5088
Enter fullscreen modeExit fullscreen mode

UsingEnumerable we can express each one of those conditions as a distinct transformation or filtering of the list:

(1..100).select{|v|v.even?&&v>4}.map{|v|v*2}.sum# => 5088
Enter fullscreen modeExit fullscreen mode

It gives us more flexibility in expressing multiple actions we want to take against a collection as distinct pieces rather than combining them all as one.

Some of those, you'll find, can be exceptionally useful beyond the trivial, like a count of what letters words start with in some text:

words=%w(the rain in spain stays mainly on the plane)words.map{|w|w[0]}.tally# => {"t"=>2, "r"=>1, "i"=>1, "s"=>2, "m"=>1, "o"=>1, "p"=>1}
Enter fullscreen modeExit fullscreen mode

...or grouping a collection:

words.group_by{|w|w.size}# => {3=>["the", "the"], 4=>["rain"], 2=>["in", "on"], 5=>["spain", "stays", "plane"], 6=>["mainly"]}
Enter fullscreen modeExit fullscreen mode

The flexibility there is really something, and because these can all be chained together you can easily break them out into separate functions and refactor out entire parts of the chain altogether if you need to.

Down the Rabbit Hole

Now there are a lot of things I could get into on where this can go and the implications, but as this is a more beginner friendly article that would not be very kind, so we'll instead hint at a few of them:

  • Block functions can have their entire context changed
  • A lot of Enumerable-like functions can be parallelizeable as they're functionally pure
  • Closures keep context, meaning you have memory to do some real fun things
  • Many Ruby classes, including your own, can be coerced into functions
  • A significant number of programming patterns are made much easier by the presence of functions

...and a lot more than I have time for in this particular article, but I would highly encourage you to read into the more advanced article on the types of functions in Ruby:

Understanding Ruby - Blocks, Procs, and Lambdas

Wrapping Up

This article is a very high level overview, and does definitely wave over some details I could get into. Be sure to read other parts of the series if you want to get more into the weeds on this, as there's a lot of fascinating detail there.

The intent of this article is for those coming from languages which primarily usefor loops rather than iterables or enumerable, depending on the way you describe them. That said, most all languages including Java have a Streaming type library which does something very close to this.

If you really want to get into the power of block functions and why that's significant be sure to watch out for future posts on functional programming, but until then that's all I have for today.

Top comments(6)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
vgoff profile image
Victor Goff
Mentor for RubyLearningMentor for Exercism (Volunteer position)Ask me about learning Ruby, VIM, Linux.
  • Location
    USA
  • Joined

Note that the range doesn't need to be converted to an array for this to work. And line 2 in the for vs loop example is invalid Ruby code, and so it raises an exception.

The code may be written like this (to include the comparison of the reports):

require'benchmark/ips'collection=(1..100)Benchmark.ipsdo|bench|bench.report("for loop")dosum=0foritemincollectionsum+=itemendsumendbench.report("each loop")dosum=0collection.eachdo|item|sum+=itemendsumendbench.compare!end
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
baweaver profile image
Brandon Weaver
Senior Staff Eng at One Medical. Autistic / ADHD, He / Him. I'm the Lemur guy.
  • Location
    San Francisco, CA
  • Work
    Senior Staff Eng at One Medical
  • Joined

Line 2 was me forgetting to comment the line. The collection doesn't explicitly need to be an Array, no. Alsocompare! doesn't need to be explicitly called.

CollapseExpand
 
vgoff profile image
Victor Goff
Mentor for RubyLearningMentor for Exercism (Volunteer position)Ask me about learning Ruby, VIM, Linux.
  • Location
    USA
  • Joined
• Edited on• Edited

Alsocompare! doesn't need to be explicitly called.

This is true, but it then won't give you the comparison report. So it does notneed to be there, it is just a nice to have. Also, not sure how to implicitly call it. To be sure, I did write in the message that it was there to show the comparison report.

CollapseExpand
 
rvirani1 profile image
Riaz Virani
Curious coder and entrepreneur
  • Location
    Toronto
  • Work
    Code Czar at My humble self
  • Joined

Love these articles. Brandon does such a great job breaking things down in a clear way. I've been a Rubyist since 2014 and I always learn something new from these. Appreciate it!

CollapseExpand
 
beyarz profile image
Beyar
  • Joined

I didn't know that for loop leaks the variable while each does not!

CollapseExpand
 
vgoff profile image
Victor Goff
Mentor for RubyLearningMentor for Exercism (Volunteer position)Ask me about learning Ruby, VIM, Linux.
  • Location
    USA
  • Joined

Andeach is not anEnumerable, but a dependency ofEnumerable. So depending on howeach is implemented, there is no guarantee that leaks do not happen. The method, probably stated obviously, does not even need to be an "iterator" but could be whatever. (don't do that though! :) )

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Senior Staff Eng at One Medical. Autistic / ADHD, He / Him. I'm the Lemur guy.
  • Location
    San Francisco, CA
  • Work
    Senior Staff Eng at One Medical
  • Joined

More fromBrandon Weaver

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp