[Ruby] Block, Proc, Lambda in Ruby

In Ruby, it is easy to get confused (not good at learning), and the questions often asked in interviews are:

"Please explain what Block, Proc and Lambda are"
"Difference between do... end in Block and curly bracket {}"
"Please explain the difference between Proc and Lambda"
"Why does the Rails scope use Lambda?"
"How can I turn Block into Proc and Lambda?"
"How do you turn Proc and Lambda into blocks?"

The above common problems are met at one time!!

Block (code block)

What is Block?

  • Ruby is a quite thorough "Object-Oriented OOP (Object-Oriented Programming)" programming language. Most things are objects, and Block is a few exceptions.
list = [1, 3, 5, 7, 9, 10, 12]

list.map { |i| i * 2 }
# The map is printed as follows
2
6
10
14
18
20
24
 => [2, 6, 10, 14, 18, 20, 24]  # Return value
# map printed as above


list.select { |j| p j.even? }
# select is printed as follows
false
false
false
false
false
true
true
 => [10, 12]  # Return value
# select printed as above


list.reduce { |x, y| p x + y }
# reduce printed as follows
4
9
16
25
35
47
 => 47  # Return value
# reduce printed as above


list.each do |num|
  p num * 2
end
# each printed as follows
2
6
10
14
18
20
24
 => [1, 3, 5, 7, 9, 10, 12]  # Return value
 # each printed as above


# It can be seen from the above that the return values of map, select, reduce and each are different
# map, select and reduce will return a new array
# each loopback receiver

In Ruby, curly braces {} and do... end are blocks, which should be followed by the method and cannot survive alone, otherwise errors will occur.

{ puts "Can't survive alone, an error will occur" }  # SyntaxError


do
  puts "Can't survive alone, an error will occur"    # SyntaxError
end

Block cannot survive alone and will spray error messages. Block is not an object and cannot exist alone.

A Block is just a piece of code attached to a method waiting to be called by the code.

Difference between curly bracket {} and do... end in Block

Generally speaking, {} will be used if it can be written in one line. If it is necessary to write more than one line in case of complex judgment, do... end will be used.

Are there any other differences?
See the following example:

list = [1, 2, 3, 4, 5]

p list.map{ |x| x * 2 }      # [2, 4, 6, 8, 10]

p list.map do |x| x * 2 end  # <Enumerator: [1, 2, 3, 4, 5]:map>

It turns out that curly braces {} and do... end have different priorities

Curly braces {} take precedence over do... end

The prototype of the above example is

list = [1, 2, 3, 4, 5]

p(list.map{ |x| x * 2 }        # [2, 4, 6, 8, 10]

p(list.map) do |x| x * 2 end   # <Enumerator: [1, 2, 3, 4, 5]:map>
# Because the priority is low, it is combined with p first, so the attached blocks will not be processed

How to execute the contents of the Block?

Using the yield method

If you want the attached Block to execute the content, you can use the yield method to temporarily hand over the control right to the Block, and then hand back the control right after the Block execution is completed.

def hi_block
  puts "Start"
  yield               # Give control to Block for the time being
  puts "End"
end

hi_block { puts "Here is Block" }
# The printed results are as follows
 Start
 Here is Block
 End
 => nil
# The printing result is as above

Pass parameters to Block

You will find that no matter in the Block of list.map {| i | i * 2} or list.each do |num| p num * 2 end, what are | i | and | num |?
The | enclosing i and num in the Block is called pipe. The intermediate i and num are parameters of the anonymous function, called token. In fact, they are local variables in the Block range. They will become invalid after leaving the Block.

list = [1, 3, 5, 7, 9, 10, 12]

list.map { |i| i * 2 }
# Variable i is only valid in Block and will generate [2, 6, 10, 14, 18, 20, 24]

list.each do |num|
  p num * 2
end
# The variable num is only valid in the Block. It will print 2, 6, 10, 14, 18, 20 and 24 in order


puts i    # It becomes invalid after leaving the Block, and an error (NameError) occurs in which the variable cannot be found
puts num  # It becomes invalid after leaving the Block, and an error (NameError) occurs in which the variable cannot be found

# Variable name customization will take meaningful names so that people can see and know what it is, rather than taking meaningless i

So, how did i and num come from?
In fact, it's just that when you use the yield method to transfer control to the Block, you bring the value to the Block.

def hi_block
  puts "Start"
  yield "Mom, I'm here"  # You can also write yield("Mom, I'm here")
  puts "End"
end


hi_block { |x| puts "Here is Block,#{x}" }
# The printed results are as follows
 Start
 Here is Block,Mom, I'm here
 End
 => nil
# The printing result is as above
  • yield can be followed by 1 or more parameters
# Example 1 (with 1 parameter)
def hi_block
  puts "Start"
  yield 123         # Temporarily give control to Block and pass the number 123 to Block
  puts "End"
end

hi_block { |x|     # This x is from the yield method
  puts "Here is Block,I got it. #{x}"
}
# The printed results are as follows
 Start
 Here is Block,I got 123
 End
 => nil
# The printing result is as above


# Example 2 (with 2 parameters)
def tow_parm
  yield(123, "Parameter 2")
end

tow_parm { |m, n|
  puts %Q(I said numbers #{m},You back#{n}~)
}
# The printed results are as follows
 When I say the number 123, you go back to parameter 2~
 => nil
# The printing result is as above
  • yield advanced use
    Example 1:
    i in line 7 is found by yield, and i in line 11, x is the entity variable @v. Print the number in the array by each method.
class Map 
  def initialize
    @v = [1, 2, 3, 4]
  end

  def each_print
    @v.each { |i| puts yield i } if block_given?
  end
end

i = "You'll understand if you look at it a few more times"
a_obj = Map.new
a_obj.each_print{ |x| "#{i} #{x}" }

# The printed results are as follows
 Read it a few more times and you'll understand 1
 Read it a few more times and you'll understand 2
 Read it a few more times and you'll understand 3
 Read it a few more times and you'll understand 4
 => [1, 2, 3, 4]
# The printing result is as above

Example 2:
The yield in line 4 takes the counter outside the method to find the Block behind the list. Because the first default is 1, it is known that the counter behind the yield is also 1, which becomes the parameter of | ary | in the external Block. After the Block is executed, return to line 4 and continue to execute.

def list(array, first = 1)
  counter = first
  array.each do |item|
    puts "#{yield counter}. #{item}"
    counter = counter.next
  end
end


list(["a","b","c"]) { |ary| ary * 3 }
# The printed results are as follows
3. a
6. b
9. c
 => ["a", "b", "c"]
# The printing result is as above

list(["a","b","c"], 100) { |ary| ary * 3 }
# The printed results are as follows
300. a
303. b
306. c
 => ["a", "b", "c"]
# The printing result is as above

list(["Ruby", "Is", "Fun"], "A") { |ary| ary * 3}
# The printed results are as follows
AAA. Ruby
BBB. Is
CCC. Fun
 => ["Ruby", "Is", "Fun"]
# The printing result is as above

Return value of Block

In fact, the yield method temporarily transfers control to the following Block

The execution result of the last line of Block will automatically become the returned value of Block
Therefore, Block can be regarded as the judgment content:

# Example 1
def dog
  puts "Woof!!Woof!!Woof!!"
end

dog { puts "You can't see me~~" }                   # Woof!! Woof!! Woof!!
# If there is no yield, the things written in the Block will not react


# Example 2
def say_hello(list)
  result = []
  list.each do |i|
    result << i if yield(i)                   # If the return value of yield is true
  end
  result
end

p say_hello([*1..10]) { |x| x % 2 == 0 }      # [2, 4, 6, 8, 10]
p say_hello([*1..10]) { |x| x < 5 }           # [1, 2, 3, 4]
p say_hello([*1..10]) { |x| return x < 5 }    # An error of LocalJumpError will be generated
p say_hello([*1..10])                         # A LocalJumpError (no block given (yield)) error message will be generated

Above example say_ The Hello method will select the qualified elements according to the setting conditions of the Block. It should be noted that adding return to the Block will cause the error of LocalJumpError, because Block is not a method and it does not know where you want to return.

Block is not a parameter

Block is like a parasite attached to or parasitic on other methods or objects, but it is not a parameter. In the following example, name is the parameter, but block is not.

def say_hello(name)
  p name
end

say_hello("side dish") { puts "This is Block"}   # "Side dish"

After the above code is executed, there will be no error, but the Block will not be executed.

How to judge whether there is a Block?

There is a situation where there is a yield in the method, but there is no Block when calling the method

def say_hello
  yield
end

say_hello    # A LocalJumpError (no block given (yield)) error message will be generated

The error message of LocalJumpError (no block given (yield)) will appear.

In this case, the method can be executed normally when it is called

You can use a judgment method block provided by Ruby_ given?

# Example 1
def hi_block
  if block_given?               # Judge whether the execution method is followed by Block
    yield
  else
    "no block"
  end
  # The above five lines can be abbreviated as block_given? ? yield : "no block"
end

hi_block                        # "no block"
hi_block { "hello" }            # "hello"
hi_block do "hello" end         # "hello"


# Example 2
def hello_world
  yield('side dish') if block_given? # Judge whether the execution method is followed by Block
  # You can also write yield 'side dish' if block_given?
end

p hello_world                   # nil
hello_world {|x| puts "#{x}" }  # side dish


# Example 3
def say_hello(name)
  yield name if block_given?    # Judge whether the execution method is followed by Block
end

say_hello(puts "hi")            # hi

Block properties

Summarize the above characteristics

  1. Not an object, not a parameter
  2. A piece of code that cannot exist alone, but is attached to a method waiting to be called by code.
  3. The execution result of the last line of Block will automatically become the returned value of Block
  4. return cannot be used in a Block
  5. Cannot be assigned to another object

Although Block is not an object, it cannot exist alone
However, Ruby has two built-in methods to objectify Block and exist separately: Proc and Lamda

Proc

Proc is a program object that can save Ruby code and execute it when necessary, or pass it into other functions as a Block.

After Proc.new is followed by a Block, a Proc object can be generated. After objectification, it is a parameter. Then, you can use the call method to execute the code in the Block.

proc1 = Proc.new { puts "Block Objectified Luo" }   # Use the Proc class to objectify a Block
# It can also be written as
proc2 = Proc.new do
  puts "Block Objectified Luo"
end

proc1.call    # Block is objectified
proc2.call    # Block is objectified

return cannot be added to Proc

  • Return don't write it in the Block of Proc, otherwise the code will stop running after this section (the execution will end immediately after return), and the code won't go on.
def hi_proc
  p "strat"
    hi_proc = Proc.new { return "The execution of this paragraph stops" }
    hi_proc.call
  p "end"
end

p hi_proc
# The display results are as follows
"strat"
"The execution of this paragraph stops"
 => "The execution of this paragraph stops"
# The display result is as above
    
# "end" will not be printed because it stops after executing line 3

Proc with parameters

# Example 1
hi_river = Proc.new { |name| puts "Hello,#{name}"}
# It can also be written as hi_river = proc { |name| puts "Hello,#{name}" }

hi_river.call("Here's the side dish")    # Hello, here's the side dish


# Example 2 (with parameters)
cal = Proc.new { |num| num * 5 }
# It can also be written as cal = proc {| num | num * 5}

cal.call(3)                   # 15


# Example 3 (with parameters)
def total_price(price)
  Proc.new { |num| num * price }
  # It can also be written as proc {| num | num * price}
end

n1 = total_price(50)
n2 = total_price(30)

puts "n1 want #{n1.call(2)} Yuan, and n2 want #{n2.call(5)} yuan“
# n1 costs 100 yuan, while n2 costs 150 yuan

Proc call mode

To execute a Proc object, in addition to the call method, there are several usage methods:

hi_river = Proc.new { |name| puts "Hello,#{name}"}


hi_river.call("Here's the side dish")    # Using the call method
hi_river.("Here's the side dish")        # Use parentheses (note that there is one more decimal point after the method)
hi_river["Here's the side dish"]         # Use square brackets
hi_river === "Here's the side dish"      # Use three equal signs
hi_river.yield "Here's the side dish"    # Using the yield method

# The above five methods are printed
# Hello, here's the side dish

Lambda

  • In addition to being converted into Proc, Block can also be converted into Lambda, which is slightly different from Proc:

retrun value
Judgment method of parameters (whether the quantity correctness of parameters will be checked)

How about Proc and Lambda

p1 = Proc.new {|x| x + 1 }
p2 = proc {|x| x + 1 }    # Another way to write Proc
l1 = lambda {|x| x + 1 }
l2 = ->(x) { x + 1 }      # Another way to write lambda


puts "p1: #{p1.lambda?}, #{p1.class}" # p1: false, Proc
puts "p2: #{p2.lambda?}, #{p2.class}" # p2: false, Proc
puts "l1: #{l1.lambda?}, #{l1.class}" # l1: true, Proc
puts "l2: #{l2.lambda?}, #{l2.class}" # l2: true, Proc

Now we know

  • Proc, like Lambda, is a proc object
    The above p1, p2, l1 and l2 can be executed using the call method. We can use Lambda? To judge whether it is Lambda. If not, it is Proc.

  • return can be added to Lambda
    One of the differences between Lambda and Proc is that the return value is different

def hi_lambda
  p "strat"
    hi_lambda = lambda { return p "Will continue" }
    hi_lambda.call
  p "end"
end


p hi_lambda
# The display results are as follows
"strat"
"Will continue"
"end"
"end"
 => "end"
# The display result is as above

Compare the return values of Proc and Lambda once

def test_return(callable_object)
  callable_object.call * 5
end

la = lambda { return 10 }  # It can also be written as La = - > {return 10}
pr = proc { return 10 }    # It can also be written as PR = proc.new {return 10}


puts test_return(la) # 50
puts test_return(pr) # Displays the LocalJumpError error error message

Lambda's return is from Lambda return
Proc is defined from the scope return of proc

Speak clearly, do you hear vaguely? Look directly and understand

def test_proc
  pr = Proc.new { return 10 }
  result = pr.call
  return result * 5
end

def test_lambda
  la = lambda { return 10 }
  result = la.call
  return result * 5
end

puts test_proc   # 10
puts test_lambda # 50

test_proc ends retrun on the pr.call line, and test_lambda can complete each line of the method.

Lambda has strict processing parameters

  • Proc processing parameters are more flexible, while Lambda is more rigorous
pr = proc { |a, b| [a, b] }    # It can also be written as PR = proc. New {a, B | [a, b]}
la = lambda { |a, b| [a, b] }  # It can also be written as La = - > (a, b) {[a, b]}

p pr.call(5, 6)     # [5, 6]
p pr.call           # [nil, nil]
p pr.call(5)        # [5, nil]
p pr.call(5, 6, 7)  # [5, 6]


p la.call(5, 6)     # [5, 6]
p la.call           # Display ArgumentError error message
p la.call(5)        # Display ArgumentError error message
p la.call(5, 6, 7)  # Display ArgumentError error message

Proc will not check the number of parameters. If it is insufficient, nil will be supplemented, and if it is too much, it will be lost automatically. Lambda will require that the number of parameters is correct, which is more rigorous. Otherwise, the ArgumentError error message will be displayed.

Why does Rails scope use Lambda?

Suppose we write a scope that will bring in parameters

scope :product_price, -> (type) { where(price: type) }

If Proc is used, SQL query can still be executed without parameters when Prodct.product_price does not bring in parameters, and will not spray errors, because Proc will preset the parameter value not brought in as nil. When SQL query is equivalent to where(price: nil), you will encounter unexpected conditions, which will be harder to find in Debug.

On the contrary, Lambda can ensure the correctness of the number of parameters. If there are too many or too few parameters, error will tell you not to do so to avoid unnecessary conditions.

This is why the ActiveRecord model in Rails uses Lambda when using scope, because it is more cautious than Proc.

Instead, Lambda behaves more like a common anonymous function.

Use the & symbol to convert Block to Proc and Lambda

Convert Block to Proc and Lambda

In Rails, if we want to find the names of all users from the database and use map, it is written as follows:

# Block to Proc example
names = User.all.map { |user| user[:name] }
# Form an Array of all names

# It can also be written as
names = User.all.map(&:name)
# Transfer Block to Proc


# Block to Proc example
pp = Proc.new { |x| puts x * 2 }
[1, 2, 3].each(&pp)
# Prototype [1, 2, 3]. Each {I | PP [i]}
# The printed results are as follows
2
4
6
 => [1, 2, 3]
# The printing result is as above


# Block to Lambda example            
lam = lambda { |x| puts x * 2 }
[1,2,3].each(&lam)
# Prototype [1, 2, 3]. Each {I | Lam [i]}
# The printed results are as follows
2
4
6
 => [1, 2, 3]
# The printing result is as above

The strange & symbol represents bringing in a Proc or lambda and converting Block into Proc.

Convert Proc or lambda to Block

One of the uses of & just introduced is to specify to convert from Block to Proc or Lambda at the same time of method declaration. In addition & you can also convert Proc or Lambda to Block:

hi_proc("Hahaha", &proc{ |s| puts s} )

hi_lambda = (1..5).map &->(x){ x*x }

When Proc or Lambda encounters & it will be converted to Block, so the above demonstration meaning is the same as the following:

hi_proc("Hahaha"){ |s| puts s }

hi_lambda = (1..5).map { |x| x * x }

&Block is placed at the end of the parameter

The Block cannot know the objectified (parameterized) Block. You need to add &. There can only be one such thing and it must be placed at the end, otherwise a syntax error will occur.

# Error demonstration
def hi_block(&p, n)
  ...
end

def hi_block(n, &p1, &p2)
  ...
end


# Example 1
def hi_block1(str, &test01)
  "#{str} #{test01.call(18)}"
end

hi_block1("Hello") { |age| "I'm #{age} years old." }
# "Hello I'm 18 years old."


# Example 2
def temp_b1
  yield("Parameter 1") # Parentheses can be omitted
end

def temp_b2(&block)
  block.call("Parameter 2")
end

block1 = Proc.new {|x| puts "This is Proc #{x}"}
block2 = lambda {|x| puts "This is lambda #{x}"}


temp_b1 { |x| puts "block0 #{x}" }  # block0 parameter 1
temp_b1(&block1)                    # This is Proc parameter 1
temp_b2(&block2)                    # This is lambda parameter 2

After reading it, you will find that & in Ruby is very magical, and a lot of things have been done behind it. In fact, it produces a Proc object. Although it is easy to use, if you don't understand the principle behind it, you won't know how to use it and what's wrong.

What if there are more than two & blocks?

def two_block(n, p1, p2)
  p1[n]      # Equivalent to p1.call(n)
  p2.call n  # Parentheses can be omitted
end

two_block('River', proc { |i| puts "#{i} 1" }, Proc.new { |i| puts "#{i} 2" } )
# The printed results are as follows
River 1
River 2
 => nil
# The printing result is as above

Create a Proc object and write a Block to the Proc.new method when the parameter is passed in. At first glance, it is very lengthy and ugly. This technique is applicable when you want to pass in multiple blocks as parameters at the same time.

Summary

This article is very brain burning. I found a lot of references. I was not very clear at the beginning of writing, but later I could explain it. I felt stronger in the process.

Keywords: Ruby

Added by brain on Mon, 25 Oct 2021 06:42:48 +0300