|||

Rake Routes

by Stephen Ball

Anonymous blocks as function arguments in Ruby

A quick tidbit that comes in handy when you want to write some idiomatic Ruby: how to write methods that accept optional blocks of code to execute.

If you’ve done any Ruby programming you may be familiar with using blocks to scope code execution.

[1,2,3].each { |n| puts n }
# >> 1
# >> 2
# >> 3

# whee!
100.times do
  puts 'Hello, World!'
end

Easy right? In these cases each and times are (essentially) Ruby methods that accept a block as a parameter. Very handy, very easy to read, very clear.  And something we can use in our own code.

Any Ruby method can accept a block. In fact any Ruby method does accept a block, it will just silently ignore it unless it’s told to do something with it.

# follow along in irb!
def eat(meal) "delicious!" end
puts eat(['cheese'])
# => 'delicious!'
puts eat(['cheese', 'steak', 'wine']) { |food| "mmm #{food}" }
# => 'delicious!'

Whoa, see that? Our method just happily said, Yeah, ok. Block parameter. Got it. Not going to even care because I don’t know what to do with it.” As you’ll see, the block is actually being passed into the method; it’s just being passed in secretly. This is great because it means that you have to do very little work to get any method to accept and process a block, but it can be a little surprising until you get used to it.

Let’s jump into your favorite editor and put a bit more into this method. Let’s get it to actually execute our passed block of code for every item of food in the meal. There are two ways to execute a block passed to your function: .call and yield.

yield

def eat(meal)
  meal.each {|food| yield(food)}
  'delicious!'
end

puts eat(['cheese', 'steak', 'wine']) {|food| puts "Mmm, #{food}"}
# >> Mmm, cheese
# >> Mmm, steak
# >> Mmm, wine
# >> delicious!

puts eat(['cheese'])
# ~> -:2:in `block in eat': no block given (yield) (LocalJumpError)

Aww. Now we’ve got our function to call out to a passed block, but it isn’t optional. Well, we can fix that right up.

def eat(meal)
  if block_given?
    meal.each {|food| yield(food)}
  end
  'delicious!'
end

puts eat(['cheese', 'steak', 'wine']) {|food| puts "Mmm, #{food}"}
# >> Mmm, cheese
# >> Mmm, steak
# >> Mmm, wine
# >> delicious!

puts eat(['cheese'])
# >> delicious!

Hooray! Now we have a cool little method that can expand its horizons if we give it a block of code.

.call

Another way to process the passed block is to explicitly list it in the method arguments. The trick here is that a block argument has to be last, and has to be prepended with an ampersand (&). The & is a bit of magic that converts the anonymous block into a Proc that we can directly reference.  Here’s how that works.

def eat(meal, &consume)
  if block_given?
    meal.each {|food| consume.call(food)}
  end
  'delicious!'
end

puts eat(['cheese', 'steak', 'wine']) {|food| puts "Mmm, #{food}"}
# >> Mmm, cheese
# >> Mmm, steak
# >> Mmm, wine
# >> delicious!

puts eat(['cheese'])
# >> delicious!

Since we’ve named our block as an argument, we can analyze it directly for logic flow.

def eat(meal, &consume)
  if consume
    meal.each {|food| consume.call(food)}
  end
  'delicious!'
end

puts eat(['cheese', 'steak', 'wine']) {|food| puts "Mmm, #{food}"}
# >> Mmm, cheese
# >> Mmm, steak
# >> Mmm, wine
# >> delicious!

puts eat(['cheese'])
# >> delicious!

As you can see, we have a few options for how to write our method that accepts an optional block. I don’t know if there are legitimate reasons to prefer one or the other, so choose whichever syntax you prefer.

/record scratch/ Update from the community

Turns out: converting the passed block into a Proc is expensive. If all you are doing with the Proc is calling it, you should consider switching that to the yield syntax. But you do get something cool out of converting to a Proc object: a Proc object. You can call it, store it, pass it somewhere else, or introspect it for doing something clever.

For example, you can ask a Proc about its parameters.

def eat(meal, &consume)
  if consume
    # only call a block with parameters
    unless consume.parameters.empty?
      meal.each {|food| consume.call(food)}
    end
  end
  'delicious!'
end

# If we have a block with defined parameters then we call it
puts eat(['cheese', 'steak', 'wine']) {|food| puts "Mmm, #{food}"}
# >> Mmm, cheese
# >> Mmm, steak
# >> Mmm, wine
# >> delicious!

# But we ignore a block without parameters
puts eat(['cheese', 'steak', 'wine']) { puts "Mmm" }
# >> delicious!

# No block still only returns delicious
puts eat(['cheese'])
# >> delicious!

Thanks to Donald Ball, Ken Collins, and Cameron Desautels for the info and insight!

Up next Parsing Dates and Times from Strings using strptime Howdy ya’ll. As much as we’d prefer to just deal with nicely formatted data; the real world sometimes requires that we parse weird datetime strings
Latest posts Where did the recent Elixir posts go? A subtle Go bug that types cannot help with swapcase with the tr command nice go test output See where vim settings came from Containers in the real world and backpressure in distributed systems Elixir Phoenix and “role postgres does not exist” From awk to a Dockerized Ruby Script Finding leap years with the cal command The Problem of State Clojure Functions in Four Ways See Some Clojure A simple language spec isn’t a feature when you’re building applications The Fastest Possible Tests Shrink your data into bitfields (and out again) Every “if” statement is an object waiting to be extracted Choose Generic Tools Hyperlinks you might find interesting — #4 Running bundle install on rails master Use tldr for command line examples Friday Lunch Links — #3 Friday Lunch Links — #2 Logical Solver: Turn facts into conclusions Programming with jq Command line tools - jq Friday Lunch Links — #1 Why diversity matters Music for coding - October 2019 Code puzzles are a poor way to gauge technical candidates Add vim to a pipeline with vipe Connecting Objects with Observable