Tìm hiểu Enumerable module part I trong Ruby

Trong bài viết này ta sẽ tìm hiểu về 2 thứ đó là:

- Enumerable module
- Enumerator class

1. Enumerable module

- Module này bao gồm:

  • Traversal methods
  • Searching methods
  • Sorting methods

- Có vài class trong Ruby include module này theo mặc định như Array, Hash, Range.
- Bây giờ ta sẽ đi vào chi tiết của 3 loại method traversal, sorting và searching.

pry(main)> [1, 2, 3, 4, 5].map {|n| n + 1}
=> [2, 3, 4, 5, 6]
pry(main)> %w[d a n g v a n n a m].sort
=> ["a", "a", "a", "d", "g", "m", "n", "n", "n", "v"]
pry(main)> [16, 6, 2018].first
=> 16

Đầu tiên, ta sử dụng phương pháp khá phổ biến đó là map method.
Sau đó, ta sử dụng sort method, sắp xếp theo thứ tự chữ cái tăng dần theo mặc định.
Cuối cùng, ta sử dụng searching method first để fetch item đầu tiên của mảng.

Include Enumerable module

Một lớp bao gồm module Enumerable phải trả về each method:

class Users
  include Enumerable
  def initialize
    @users = %w[Mai Tao Bao]  
  end
end
pry(main)> Users.new.map { |user| user.upcase }
NoMethodError: undefined method `each' for <Users:000fff>

Trong ví dụ trên, NoMethodError được raised bởi vì Enumerable#map method làm cho việc gọi each method không được định nghĩa trong class Users.
Vì vậy, ta sẽ định nghĩa nó

class Users
  include Enumerable
  def initialize
    @users = %w[Mai Tao Bao] 
  end
  def each
    for user in @users do
      yield user
    end
  end
end
pry(main)> Users.new.map { |user| user.upcase }
=> ["MAI", "TAO", "BAO"]

Ở đây, chúng ta định nghĩa Users#each method, trong đó ta lặp qua mảng @users, chúng ta yield từng giá trị của mảng @users
Vì Users#each method được định nghĩa nên Enumerable#map method có thể sử dụng nó và nhận từng user làm đối số cho block.

2. Enumerator class

- Cách sử dụng một Enumerator

pry(main)> enumerator = [1, 5, 7, 8].map
#<Enumerator: [1, 5, 7, 8]:map>

Nếu không có đối số nào được truyền đến map(hoặc gần như tất cả các method của Enumerable module) thì một thể hiện của lớp Enumerator được trả về.
enumerator là liên kết giữa mảng [1, 5, 7, 8] với map method

pry(main)> enumerator.each { |n| puts n; n + 1 }
1
5
7
8
=> [2, 6, 8, 9]

Trong ví dụ trên, map method được thực hiện trong ngữ cảnh của each method.
Ta có thể thấy, enumerator chỉ thực hiện một lần nội dung trong block
Trường hợp này sử dụng không hiệu quả lắm vì thế ta có thể gọi map method kiểu sau: [1, 5, 7, 8].map { |n| puts n; n + 1 } nó sẽ tương tự so với việc ta sử dụng enumerator.each trong ví dụ bên trên.
Tiếp theo, ta xét ví dụ sau:

pry(main) %w[VietNam NhatBan HanQuoc].map.with_index do |q, index|
pry(main)*   "STT: #{index + 1}, QuocTich: #{q}"  
pry(main)* end  
=> ["STT: 1, QuocTich: VietNam", "STT: 2, QuocTich: NhatBan", "STT: 3, QuocTich: HanQuoc"]

map_with_index chưa phải là một phần của Enumerable module nên cách phù hợp nhất để sao chép phương thức này là sử dụng Enumerator được trả về bởi map method mà không có đối số nào và gọi đến Enumerator#with_index method.
Vì thế, map method sẽ được thực thi trong bối cảnh của with_index method.

Chaining Enumerators

Ruby cung cấp cho bạn khả năng kết nối các Enumerator với nhau.

pry(main)> enumerator = [1, 5, 7, 8].map
#<Enumerator: [1, 5, 7, 8]:map>
pry(main)> enumerator = enumerator.with_index
 => #<Enumerator: #<Enumerator: [1, 5, 7, 8]:map>:with_index>
 pry(main)> enumerator.each { |x, i| puts x; "#{x}:#{i}" }
1
5
7
8
=> ["1:0", "5:1", "7:2", "8:3"]

Ở đây, map method được thực hiện trong ngữ cảnh của with_index method và each method.

External Iterator

Iterator có thể hiểu nó cung cấp một cách để truy cập những phần tử của một tập các đối tượng một cách tuần tự mà không làm lộ các cách thức thể hiện của chúng.

pry(main)> enumerator = [1, 5, 7, 8].to_enum
#<Enumerator: [1, 5, 7, 8]:each>

Kernel#to_enum trả về một thể hiện của lớp Enumerator bao gồm self(là mảng [1, 5, 7, 8] trong trường hợp này) làm data source cùng với each method

 pry(main)> Enumerator.instance_methods(false)
=> [:inspect, :size, :each, :each_with_index, :each_with_object, 
    :next, :rewind, :with_index, :with_object, :next_values, 
    :peek_values, :peek, :feed]

Enumerator class cung cấp một loạt các phương thức để thao tác một con trỏ bên trong mà không làm ảnh hưởng đến trạng thái của phép lặp bên ngoài.

pry(main)> enumerator.next
=> 1
pry(main)> enumerator.peek
=> 5
pry(main)> enumerator.next
=> 5
pry(main)> enumerator.next
=> 7
pry(main)> enumerator.next
=> 8
pry(main)> enumerator.next
StopIteration: iteration reached an end
pry(main)> enumerator.rewind
=> #<Enumerator: [1, 5, 7, 8]:each>
pry(main)> enumerator.peek
=> 1

Enumerator#peek method trả về giá trị được lưu tại vị trí con trỏ.
Enumerable#next method di chuyển con trỏ đến vị trí tiếp theo.
Enumerable#next đưa ra lỗi StopIteration khi nó được gọi và con trỏ ở vị trí cuối cùng của dữ liệu.
Enumerable#rewind method di chuyển con trỏ đến vị trí trước đó.

Tài liệu tham khảo: https://ruby-doc.org/core-2.2.0/Enumerator.html