65

Closures in Ruby: Blocks, Procs and Lambdas

 5 years ago
source link: https://www.tuicool.com/articles/hit/Q73aUvy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In Ruby Magic we love to dive into the magic behind the things we use every day to understand how they work. In this edition, we’ll explore the differences between blocks, procs and lambdas.

In programming languages with first-class functions, functions can be stored in variables and passed as arguments to other functions. Functions can even use other functions as their return values.

A closure is a first-class function with an environment. The environment is a mapping to the variables that existed when the closure was created. The closure will retain its access to these variables, even if they’re defined in another scope.

Ruby doesn’t have first-class functions, but it does have closures in the form of blocks, procs and lambdas. Blocks are used for passing blocks of code to methods, and procs and lambda’s allow storing blocks of code in variables.

Blocks

In Ruby, blocks are snippets of code that can be created to be executed later. Blocks are passed to methods that yield them within the do and end keywords. One of the many examples is the #each method, which loops overenumerable objects.

[1,2,3].each do |n|
  puts "#{n}!"
end

[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.

In this example, a block is passed to the Array#each method, which runs the block for each item in the array and prints it to the console.

def each
  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

In this simplified example of Array#each , in the while loop, yield is called to execute the passed block for every item in the array. Note that this method has no arguments, as the block is passed to the method implicitly.

Implicit Blocks and the yield Keyword

In Ruby, methods can take blocks implicitly and explicitly. Implicit block passing works by calling the yield keyword in a method. The yield keyword is special. It finds and calls a passed block, so you don’t have to add the block to the list of arguments the method accepts.

Because Ruby allows implicit block passing, you can call all methods with a block. If it doesn’t call yield , the block is ignored.

irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]

If the called method does yield, the passed block is found and called with any arguments that were passed to the yield keyword.

def each
  return to_enum(:each) unless block_given?

  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

This example returns an instance of Enumerator unless a block is given.

The yield and block_given? keywords find the block in the current scope. This allows passing blocks implicitly, but prevents the code from accessing the block directly as it’s not stored in a variable.

Explicitly Passing Blocks

We can explicitly accept a block in a method by adding it as an argument using an ampersand parameter (usually called &block ). Since the block is now explicit, we can use the #call method directly on the resulting object instead of relying on yield .

The &block argument is not a proper argument, so calling this method with anything else than a block will produce an ArgumentError .

def each_explicit(&block)
  return to_enum(:each) unless block

  i = 0
  while i < size
    block.call at(i)
    i += 1
  end
end

When a block is passed like this and stored in a variable, it is automatically converted to a proc .

Procs

A “proc” is an instance of the Proc class, which holds a code block to be executed, and can be stored in a variable. To create a proc, you call Proc.new and pass it a block.

proc = Proc.new { |n| puts "#{n}!" }

Since a proc can be stored in a variable, it can also be passed to a method just like a normal argument. In that case, we don’t use the ampersand, as the proc is passed explicitly.

def run_proc_with_random_number(proc)
  proc.call(random)
end

proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)

Instead of creating a proc and passing that to the method, you can use Ruby’s ampersand parameter syntax that we saw earlier and use a block instead.

def run_proc_with_random_number(&proc)
  proc.call(random)
end

run_proc_with_random_number { |n| puts "#{n}!" }

Note the added ampersand to the argument in the method. This will convert a passed block to a proc object and store it in a variable in the method scope.

Tip : While it’s useful to have the proc in the method in some situations, the conversion of a block to a proc produces a performance hit. Whenever possible, use implicit blocks instead.

#to_proc

Symbols, hashes and methods can be converted to procs using their #to_proc methods. A frequently seen use of this is passing a proc created from a symbol to a method.

[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }

This example shows three equivalent ways of calling #to_s on each element of the array. In the first one, a symbol, prefixed with an ampersand, is passed, which automatically converts it to a proc by calling its #to_proc method. The last two show what that proc could look like.

class Symbol
  def to_proc
    Proc.new { |i| i.send(self) }
  end
end

Although this is a simplified example, the implementation of Symbol#to_proc shows what’s happening under the hood. The method returns a proc which takes one argument and sends self to it. Since self is the symbol in this context, it calls the Integer#to_s method.

Lambdas

Lambdas are essentially procs with some distinguishing factors. They are more like “regular” methods in two ways: they enforce the number of arguments passed when they’re called and they use “normal” returns.

When calling a lambda that expects an argument without one, or if you pass an argument to a lambda that doesn’t expect it, Ruby throws an ArgumentError .

irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
        from (irb):8:in `block in irb_binding'
        from (irb):8
        from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'

Also, a lambda treats the return keyword the same way a method does. When calling a proc, the program yields control to the code block in the proc. So, if the proc returns, the current scope returns. If a proc is called inside a function and calls return , the function immediately returns as well.

def return_from_proc
  a = Proc.new { return 10 }.call
  puts "This will never be printed."
end

This function will yield control to the proc, so when it returns, the function returns. Calling the function in this example will never print the output and return 10.

def return_from_lambda
  a = lambda { return 10 }.call
  puts "The lambda returned #{a}, and this will be printed."
end

When using a lambda, it will be printed. Calling return in the lambda will behave like calling return in a method, so the a variable is populated with 10 and the line is printed to the console.

Blocks, procs and lambdas

Now that we’ve gone all the way into both blocks, procs and lambdas, let’s zoom back out and summarize the comparison.

yield

This concludes our look into closures in Ruby. There’s more to learn about closures like lexical scopes and bindings, but we’ll keep that for a future episode. In the meantime, please let us know what you’d like to read about in a future installment of Ruby Magic, closures or otherwise at @AppSignal .


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK