Writing a Method That Accepts a Block

Problem

You want to write a method that can accept and call an attached code block: a method that works like Array#each, Fixnum#upto, and other built-in Ruby methods.

Solution

You don't need to do anything special to make your method capable of accepting a block. Any method can use a block if the caller passes one in. At any time in your method, you can call the block with yield:

	def call_twice
	 puts "I'm about to call your block."
	 
yield
	 puts "I'm about to call your block again."
	 yield
	end

	call_twice { puts "Hi, I'm a talking 
code block." }
	# I'm about to call your block.
	# Hi, I'm a talking code block.
	# I'm about to call your block again.
	# Hi, I'm a talking code block.

Another example:

	def repeat(n)
	 if block_given?
	 n.times { yield }
	 else
	 raise ArgumentError.new("I can't repeat a block you don't give me!")
	 end
	end

	repeat(4) { puts "Hello." }
	# Hello.
	# Hello.
	# Hello.
	# Hello.

	repeat(4)
	# ArgumentError: I can't repeat a block you don't give me!

 

Discussion

Since Ruby focuses so heavily on iterator methods and other methods that accept code blocks, it's important to know how to use code blocks in your own methods.

You don't have to do anything special to make your method capable of taking a code block. A caller can pass a code block into any Ruby method; it's just that there's no point in doing that if the method never invokes yield.

	puts("Print this message.") { puts "And also run this code block!" }
	# Print this message.

The yield keyword acts like a special method, a stand-in for whatever code block was passed in. When you call it, it's exactly as the code block were a Proc object and you had invoked its call method.

This may seem mysterious if you're unfamiliar with the practice of passing blocks around, but it is usually the preferred method of calling blocks in Ruby. If you feel more comfortable receiving a code block as a "real" argument to your method, see Recipe 7.3.

You can pass in arguments to yield (they'll be passed to the block) and you can do things with the value of the yield statement (this is the value of the last statement in the block).

Here's a method that passes arguments into its code block, and uses the value of the block:

	def call_twice
	 puts "Calling your block."
	 ret1 = yield("very first")
	 puts "The value of your block: #{ret1}"

	 puts "Calling your block again."
	 ret2 = yield("second")
	 puts "The value of your block: #{ret2}"
	end

	call_twice do |which_time|
	 puts "I'm a code block, called for the #{which_time} time."
	 which_time == "very first" ? 1 : 2
	end
	# Calling your block.
	# I'm a code block, called for the very first time.
	# The value of your block: 1
	# Calling your block again.
	# I'm a code block, called for the second time.
	# The value of your block: 2

Here's a more realistic example. The method Hash#find takes a code block, passes each of a hash's key-value pairs into the code block, and returns the first key-value pair for which the code block evaluates to true.

	squares = {0=>0, 1=>1, 2=>4, 3=>9}
	squares.find { |key, value| key > 1 } # => [2, 4]

Suppose we want a method that works like Hash#find, but returns a new hash containing all the key-value pairs for which the code block evaluates to true. We can do this by passing arguments into the yield statement and using its result:

	class Hash
	 def find_all
	 new_hash = Hash.new
	 each { |k,v| new_hash[k] = v if yield(k, v) }
	 new_hash
	 end
	end

	squares.find_all { |key, value| key > 1 } # => {2=>4, 3=>9}

As it turns out, the Hash#delete_if method already does the inverse of what we want. By negating the result of our code block, we can make Hash#delete_if do the job of Hash#find_all. We just need to work off of a duplicate of our hash, because delete_if is a destructive method:

	squares.dup.delete_if { |key, value| key > 1 } # => {0=>0, 1=>1}
	squares.dup.delete_if { |key, value| key <= 1 } # => {2=>4, 3=>9}

Hash#find_all turns out to be unnecessary, but it made for a good example.

You can write a method that takes an optional code block by calling Kernel#block_given? from within your method. That method returns true only if the caller of your method passed in a code block. If it returns false, you can raise an exception, or you can fall back to behavior that doesn't need a block and never uses the yield keyword.

If your method calls yield and the caller didn't pass in a code block, Ruby will throw an exception:

	[1, 2, 3].each
	# LocalJumpError: no block given

 

See Also

  • Recipe 7.3, " Binding a Block Argument to a Variable"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net