Blocks and Procs

Methods can accept a block of code that is executed
with the yield keyword. For example:

  1. def twice
  2. yield
  3. yield
  4. end
  5. twice do
  6. puts "Hello!"
  7. 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 argument, indicated as a last argument prefixed with ampersand (&):

  1. def twice(&block)
  2. yield
  3. yield
  4. end

To invoke a method and pass a block, you use do ... end or { ... }. All of these are equivalent:

  1. twice() do
  2. puts "Hello!"
  3. end
  4. twice do
  5. puts "Hello!"
  6. end
  7. 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:

  1. foo bar do
  2. something
  3. end
  4. # The above is the same as
  5. foo(bar) do
  6. something
  7. end
  8. foo bar { something }
  9. # The above is the same as
  10. 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:

  1. open file "foo.cr" do
  2. something
  3. end
  4. # Same as:
  5. open(file("foo.cr")) do
  6. end

You wouldn’t want the above to be:

  1. open(file("foo.cr") do
  2. 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:

  1. def twice
  2. yield 1
  3. yield 2
  4. end
  5. twice do |i|
  6. puts "Got #{i}"
  7. end

The above prints “Got 1” and “Got 2”.

A curly braces notation is also available:

  1. twice { |i| puts "Got #{i}" }

You can yield many values:

  1. def many
  2. yield 1, 2, 3
  3. end
  4. many do |x, y, z|
  5. puts x + y + z
  6. end
  7. # Output: 6

A block can specify less than the arguments yielded:

  1. def many
  2. yield 1, 2, 3
  3. end
  4. many do |x, y|
  5. puts x + y
  6. end
  7. # Output: 3

It’s an error specifying more block arguments than those yielded:

  1. def twice
  2. yield
  3. yield
  4. end
  5. twice do |i| # Error: too many block arguments
  6. end

Each block variable has the type of every yield expression in that position. For example:

  1. def some
  2. yield 1, 'a'
  3. yield true, "hello"
  4. yield 2, nil
  5. end
  6. some do |first, second|
  7. # first is Int32 | Bool
  8. # second is Char | String | Nil
  9. end

The block variable second also includes the Nil type because the last yield expression didn’t include a second argument.

Short one-argument syntax

If a block has a single argument and invokes a method on it, the block can be replaced with the short syntax argument.

This:

  1. method do |argument|
  2. argument.some_method
  3. end

and

  1. method { |argument| argument.some_method }

can both be written as:

  1. method &.some_method

Or like:

  1. 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 parameters, the short syntax argument should also be supplied in the method’s argument list.

  1. ["a", "b"].join(",", &.upcase)

Is equivalent to:

  1. ["a", "b"].join(",") { |s| s.upcase }

Arguments can be used with the short syntax argument as well:

  1. ["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))

Operators can be invoked too:

  1. method &.+(2)
  2. method(&.[index])

yield value

The yield expression itself has a value: the last expression of the block. For example:

  1. def twice
  2. v1 = yield 1
  3. puts v1
  4. v2 = yield 2
  5. puts v2
  6. end
  7. twice do |i|
  8. i + 1
  9. 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:

  1. ary = [1, 2, 3]
  2. ary.map { |x| x + 1 } #=> [2, 3, 4]
  3. ary.select { |x| x % 2 == 1 } #=> [1, 3]

A dummy transformation method:

  1. def transform(value)
  2. yield value
  3. end
  4. 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:

  1. def transform_int(start : Int32, &block : Int32 -> Int32)
  2. result = yield start
  3. result * 2
  4. end
  5. transform_int(3) { |x| x + 2 } #=> 10
  6. 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:

  1. def thrice
  2. puts "Before 1"
  3. yield 1
  4. puts "Before 2"
  5. yield 2
  6. puts "Before 3"
  7. yield 3
  8. puts "After 3"
  9. end
  10. thrice do |i|
  11. if i == 2
  12. break
  13. end
  14. 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:

  1. def twice
  2. yield 1
  3. yield 2
  4. end
  5. twice { |i| i + 1 } #=> 3
  6. 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 breaks:

  1. value = twice do |i|
  2. if i == 1
  3. break "hello"
  4. end
  5. i + 1
  6. end
  7. value #:: Int32 | String

If a break receives many arguments, they are automatically transformed to a Tuple:

  1. values = twice { break 1, 2 }
  2. values #=> {1, 2}

If a break receives no arguments, it’s the same as receiving a single nil argument:

  1. value = twice { break }
  2. value #=> nil

next

The next expression inside a block exits early from the block (not the method). For example:

  1. def twice
  2. yield 1
  3. yield 2
  4. end
  5. twice do |i|
  6. if i == 1
  7. puts "Skipping 1"
  8. next
  9. end
  10. puts "Got #{i}"
  11. end
  12. # Ouptut:
  13. # Skipping 1
  14. # Got 2

The next expression accepts arguments, and these give the value of the yield expression that invoked the block:

  1. def twice
  2. v1 = yield 1
  3. puts v1
  4. v2 = yield 2
  5. puts v2
  6. end
  7. twice do |i|
  8. if i == 1
  9. next 10
  10. end
  11. i + 1
  12. end
  13. # Output
  14. # 10
  15. # 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:

  1. class Foo
  2. def one
  3. 1
  4. end
  5. def yield_with_self
  6. with self yield
  7. end
  8. def yield_normally
  9. yield
  10. end
  11. end
  12. def one
  13. "one"
  14. end
  15. Foo.new.yield_with_self { one } # => 1
  16. Foo.new.yield_normally { one } # => "one"

Unpacking block arguments

A block argument can specify sub-arguments enclosed in parentheses:

  1. array = [{1, "one"}, {2, "two"}]
  2. array.each do |(number, word)|
  3. puts "#{number}: #{word}"
  4. end

The above is simply syntax sugar of this:

  1. array = [{1, "one"}, {2, "two"}]
  2. array.each do |arg|
  3. number = arg[0]
  4. word = arg[1]
  5. puts "#{number}: #{word}"
  6. end

That means that any type that responds to [] with integers can be unpacked in a block argument.

For Tuple arguments you can take advantage of auto-splatting and do not need parentheses:

  1. array = [{1, "one", true}, {2, "two", false}]
  2. array.each do |number, word, bool|
  3. puts "#{number}: #{word} #{bool}"
  4. end

Hash(K, V)#each:Nil-instance-method) passes Tuple(K, V) to the block so iterating key-value pairs works with auto-splatting:

  1. h = {"foo" => "bar"}
  2. h.each do |key, value|
  3. key # => "foo"
  4. value # => "bar"
  5. end

Performance

When using blocks with yield, the blocks are always inlined: no closures, calls or function pointers are involved. This means that this:

  1. def twice
  2. yield 1
  3. yield 2
  4. end
  5. twice do |i|
  6. puts "Got: #{i}"
  7. end

is exactly the same as writing this:

  1. i = 1
  2. puts "Got: #{i}"
  3. i = 2
  4. puts "Got: #{i}"

For example, the standard library includes a times method on integers, allowing you to write:

  1. 3.times do |i|
  2. puts i
  3. end

This looks very fancy, but is it as fast as a C for loop? The answer is: yes!

This is Int#times definition:

  1. struct Int
  2. def times
  3. i = 0
  4. while i < self
  5. yield i
  6. i += 1
  7. end
  8. end
  9. end

Because a non-captured block is always inlined, the above method invocation is exactly the same as writing this:

  1. i = 0
  2. while i < 3
  3. puts i
  4. i += 1
  5. end

Have no fear using blocks for readability or code reuse, it won’t affect the resulting executable performance.