Ruby from the ground up

Methods: naming a piece of work

Defining methods, the many shapes of an argument list, the return value that is already there, and the naming conventions that carry meaning.

Part 9 of 13

Intermediate17 min readBack to Courses

Def, and the return you do not write

A method is declared with def name ... end, called by writing its name, and closed with end. The last expression of the body is the return value — return exists for leaving early, but the ordinary path just ends, and the value of the last line is what the caller receives. That is why small Ruby methods read like a chain of thoughts with no ceremony around the exit.

A method with no arguments may be called with or without parentheses: greet and greet() are the same call. The convention is to drop them for a bare call and keep them when arguments are involved, which keeps the reading quiet in the common case.

Every shape of argument list

A parameter may have a default — def area(width, height = width) — and the default may refer to an earlier parameter, which is a tidy way to express a square as a special case of a rectangle. A star before a parameter name collects the rest into an array, so the method accepts as many arguments as you like and sees them as one ordinary array inside.

Keyword arguments are named at the call site: def price_with_tax(amount, rate: 0.2) is called as price_with_tax(100, rate: 0.05). They cost a little more to write and repay it immediately in a call with three numbers in a row, where nobody can remember which is which. A method can also return several values at once — return min, max — which arrives as an array that can be unpacked back into names.

Names that say what they are

A method ending in ? returns true or false; one ending in ! is the version that changes the receiver or does something emphatic. Neither is syntax — both are conventions, and both are followed so consistently that a reader treats them as promises. save and save! in most Ruby libraries differ exactly as the convention describes.

A method that does one thing can be tested, named in an error message and read on its own. When a method grows past a screen, the honest fix is usually to name the two thoughts inside it and let the original become three lines that read like a table of contents.

The call, the body, the answer

Calling a method creates a new local scope: parameters are names for the arguments, and any name assigned inside belongs to that call. When the body ends, the scope goes away and the last expression's value travels back. Recursion works because each call gets its own scope — the n in one call is not the n in the call it makes.

Arguments are matched to parameters by position, then by name for keywords. Defaults are evaluated at call time, in order, so a later default can use an earlier parameter. A star collects the rest into an array, and a method can hand back several values by returning an array that the caller unpacks with parallel assignment.

The last line is the answer

A Ruby method returns the value of its last expression, so the shape of the body decides the answer. A method that ends with puts returns nil; one that ends with an assignment returns the assigned value; one that ends with if returns the branch that ran. That is convenient and easy to forget when a line is added at the bottom.

When a caller receives something unexpected, read the method from the bottom up. The last line is the answer, whatever the earlier lines seemed to promise.

Guard clauses keep the shape flat

A guard clause answers the impossible case at the top and leaves: return 0 if numbers.empty?. The rest of the method then reads without a wrapping if, and the reader sees the normal path at the left margin. Two guards are usually clearer than one nested condition, and they cost nothing in Ruby.

A method returns its last expression, and puts returns nil — so a method that ends with a print returns nothing useful. Watch the name too: a method ending in ? should answer true or false, because callers will treat it as a question. And a keyword argument is matched by name exactly, so a typo raises ArgumentError instead of being ignored — better than silence, still a call site that has to agree with the definition.

Arguments and answers

def greet(name)
  "Hello, #{name}"
end

puts greet('Ada')

def area(width, height = width)
  width * height
end

puts area(3)
puts area(3, 4)

def total(*numbers)
  numbers.sum
end

puts total(1, 2, 3, 4)

def price_with_tax(amount, rate: 0.2)
  amount * (1 + rate)
end

puts price_with_tax(100)
puts price_with_tax(100, rate: 0.05)

def min_max(numbers)
  [numbers.min, numbers.max]
end

low, high = min_max([3, 1, 4])
puts "#{low} to #{high}"

def adult?(age)
  age >= 18
end

puts adult?(20)
puts adult?(12)

def countdown(n)
  return if n < 0
  puts n
  countdown(n - 1)
end

countdown(3)

A little report toolkit

Run it and read the output of each method. Then write a method that takes a hash of readings and returns the average, with a guard that answers zero when the hash is empty.

Output
Click "Run" to execute your code

A return at the top of a method is a guard clause: it answers the impossible case and gets out of the way. Two guards at the top usually read better than a body wrapped in nested ifs, and they cost nothing in Ruby.

Try these next

  • Write percentage(part, whole) that returns a float rounded to one decimal, with a guard for a zero whole.
  • Write a method with a splat that returns the largest of any number of arguments.
  • Make a method return the smallest and largest values at once, then unpack both in the caller.

Splitting a bill

Run it and check the arithmetic. Then add a service charge, change the number of people to zero to see the guard, and put it back.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a method return if it has no return statement?