+1

Ruby's Lookups & Scopes

Cách Ruby tìm kiếm định danh

Vấn đề khó khăn nhất trong Ruby đó là việc xác định một định danh. Một định danh trong Ruby bao gồm:

  • tên hàm: x.name hoặc name

  • biến cục bộ: name

  • biến class: @@name

  • biến toàn cục: $name

  • hằng: NAME

  • modules: Name

Việc xác định một tên (định danh) đại diện cho một trong những thứ trên trong Ruby không phải là dễ dàng. Trong Ruby thì Class < Module nên khi ta nói đến modules thì nó bao gồm cả class và module.

Có một vài khái niệm lớn liên quan đến việc tìm kiếm tên trong ruby như sau:

  • self: self sẽ trỏ tới cái gì ?

  • definee: khi ta khởi tạo một method thì nó sẽ được khởi tạo vào class nào?

  • scoping: khi nào thì một scope mới cho biến cục bộ được khởi tạo, khi nào thì không

  • nesting: module bao bọc của scope hiện tại là gì ?

Hãy xem ruby giải quyết từng vấn đề như thế nào.

Biến cục bộ và toàn cục

Biến toàn cục có ký tự $ đi trước và chúng có thể được truy cập từ mọi nơi của chương trình.

Biến cục bộ phụ thuộc vào scope. Một scope mới được khởi tạo khi một hàm mới hoặc module hay một block được tạo. Những câu lện vòng lặp hay điều kiện không tạo ra scope mới.

Chỉ có scope được tạo bởi block mới có thể kế thừa scope bên ngoài của nó. Scope được tạo bởi hàm và module đều tồn tại độc lập với scope cha. Ví dụ:

x = 1
module Foo
  p x # NameError
end

Tuy nhiên, bạn có thể tận dụng việc block kế thừa scope cha của nó như sau:

x = 1
Foo = Module.new do
  p x # 1
end

Các bạn cũng có thể refer đên biến cục bộ của một biến bên ngoài scope của nó (aka closure) như sau:

def foo
  x = 0
  [-> {x += 1}, -> {p x}]
end
x, y = foo
x.call
y.call # 1

Có một điều cũng khá đặc biệt trong ruby đó là: một biến sẽ được "nằm trong" scope nếu nó nằm trong một phép gán trước đó trong cùng một scope. Ví dụ:

p x # NameError
x = 1

# but

x = 1 if false
p x # nil

Một điều đáng lưu ý nữa là hàm binding (từ Kernel#bindingProc#binding) sẽ trả ra một đối tượng Binding mà trong đấy mô tả biến cục bộ trong scope.

Biến Instance

Biến instance thường có @ đứng trước. Chúng thường được tìm kiếm trong self. Nếu một biến instance không tồn tại (chưa được gán đâu đó trong chương trình) thì giá trị mặc định sẽ là nil.

Điểm đáng chú ý ở đây là biến instance chỉ tồn tại private trong một instance: một instance a của X không thể truy cập vào biến instance của một instance b khác cũng của X. Để access được nó thì ta cần sử dụng Object#instance_variable_get/set.

Biến Class

Biến class thường có @@ đứng trước. Chúng được định nghĩa trên modules và có thể được access trong thân của module cũng như các method khác trong module ấy. Kỳ lạ hơn là chúng còn có thể được access từ cả meta-class của module (meta-meta-class cũng tương tự vậy).

Nếu một module kế thừa nhiều phiên bản của một biến class, phiên bản kế thừa đầu tiên luôn được ưu tiên.

Các bạn có thể liệt kê các biến class của một module với Module#class_variables(false) (false ở đây để xác định là ta sẽ không lấy các biến class được kế thừa). Có một điều khá thú vị ở đây là biến class có thể bị thay thế: nếu module là một class mà có một biến class là @@a, và nếu biến @@a này được định nghĩa ở những class mà nó kế thừa, biến class này sẽ bị ghi đè. Ví dụ:

class T; @@a = 't'; end
class Object; @@a = 'o'; end
class T
  p @@a # o
  p class_variables(false) # []
end

Điều này sẽ không xảy ra nếu là module không phải là class:

module A; @@a = '1'; end
class B
  include A
  @@a = '2'
  puts @@a # 2
end

Nếu biến class được access trước phép gán thì lỗi NameError sẽ được báo.

Điều cuối cùng là biến class chỉ có thể được access bên trong thân của một class mà kế thừa class/module mà định nghĩa biến đó. Điều này có nghĩa là @@x sẽ không thể sử dụng được với eval. Ta chỉ có thể access nó thông qua Module#class_variable_get/set.

Constants (Modules)

Constant (hằng) thường bắt đầu với chữ cái đầu viết hoa, và module thực chất cũng là một loại constant.

Constant phụ thuộc vào một khái niệm là nesting: bạn có thể access một constant nếu nó được khai báo bên trong thân của module hiện thời, hoặc trong các module mà module hiện tại kế thừa (tuy nhiên bạn không thể access đến constant nằm trong các module tỏ tiên của module bao bọc bên ngoài). Các bạn cũng có thể sử dụng :: để access đến các module:

X = 0
module A
  X = 1
  p ::X # 0
  module B; module C; Y = 2; end; end
  module D
    p X # 1
    p B::C::Y
  end
end

Ví dụ dưới đây sẽ cho thấy 2 vấn đề:

  • Bạn không thể access constants được định nghĩa trong module tổ tiên của một module bao ngoài.

  • Constant trong module bao ngoài sẽ được ưu tiên hơn so với cononstant trong module tỏ tiên.

module A; X = 'a'; end
module B; Y = 'b'; end
module C
  include A
  p X # ok
  Y = 'c'
  module D
    include B
    p Y # 'c'!
    p X # NameError
  end
end

Ta có thể truy cập đến scope nesting hiện tại của module với Module::nesting:

module A
  module D; p Module.nesting; end # [A, A::D]
end
module A::D; p Module.nesting; end # [A::D]

Nếu bạn access đến constant trước phép gán thì NameError sẽ được đưa ra. Module::constants sẽ liệt kê những constant được định nghĩa ở thời điểm gọi, và Module#constants sẽ liệt kê những constant được định nghĩa bởi module.

Methods

Method được tìm kiếm ở object nhận lời gọi hàm. Nếu không tồn tại object nhận hàm gọi, mặc định self sẽ nhận lời gọi hàm này. self có thể có meta-class mà nó có độ ưu tiên cao hơn các tổ tiên kế thừa khác.

Method được định nghĩa ở module nào ?

Cuối cùng, khi bạn sử dung def để khởi tạo hàm, module nào sẽ là nơi method được định nghĩa ? Một câu trả lời thông thường là nó sẽ trở thành hàm instance của module đấy.

Self và object định nghĩa (definee) thay đổi như thế nào

where self deefinee
top-level (file or REPL) main (an instance of Object) Object
in a module module module
in class << X metaclass of X metaclass of X
in a def method receiver surrounding module
in a def X.method receiver (X) metaclass of X
X.instance_eval receiver (X) metaclass of X
X.class_eval receiver (X, a module) receiver (X)

Chú ý rằng, Module#module_eval là alias của Module#class_eval. Cũng có một biến thể là class/module/instance_exec cũng làm việc tương tự như eval nhưng cho phép thêm một tham số vào block (thích hợp để một biến instance có thể access được bởi block). Đây là điểm khác biệt duy nhất giữa execeval.

Mặc dù có vẻ class/module_eval hoạt động giống như block của module hoặc class. Tuy nhiên, bạn không thể mở một class bên trong method, nên class_eval sẽ giúp ích nếu như bạn muốn thực thi việc gì đó mà dựa trên context của một class như là một phần trong hàm.

Điều cuối cùng là các bạn không thể tạo singleton method trên IntegerSymbol (ví dụ: def Integer.foo).

Kết luận

Việc truy xuât tên trong ruby quả thực rất phức tạp và bài viết này một phần nào đó giải thích cơ chế hnayf của Ruby.

References: Ruby lockups scopes


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.