12/08/2018, 15:15

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

... to be continued

0