Methods & Blocks
๐ง Methods & Blocks
.each do |n| ... end without fully explaining what was happening. This chapter defines methods properly โ arguments, implicit return, naming conventions โ and then opens up what a block actually is: not a regular argument, but a distinct, implicit part of a Ruby method call unlike anything PHP or Python's callables offer directly.
๐ Defining a Method
def greet(name) "Hello, #{name}!" end greet("Philip") # => "Hello, Philip!"
def...end defines a method, with no braces and no function/def-with-colon syntax to worry about beyond that. Notice there's no return keyword โ the method's last evaluated expression is automatically its return value. This is Ruby's implicit return, and it's used constantly.
Because Ruby returns whatever the final expression evaluates to, a method that ends with a puts call returns nil โ puts always returns nil, regardless of what it printed. This trips up almost everyone coming from PHP or Python, where forgetting an explicit return in a similar spot would give a much more obvious "why is this None/null?" bug. In Ruby, it fails just as silently, but looks like working code at a glance.
The explicit return keyword still exists, and is the right choice for an early exit from inside a conditional:
def discount_price(price, is_member) return price unless is_member # early exit -- return IS needed here price * 0.9 # implicit return -- this is the last line end
๐ท๏ธ Naming Conventions: ? and !
Two method-naming conventions carry real meaning in Ruby and aren't just style preferences:
? โ returns true/false
A method ending in ? signals it returns a boolean, by convention (not enforced by the language). You've already seen this: 5.even?, nil.nil?, array.empty?. PHP and Python have no equivalent naming convention โ you'd typically prefix with is_ instead.
! โ "dangerous" / mutates in place
A method ending in ! signals it mutates the receiver directly or is otherwise more "dangerous" than its non-! counterpart โ "ruby".upcase returns a new string, while "ruby".upcase! modifies the original string in place. Always check what the non-! version does first; it's usually the safer default.
โ๏ธ Default Arguments
def greet(name = "World") "Hello, #{name}!" end greet # => "Hello, World!" -- parentheses are optional on call too greet("Philip") # => "Hello, Philip!"
Default arguments work almost identically to PHP and Python โ nothing unusual here. The more interesting difference is keyword arguments, next.
๐ Keyword Arguments
def build_profile(name:, age: 18) "#{name}, age #{age}" end build_profile(name: "Philip", age: 33) # => "Philip, age 33" build_profile(name: "Alex") # => "Alex, age 18" -- age uses its default
name: with no default is a required keyword argument โ calling build_profile without it raises an ArgumentError. This mirrors the symbol-key hash shorthand from Chapter 2 for a reason: keyword arguments and symbol-keyed hashes share the same colon syntax deliberately.
Named Arguments: PHP vs Python vs Ruby
| PHP (8+) | Python | Ruby | |
|---|---|---|---|
| Definition | function f($name, $age = 18) | def f(name, age=18): | def f(name:, age: 18) |
| Call site | f(name: "Philip") | f(name="Philip") | f(name: "Philip") |
| Can require by name? | No โ still positional unless named | Yes, with keyword-only * marker | Yes โ name: with no default is required |
๐ฅ Splat Args โ Variable-Length Arguments
def sum_all(*numbers) # like PHP's ...$args or Python's *args numbers.sum end sum_all(1, 2, 3, 4) # => 10 -- numbers becomes [1, 2, 3, 4]
Ruby's single-splat *args and double-splat **kwargs (for collecting arbitrary keyword arguments into a hash) work almost exactly like Python's identical syntax โ one of the closer matches between the two languages in this chapter.
๐ง Blocks โ Not a Regular Argument
Here's the real departure from PHP and Python. In both of those languages, if you want to pass "a chunk of code" into a function, you pass it as a regular argument โ a closure, an anonymous function, a lambda. In Ruby, every method call can carry an implicit block that isn't a normal argument at all:
def repeat(times) times.times do yield # hands control to whatever block was passed in end end repeat(3) { puts "Hi!" } # prints "Hi!" three times
yield calls whatever block was attached to the current method call โ the block itself doesn't appear anywhere in repeat's parameter list. You can check whether a block was even given with block_given?, and pass values into the block by giving yield arguments:
def describe_each(items) return items unless block_given? # graceful fallback with no block items.each do |item| yield item end end describe_each(["apple", "banana"]) do |fruit| puts "Fruit: #{fruit}" end
Passing "a Chunk of Code": PHP vs Python vs Ruby
| PHP | Python | Ruby | |
|---|---|---|---|
| Mechanism | Closure passed as a normal argument | Lambda/function passed as a normal argument | Implicit block, not a regular argument at all |
| Multi-statement? | Yes, closures can have a full body | No โ lambdas are single-expression only | Yes, blocks can contain any number of statements |
| How the method uses it | Calls the closure variable directly, e.g. $callback() | Calls the function variable directly, e.g. callback() | yield โ the block isn't a named variable at all |
Blocks can be converted into real objects (Procs and Lambdas) that can be stored in a variable and passed around explicitly โ using the & operator you'll have seen hinted at once or twice already. That's genuinely the single most distinctive feature of Ruby as a language, and it gets a full chapter of its own (Chapter 8) once classes and modules are in place. For now, the goal is just recognizing do...end/{ } as a block, and yield as how a method hands control to one.
๐ป Coding Challenges
Challenge 1: Implicit Return with Keyword Arguments
Write a method full_name that takes required keyword arguments first: and last:, and an optional keyword argument middle: defaulting to nil. Return the combined name (handling the case where middle is nil) using implicit return โ no return keyword anywhere in the method.
Goal: Practice keyword arguments and trusting implicit return for the method's final value.
Challenge 2: Splat Args
Write a method average that accepts any number of numeric arguments using *numbers and returns their average. Handle the edge case of zero arguments by returning 0 instead of dividing by zero.
Goal: Get comfortable with *args collecting a variable number of arguments into an array.
Challenge 3: A Method That Yields
Write a method with_timing that takes no arguments, records nothing fancy (no real timer needed) but prints "Starting...", then yields, then prints "Done!". Use block_given? to print "No block given!" instead if no block was passed. Call it once with a block and once without.
Goal: Write your own yield-based method instead of just using one Ruby provides.
๐ฏ What's Next
Next chapter: Arrays & Hashes Deep Dive โ Enumerable methods like map, select, and reduce, which lean heavily on the block mechanics this chapter just introduced.