12/08/2018, 13:06

Blocks trong ruby

Giới thiệu về block Block là một công cụ đặc biệt có thể dùng được điều khiển các scope trong ruby, giúp chúng ta có thể nhìn các biến và các phương thức trên các dòng code. Block cũng là một thành viên trong gia đình rộng lớn callable objects, trong đó bao gồm các đối tượng như procs và ...

Giới thiệu về block

Block là một công cụ đặc biệt có thể dùng được điều khiển các scope trong ruby, giúp chúng ta có thể nhìn các biến và các phương thức trên các dòng code.

Block cũng là một thành viên trong gia đình rộng lớn callable objects, trong đó bao gồm các đối tượng như procs và lambdas.

ví dụ về 1 block

def a_method(a, b)
  a + yield(a, b)
end
a_method(1, 2) {|x, y| (x + y) * 3 } # => 10

như chúng ta có thể thấy một block được khai báo giữa 2 dấu ngoặc nhọn {}(với một dòng lệnh) hoặc từ khoá do..end(với nhiều dòng lệnh). tuy nhiên chúng ta có thể tuỳ chọn 1 trong 2 cách để cho code có thể dễ nhìn nhất.

chúng ta có thể khai báo một block khi chúng ta gọi một phương thức đã khai báo, một block đươc truyền thẳng vào phương thức và được phương thức gọi thông qua từ khoá yield

ngoài ta các block cũng có thể sử dụng đối số như ví dụ ở trên, và khi gọi các block này chúng ta phải truyền giá tri cho các đối số của chúng, kết quả trả ra sẽ là dòng lệnh cuối trong block.

Kiểm tra block có tồn tại hay không

Trong một phương thức, chúng ta có thể kiểm tra xem nó có được truyền vào một block nào hay không thông qua phương thức Kernel#block_given?()

def a_method
  return yield if block_given?
  'no block'
end
a_method # => "no block"
a_method { "here's a block!" } # => "here's a block!"

Nếu như chúng ta sử dụng yield mà block_given? trả về false thì có lỗi. do vậy khi chúng ta sử dụng yield lên sử dụng cả block_given?.

Bao đóng

Một block không phải là một đoạn code trôi nổi, chúng ta không thể chạy nó mà nó không có gì. Nó cần một môi trường để chạy với các biến cục bộ, biến thực thể, self …cùng với các ràng buộc của nó. xét ví dụ sau

def my_method
  x = "Goodbye"
  yield("cruel" )
end
x = "Hello"
my_method {|y| "#{x}, #{y} world" } # => "Hello, cruel world"

Chúng ta có thể thấy trong phương thức chúng ta đã khai báo biến x = 'Goodbye' nhưng kết quả xuất ra lại lấy giá trị là x = Hello trước lúc khai báo block. tại sao lại như vậy?

Bởi vì khi chúng ta tạo một một block thì chúng sẽ xét các ràng buộc hiện đang có và cụ thể ở đây là đối số x. Khi truyền một block vào một phương thức thì code trong block sẽ nhìn biến x khi block được khai báo chứ không phải biên x trong method và đây chính là thuộc tính bao đóng của block.

Callable Objects

Từ các ví dụ trên chúng ta có thể thấy, block khi được sử dụng được tiến hành qua 2 bước, đầu tiên là viết một vài đoạn code và thứ 2 là gọi block đã viết thông qua yield để chạy các code đã viết. cơ chế "đóng gói code đầu tiên, sau đó gọi" không chỉ dành riêng cho blocks, mà còn có ít nhất ba cách khác trong ruby có thể đóng gói code

  • Proc,là cơ bản là một đối tượng trả lại block.
  • Lambda, là một sự thay đổi nhỏ về một proc
  • Trong một phương thức.

Trong phần này chúng ta sẽ tập trung vào procs và lambdas

Proc Objects

Khi chúng ta viết một block, muốn lưu nó lại để dùng lại sau này để tránh trùng lặp code thì chúng ta cần một đối tượng chứa nó để gọi khi cần, khi đó chúng ta sẽ sử dụng Proc. Proc là một block và được trả về bên trong một đối tượng.

Chúng ta có thể tạo một proc với lệnh Proc.new và dùng lệnh call để để gọi block có trong nó.

irb(main):011:0> pro = Proc.new {|x| x + 1 }
=> #<Proc:0x0000000392f358@(irb):11>
irb(main):012:0> # more code...
irb(main):013:0* pro.call(2)
=> 3

Ruby cũng có phương thức thứ 2 để có thể chuyển đổi một block thành một Proc:lambda() và proc(). Ví dụ dưới đây là cách sử dụng lambda()

dec = lambda {|x| x - 1 }
dec.class # => Proc
dec.call(2) # => 1

một lambda thì có nhiều cách gọi khác nhau như

dec.call(7)
dec[7]
dec.(7)

The & Operator

Một block có thể coi như là một thành phần bô sung tới một phương thức. Hầu hết trong các trường hợp chúng ta sẽ thực thi block ở bên phải phương thức và gọi yield bên trong phương thức. Nhưng có 2 trường hợp sau thì yield không đáp ứng được nhu cầu sử dụng

  • Sử dụng một block qua phương thức khác
  • Chuyển đổi một block thành một Proc

Trong cả 2 trường hợp chúng ta cần tập trung vào block và có thể gọi nó, do vậy nó cần một cái tên để gọi. Để tạo liên kết với blocks chúng ta có thể thêm một tham số đặc biệt vào trong phương thức, tham số này phải cuối trong trong các tham số và bắt đầu bằng dấu &.

def test(a, b)
  yield(a, b)
end
def write_test(a, b, &block)
  puts math(a, b, &block)
end
write_test(2, 3) {|x, y| x * y}
)
=> 6

Nếu khi gọi phương thức write_test(), chúng ta không truyền một block nào thì phương thức test() sẽ lỗi.

nếu như chúng ta muốn chuyển đổi một block thành một Proc thì chỉ cần bỏ dấu & trước tên block là được.

def my_method(&the_proc)
  the_proc
end
p = my_method {|name| "Hello, #{name}!" }
puts p.class
puts p.call("Chao")
)
Proc
Hello, Chao!

Và ngược lại muốn chuyển đổi một Proc sang một block chúng ta vẫn sử dụng dấu &

def my_method(a)
  puts "#{a}, #{yield}!"
end
my_proc = Proc.new { "Chao" }
my_method("Hello" , &my_proc)
)
=> Hello, Chao!

Procs vs. Lambdas

Procs và Lambdas có 2 sư khác biệt chính

  • Sử dụng từ khoá return
  • Sự khác nhau giữa việc kiểm tra các tham số

Procs, Lambdas, và return

Trong một lambda, return được trả về từ lambda

def double(callable_object)
  callable_object.call * 2
end
l = lambda { return 10 }
double(l) # => 20

còn trong một Proc, return trở lên khác biệt, thay vì được trả về từ các Proc nó được trả về từ trường hợp nó được xác định.

def another_double
  p = Proc.new { return 10 }
  result = p.call
  return result * 2
end
another_double # => 10

Procs, Lambdas, và Arity Sự khác biệt thứ hai giữa Proc và lambda là viêc kiểm tra các tham số của chúng. Ví dụ như chúng ta có một Proc hay lambda có 2 arity, điều đó có nghĩa nó chỉ chấp nhận 2 tham số

p = Proc.new {|a, b| [a, b]}
p.arity # => 2

nếu như giả sử ta gọi Proc hay lambda trên với 3 tham số hoặc 1 tham số truyền vào thì kết quả lại rất khác nhau. Với lambda thì nó sẽ lỗi arity và sẽ về thông báo lỗi ArgumentError, còn với Proc sẽ vẫn chạy với một cách xử lý khác biệt

p = Proc.new {|a, b| [a, b]}
p.call(1, 2, 3) # => [1, 2]
p.call(1) # => [1, nil]

Thông qua các phần trên chúng ta có thể hiểu như sau:

  • Blocks: không phải là đối tượng nhưng chúng vẫn có thể gọi.
  • Procs: Lớp của đối tương là Proc.
  • Lambdas: Cũng giống như Proc nhưng cách sử dụng khác. tính chất bao đóng cũng giống như Blocks, Procs
0