Tìm hiểu Enumerable methods bằng cách re-implement chúng bằng Ruby (part I)
Bài đăng này đã không được cập nhật trong 8 năm
Enumerable là một module rất quan trọng trong Ruby, ngoài ra nó cũng là một ví dụ cho thấy vì sao Ruby lại sinh ra khái niệm module. Enumerable cung cấp một tập hợp gồm rất nhiều method giúp cho việc handle các data structer trong Ruby dễ dàng hơn, mặc dù cực kì mạnh mẽ nhưng nó chỉ yêu cầu 1 method duy nhất: each. Điều đó giải thích vì sao bất kì class nào trong Ruby muốn sử dụng như là một enumerable thì phải implement method each.
Trong bài này, chúng ta sẽ lần lượt re-implement lại các method của Enumerable, bằng cách đó chúng ta có thể hiểu sâu hơn về cách sử dụng, cũng như biết được cách mà Enumerable có thể được xây dựng chỉ dựa trên một method each.
Chuẩn bị
Chúng ta sẽ xây dựng một class ArrayWrapper để demo cho các hàm mà chúng ta sẽ re-implement.
class ArrayWrapper
include CustomEnumerable
def initialize *items
@items = items.flatten
end
def each &block
@items.each &block
self
end
def == other
@items == other
end
end
Đọc qua đoạn code trên 1 lượt chúng ta thấy,
- class
ArrayWrappersẽ include moduleCustomEnumerable(chúng ta sẽ implement module này ngay dưới đây). - methods
each: theo như chúng ta đã nói ở trênArrayWrapperbắt buộc phải implement methodeachđể có thể sử dụng được như mộtEnumerable - methods
==: vì chúng ta sẽ sử dụng Rspec để test nên phải implement method==này để chúng ta có thể sử dụng đượceqtrong Rspec.
map
Returns a new array with the results of running block once for every element in enum.
Như vậy chúng ta sẽ truyền vào each một block, mỗi item trên collection sẽ thực hiện block đó, kết quả sẽ được nối vào mảng kết quả.
module CustomEnumerable
def map &block
result = []
each do |element|
result << block.call(element)
end
result
end
end
Đây cũng là cách mà những hàm còn lại sẽ sử dụng để implement, thực hiện each trên từng phần tử, sau đó trả về kết quả. Có một chú ý là CustomEnumerable không biết nơi mà nó sẽ được include, nhưng nó biết chắc chắn 1 điều là class nào include nó phải implement method each.
Chúng ta sẽ sử dụng method map đã được implement trên để tạo ra một array mới bằng cách nhân đôi giá trị của từng element trên array cũ.
RSpec.describe CustomEnumerable do
context "map" do
it "map the numbers multiplying them by 2" do
items = ArrayWrapper.new 1, 2, 3, 4
result = items.map{|item| item * 2}
expect(result).to eq([2, 4, 6, 8])
end
end
end
find
Passes each entry in enum to block. Returns the first for which block is not false. If no object matches, calls ifnone and returns its result when it is specified, or returns nil otherwise.
Method find sẽ thực hiện block trên mỗi phần tử, trả về phần từ đầu tiên khiến cho giá trị của block là true, nếu không có phần tử nào thõa mãn, nó sẽ xem xét ifnone, nếu ifnone được chỉ định thì sẽ trả về kết quả của hàm ifnone, còn không sẽ trả về nil
def find ifnone = nil, &block
result = nil
found = false
each do |element|
if block.call(element)
result = element
found = true
break
end
end
found ? result : ifnone && ifnone.call
end
Trong trường hợp chúng ta tìm được kết quả phù hợp.
it "find on findable enumerable" do
items = ArrayWrapper.new 1, 2, 3, 4, 5
result = items.find do |item|
item == 3
end
expect(result).to eq(3)
end
Trường hợp không tìm được phần tử phù hợp, ifnone được xác định
it "find on unfindable enumerable and ifnone is specified" do
items = ArrayWrapper.new 1, 2, 4, 5, 6
result = items.find(lambda {0}) do |element|
element == 3
end
expect(result).to eq 0
end
Ở trên chúng ta đã truyền cho ifnone một anynomus method bằng lambda, methodnày sẽ được thực hiện, giá trị của nó sẽ được lấy làm giá trị của hàm find trong trường hợp không tìm được giá trị nào phù hợp.
Trường hợp không tìm được phần tử phù hợp, ifnone không được xác định
it "find on unfindable enumerable and ifnone is not specified" do
items = ArrayWrapper.new 1, 2, 4, 5, 6
result = items.find do |element|
element == 3
end
expect(result).to be_nil
end
Giá trị trả về sẽ là nil
find_all
Returns an array containing all elements of enum for which the given block returns a true value.
def find_all &block
result = []
each do |element|
result << element if block.call(element)
end
result
end
Thực hiện bằng RSpec: trường hợp có item thỏa mãn
context "find_all" do
it "find_all on enumerable" do
items = ArrayWrapper.new 1, 2, 3, 4, 5, 6
result = items.find_all do |element|
element % 2 == 0
end
expect(result).to eq([2, 4, 6])
end
end
Trường hợp không có item thỏa mãn, sẽ trả về empty
it "find_all on enumerable have no items which sastify condition" do
items = ArrayWrapper.new 1, 2, 3, 4, 5, 6
result = items.find_all do |element|
element > 7
end
expect(result).to be_empty
end
... to be continued
All rights reserved