Destructuring Methods in Ruby

Điều gì sẽ xảy ra khi chúng ta viết đoạn mã thú vị như thế này trong Ruby ?:

destructure def adds(a: 1, b: 2)
  a + b
end
adds(a: 1, b: 2)
# => 3
adds(OpenStruct.new(a: 1, b: 2))
# => 3
Foo = Struct.new(:a, :b)
adds(Foo.new(1,2))
# => 3

Destructure

  • Đây là đoạn code ta sẽ tìm hiểu trong bài viết này:
def destructure(method_name)
  meta_klass  = class << self; self end
  method_proc = method(method_name)
  unless method_proc.parameters.all? { |t, _| t == :key }
    raise "Only works with keyword arguments"
  end
  arguments = method_proc.parameters.map(&:last)
  destructure_proc = -> object {
    values = if object.is_a?(Hash)
      object
    else
      arguments.map { |a| [a, object.public_send(a)] }.to_h
    end
    method_proc.call(values)
  }
  meta_klass.send(:define_method, method_name, destructure_proc)
  method_name
end

Cùng tìm hiểu destructing được thực hiện như thế nào trong Ruby nhé.

Definition of a Method

Như đã thấy trong phần đầu tiên của bài viết, chúng ta dễ dàng thấy được cách định nghĩa 1 method

destructure def adds(a: 1, b: 2)
  a + b
end

Trong các version mới hơn của Ruby, sau khi định nghĩa 1 method sẽ trả về tên method dưới dạng Symbol:

def foo; end
=> :foo

Destructure chỉ đơn thuần là một method nhận đối số chính là method_name và trả về Symbol:

def destructure(method_name)

Nhưng chính xác thì nó đang làm gì với cái tên đó, giờ đó lại là một vấn đề hoàn toàn khác.

Meta Klass

Nếu chúng ta muốn override một dynamic method ngày tại body của nó, chúng ta sẽ làm như thế nào? Làm thế nào về việc tạo một tham chiếu meta cho lớp ?:

meta_klass  = class << self; self end

Khi định nghĩa như vậy, chúng ta có thể dễ dàng tái cấu trúc lại nội dung của method như sau:

meta_klass.send(:define_method, method_name, destructure_proc)

Tuy nhiên, làm cách nào để ta có được phần body của method gốc?

The Method method

Ruby cung cấp hàm method để chuyển đổi :method_name ở dạng symbol thành một proc:

def add(a, b) a + b end
=> :add
method(:add)
=> #<Method: Object#add>
method(:add).call(1, 2)
=> 3

Chúng ta sử dụng .call để thực thi proc như bình thường. Điều đó có nghĩa là chúng ta có thể tham chiếu đến method sử dụng để thực hiện 1 nhiệm vụ mà chúng ta có thể thực hiện bất cứ khi nào chúng ta muốn.

`curl json_source.com/users.json`.yield_self(&JSON.method(:parse))

Thực ra đây không phải là 1 cách tối ưu, mà đây là 1 tính năng rất thú vị của Rails mà bạn hãy nên thử. Những điều này rất mới mẻ mà không phải Ruby dev nào cũng biết đâu nha. Vậy còn về arguments thì sao nhỉ?

Parameters!

Procs và các đối tượng giống Proc (Proc-like objects) có method tiện lợi được gọi là parameters

-> a, *b, c: 1, **d, &fn {}.parameters
=> [[:req, :a], [:rest, :b], [:key, :c], [:keyrest, :d], [:block, :fn]]

Mỗi giá trị là một mảng [type, name]. Nếu bạn chỉ muốn method hoạt động khi có đối số thì hãy làm như sau:

unless method_proc.parameters.all? { |t, _| t == :key }
  raise "Only works with keyword arguments"
end

Hashing things out

Ta đang đặt các giá trị bằng kết quả của điều kiện:

values = if object.is_a?(Hash)
  object
else
  ...
end

Chúng ta không cần quan tâm đến nhánh else. Tuy nhiên, điều muốn nói ở đây nếu những gì chúng tôi nhận được là một Hash (từ khóa hoặc một hàm băm theo nghĩa đen), chúng tôi có thể đang gọi phương thức như dự định:

Mappy Map Map

Các đối số ta truyền cho methods adds là một mảng, [: a,: b]. Nếu chúng ta truyền OpenStruct đến đó:

data = OpenStruct.new(a: 1, b: 2)

chúng ta có thể gọi a hoặc b để lấy ra các giá trị đó:

data.a # => 1
data.b # => 2

Lưu ý rằng OpenStruct có thể hoạt động giống như một Hash, nhưng không phải là điểm của phân đoạn này.

Vì vậy, nếu chúng ta có một mảng các method mà chúng ta muốn gọi trên đó, chúng ta có thể sử dụng map để kéo chúng ra!

[:a, :b].map { |method_name| data.public_send(method_name) }

Nhưng trong trường hợp này, chúng ta cần đưa dữ liệu của mình vào Hash để Ruby biết cách parse nó thành danh sách từ khóa, chúng ta nhánh khác:

values = if object.is_a?(Hash)
  ...
else
  arguments.map { |a| [a, object.public_send(a)] }.to_h
end

=> Kết quả

{ a: 1, b: 2 }

Your Call!

Bây giờ chúng ta đã lấy ra các giá trị đó, chúng ta có thể gọi method gốc bằng bất cứ thứ gì mình lấy ra được! Điều đó có nghĩa là nếu chúng ta định nghĩa bất kỳ thứ gì giống như Hash, ta sẽ gọi nó bằng Hash.

method_proc.call(values)

Đáng chú ý là phiên bản đầu tiên chỉ sử dụng meta_klass.send ở phần cuối. Nếu chúng ta có thể trả về cùng một tên phương thức để tạo chuỗi thú vị hơn nữa. Nếu bạn kiểm tra tweet ở trên, bạn cũng sẽ tìm thấy một chủ đề tham chiếu đến Gist được nhận xét đầy đủ về mã.


All Rights Reserved