Methods and blocks
This chapter introduces methods (named, reusable pieces of code) and blocks (pieces of code that you pass to a method). This is a fundamental chapter: from now on, we can structure our code instead of writing everything in a single script.
Principle
Up until now, all our code ran from top to bottom. If we wanted to reuse a calculation or a display, we had to copy-paste the lines. This is fragile: if we fix a bug, we have to fix it everywhere.
A method solves this problem. It is a set of instructions that you name once, and can call as many times as you want. You can also pass it parameters to make it flexible.
A block is a piece of code that you pass to a method. We have already used them without knowing it with .each, .map, .select: the code between { } or do...end is a block.
Defining a method
You create a method with def, a name, and end:
def greet
puts 'Hello, trainer!'
end
greet # Prints: Hello, trainer!
greet # Prints: Hello, trainer!
def greetdeclares the method. The code inside is not executed at that point.greetcalls the method. The code runs on each call.- Method names follow the
snake_caseconvention, just like variables.
Parameters
A method can receive parameters that make it flexible:
def greet_pokemon(name)
puts "Go, #{name} !"
end
greet_pokemon('Pikachu') # Prints: Go, Pikachu !
greet_pokemon('Charizard') # Prints: Go, Charizard!
nameis a parameter. When you call the method, you pass a value that replaces this parameter.- If you call
greet_pokemonwithout an argument, Ruby raises an error.
Default values
You can give a parameter a default value:
def greet_pokemon(name, greeting = 'Go')
puts "#{greeting}, #{name} !"
end
greet_pokemon('Pikachu') # Prints: Go, Pikachu !
greet_pokemon('Pikachu', 'Onward') # Prints: En avant, Pikachu !
greeting = 'Go': if the argument is not provided,greetingdefaults to'Go'.
Named parameters (keyword)
For methods with multiple optional parameters, named parameters are more readable:
def create_pokemon(name, level:, type: :normal)
return { name: name, level: level, type: type }
end
pikachu = create_pokemon('Pikachu', level: 25, type: :electric)
rattata = create_pokemon('Rattata', level: 3)
p pikachu # => {:name=>"Pikachu", :level=>25, :type=>:electric}
p rattata # => {:name=>"Rattata", :level=>3, :type=>:normal}
level:is a required named parameter (no default value).type: :normalis a named parameter with a default value.- At the call site, you write
level: 25instead of just25. It is longer but much clearer when there are multiple parameters.
The splat *args
The splat collects a variable number of arguments into an Array:
def display_team(trainer, *pokemon_names)
puts "Team of #{trainer}:"
pokemon_names.each do |pokemon_name|
puts " - #{pokemon_name}"
end
end
display_team('Ash', 'Pikachu', 'Charizard', 'Blastoise')
*pokemon_namesgathers all arguments aftertrainerinto an Array.- You can pass 0, 1, or 100 names -- the splat adapts.
The double-splat **options
The double-splat collects undeclared named parameters into a Hash:
def create_pokemon(name, level:, **stats)
pokemon = { name: name, level: level }
return pokemon.merge(stats)
end
pikachu = create_pokemon('Pikachu', level: 25, attack: 55, speed: 90)
p pikachu # => {:name=>"Pikachu", :level=>25, :attack=>55, :speed=>90}
**statscatchesattack: 55, speed: 90into a Hash{ attack: 55, speed: 90 }..merge(stats)merges this Hash with the base Hash.
Return values
Every Ruby method returns a value. You can return it explicitly with return:
def calculate_damage(attack, defense)
return attack - defense
end
damage = calculate_damage(55, 30)
puts "Damage: #{damage}" # Prints: Dégâts : 25
Without return, Ruby automatically returns the last evaluated expression:
def calculate_damage(attack, defense)
attack - defense
end
Both forms work. In this tutorial series, we use return explicitly so the intent is clear, especially for beginners.
return can also be used to exit a method early:
def heal(pokemon)
if pokemon[:hp] >= pokemon[:max_hp]
puts "#{pokemon[:name]} already has full HP!"
return
end
pokemon[:hp] = pokemon[:max_hp]
puts "#{pokemon[:name]} is healed!"
end
- The first
returnexits the method immediately if the Pokemon is already at maximum HP. The rest of the code is not executed.
Blocks in depth
We have been using blocks since chapter 3 with .each, .map, .select. Let's see how to create our own methods that accept blocks.
yield -- execute the block
yield allows a method to execute the block that was passed to it:
def with_announcement(pokemon_name)
puts "--- Start ---"
yield(pokemon_name)
puts "--- End ---"
end
with_announcement('Pikachu') do |name|
puts "#{name} uses Thunderbolt!"
end
Outputs:
--- Start ---
Pikachu uses Thunderbolt!
--- End ---
yield(pokemon_name)executes the block, passingpokemon_nameas an argument.- The block receives this argument in
|name|. - The method controls what happens before and after the block.
block_given? -- check if a block was passed
def display_pokemon(pokemon)
puts "#{pokemon[:name]} Lvl.#{pokemon[:level]}"
if block_given?
yield(pokemon)
else
puts " Type : #{pokemon[:type]}"
end
end
pikachu = { name: 'Pikachu', level: 25, type: :electric, hp: 35, max_hp: 55 }
# Without a block: default display
display_pokemon(pikachu)
# With a block: custom display
display_pokemon(pikachu) do |pokemon|
puts " PV : #{pokemon[:hp]}/#{pokemon[:max_hp]}"
end
block_given?returnstrueif a block was passed,falseotherwise.- This allows having a default behavior that the caller can customize.
&block -- capture the block as an object
The & prefix converts a block into a Proc object, which can be stored and called with .call:
def find_pokemon(team, &condition)
team.each do |pokemon|
return pokemon if condition.call(pokemon)
end
return nil
end
team = [
{ name: 'Pikachu', level: 25 },
{ name: 'Charizard', level: 36 },
{ name: 'Blastoise', level: 40 }
]
result = find_pokemon(team) { |pokemon| pokemon[:level] >= 35 }
puts "Found: #{result[:name]}" # Prints: Found: Charizard
&conditioncaptures the block and transforms it into a Proc object stored incondition.condition.call(pokemon)executes this Proc withpokemonas an argument.- The difference from
yield: the Proc can be stored in a variable or passed to another method.
Proc and lambda
Ruby also allows creating "standalone" blocks that you store in variables:
# Lambda (recommended)
calculate = lambda do |attack, defense|
return attack - defense
end
puts calculate.call(55, 30) # => 25
# Shorthand syntax (stabby lambda)
calculate = ->(attack, defense) { attack - defense }
puts calculate.call(55, 30) # => 25
- A lambda is a standalone block stored in a variable. You call it with
.call. ->() { }is a shorthand forlambda do ... end.
There is also Proc.new, which works similarly but with an important difference: return inside a Proc exits the enclosing method, while return inside a lambda only exits the lambda. For this reason, prefer lambda which is more predictable.
You can pass a lambda to a method that expects a block using &:
high_level = ->(pokemon) { pokemon[:level] >= 35 }
result = team.select(&high_level)
&high_levelconverts the lambda into a block to pass it to.select.
Conclusion
def method_name(params) ... enddefines a method. Never on a single line.- Parameters can be required, with defaults, named (
key:), splat (*args), or double-splat (**options). - Use
returnexplicitly to clarify what the method returns. yieldexecutes the block passed to the method.block_given?checks its presence.&blockcaptures the block as a Proc for storing or forwarding.- A lambda is a standalone block stored in a variable. Prefer
lambdaoverProc.new. &variableconverts a lambda/Proc into a block to pass it to a method.