ruby – ruby blocks and yield statement

Ruby Blocks

Ruby has a concept of Block.

  • A block consists of chunks of code.
  • You assign a name to a block.
  • The code in the block is always enclosed within braces {}.
  • A block is always invoked from a function with the same name as that of the block. This means that if you have a block with the name test, then you use the function test to invoke this block.
  • You invoke a block by using the yield statement.
1
2
3
4
5
block_name {
statement1
statement2
..........
}

The yield Statement

yield is a statement in ruby that is widely used in order to share common logic.

Imagine that you have two huge business logic that is only different from each other in a few couple lines. How to avoid checking in the 95% shared logic twice? You definitely don’t want to write code like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def 
// 1000 lines logic X

param_A.special_logic_A

// 1000 lines logic Y
end

def handle_B
// 1000 lines logic X

param_B.special_logic_B

// 1000 lines logic Y
end

Wouldn’t it nice to be

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def 
handle(special_logic_of_A)
end

def handle_B
handle(special_logic_of_B)
end

def handle(logic)
// 1000 lines logic X

run_special_logic(logic)

// 1000 lines logic Y
end

In Java, you can a chieve it either with Java 8 new lambda feature (highly recommended, because it gives you much more flexibility) or executing methods in interface/abstract class defined in both classes (old style before lambda is available).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void () {
handle(t -> t.doSomething());
}

public void handle_B() {
handle(t -> t.doSomethingElse());
}

public void handle(Consumer<T> c) {


c.accept();

// 1000 lines logic Y
}

How can you achieve this in Ruby? With yield!

From the name you can tell that, just like yielding to people in a highway traffic, it’s about yielding to an inserted/passed-in statement when program hit this key word. yeild statement can take either no params or any number of params.

yield with no params

1
2
3
4
5
6
7
8
9
10
def test
puts "xxx"

yield

puts "yyy"

yield
end
test { puts "000" }

Output:

1
2
3
4
xxx
000
yyy
000

You can see that yield will be replaced by statement puts "000"

yield with params

Here’s a simple example of yield taking 1 param

1
2
3
4
5
6
7
8
9
def test
yield "xxx"

puts "000"

yield "yyy"
end

test {|i| puts "#{i}"}
1
2
3
xxx
000
yyy

The format to rake two params is:

1
yield a, b

and the block is:

1
test {|a, b| statement}

Here’s an example to encrypt and decrypt a yaml file. Most logic is the same between processes of encrypting and decrypting. So we use yield to abstract that and maintain the common logic in only one copy.

The first version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'yaml'

class YamlInterpreter
def initialize(encryption_service)
@encryption_service = encryption_service
end

def decrypt_yaml(file_path)
interpret_yaml(file_path) { |k, v| v = @encryption_service.decryptIfNecessary(k, v) }
end

def encrypt_yaml(file_path)
interpret_yaml(file_path) { |k, v| v = @encryption_service.encryptIfNecessary(k, v) }
end

def interpret_yaml(file_path)
begin
configs = YAML::load(yaml_file_path.read)

configs.each do |k,v|
yield k, v
configs[k] = v
end

File.open(file_path, "w") { |f| YAML.dump(configs, f) }
rescue Exception => ex
raise ex, "Failed interpreting file #{file_path}.n#{ex.class}: #{ex.message}n#{ex.backtrace.join("n")}"
end
end
end

The problem with the first version is that you’ll find your new file doesn’t have all the updated encrypted/decrypted values. The root cause is the valid scope of a yield statement. In the above example, the updated v only lives in the line 9 or line 13, and doesn’t last when jumping back to the context in line 22.

The fix is to pass in not only the k-v pair, but also the Hash object itself to make sure the changes persist.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'yaml'

class YamlInterpreter
def initialize(encryption_service)
@encryption_service = encryption_service
end

def decrypt_yaml(file_path)
interpret_yaml(file_path) { |configs, k, v| configs[k] = @encryption_service.decryptIfNecessary(k, v) }
end

def encrypt_yaml(file_path)
interpret_yaml(file_path) { |configs, k, v| configs[k] = @encryption_service.encryptIfNecessary(k, v) }
end

def interpret_yaml(file_path)
begin
configs = YAML::load(yaml_file_path.read)

configs.each do |k,v|
yield configs, k, v
configs[k] = v
end

File.open(file_path, "w") { |f| YAML.dump(configs, f) }
rescue Exception => ex
raise ex, "Failed interpreting file #{file_path}.n#{ex.class}: #{ex.message}n#{ex.backtrace.join("n")}"
end
end
end