21
JAN2012
Iteration Patterns
I love studying how the human brain works. It's an amazing biological machine capable of impressive feats. Of course, it also has its quirks.
For example, the brain is a terrific pattern matcher. That's probably one of its favorite activities. It wants to do this so much that it will often even find patterns that aren't there.
While this "feature" of your brain can get you into trouble, you can also make it work for you. Some parts of programming really are about patterns. If you prime your brain with the right data, it will just take over and do one of the things it does best.
How I Teach Iterators
One evening, at a post Ruby conference dinner, Glenn Vanderburg and I had a lengthy discussion about iterators and patterns. During this discussion we nailed down a plan for how the iterators could be taught.
I know that we've both used the strategy we came up with in multiple Ruby trainings and we have both seen it work wonders. This is hands down the best way to learn the iterators, in my opinion.
The key insight is that iterators are just patterns. Think about it. Early programming languages just had a few general loops. Those got used for everything. Need to repeat a task? Write a loop. Have to walk some list? Loop over it. Want to transform some items? Loop. Need to filter things out? That's another kind of loop.
Iterators turn this old system on its head. With them, each one of those tasks is solved by a specialized tool. The downside is that you now have to learn many methods, instead of just mastering one or two loops. The upside is that the solutions are cleaner and the iterators can reduce common mistakes, like off-by-one errors.
Now, stop and think for a second about how we chose which specialized tools to make. Simple: it's the patterns we kept seeing over and over again in that old looping code. Iterator are literally the patterns of looping.
Once we know that, all we need to do is load those raw patterns into that great pattern matcher inside our heads.
My System
When I teach the iterators now, I do it like this.
First, I show my students a trivial example of using each()
. For example, I might show how to print a few squares:
numbers = [1, 2, 3, 4, 5]
numbers.each do |n|
puts n * n
end
I walk them through what this code does and explain that it's just a shortcut. I tell them that you can accomplish the same thing with Ruby's while
loop or C's for
loop.
Then I ask them to do that. I ask them to rewrite my example, using just Ruby's while
loop. (I cover the loop before I teach iterators.) In time, they come up with some code like this:
numbers = [1, 2, 3, 4, 5]
i = 0
while i < numbers.size
puts numbers[i] * numbers[i]
i += 1
end
That's a raw pattern for walking a list of items and, by puzzling it out, they have imprinted it into their brain.
Then we do it again. I say something like, "Let's say we don't just want to print the squares, but that we need to save the list of them instead." I show how to do that using map()
:
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n * n }
p squares
This time, I allow them to use each()
instead of while
. There's actually a good reason for that, which we will talk about in a bit. The exercise is the same though: rewrite that code without using map()
. They come up with something like:
numbers = [1, 2, 3, 4, 5]
squares = [ ]
numbers.each do |n|
squares << n * n
end
p squares
Again, that's a raw pattern for transforming a list with map()
.
Let's do one more. I typically show select()
as the third iterator. I'll say something like, "Let's say we need to grab just the even numbers of a list." Then I show some code like:
numbers = [1, 2, 3, 4, 5]
evens = numbers.select { |n| n.even? }
p evens
I realize I could use Symbol#to_proc()
to make that even shorter, but that would hide what's going on here and we don't want to do that when teaching. So I type it out.
Again, I ask them to rewrite it without using select()
. I do still allow them to do it with each()
. They generate something like the following code:
numbers = [1, 2, 3, 4, 5]
evens = [ ]
numbers.each do |n|
evens << n if n.even?
end
p evens
That's a raw pattern for filtering with select()
.
I'm sure you get the idea by now, so we'll stop there. In trainings I will usually teach a few more, but the technique remains the same.
What I am Doing and What the Brain is Doing
There are two important things going on in the exchange above.
First, there's what I am doing. I'm purposefully tilting the patterns to Ruby code the student might run into someday. If inexperienced Rubyists (that could be the students themselves) try to work out an each()
style iteration, they will most likely do it with while
. That's not the only option, of course. You could handle an each()
like this:
numbers = [1, 2, 3, 4, 5]
i = 0
loop do
puts numbers[i] * numbers[i]
i += 1
break if i >= numbers.size
end
However, it's unlikely they would reach for loop()
and break
if they haven't caught on to each()
yet. It could happen, but I doubt it's going to happen as often as the while
version.
There are exceptions though and it pays to know your audience. For example, Perl programmers have a habit of destructively walking a list of arguments. If I was doing this exercise with a group like that, I would make sure to show this version of each()
, assuming they didn't bring it up:
numbers = [1, 2, 3, 4, 5]
while (n = numbers.shift)
puts n * n
end
Similarly, I would show C and Java programmers a for
loop. They won't find it in Ruby, but their brain already knows that pattern very well.
This is also why I show the later iterators in terms of each()
. Most Ruby programers get a handle on each()
pretty early on in their time with the language. It can be quite a while though before they pick up the rest of the iterators. Given that, they are likely to handle most iteration problems using each()
. If we imprint that pattern, it makes it extremely likely to trigger some recognition when they see code like this:
def get_unread_emails
unread = [ ]
@emails.each do |email|
unread << email if email.unread?
end
unread
end
We want them to see that and think, "Oh, that's spelled s-e-l-e-c-t."
I'm doing this so the brain can imprint patterns it is likely to encounter. But we don't have to count on that. Remember, the brain loves patterns and it's going to try hard to find them. If you've seen the raw pattern for each()
and you run into this code:
numbers = [1, 2, 3, 4, 5]
i = numbers.size
while i > 0
i -= 1
puts numbers[i] * numbers[i]
end
There's a decent chance your brain will still want to call that each()
. If you know that Array
has a reverse()
method, it's likely your brain will translate the pattern as such:
numbers = [1, 2, 3, 4, 5]
numbers.reverse.each do |n|
puts n * n
end
That's just what brains do. Of course, it would be more efficient to use reverse_each()
:
numbers = [1, 2, 3, 4, 5]
numbers.reverse_each do |n|
puts n * n
end
But the brain isn't likely to make that leap, unless it has previously encountered the raw pattern for reverse_each()
. By the way, that's what the while
loop above was: the raw pattern for reverse_each()
.
That's my system for teaching patterns: set the brain up with material it is likely to encounter. Then trust it to do what it loves to do.
Pop Quiz: Do You Know Your Iterators?
Let's test the pattern matching in your brain. I'll show a few raw patterns for iterators below. See if you can name the Ruby iterator that implements the pattern. I'll make sure the code runs, but try to come up with the answer before you execute it. We want to test your brain, not your Ruby interpreter.
-
Say we wanted to see if a list of names were really full names?
Name = Struct.new(:first, :last) names = [ Name.new("James", "Gray"), Name.new("Dana", "Gray"), Name.new("Summer") ] full = true names.each do |name| full = false unless name.first && name.last end if full puts "We have all full names." else puts "Some names are not full." end
-
What if we want to hunt for a specific email message?
Email = Struct.new(:subject, :labels) # ... emails = [ Email.new("Earn $10,000 a month!", %w[spam]), Email.new("Monthly Meeting", %w[inbox]), Email.new("This Month's Report", %w[inbox reports]) ] match = nil emails.each do |email| if email.subject =~ /month/i && email.labels.include?("inbox") match = email break end end if match puts "Here's your email: #{match.subject}" end
-
How would we dynamically build up a linked list?
LinkedList = Struct.new(:value, :next) values = %w[a b c d e] list = nil values.reverse_each do |value| list = LinkedList.new(value, list) end p list
How did you do? I used pretty common iterator patterns above, so if you didn't didn't ace this quiz you may want to spend some time actually trying to puzzle out these patterns and those of some other iterators. I promise, it's time well spent.
Manuals for Your Brain
If you enjoy learning how your brain works and how you can take advantage of it, there are many resources available. I've read a ton of books on the subject, but here are a few of my favorites:
- Pragmatic Thinking and Learning is a terrific book that digs into how our brain works and how we learn new things. It then uses that information to suggest several things you can try to make more efficient use of your brain. Not all of the tips will work for everyone, since each brain is a little different, but I still use multiple ideas from this book every single day. As an added bonus, it is slanted towards a geek audience.
- Mind Performance Hacks has less focus on explaining how our brain works and more focus on teaching you tricks that take advantage of how you think. Again, all of the tricks won't be useful to everyone, but you're likely to find something helpful in here. I did. In my opinion, this book's number and math tricks are where it really shines.
- Brain Rules is a book I would recommend even though I haven't read it yet. I have read the companion book, Brain Rules for Baby, and I can tell you that it's one of only two parenting books I have found so far that are worth reading. It's simply an excellent look at what science can tell us about the brains of our children and how we can use that knowledge to our advantage. The other book is more for us adults. It's on my reading list and I'm pretty darn sure I'm going to love it.
Comments (1)
-
Ashish Dixit January 25th, 2012 Reply Link
I can't believe I have not taken time to go through the backlog of Rubies in the Rough but I am so glad I took time to read this one. I really liked this idea when you first mentioned it in RubyRogues I believe but this article has helped me gel the idea even further.