DEV Community

loading...

Let's Read – Eloquent Ruby – Ch 2

Brandon Weaver
Staff Eng / Ruby Lead / Global Neurodiversity Chair at @Square. Autistic / ADHD, He / Him. I'm the Lemur guy.
・8 min read

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 2. Chose the Right Control Structure

This chapter covers the use of control structures in Ruby like if, unless, and other branch type methods.

If, Unless, While, and Until

if statements look pretty well the same as you’d expect, except for the lack of parens:

class Document
  attr_accessor :writeable
  attr_reader :title. :author, :content

  # Much of the class omitted...

  def title=(new_title)
    if @writeable
      @title = new_title
    end
  end

  # Similar author= and content= methods omitted...
end
Enter fullscreen mode Exit fullscreen mode

Now the interesting part in Ruby is if we inverted that logic to say something quite the opposite, as the book mentions:

def title=(new_title)
  if !@read_ony
    @title = new_title
  end
end
Enter fullscreen mode Exit fullscreen mode

(Note that I’d switched *not* with *!* as english operators aren’t often used. Granted I abuse this, but again, grain of salt)

The book mentions inverse operators, essentially saying:

if !    == unless
while ! == until
Enter fullscreen mode Exit fullscreen mode

They’re just more concise ways to say the opposite.

It should be noted that in the case of unless , it’s typically considered bad form to use an else branch in the Ruby Style Guide:

unless condition
  # ...
else
  # ...
end
Enter fullscreen mode Exit fullscreen mode

This is done to assert the positive part of the condition first and foremost, as else is essentially the inverse of an unless condition.

Use the modifier forms where appropriate

If you’re wondering why I didn’t mention the post conditional form earlier, it’s because the book brings it up in this section.

Essentially the post conditional, or modifier form is taking this:

unless @read_only
  @title = new_title
end
Enter fullscreen mode Exit fullscreen mode

and writing it with the condition as a suffix:

@title = new_title unless @read_only
Enter fullscreen mode Exit fullscreen mode

This can be done with while and until as well

document.print_next_page until document.printed?
Enter fullscreen mode Exit fullscreen mode

These are very widely used, but be careful when lines start getting long and blocks get involved:

data.each do |datum|
  # ...
end if condition
Enter fullscreen mode Exit fullscreen mode

...because that becomes less readable and more a battle of remembering to check the end of the block for surprises.

This is one of the few cases where I’d argue against 80+ characters on a line: If you cannot see the intent of a line at a glance while reading down the page it’s too long. Doubly so for modifiers at the end of them.

Use each, not for

Ruby does have a for loop, but it’s also slower and implemented in terms of each:

for font in fonts
  puts font
end
Enter fullscreen mode Exit fullscreen mode

each is preferred, especially once you get into Enumerable methods which build off of it:

fonts.each do |font|
  puts font
end
Enter fullscreen mode Exit fullscreen mode

There are additional advantages to using block style methods, primarily around block functions themselves and symbol’s fun little to_proc coercion:

fonts.each(&:register)

# ...is the same as:
fonts.each { |font| font.register }
Enter fullscreen mode Exit fullscreen mode

If you end up into composition and other things, the fact that all these methods take blocks becomes an insane advantage for more advanced Ruby.

Put bluntly there are no advantages to using a for loop in Ruby except that it feels more like another language.

A case of programming logic

The case statement is one of my personal favorites:

case title
when 'War And Peace'
  puts 'Tolstoy'
when 'Romeo And Juliet'
  puts 'Shakespeare'
else
  puts "Don't know"
end
Enter fullscreen mode Exit fullscreen mode

As it’s an expression, much like if, we can use it to assign a variable:

author =
  case title
  when 'War And Peace'
    'Tolstoy'
  when 'Romeo And Juliet'
    'Shakespeare'
  else
    "Don't know"
  end
Enter fullscreen mode Exit fullscreen mode

A Note on Indentation

You might notice my style of indentation is different here. The original looks like this:

author = case title
         when 'War And Peace'
           'Tolstoy'
         when 'Romeo And Juliet'
           'Shakespeare'
         else
           "Don't know"
         end
Enter fullscreen mode Exit fullscreen mode

The problem with this style of indentation is that it will be subject to the variable name. What if I changed it to author_name ? Now I have to indent every other branch in the statement to match it:

author_name = case title
         when 'War And Peace'
           'Tolstoy'
         when 'Romeo And Juliet'
           'Shakespeare'
         else
           "Don't know"
         end
Enter fullscreen mode Exit fullscreen mode

Instead, I would advocate for using a line-break and two space indentation as mentioned above:

author =
  case title
  when 'War And Peace'
    'Tolstoy'
  when 'Romeo And Juliet'
    'Shakespeare'
  else
    "Don't know"
  end
Enter fullscreen mode Exit fullscreen mode

Now I could call the variable whatever, and I don’t have any additional work. It also shortens the length of the expression line, making it easier to read at a glance.

The biggest reason? Code diffs will be much easier to parse through and reconcile later.

Triple Equals

Case statements use === behind the scenes, and that’s precisely why I like them so much. If you haven't yet I would encourage reading an article on ===.

It’ll be mentioned more in chapter 12, but if you want to explore a bit it’s quite a ride.

Also remember that case statements can use commas to separate multiple conditions like an OR of sorts:

type_name =
  case 1
  when Integer, Float
    "Number!"
  when String
    "String!"
  else
    "Dunno, too lazy"
  end
Enter fullscreen mode Exit fullscreen mode

Really it gets a bit close to pattern matching after a fashion. I'll mention that a bit more in a moment.

The chapter mentions the use of Regex as well to match against titles. That’s because regex use === too. I’m telling you, it’s Ruby’s best magic, especially once you figure out you can implement your own.

Pattern Matching

Where this gets very powerful is from Ruby 2.7 onwards, which introduced an additional pattern matching syntax to case statements:

Point = Struct.new(:x, :y)

case Point[1, 2]
in Point[0.., 0..] then :positive
in Point[..0, ..0] then :negative
else :unsure
end
# => :positive

big_point = Point[10, 10]
case big_point
in Point[x: 10.. => x, y: 10.. => y] then Point[x + 1, y + 1]
else big_point
end
# > #<struct Point x=11, y=11>
Enter fullscreen mode Exit fullscreen mode

I would suggest reading through the Pattern Matching Applied series I've written if you really want to dig into this.

Staying out of trouble

The chapter mentions that 0 is truthy in Ruby. That’s still the case, much to the annoyance of other language programmers. If it’s not false or nil it’s truthy.

puts 'Sorry Dennis Ritchie, but 0 is true!' if 0
Enter fullscreen mode Exit fullscreen mode

( Dennis Ritchie created the C programming language among other things. He’ll be missed. )

Now it’s mentioned that the string "false" isn’t false. When it was said earlier that everything is truthy except nil and false that includes things which “look” like them. It’s certainly given lots of extra fun to Rails programmers with stringy booleans. Well, nightmares.

puts 'Sorry but "false" is not false' if 'false'
Enter fullscreen mode Exit fullscreen mode

Explicit truthy comparisons are rare in Ruby, even today:

if flag == true
  # do something
end
Enter fullscreen mode Exit fullscreen mode

One of the main reasons that comes up is duck typing. We only really care if something is an approximation of truthyness. That, and it involves extra typing.

defined? is used as an example of where this can go wrong:

doc = Document.new('A Question', 'Shakespeare', 'To be...')
flag = defined?(doc)
Enter fullscreen mode Exit fullscreen mode

defined? is an odd one, it returns a string for what type the variable is, but not as in data types:

defined? a
# => nil

a = 5
# => 5

defined? a
# => "local-variable"
Enter fullscreen mode Exit fullscreen mode

So to compare that explicitly to true would break the intent of what we’re probably checking for if we were to do this:

if defined?(a)
  # ...
end
Enter fullscreen mode Exit fullscreen mode

It’s mentioned as being in a Boolean context. Granted it’s this Rubyist's opinion that methods ending with a question mark should return a straightforward Boolean answer, but such it is.

The next issue that’s brought up is by not paying close attention to nil:

# Broken in a subtle way...
while next_object = get_next_object
  # Do something with the object
end
Enter fullscreen mode Exit fullscreen mode

Remember how false and nil are both falsy? If you’re expecting nil explicitly, you should say so to prevent Ruby from breaking out of that loop early:

until (next_object = get_next_object).nil?
  # Do something with the object
end
Enter fullscreen mode Exit fullscreen mode

Likewise this does horrid things to ||= , but that’s mentioned in the next section so we’ll defer until then.

In the wild

The example used for a bit of a larger if block is from Ruby’s X509 certificate validation (with some cleaning):

ret =
  if @not_before && @not_before > time
    [false, :expired, "not valid before '#{@not_before}"]
  elsif @not_after && @not_after < time
    [false, :expired, "not valid after '#{@not_after}'"]
  elsif issuer_cert && !verify(issuer_cert.public_key)
    [false, :issuer, "#{issuer_cert.subject} is not an issuer"]
  else
    [true, :ok, 'Valid certificate']
  end
Enter fullscreen mode Exit fullscreen mode

Now one of the fun things to change in Ruby since 2011 is the null coercion, or lonely operator (&.), which lets us do this (assuming verify deals well with nil):

ret =
  if @not_before&.> time
    [false, :expired, "not valid before '#{@not_before}"]
  elsif @not_after&.< time
    [false, :expired, "not valid after '#{@not_after}'"]
  elsif !verify(issuer_cert&.public_key)
    [false, :issuer, "#{issuer_cert.subject} is not an issuer"]
  else
    [true, :ok, 'Valid certificate']
  end
Enter fullscreen mode Exit fullscreen mode

Note that I’m not using the instance variable interpolation syntax, "#@var", as it’s exceptionally rare to see out in the wild. It's only real benefit is being a character shorter while making code a bit more confusing for newer programmers. When possible stick with common over clever.

Another example used is the classic ternary operator:

file = all ? 'specs' : 'latest_specs'
Enter fullscreen mode Exit fullscreen mode

It’s still very much in use, and the reason why sometimes I tend to use parens around methods ending in question marks for clarity like defined?(a). Visual cues go a long way for understandability some times.

I’ve seen this done before, but at this point just use an if statement instead:

file =
  all ?
    'specs' :
    'latest_specs'
Enter fullscreen mode Exit fullscreen mode

…and please don’t use multiple nested ternaries, that’s just right unpleasant to read.

Next up postfix expressions!

@first_name = '' unless @first_name
Enter fullscreen mode Exit fullscreen mode

Though more commonly, and noted in the book, this is used:

@first_name ||= ''
Enter fullscreen mode Exit fullscreen mode

which is to say essentially:

@first_name = @first_name || ''
Enter fullscreen mode Exit fullscreen mode

Same rules of truthyness apply here, and remember, only false and nil. Is an empty string one of those? No? Then it’s still truthy, be very careful to remember this.

The book goes on to compare this to += from the example:

count += 1
Enter fullscreen mode Exit fullscreen mode

The same idea applies. If we were to go into a bit more depth, Ruby literally uses the operators for this type of syntax sugar. If you were to define your own + or || it’d override the += and ||= respectively to potentially do very bad things.

Wrapping Up

Most everything you’re going to see for control structures like if and friends has not changed much, and the warnings are still very relevant. Take heed as there’s some advice in programming which ages quite well.

Next up we get the fun of Arrays and Hashes to play with, and oh my do they have some lovely new features.

Discussion (0)