Enumerable trong Ruby là gì và tại sao bạn nên sử dụng nó

Module Enumerable thì đã rất phổ biến trong Ruby. Nó đóng gói rất nhiều những method tuyệt vời bên trong và thật khó để tìm ra ứng dụng nào đó không sử dụng nó. Bài viết này mình xin trình bày những khái niệm cơ bản của Enumerable

Vì tính kế thừa trong Ruby bị giới hạn trong một lớp cha duy nhất thôi nên các module cho phép bạn chia sẻ chức năng giữa các class không liên quan đến nhau.

Enumerable là một module như thế, một trong số các chức năng đó sẽ được bạn tích hợp bên trong class của mình.

Hãy xem các module được tích hợp trong các lớp ArrayHash

[10] pry(main)> Array.included_modules
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
 JSON::Ext::Generator::GeneratorMethods::Array,
 MessagePack::CoreExt,
 Enumerable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 RequireAll,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel]
[10] pry(main)> Hash.included_modules
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
 DeepMerge::DeepMergeHash,
 JSON::Ext::Generator::GeneratorMethods::Hash,
 MessagePack::CoreExt,
 Enumerable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 RequireAll,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel]

Tại sao nên sử dụng Enumerable trong class của bạn

Module Enumerable cung cấp sẵn cho bạn những method để tìm kiếm, duyệt qua và sắp xếp tập hợp. Nên khi cần làm việc với tập hợp, bạn có thể đơn giản là sử dụng luôn Enumerable.

Tạo class Enumerable của bạn

Do đó, để sử dụng module Enumerable, bạn đơn giản chỉ cần khai báo include trong class của mình và cung cấp một method each như sau:

class Person
  attr_accessor :name, :emails

  include Enumerable

  def initialize(name)
    @name = name
  end

  def each
    yield name
  end
end

me = Person.new("Cezar").to_a # => ["Cezar"]
me.emails = ["[email protected]", "[email protected]", "[email protected]"]
me.emails.sort
# => ["[email protected]", "[email protected]", "[email protected]"]

Ruby phiên hiện tại đã mặc định tích hợp sẵn Enumerable trong class của mình.

[10] pry(main)> Person.class
=> Class
[11] pry(main)> Person.included_modules
=> [Enumerable,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 RequireAll,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel]

Do đó bạn có thể sử dụng luôn các method như to_a, sort như bên trên mà không cần phải định nghĩa

Enumerable khác gì Enumerator?

Mặc dù có vẻ gần giống nhau nhưng có một khác biệt lớn giữa 2 khái niệm này.

  • Module Enumerable là thứ bạn tích hợp trong class của mình để sử dụng tất cả những method Enumerable.
  • Enumerator là một class có tích hợp sẵn module Enumerable, cũng giống như các class khác của bạn.
[10] pry(main)> e = [1, 2, 3].map
=> #<Enumerator: ...>
[11] pry(main)> a.class
=> Enumerator
[12] pry(main)> e.class.included_modules
=> [Enumerable,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 RequireAll,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel]
[12] pry(main)> e.each_with_index { |n, i| n * i }
=> [0, 2, 6]
[13] pry(main)> e.next
=> 1
[14] pry(main)> e.next
=> 2

Xây dựng module Enumerable của bạn.

Cách tốt nhất để tìm hiểu là tạo một module từ đầu:

module Reduceable
  def reducer(accumulator)
    each do |value|
      accumulator = yield(acc, value)
    end
    accumulator
  end
end

class Person
  attr_accessor :emails
  include Reduceable

  def number_of_emails
    reducer(0) { |total, email| total + email.size }
  end

  def each
    yield emails
  end
end

me = Person.new
me.emails = ["[email protected]", "[email protected]", "[email protected]"]
me.number_of_emails # => 3

Method reducer nhận vào giá trị ban đầu accumulator (trong ví dụ trên là 0), và với mỗi giá trị đó, nó truyền cả hai accumulatoremail đến block mà nó vừa nhận được. Cuối cùng, nó trả về giá trị accumulator.

Bạn có thể làm bất cứ điều gì bạn muốn với các đối số đó bên trong block.

Tìm giá trị min/max

Với Enumerable thì bạn sẽ có thể làm rất đơn giản như sau:

[10] pry(main)> [1, 2, 3].min
=> 1
[11] pry(main)> [1, 2, 3].max
=> 3

min/max method cũng có thể nhận vào các đối số

[10] pry(main)> [1, 2, 3].min(2)
=> [1, 2]
[11] pry(main)> [1, 2, 3].max
=> [3, 2]

Reset một enumerable

Một mẹo nhỏ mà bạn có thể thực hiện với Enumerator là method rewind - dịch về phần tử đầu tiên.

[10] pry(main)> e = [1, 2, 3].map
=> #<Enumerator: ...>
[11] pry(main)> e.next
=> 1
[12] pry(main)> e.next
=> 2
[13] pry(main)> e.rewind
=> #<Enumerator: ...>
[14] pry(main)> e.next
=> 1

Đảo một enumerable

Method reverse_each sẽ trả về một Enumerator với các phần tử đã bị đảo ngược.

[10] pry(main)> [1, 2, 3].reverse_each.to_a
=> [3, 2, 1]

Method count, sizelength

Method size chỉ là cách viết khác của length mà thôi, còn method count sẽ hơi khác một chút. count có thể dùng với a block. Nếu truyền vào block cho method count, nó chỉ đếm những phần tử mà block trả về giá trị tin cậy (không phải nil hay là false)

[10] pry(main)> [1, :a, "b", nil, false].length
=> 5
[11] pry(main)> [1, :a, "b", nil, false].count { |n| n }
=> 3

Kết

Trên đây là những kiến thức cũng như cách sử dụng cơ bản module Enumerable. Hi vọng nó sẽ có ích với bạn.

Tham khảo