Ruby 2.5: yield_self

Posted by on December 19, 2017

yield_self is coming to Ruby 2.5. What is this long requested feature, and how does it work?

Some features take a while to get into a Ruby release. As you can see from the original request on Ruby’s Redmine issue tracker, yield_self has been brewing for 5 years. It has been waiting on a good name, and the Ruby team has settled on one.

But what is it? To understand it, perhaps it is best to look at some existing features.
Ruby blocks are one of the ways of passing around functions in Ruby. Every method parameter list has an implicit block argument, which can be used within the method. One of the ways of activating a block is to call yield. For example:

class GiftGiver
  ...
  def deliver(gift, &additional_behaviour)
    raise NoGiftError if @gift_tracker[gift.name] == 0
    @gift_tracker[gift.name] -= 1
    yield
  end
end

gifter = GiftGiver.new(gift)

# Santa Claus
gifter.deliver(new_ruby_version) do |gift|
  puts “Ho ho ho, a holly Merry Christmas. Here’s your #{gift}”
end

# Ruby core team
gifter.deliver(new_ruby_version) do |gift|
  puts “メリークリスマス!”
end

The tap method was introduced in Ruby 1.9, and allows you to inspect on and act on a chain of methods. You can see how it’s used in the documentation.

(1..10)                .tap {|x| puts "original: #{x.inspect}"}
  .to_a                .tap {|x| puts "array: #{x.inspect}"}
  .select {|x| x%2==0} .tap {|x| puts "evens: #{x.inspect}"}
  .map {|x| x*x}       .tap {|x| puts "squares: #{x.inspect}"}
“original: 1..10”
“array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
“evens: [2, 4, 6, 8, 10]
“squares:  [1,4,9,16,25,36,49,64,81,100]”
=> [1,4,9,16,25,36,49,64,81,100]

The advantage of tap is that it always returns the value of the method tap was called on. This means in the above chain, apart from the puts calls, the result is the same as doing:

(1..10)
  .to_a
  .select {|x| x%2==0}
  .map {|x| x*x}
=> [1,4,9,16,25,36,49,64,81,100]

So, where does yield_self come in? It works similarly to tap, but it returns the value of the block you execute each time.

4.tap { |num| num*num } #=> 4
4.yield_self { |num| num*num } #=> 16

This lets you chain together methods that ordinarily wouldn’t be chainable. An example of this is using class methods:

filename.yield_self {|filename| File.open(filename)}.
               yield_self {|file| file.read}.
               yield_self {|contents| YAML.load(contents)}.
               yield_self {|yaml| yaml.fetch[“spec_directory”]}

To do this naively without yield_self, you can nest arguments:

YAML.load(File.open(filename).read).fetch[“spec_directory”]

So, the trade off here is between a sequence and nesting arguments. There may be some cases where having some sequence methods allows you to be either more expressive or more comprehensible.

You can see a good discussion of this over on Michał Łomnicki’s blog. We’re interested to see where this goes and how frequently it is used!

Tagged

About the author…

James (he/him) is the Engineering Manager for the Practice Growth team, and has been at FreeAgent for over 10 years. Based in Edinburgh, he likes cycling, eating food and learning from the history of computing.

See all posts by James

Leave a reply

Your email address will not be published. Required fields are marked *