One More step in Ruby metaprogramming: Closure, Block, Proc, Lambda, Method
Bài đăng này đã không được cập nhật trong 3 năm
The other day I delved more into Ruby metaprogramming from which I encounter concepts such as Closure
,Block
,Proc
,Lamda
and Method
, which I think I know but at the same time perplexing and fascinating. One cannot just get away with it if Ruby is to be understood in great depth.
1.What is a closure?
I'll bring in a very good definition from StackOverFlow.com:
A closure is a persistent scope which holds on to local variables even after the code execution has moved out of that block. Languages which support closure (such as JavaScript, Swift and Ruby) will allow you to keep a reference to a scope (including its parent scopes), even after the block in which those variables were declared has finished executing, provided you keep a reference to that block or function somewhere.
The scope object, and all it's local variables is tied to the function, and will persist as long as that function persists.
This gives us function portability. We can expect any variables that were in scope when the function was first defined to still be in scope when we later call the function, even if we call the function in a completely different context.
A closure is a block of code which meets three criteria:
-
It can be passed around as a value and
-
executed on demand by anyone who has that value, at which time
-
it can refer to variables from the context in which it was created (i.e. it is closed with respect to variable access, in the mathematical sense of the word "closed")
So here is an example of something which act like a closure but not quite (we see later why):
def twice
yield
yield
end
x = 1
x #=> 1
twice { x += 1 }
x #=> 3
So { x += 1 }
acts like a closure in which local variable x
can be referred to from their defining context.
2.What is Block?
Block is not an object. Block combined of two things code itself and local bindings which means: Block is ready to be executed all the time.
def just_yield
yield
end
top_level_variable = 1
just_yield do
top_level_variable += 1
local_to_block = 1
end
top_level_variable # => 2
local_to_block # => Error!
One more thing about block is that: A block refers to variables in the context it was defined, not the context in which it is called, for example:
def twice
x = 100
yield
yield
puts "value of x at end of twice: #{x}"
end
x = 5
twice { x += 1 } #=> value of x at end of twice: 100
x #=> 7
In most cases, you execute the block right there in the method, using yield. In two cases, yield is not enough:
- You want to pass the block to another method (or even another block).
- You want to convert the block to a Proc
In both cases, you need to point at the block and say, “I want to use this block”—to do that, you need a name. To attach a binding to the block, you can add one special argument to the method. This argument must be the last in the list of arguments and prefixed by an & sign. Here’s a method that passes the block to another method:
def math(a)
yield(a)
end
def do_math(a, &operation)
math(a, &operation)
end
do_math(2) {|x| x * x} # => 4
If you call do_math without a block, the &operation argument is bound to nil, and the yield operation in math fails.
3.Proc
What if you want to convert the block to a Proc? As it turns out, if you referenced operation in the previous code, you’d already have a Proc object. The real meaning of the & is this: “I want to take the block that is passed to this method and turn it into a Proc.” Just drop the &, and you’ll be left with a Proc again:
def my_method(&the_proc)
the_proc
end
p = my_method {|name| "Hello, #{name}!" }
p.class # => Proc
p.call("Bill") # => "Hello, Bill!"
There is also a way to convert a Proc to a block back. When you call my_method, the & converts my_proc to a block and passes that block to the method.
def my_method(greeting)
"#{greeting}, #{yield}!"
end
my_proc = proc { "Bill" }
my_method("Hello", &my_proc)
4.Lambda
Lambda and Proc quite similar, yet with a few difference that can be confusing,Which we see later. Lambda is defined using Proc#lamda methods. Lambda in the real sense is a true closure.
lb = lambda { puts "I'm declared with lambda." }
lb.call #=> "I'm declared with lambda."
5.Method
Method is another kind of closure.
def some_method
puts "I'm declared as a method."
end
@method_as_closure = method(:some_method)
6.Comparative study between Block, Proc, Lambda and Methods
Blocks are quite different from the others since block itself is not an object so that We can't hold on to a &block and call it later at an arbitrary time. But there is some trick to do this:
def save_for_later(&b)
saved = b # Note: no ampersand! This turns a block into a closure of sorts.
end
save_for_later { puts "Hello!" }
puts "Deferred execution of a block:"
saved.call
Yet we can not pass multiple blocks to a function, for example:
def f(&block1, &block2) ...
def f(&block1, arg_after_block) ...
So now Proc, Lambda and Methods come into play: They all behave identically without a 'return' statement.
def f(closure)
puts
puts "About to call closure"
result = closure.call
puts "Closure returned: #{result}"
"Value from f"
end
puts "f returned: " + f(Proc.new { "Value from Proc.new" })
puts "f returned: " + f(proc { "Value from proc" })
puts "f returned: " + f(lambda { "Value from lambda" })
def another_method
"Value from method"
end
puts "f returned: " + f(method(:another_method))
When you run above code in console you find the same results. However There are two differences between procs and lambdas. One has to do with the return keyword, and the other concerns the checking of arguments. Let’s start with return.
def double(callable_object)
callable_object.call * 2
end
l = lambda { return 10 }
double(l) # => 20
In a proc, return behaves differently. Rather than return from the proc, it returns from the scope where the proc itself was defined:
def another_double
p = Proc.new { return 10 }
result = p.call
return result * 2 # unreachable code!
end
another_double # => 10
If you’re aware of this behavior, you can steer clear of buggy code like this:
def double(callable_object)
callable_object.call * 2
end
p = Proc.new { return 10 }
double(p) # => LocalJumpError
The second difference between procs and lambdas concerns the way they check their arguments. For example, a particular proc or lambda might have an arity of two, meaning that it accepts two arguments:
p = Proc.new {|a, b| [a, b]}
p.arity # => 2
What happens if you call this callable object with three arguments, or one single argument? The long answer to this question is complicated and littered with special cases.1 The short answer is that, in general, lambdas tend to be less tolerant than procs (and regular blocks) when it comes to arguments. Call a lambda with the wrong arity, and it fails with an ArgumentError. On the other hand, a proc fits the argument list to its own expectations:
p = Proc.new {|a, b| [a, b]}
p.call(1, 2, 3) # => [1, 2]
p.call(1) # => [1, nil]
If there are too many arguments, a proc drops the excess arguments. If there are too few arguments, it assigns nil to the missing arguments
7.Conclusion
What we learn today are fundamental to understand Ruby deeply. I hope you enjoy reading the post and might pick something useful. Thank you and welcome for any comments.
All rights reserved