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 7 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
ArrayWrapper
sẽ include moduleCustomEnumerable
(chúng ta sẽ implement module này ngay dưới đây). - methods
each
: theo như chúng ta đã nói ở trênArrayWrapper
bắ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 đượceq
trong 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
, method
nà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