Blocks and Procs
Methods can accept a block of code that is executed
with the yield
keyword. For example:
def twice
yield
yield
end
twice do
puts "Hello!"
end
The above program prints “Hello!” twice, once for each yield
.
To define a method that receives a block, simply use yield
inside it and the compiler will know. You can make this more evident by declaring a dummy block parameter, indicated as a last parameter prefixed with ampersand (&
):
def twice(&block)
yield
yield
end
To invoke a method and pass a block, you use do ... end
or { ... }
. All of these are equivalent:
twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
The difference between using do ... end
and { ... }
is that do ... end
binds to the left-most call, while { ... }
binds to the right-most call:
foo bar do
something
end
# The above is the same as
foo(bar) do
something
end
foo bar { something }
# The above is the same as
foo(bar { something })
The reason for this is to allow creating Domain Specific Languages (DSLs) using do ... end
to have them be read as plain English:
open file "foo.cr" do
something
end
# Same as:
open(file("foo.cr")) do
something
end
You wouldn’t want the above to be:
open(file("foo.cr") do
something
end)
Overloads
Two methods, one that yields and another that doesn’t, are considered different overloads, as explained in the overloading section.
Yield arguments
The yield
expression is similar to a call and can receive arguments. For example:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got #{i}"
end
The above prints “Got 1” and “Got 2”.
A curly braces notation is also available:
twice { |i| puts "Got #{i}" }
You can yield
many values:
def many
yield 1, 2, 3
end
many do |x, y, z|
puts x + y + z
end
# Output: 6
A block can specify fewer parameters than the arguments yielded:
def many
yield 1, 2, 3
end
many do |x, y|
puts x + y
end
# Output: 3
It’s an error specifying more block parameters than the arguments yielded:
def twice
yield
yield
end
twice do |i| # Error: too many block parameters
end
Each block parameter has the type of every yield expression in that position. For example:
def some
yield 1, 'a'
yield true, "hello"
yield 2, nil
end
some do |first, second|
# first is Int32 | Bool
# second is Char | String | Nil
end
The underscore is also allowed as a block parameter:
def pairs
yield 1, 2
yield 2, 4
yield 3, 6
end
pairs do |_, second|
print second
end
# Output: 246
Short one-parameter syntax
If a block has a single parameter and invokes a method on it, the block can be replaced with the short syntax argument.
This:
method do |param|
param.some_method
end
and
method { |param| param.some_method }
can both be written as:
method &.some_method
Or like:
method(&.some_method)
In either case, &.some_method
is an argument passed to method
. This argument is syntactically equivalent to the block variants. It is only syntactic sugar and does not have any performance penalty.
If the method has other required arguments, the short syntax argument should also be supplied in the method’s argument list.
["a", "b"].join(",", &.upcase)
Is equivalent to:
["a", "b"].join(",") { |s| s.upcase }
Arguments can be used with the short syntax argument as well:
["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))
Operators can be invoked too:
method &.+(2)
method(&.[index])
yield value
The yield
expression itself has a value: the last expression of the block. For example:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
i + 1
end
The above prints “2” and “3”.
A yield
expression’s value is mostly useful for transforming and filtering values. The best examples of this are Enumerable#map and Enumerable#select:
ary = [1, 2, 3]
ary.map { |x| x + 1 } # => [2, 3, 4]
ary.select { |x| x % 2 == 1 } # => [1, 3]
A dummy transformation method:
def transform(value)
yield value
end
transform(1) { |x| x + 1 } # => 2
The result of the last expression is 2
because the last expression of the transform
method is yield
, whose value is the last expression of the block.
Type restrictions
The type of the block in a method that uses yield
can be restricted using the &block
syntax. For example:
def transform_int(start : Int32, &block : Int32 -> Int32)
result = yield start
result * 2
end
transform_int(3) { |x| x + 2 } # => 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String
break
A break
expression inside a block exits early from the method:
def thrice
puts "Before 1"
yield 1
puts "Before 2"
yield 2
puts "Before 3"
yield 3
puts "After 3"
end
thrice do |i|
if i == 2
break
end
end
The above prints “Before 1” and “Before 2”. The thrice
method didn’t execute the puts "Before 3"
expression because of the break
.
break
can also accept arguments: these become the method’s return value. For example:
def twice
yield 1
yield 2
end
twice { |i| i + 1 } # => 3
twice { |i| break "hello" } # => "hello"
The first call’s value is 3 because the last expression of the twice
method is yield
, which gets the value of the block. The second call’s value is “hello” because a break
was performed.
If there are conditional breaks, the call’s return value type will be a union of the type of the block’s value and the type of the many break
s:
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value # :: Int32 | String
If a break
receives many arguments, they are automatically transformed to a Tuple:
values = twice { break 1, 2 }
values # => {1, 2}
If a break
receives no arguments, it’s the same as receiving a single nil
argument:
value = twice { break }
value # => nil
next
The next
expression inside a block exits early from the block (not the method). For example:
def twice
yield 1
yield 2
end
twice do |i|
if i == 1
puts "Skipping 1"
next
end
puts "Got #{i}"
end
# Output:
# Skipping 1
# Got 2
The next
expression accepts arguments, and these give the value of the yield
expression that invoked the block:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
if i == 1
next 10
end
i + 1
end
# Output
# 10
# 3
If a next
receives many arguments, they are automatically transformed to a Tuple. If it receives no arguments it’s the same as receiving a single nil
argument.
with … yield
A yield
expression can be modified, using the with
keyword, to specify an object to use as the default receiver of method calls within the block:
class Foo
def one
1
end
def yield_with_self
with self yield
end
def yield_normally
yield
end
end
def one
"one"
end
Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one } # => "one"
Unpacking block parameters
A block parameter can specify sub-parameters enclosed in parentheses:
array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
puts "#{number}: #{word}"
end
The above is simply syntax sugar of this:
array = [{1, "one"}, {2, "two"}]
array.each do |arg|
number = arg[0]
word = arg[1]
puts "#{number}: #{word}"
end
That means that any type that responds to []
with integers can be unpacked in a block parameter.
For Tuple parameters you can take advantage of auto-splatting and do not need parentheses:
array = [{1, "one", true}, {2, "two", false}]
array.each do |number, word, bool|
puts "#{number}: #{word} #{bool}"
end
Hash(K, V)#each:Nil-instance-method) passes Tuple(K, V)
to the block so iterating key-value pairs works with auto-splatting:
h = {"foo" => "bar"}
h.each do |key, value|
key # => "foo"
value # => "bar"
end
Performance
When using blocks with yield
, the blocks are always inlined: no closures, calls or function pointers are involved. This means that this:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got: #{i}"
end
is exactly the same as writing this:
i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"
For example, the standard library includes a times
method on integers, allowing you to write:
3.times do |i|
puts i
end
This looks very fancy, but is it as fast as a C for loop? The answer is: yes!
This is Int#times
definition:
struct Int
def times
i = 0
while i < self
yield i
i += 1
end
end
end
Because a non-captured block is always inlined, the above method invocation is exactly the same as writing this:
i = 0
while i < 3
puts i
i += 1
end
Have no fear using blocks for readability or code reuse, it won’t affect the resulting executable performance.