I started with the insert sort and came up with the following:
# array filled with random numbers
a = Array.new(20).map! { Random.rand(50) }
# sort the array
a.each.with_object([]) do | value, asort |
head = asort.take_while { |value2| value2 < value }
tail = asort[head.length...asort.length]
asort = head + [value] + tail
end
It's probably inefficient, but it should work, right? Instead, it returns an empty array for any input. Trying out the contents of the block in irb
works as it should, but adding puts
to the block shows that it starts with an empty array each time it's called. So here I hit a wall. As much as starting out was a high of discovery, this was a low of frustration. I cycled between disappointment and anger, between wondering why I wasn't getting it, and wondering why this language wasn't doing what I expected. Instead of giving up, I did some frustrated googling and fiddling in the irb
, and finally discovered that the following does work:
asort = []
a.each do |value|
head = asort.take_while { |value2| value2 < value }
tail = asort[head.length...asort.length]
asort = head + [value] + tail
end
It looks almost exactly the same. So why does the second one work but the first fail? It looks like
with_object
just doesn't work.It's a subtle point, and these subtle points are exactly why you learn a language better if you write code than if you just read a book. In the last line of the block, the
asort
array gets reassigned. When the array is reassigned, the symbol asort
points to the new array created. This works fine in the second version because asort
is part of a closure. The symbol asort
is part of the enclosing scope outside the block, so the reassignment is carried over to the next iteration of the block. But in the first version, asort
isn't part of a closure; it's just a local variable. It goes out of scope at the end of the block, so the array created by the assignment disappears. The with_object
method yields the originally provided array to the block, which continues to be empty because no code has actually modified it. Because of this, when you're using
with_object
you can only use methods that change the underlying object. Luckily, the documentation for Ruby makes this easy to spot. A method that creates a new array will look as follows:
while a method that acts on the underlying array will appear like this:
new_ary
means a new array will be created, while ary
means the underlying array will be changed. Close by these two methods is one that can solve our problem. If we really need to do a reassignment inside a block, we can use replace. If we put that method call in the original code, everything works.
a.each.with_object([]) do | value, asort |
head = asort.take_while { |value2| value2 < value }
tail = asort[head.length...asort.length]
asort.replace head + [value] + tail
end
We've learned something about Ruby, but since this blog is about the psychology of programming as well, I want to talk about that too. The feelings I went through - initial elation, frustration and doubt, and then a final resolution into a higher understanding - is a pattern. It's the pattern that all learning takes. Way back when I was an undergraduate learning mathematics it happened so often I got bored with it (here's the part where I'm frightened and confused by the new notation, ho hum.) For the learning to take place, you have to go through the whole cycle. If you don't keep pushing when you feel confused, you'll never get to that final resolution, and however fun that initial joy is, the higher understanding just feels more satisfying. It's as good as the final chord in a piece of music, or the end of a novel when everything comes together. That cycle of feelings is why learning is so satisfying, and why you should keep doing it as long as you can.