Chúc mừng năm mới, và welcome back to Ruby Magic! Trong episode mùa đông này, chúng ta sẽ dive vào bindings và scopes. Vậy hãy mang theo ván trượt và follow chúng ta deep vào rừng.
Lần trước, chúng ta nhìn vào closures trong Ruby bằng cách so sánh blocks, procs và lambdas. Bên cạnh sự khác biệt giữa ba loại, chúng ta chạm đến cái định nghĩa closure.
Closure là first-class function với environment. Environment là mapping đến các variable tồn tại khi closure được tạo. Closure retain quyền truy cập vào các variable này, ngay cả khi chúng được định nghĩa trong scope khác.
Chúng ta đã explore equivalent của Ruby cho first-class functions, nhưng chúng ta convenient bỏ qua environments. Trong episode này, chúng ta sẽ xem environment đó hoạt động như thế nào cho closures, classes và class instances bằng cách examine cách Ruby handle lexical scope thông qua bindings của nó.
Lexical Scope (Phạm vi Từ vựng)
Trong lập trình, scope đề cập đến bindings available tại phần code cụ thể. Binding, hoặc name binding, bind name đến memory reference, như tên variable đến value của nó. Scope định nghĩa self nghĩa gì, method nào có thể gọi, và variable nào available.
Ruby, giống hầu hết modern programming languages, dùng static scope, thường gọi là lexical scope (khác với dynamic scope). Current scope dựa trên cấu trúc code và xác định variable available tại các phần code cụ thể. Nghĩa là scope thay đổi khi code nhảy giữa methods, blocks và classes — vì chúng có thể có local variables khác nhau.
def bar foo = 1 foo end bar # => 1Trong method này, chúng ta tạo local variable bên trong method và in ra console. Variable in scope bên trong method, vì nó được tạo đó.
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object)Trong ví dụ này, chúng ta tạo variable bên ngoài method. Khi gọi variable bên trong method, chúng ta get error, vì variable out of scope. Local variables được scoped chặt, nghĩa là method không thể truy cập variable bên ngoài nó trừ khi pass as argument.
@foo = 1 def bar @foo end bar # => 1Trong khi local variables available locally, instance variables available cho tất cả methods của class instance.
Inherited Scopes và Proc Bindings
Như chúng ta thấy trong ví dụ trước, scope dựa trên location trong code. Local variable định nghĩa bên ngoài method không in scope bên trong method nhưng có thể làm available bằng cách biến thành instance variable. Methods không thể truy cập local variable định nghĩa bên ngoài vì methods có scope riêng, với bindings riêng.
Procs (bao gồm blocks và lambdas, bởi extension) khác. Bất cứ khi nào proc được instantiate, binding được tạo kế thừa reference đến local variables trong context block được tạo.
foo = 1 Proc.new { foo }.call # => 1Trong ví dụ này, chúng ta set variable foo thành 1. Nội bộ, Proc object tạo ở dòng hai tạo binding mới. Khi gọi proc, chúng ta có thể ask value của variable.
Vì binding được tạo khi proc initialized, chúng ta không thể tạo proc trước khi định nghĩa variable, ngay cả khi block không được gọi cho đến sau khi variable được định nghĩa.
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object)Gọi proc sẽ produce NameError vì variable không defined trong proc's bindings. Vì vậy, bất kỳ variable nào accessed trong proc nên được định nghĩa trước khi proc được tạo hoặc pass as argument.
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2Chúng ta có thể, tuy nhiên, thay đổi variable sau khi đã định nghĩa trong main context vì proc's binding hold reference đến nó thay vì copy nó.
foo = 1 Proc.new { foo = 2 }.call foo #=> 2Trong ví dụ này, chúng ta thấy foo variable trỏ đến cùng object khi trong proc cũng như bên ngoài. Chúng ta có thể update nó bên trong proc để variable bên ngoài cũng update.
Bindings
Để track current scope, Ruby dùng bindings, encapsulate execution context tại mỗi vị trí trong code. Phương thức binding trả về object Binding mô tả bindings tại vị trí hiện tại.
foo = 1 binding.local_variables # => [:foo]Binding object có method tên #local_variables trả về tên tất cả local variables available trong current scope.
foo = 1 binding.eval("foo") # => 1Code có thể evaluated trên binding bằng dùng phương thức #eval. Ví dụ trên không quá useful, vì simply gọi foo có cùng result. Tuy nhiên, vì binding là object có thể pass around, nó có thể dùng cho điều thú vị hơn. Hãy xem ví dụ.
Ví dụ Thực tế
Bây giờ chúng ta đã học bindings trong an toàn garage, hãy bring them ra slopes và chơi trong tuyết. Bên cạnh internal use của Ruby cho bindings xuyên suốt language, có tình huống binding objects được dùng explicit. Ví dụ tốt là ERB — templating system của Ruby.
require 'erb' x = 1 def y 2 end template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`"Trong ví dụ này, chúng ta tạo variable x, method y, và ERB template reference cả hai. Chúng ta sau đó pass current binding cho ERB#result, evaluate ERB tags trong template và trả về string với variables filled in.
Under the hood, ERB dùng Binding#eval evaluate contents của mỗi ERB tag trong scope của passed binding. Triển khai simplified hoạt động cho ví dụ trên có thể trông như sau:
class DiyErb def initialize(template) @template = template end def result(binding) @template.gsub(/<%=(.+?)%>/) do binding.eval($1) end end end x = 1 def y 2 end template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`"Class DiyErb lấy template string khi initialize. Phương thức #result tìm tất cả ERB tags và replace chúng với result của evaluate contents. Để làm vậy, nó gọi Binding#eval trên passed binding, với contents của ERB tags.
Bằng cách pass current binding khi gọi #result, các eval calls có thể truy cập variables định nghĩa bên ngoài method, thậm chí bên ngoài class, không cần pass explicit.
Chúng Ta Có Làm Bạn Mất Hương Trong Rừng Không?
Hy vọng bạn enjoy chuyến ski vào rừng. Chúng ta đi sâu vào scopes và closures, sau khi gloss over chúng. Hy vọng chúng ta không làm bạn mất hương. Cho chúng ta biết nếu bạn muốn học thêm về bindings, hoặc có Ruby topic nào bạn muốn dive vào.
Cảm ơn đã follow và please kick tuyết khỏi bindings trước khi để cho developer tiếp theo. Nếu bạn thích những magical trips này, bạn có thể muốn subscribe Ruby Magic để nhận email khi chúng ta publish bài mới khoảng một tháng một lần.