/
about_blocks.rb
166 lines (144 loc) · 5.46 KB
/
about_blocks.rb
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
require File.expand_path(File.dirname(__FILE__) + '/neo')
class AboutBlocks < Neo::Koan
# result = yield will return result without requiring another explicit
# reference to result. Is this done for clarity's sake?
def method_with_block
result = yield
result
end
def test_methods_can_take_blocks
yielded_result = method_with_block { 1 + 2 }
assert_equal 3, yielded_result
end
def test_blocks_can_be_defined_with_do_end_too
yielded_result = method_with_block do 1 + 2 end
assert_equal 3, yielded_result
end
# ------------------------------------------------------------------
def method_with_block_arguments
yield("Jim")
end
# This makes sense intuitively -- when yield is invoked with arguments, they
# are passed into the block.
#
# Question? What happens if there's a mismatch between the # of block
# arguments vs. yield arguments?
#
# Answer: any extra block arguments that don't have a corresponding yield
# argument are nil. Any extra yield arguments are ignored by the block.
def test_blocks_can_take_arguments
method_with_block_arguments do |argument|
assert_equal 'Jim', argument
end
end
# ------------------------------------------------------------------
def many_yields
yield(:peanut)
yield(:butter)
yield(:and)
yield(:jelly)
end
# You can't pass in multiple blocks to a method, but a method can invoke the
# block multiple times.
#
# Question: is result "captured" by the block as a closure variable?
#
# Answer: I think so!
#
# Observation: result is a local variable scoped to this test method. It
# can't be accessed within many_yields. However, in closure parlance, result
# is an "open" or "free" variable" that's "enclosed" by the block. Therefore,
# when many_yields invokes yield, it indirectly modifies result.
#
# If Ruby did not support closures, result would be some uninitialized value.
def test_methods_can_call_yield_many_times
result = []
many_yields { |item| result << item }
assert_equal [:peanut, :butter, :and, :jelly], result
end
# ------------------------------------------------------------------
def yield_tester
if block_given?
yield
else
:no_block
end
end
# Introduction to the block_given? method, which is defined in the Ruby
# kernel
def test_methods_can_see_if_they_have_been_called_with_a_block
assert_equal :with_block, yield_tester { :with_block }
assert_equal :no_block, yield_tester
end
# ------------------------------------------------------------------
def test_block_can_affect_variables_in_the_code_where_they_are_created
value = :initial_value
method_with_block { value = :modified_in_a_block }
assert_equal :modified_in_a_block, value
end
# Damnit, earlier I was trying to figure out how to define a block as a
# variable! I kept doing x = {} which would bomb out with a syntax error, as
# it expects it to be a hash. Precede it with the 'lambda' keyword!
#
# Note that call invokes the lambda function. You can use this if you have a
# standalone lambda block as a variable. Otherwise, use yield.
#
# Note the weird alternate syntax of using [] to invoke call.
#
# Lambdas are instances of the Proc class. Careful passing the wrong number
# of arguments to lambdas!
# Straight from the Ruby docs: or procs created using lambda or ->() an error
# is generated if the wrong number of parameters are passed to a Proc with
# multiple parameters. For procs created using Proc.new or Kernel.proc, extra
# parameters are silently discarded.
def test_blocks_can_be_assigned_to_variables_and_called_explicitly
add_one = lambda { |n| n + 1 }
assert_equal 11, add_one.call(10)
# Alternative calling syntax
assert_equal 11, add_one[10]
end
# Note the & syntax to pass a lambda (standalone block) to a method that
# takes a block.
#
# Poetry mode is legal:
#
# method_with_block_arguments &make_upper
#
# This is not legal (you get ArgumentError!):
#
# method_with_block_arguments make_upper
# method_with_block_arguments(make_upper)
def test_stand_alone_blocks_can_be_passed_to_methods_expecting_blocks
make_upper = lambda { |n| n.upcase }
result = method_with_block_arguments(&make_upper)
assert_equal 'JIM', result
end
# ------------------------------------------------------------------
# Here's a method where the block explicitly declares that it accepts a
# block. Note the ampersand again.
#
# Side note: you can pass a block to *any* method, even one that never calls
# yield. Methods can use block_given? to figure out how to behave.
def method_with_explicit_block(&block)
block.call(10)
end
# Note that when a method explicitly declares that it takes a block argument,
# you can pass it either a normal block and a standalone lambda block.
#
# However, this is one case where you *cannot* pass a block to it implicitly
# if you pass it explicitly, i.e. you can't hand it two blocks.
#
# def method_with_explicit_block(&block)
# block.call(10)
# yield(20)
# end
#
# method_with_explicit_block(&block) { |n| n * 4 } # SyntaxError
# method_with_explicit_block({ |n| n * 4 }) # SyntaxError
# method_with_explicit_block { |n| n * 4 } # This is okay!
def test_methods_can_take_an_explicit_block_argument
assert_equal 20, method_with_explicit_block { |n| n * 2 }
add_one = lambda { |n| n + 1 }
assert_equal 11, method_with_explicit_block(&add_one)
end
end