Tìm hiểu Enumerable methods bằng cách re-implement chúng bằng Ruby (part I)
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 ...
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 module CustomEnumerable (chúng ta sẽ implement module này ngay dưới đây).
- methods each: theo như chúng ta đã nói ở trên ArrayWrapper bắt buộc phải implement method each để có thể sử dụng được như một Enumerable
- 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 được eq 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, 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