Proc và Lambda

Một phần của tài liệu Tìm hiểu ngôn ngữ lập trình Ruby (Trang 26)

Block là các cấu trúc cú pháp của Ruby; chúng không phải là đối tượng, và

không thể được sử dụng như là một đối tượng. Tuy nhiên, ta có thể tạo một đối tượng biểu diễn một block. Phụ thuộc vào cách đối tượng được tạo, ta có một proc

hoặc một lambda. Proc có các thao tác giống như một block và lambda có thao tác giống như một phương thức. Tuy nhiên, cả hai đều là thể hiện của lớp Proc.

2.1. Tạo Proc

Ta đã biết một cách tạo một đối tượng Proc: bằng việc kết hợp một block với một phương thức được định nghĩa với tham số block bắt đầu bằng dấu và (&). Không có gì ngăn cản một phương thức trả về từ đối tượng Proc như vậy được sử dụng bên ngoài phương thức:

# Phương tthức này tạo một proc từ một block

def makeproc(&p) # Chuyển block tương ứng thành một Proc và lưu trữ trong p

p # Trả về đối tượng Proc

end

Với phương thức makeproc như trên, chúng ta có thể tạo một đối tượng Proc theo cách mình:

adder = makeproc{|x,y| x+y}

Biến adder giờ ám chỉ là một đối tượng Proc. Các đối tượng Proc được tạo theo cách này là proc, không phải là lambda. Tất cả các đối tượng Proc có một phương thức call, khi được gọi, sẽ thực hiện đoạn code bên trong block mà proc được tạo ra từ đó. Ví dụ:

sum = adder.call(2,2) # => 4

Ngoài ra khi được gọi, đối tượng Proc có thể được truyền vào phương thức, lưu trữ trong các cấu trúc dữ liệu và không thì được sử dụng như bất cứ đối tượng Ruby nào khác.

Khi tạo proc bằng lời gọi phương thức, có ba phương thức tạo đối tượng

Proc (cả proc và lambda) trong Ruby. Những phương thức này hay được sử dụng,

và thực sự không cần thiết để định nghĩa một phương thức makeproc như trên. Ngoài các phương thức tạo Proc này, Ruby 1.9 còn hỗ trợ một cú pháp literal mới để định nghĩa lambda.

2.1.1 Proc.new

Đây là phương thức new bình thường mà có trong hầu hết các lớp, và là cách rõ ràng nhất để tạo một thể hiện mới của lớp Proc. Proc.new không yêu cầu tham số này, và trả về một đối tượng của lớp Proc là proc (không phải là một lambda). Khi ta gọi Proc.new ứng với một block, nó trả về một proc biểu diễn cho block đó. Ví dụ:

Nếu Proc.new được gọi mà không có một block trong phương thức mà block tương ứng, thì nó trả về một proc biểu diễn block tương ứng với phương thức chứa. Sử dụng Proc.new theo cách này cung cấp một cách sử dụng khác của tham số block bắt đầu bằng dấu ‘và’ trong định nghĩa phương thức. Hai phương thức sau là tương đương, ví dụ:

def invoke(&b) def invoke

b.call Proc.new.call end end

2.1.2 Kernel.lambda

Một kỹ thuật khác để tạo đối tượng Proc là sử dụng phương thức lambda.

Lambda la một phương thức của module Kernel, vì thế nó hoạt động như một hàm

toàn cục. Như tên của nó, đối tượng Proc trả về bởi phương thức là một lambda chứ không phải là proc. Lambda không cần tham số, nhưng phải có một block tương ứng với lời gọi:

is_prositive = lambda {|x| x > 0}

2.1.3 Kernel.proc

Trong Ruby 1.8 phương thức proc toàn cục là đồng nghĩa với lambda. Mặc dù là tên như vậy, nhưng nó trả về một lambda, không phải là một proc. Ruby 1.9 đã sửa điều này; và trong phiên bản này proc là đồng nghĩa với Proc.new.

Chính vì sự nhập nhằng này, ta không nên sử dụng proc trong code Ruby 1.8. Sự hoạt động của đoạn code của ta có thể thay đổi nếu trình thông dịch được nâng cấp lên phiên bản mới hơn. Nếu ta sử dụng code Ruby 1.9 và chắc chắn rằng nó sẽ không bao giờ được thực hiện trên trình thông dịch Ruby 1.8, ta có thể sử dụng

proc một cách an toàn như một cách viết nhanh cho Proc.new

2.1.4 Literal Lambda

Ruby 1.9 hỗ trợ cú pháp hoàn toàn mới để định nghĩa lamba literal. Ta bắt đầu với lambda Ruby 1.8, được tạo bởi phương thức lambda:

succ = lambda{|x| x+1}

Trong Ruby 1.9, ta có thể chuyển thành một literal như sau: - Thay thế tên phương thức lambda bằng dấu ->.

- Chuyển danh sách tham số ra ngoài và ngay trước dấu ngoặc nhọn - Thay đổi ký tự ngăn cách trong danh sách tham số từ || thành (). Với những thay đổi này, ta có literal lambda trong Ruby 1.9:

succ giờ lưu giữ một đối tượng Proc, và ta có thể sử dụng nhưng các đối tượng khác:

succ.call(2) # => 3

Như block trong Ruby 1.9, danh sách tham số của một literal lambda có thể

chứa khai báo của các biến có phạm vi trong block mà được đảm bảo là sẽ không ghi đè lên biến cùng tên ở phạm vi bên ngoài.

# Lambda này nhận 2 tham số và khai bấo 3 biến địa phương

f = ->(x,y; i,j,k) { ... }

Một lợi ích của cú pháp lambda so với phương thức truyền thống tạo lambda dựa vào block mới này là cú pháp Ruby 1.9 cho phép lambda có thể được khai báo với tham số mặc định, như phương thức sau:

zoom = ->(x,y,factor=2) { [x*factor, y*factor] }

Như khai báo phương thức, các dấu ngoặc trong literal lambda là không bắt buộc, bởi vì danh sách tham số và danh sách biến địa phương đã được ngăn cách bởi dấu ->,;, và {. Ta có thể viết lại ba lambda ở trên như sau:

succ = ->x { x+1 }

f = -> x,y; i,j,k { ... }

zoom = ->x,y,factor=2 { [x*factor, y*factor] }

Tham số của lambda và biến địa phương là không bắt buộc, và một literal

lambda có thể bỏ qua chúng. Lambda đơn giản nhất, không nhân tham số gì và trả

về nil, như sau:

->{}

Một lợi ích mà cú pháp mới này đem lại là sự ngắn gọn. Điều này có ích khi ta muốn truyền một lambda như là một tham số cho một phương thức hay một

lambda khác:

def compose(f,g) # Hợp 2 lambda ->(x) { f.call(g.call(x)) }

end

succOfSquare = compose(->x{x+1}, ->x{x*x})

succOfSquare.call(4) # => 17: Tính (4*4)+1

Các literal lambda tạo các đối tượng Proc và không giống như block. Nếu ta muốn truyền một literal lambda cho một phương thức mà yêu cầu một block, thêm

vào đầu literal dấu &, như ta làm với các đối tượng Proc khác. Ở dưới đây ta có thể sắp xếp một mảng các số theo thứ tự giảm dần sử dụng cả block và literal

lambda:

data.sort {|a,b| b-a } # Phiên bản sử dụng block

data.sort &->(a,b){ b-a } # Phiên bản sử dụng literal lambda

2.2. Gọi Proc và Lambda

Proc và Lambda là các đối tượng, không phải là các phương thức, và chúng

không thể được gọi theo cách giống như các phương thức. Nếu p trỏ đến một đối tượng Proc, ta không thể gọi p như một phương thức. Nhưng vì p làm một đối tượng, ta có thể gọi một phương thức của p. Ta đã nhắc tới việc lớp Proc định nghĩa một phương thức tên là call. Gọi phương thức này sẽ thực hiện đọan code trong block gốc. Các tham số ta truyền vào phương thức call trở thành các tham số của block, và giá trị trả về của block là giá trị trả về của phương thức call:

f = Proc.new {|x,y| 1.0/(1.0/x + 1.0/y) } z = f.call(x,y)

Lớp Proc đồng thời cũng định nghĩa toán tử truy cập mảng để sử dụng giống như call. Điều này có nghĩa là ta có thể gọi một proc hoặc lambda sử dụng cú pháp giống như lời gọi phương thức, với dấu ngoặc tròn được thay thế bằng dấu ngoặc vuông. Ví dụ lời gọi proc ở trên, có thể được thay thế bằng:

z = f[x,y]

Ruby1.9 đưa ra một cách khác để gọi một đối tượng Proc; là ta có thể sử dụng dấu ngoặc tròn sau dấu chấm:

z = f.(x,y)

.() trông giống như một lời gọi phương thức thiếu tên phương thức. Nó không phải là một toán tử có thể định nghĩa, mà là cú pháp đơn giản để gọi phương thức call. Nó có thể được sử dụng với bất cứ đối tượng nào định nghĩa một phương thức call và nó không giới hạn chỉ riêng cho đối tượng Proc.

Một phần của tài liệu Tìm hiểu ngôn ngữ lập trình Ruby (Trang 26)