Excerpt
Perhaps my personal favorite recommendation for learning to program Ruby like a Rubyist, Eloquent Ruby is a book I recommend frequently to this day. That said, it was released in 2011 and things have changed a bit since then.
This series will focus on reading over Eloquent Ruby, noting things that may have changed or been updated since 2011 (around Ruby 1.9.2) to today (2021 β Ruby 3.0.x).
Note: This is an updated version of a previous unfinished Medium series of mine you can find here.
## Chapter 3. Take Advantage of Ruby's Smart Collections
This chapter covers some of Ruby's collection classes and how to work with them. In Ruby this is probably going to be one of your most powerful and often used sets of libraries and features to work with, and in my early days I frequently found myself consulting this chapter and all the documentation related to it.
The book mentions here that if you were to look into any well-sized Ruby program you're going to find a ton of Arrays and Hashes
Perhaps my personal favorite recommendation for learning to program Ruby like a Rubyist, Eloquent Ruby is a book I recommend frequently to this day. That said, it was released in 2011 and things have changed a bit since then.
This series will focus on reading over Eloquent Ruby, noting things that may have changed or been updated since 2011 (around Ruby 1.9.2) to today (2021 β Ruby 3.0.x).
Note: This is an updated version of a previous unfinished Medium series of mine you can find here.
## Chapter 3. Take Advantage of Ruby's Smart Collections
This chapter covers some of Ruby's collection classes and how to work with them. In Ruby this is probably going to be one of your most powerful and often used sets of libraries and features to work with, and in my early days I frequently found myself consulting this chapter and all the documentation related to it.
The book mentions here that if you were to look into any well-sized Ruby program you're going to find a ton of Arrays and Hashes scattered throughout, and all of the operations that occur on them. I've found this to be exceptionally true, and as mentioned above it's one of the most powerful parts of the language.
Enumerable's documentation is going to get a lot of reference as you learn Ruby and become more effective in it.
### Literal Shortcuts
The book mentions a few ways of constructing an Array to start with:
```plain text
# The normal way
poem_words = ['twinkle', 'little', 'star', 'how', 'I', 'wonder']
# Whitespace delimited words, same as the above
poem_words = %w{twinkle little star how I wonder}
```
Now one qualm I have with the book here is that {} can be confused with Hash later, and you're more likely to find common usage of either %w() or %w[] in its place. There are others, but by that point it's becoming a bit ridiculous. Personally I prefer %w(), but can see why the other is popular:
```plain text
poem_words = %w(twinkle little star how I wonder)
```
The next bit covers Hashes, and the hash rocket (=>):
```plain text
freq = { "I" => 1, "don't" => 1, "like" => 1, "spam" => 963 }
```
For String keys you'll find that in common use, but Symbol keys are a bit different:
```plain text
book_info = { :first_name => 'Russ', :last_name => 'Olsen' }
book_info = { first_name: 'Russ', last_name: 'Olsen' }
```
They produce the same Hash, but one uses the 1.9.x "JSON-style" syntax. Very likely that syntax will look familiar as you find keyword arguments in methods as well, and it's generally preferred.
Personal Hill: Granted I really wish that Symbol keys would just be translated to String as this causes a lot of confusion for newer programmers for little gain, especially now that Strings are so commonly frozen. As always when I mention this I acknowledge there's 0% chance this changes, but I still find it a particularly frustrating part of Ruby.
### Instant Arrays and Hashes from Method Calls
The book goes on to mention ways of getting an Array or a Hash from a method call, in what is called a "splat" (*) or "keyword/hash splat" (**):
Wherein the first is called a "varadic" method taking any number of arguments, and the second is much the same except that splat can be anywhere in the list. You can also do this on assignments:
The book then goes on to mention explicit versus implicit Arrays:
In the first case it would need to be called as such:
...but the second might be:
(Note that Ursula Le Guin would break that code's space delimiter, how might you fix it?)
The book then goes into a similar section on Hashes with load_font:
If we happened to use the double-splat (or keyword splat, hash splat, or other names):
Warning: The book mentions that you can omit braces in the first case, but with Ruby 3.x+ this becomes complicated and is not recommended. Prefer to be explicit, and give the Ruby 2 Keyword Argument conundrum a read.
### Running Through Your Collection
The book starts with a for loop example it quickly discourages afterwards:
If you really want to know why you should avoid for in detail beyond "just use each" give this post a read, or just take our word for it on using each instead. The each variant of that code would be:
As the book mentions Hashes also have an each method:
Note: The book likes to use pp without mentioning it. It stands for Pretty Print, which is not really useful for most of the cases in this chapter, but can be for larger collections when you want it to be readable rather than a single-line you have to scroll to get through.
Now after this the book starts dropping hints for Enumerable and all the handy methods in it. Consider this, a potential method for finding the index of a word in a document:
Ruby has a lovely method which already takes care of this called find_index:
Now I do have some slight qualms with the naming here, as word would be better as the iterated variable, and this_word could be the argument instead and renamed to target_word for intent, making this:
...which personally I find a bit easier to read at a glance for a few reasons:
1. target_word makes the searched word more distinct than word
2. word is more suited to the generic word in a collection
3. Shifting word to the left gives it proximity to the block argument, making it easier to read left-to-right.
Now that last one is interesting, and will come up again in later sections. Proximity, and left-to-right, are very important in general readability of code and should be kept in mind. Names equally so.
If we had word at the end our eye has to go to the right to find where it's used, then back left to find what it's being compared to. Because word is more proximate to that scope it reads better when put first in that block.
I find this to be something I tend to do without thinking of it, but it has made code a lot easier to read at a glance and reduce jumping.
Anyways, back to the book, where we take a look into map which returns a new Array transformed by a block function:
The book uses this example of a hypothetical document, which I find a bit less clear for building immediate intuition:
...and a way to lowercase all of those words:
It then goes into inject which I'll be using reduce instead as you'll find it more commonly in other languages (also foldLeft which isn't present in Ruby, but does more clearly articulate the function of the method).
The example the book uses is finding the average word length in a document:
Now that's a bit hard to understand if you've never seen reduce before, so try this example real quick:
The idea is that reduce is reducing a collection of items into one item, in this case a number 0. accumulator starts as 0, and each loop becomes the value returned from the block function as we see in new_accumulator before the next loop. Whatever the value of the accumulator is at the end is the value we get returned.
If you want to read more into reduce take a look at this article where I explain it in more detail, and a conference talk I did called Reducing Enumerable which goes into far more detail.
Now that all said Ruby 2.4 introduced a method called sum which makes all of this much easier:
...which makes that above document method look more like this:
How's that for succinct? See this post and search for Ampersand (&) and to_proc for more information on that shorthand.
Note: fdiv is float division, which is more explicit than using 0.0 for an accumulator, or converting one of the values to a float.
There are still uses for reduce, sure, but I find they're rather rare and normally there are clearer methods to use in Enumerable you should consider first. That said, it's also exceptionally powerful and you could quite literally reimplement every other Enumerable method with reduce as well.
Chainsaws for trimming bonsai trees and such, use the least amount of power you need to get something done, and appearing clever surely isn't a valid reason to reach for a more powerful tool. Readability first, remember that one, it'll save you nightmares later.
### Beware the Bang!
In Ruby a bang (ending with !) method is usually a warning sign, often related to a method mutating something or having some other side effect. Let's take a look at the book's example on reverse:
This version returns a new Array without mutating the old one, but the bang method?
It mutates a. As an aside, Javascript's reverse totally mutates an Array, ask me sometime why I know...
The book mentions that sort and sort! behave similarly, but makes the lovely followup that methods like push, pop, delete, and shift also modify an Array too without a bang.
It mentions several methods may mutate things, so there's not always consistency here and it's best to be aware. Bang methods also tend to return nil in some cases when something does not change:
...meaning you can't chain things. They're faster, sure, but you trade a lot of what makes Ruby chaining so nice by doing so.
### Rely on the Order of Your Hashes
In the next section it mentions something which can be very useful, but very different from other languages:
Hashes in Ruby retain insertion order.
That means running this code does this:
...and adding one more element:
...puts it at the end. The book also mentions that changing an existing element does not reorder things.
This section hasn't really changed much if at all from Ruby 1.9, so there's not much to comment on here.
### In the Wild
The book then goes into a few examples. The first examples are mainly covering class-methods which don't return an instance, but rather a collection. A few of those examples:
Those types of methods tend to be common where Ruby will return a collection or other appropriate type, but the next example is a little clearer where generic collections really come in handy as a return type.
Let's take this XML:
If we were to parse that into Ruby we'd get something like this:
It's really something just how much you can express with Hashes and Arrays in Ruby, and very frequently (and admittedly not always a great idea) I tend to avoid classes in favor of them until I get a tangible benefit from abstracting that data into a class wrapper.
Arrays and Hashes are simple to work with, have known interfaces, and don't require additional effort beyond language knowledge to use effectively.
### Staying Out of Trouble
The book warns about manipulating a collection you're currently iterating on. In one case it's not a great idea to mutate state, but mutating the state of something that's currently in use like iteration? That's just asking for problems and errors.
Consider the book's example here:
It skips -9 because the element at that index just shifted with the deleted element.
The book then mentions making large Arrays inadvertently like so:
...being a reference to Les Miserables, but every element up to 24601 is now nil and that's not free in terms of memory.
The next example the book gets into is a unique list of words using a few different methods:
The second being much much slower as it has to iterate that unique list multiple times to find if a word is unique. Hashes have a lookup time of O(1), whereas Arrays are more of an O(n) lookup time.
While one can use the Hash case for this, Set is easier:
...or get into Enumerable and find the uniq method:
...which encapsulates the entire idea quite nicely without the extra work.
### Wrapping Up
In Ruby chances are real high you're going to encounter collections, so knowing what they are and how they're used is a massive boost in your early productivity for both reading and writing.
Over the years Ruby has added a lot of Enumerable, Pattern Matching, and other features which make dealing with collections much easier. Later chapters will cover this, but that shouldn't stop you from taking a glance at a few in the docs.
The next chapter covers Strings, which will be the other most likely candidate for things you'll encounter and have to work with in Ruby.