12/08/2018, 16:57

Sử dụng enumeration trong Ruby

Enumeration (liệt kê) có thể được định nghĩa là quá trình trích xuất những thông tin trong một bộ dữ liệu ra thành một hệ thống có trật tự. Trong lập trình, trật tự này là bất cứ hành động nào, có thể chỉ là hiển thị ra hoặc là sắp xếp dữ liệu. Có thể thực hiện chuyển đổi theo từng bước, ...

Enumeration (liệt kê) có thể được định nghĩa là quá trình trích xuất những thông tin trong một bộ dữ liệu ra thành một hệ thống có trật tự. Trong lập trình, trật tự này là bất cứ hành động nào, có thể chỉ là hiển thị ra hoặc là sắp xếp dữ liệu. Có thể thực hiện chuyển đổi theo từng bước, mỗi bước xử lý toàn bộ tập dữ liệu trước khi đưa ra kết quả cho bước tiếp theo, hoặc có thể được xử lý kiểu "lazy" - xử lý nhiều mục cùng một lúc thông qua tất cả các biến đổi.

Cách sử dụng Enumeration trong Ruby

Trong bài viết này, tôi sẽ đánh giá nhanh về block và yield. Block là phần mã định nghĩa trong các method hoặc procs/lambdas. Yield là nơi mà một khối mã khác được dán vào khối mã hiện tại.

def my_printer
  puts "Hello World!"
end

def thrice
  3.times do
    yield
  end
end

thrice &method(:my_printer)
# Hello World!
# Hello World!
# Hello World!

thrice { puts "Ruby" }
# Ruby
# Ruby
# Ruby

Method (thrice) chấp nhận hai dạng block cho yield: procs hoặc blocks. Trong trường hợp đầu tiên yield thay thế cho puts "Hello World!" và trường hợp thứ hai là puts "Ruby". yield cũng có thể hoạt động như một enumerator đơn giản. Bạn có thể truyền bất kỳ giá trị nào dưới dạng tham số cho block/proc bằng cách thêm nó vào sau yield.

def simple_enum
  yield 4
  yield 3
  yield 2
  yield 1
  yield 0
end

simple_enum do |value|
  puts value
end
# 4
# 3
# 2
# 1
# 0

Điều kiện sử dụng

Điều kiện để tạo ra enumerator là phương thức each. Vì thế, bạn có thể định nghĩa một phương thức each với bất kỳ đối tượng Ruby nào rồi include Enumerable vào đối tượng đó và sau đó được sử dụng hơn 50 phương thức từ mô đun Enumerable. Enumerator có trong tất cả các collection cơ bản, như Array hay bất kỳ collection nào có phương thức each (và sẽ có mô đun Enumerable trong ancestors của nó).

Array.ancestors
# => [Array, Enumerable, Object, Kernel, BasicObject]
Hash.ancestors
# => [Hash, Enumerable, Object, Kernel, BasicObject]
Hash.method_defined? :each
# => true
require "set"
Set.ancestors
# => [Set, Enumerable, Object, Kernel, BasicObject]

Lazy and Not Lazy Enumeration

Lazy enumeration là cách tốt để xử lý một tập dữ liệu vô hạn.Ví dụ về một dây chuyền làm pizza mà mỗi người chỉ chịu trách nhiệm một bước trong quy trình làm bánh. Người đầu tiên nặn bột người tiếp theo cho nước sốt, tiếp theo là phomat, một người nướng bánh, và người cuối cùng chuyển pizza cho bạn. Trong ví dụ này, khi sử dụng "lazy", nếu có bất kỳ đơn đặt hàng nào, tất cả mọi người chỉ cần thực hiện một công việc trên 1 chiếc pizza, sau đó chuyển sang ngay chiếc tiếp theo. Nếu không sử dụng "lazy", thì mỗi bước sẽ phải chờ cho toàn bộ công việc trước đó được thực hiện xong. Ví dụ: nếu có 20 đơn đặt hàng, người cho nước sốt sẽ phải chờ người nặn bột nặn xong hết 20 chiếc, mỗi bước tiếp theo tương tự. Hãy tưởng tượng với số lượng lớn dữ liệu cần được xử lý? Một ví dụ thực tế hơn là xử lý gửi email đến tất cả người dùng. Nếu có một lỗi trong code và nó không được xử lý kiểu "lazy", thì rất có thể là không ai nhận được mail. Nhưng trong trường hợp dùng "lazy", có thể hầu hết người dùng sẽ nhận được email. Khi có sự cố với một vài email, nếu lưu lại thông tin các email gửi thành công, thì việc điều tra sẽ dễ dàng hơn rất nhiều. Tạo một lazy enumerator bằng cách gọi lazy trong object có included Enumerable hoặc to_enum.lazy trên một object đã định nghĩa phương thức each.

class Thing
  def each
    yield "winning"
    yield "not winning"
  end
end

a = Thing.new.to_enum.lazy

Thing.include Enumerable
b = Thing.new.lazy

a.next
# => "winning"
b.next
# => "winning"

Phương thức to_enum sẽ trả về một object Enumerator và cả Enumerable nên có quyền truy cập vào tất cả các phương thức của chúng. Quan trọng là chú ý đến những phương thức nào sẽ xử lý toàn bộ dữ liệu và sẽ sử dụng "lazy". Ví dụ: phương thức partition sử dụng toàn bộ tập dữ liệu, vì vậy nó không phù hợp với tập dữ liệu vô hạn. Các lựa chọn tốt hơn sẽ là các phương thức như chunk hoặc select.

x = (0..Float::INFINITY)

y = x.chunk(&:even?)
# => #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x0055eb840be350>:each>>
y.next
# => [true, [0]]
y.next
# => [false, [1]]
y.next
#=> [true, [2]]

z = x.lazy.select(&:even?)
# => #<Enumerator::Lazy: #<Enumerator::Lazy: 0..Infinity>:select>
z.next
# => 0
z.next
# => 2
z.next
# => 4

Trong trường hợp sử dụng select với tập vô hạn, trước tiên bạn phải gọi phương thức lazy để tránh việc select toàn bộ dữ liệu.

Tạo một Lazy Enumerator

Lớp Enumerator::Lazy cho phép viết các phương thức lazy enumerator giống như phương thức take.

(0..Float::INFINITY).take(2)
# => [0, 1]

Ví dụ, bắt đầu với một số nguyên, tìm các số chia hết cho 3, 5, 15 trả ra các kết quả tương ứng Fizz, Buzz, FizzBuzz.

def divisible_by?(num)
  ->input{ (input % num).zero? }
end

def fizzbuzz_from(value)
  Enumerator::Lazy.new(value..Float::INFINITY) do |yielder, val|
    yielder << case val
    when divisible_by?(15)
      "FizzBuzz"
    when divisible_by?(3)
      "Fizz"
    when divisible_by?(5)
      "Buzz"
    else
      val
    end
  end end

x = fizzbuzz_from(7)
# => #<Enumerator::Lazy: 7..Infinity:each>

9.times { puts x.next }
# 7
# 8
# Fizz
# Buzz
# 11
# Fizz
# 13
# 14
# FizzBuzz

Sử dụng Enumerator nâng cao

Khi xử lý tập dữ liệu, bạn nên lọc dữ liệu trước khi xử lý. Ví dụ, nếu bạn lấy dữ liệu từ cơ sở dữ liệu để xử lý, lọc trước dữ liệu bằng ngôn ngữ cơ sở dữ liệu trước khi sử dụng Ruby nếu có thể. Điều đó sẽ hiệu quả hơn.

require "prime"
x = (0..34).lazy.select(&Prime.method(:prime?))
x.next
# => 2
x.next
# => 3
x.next
# => 5
x.next
# => 7
x.next
# => 11

Sau phương thức select trên, bạn có thể đưa các phương thức khác vào để xử lý dữ liệu. Những phương thức sau sẽ chỉ xử lý trên tập số nguyên tố đã được lọc.

Grouping

Một cách xử lý dữ liệu để chia thành các cột là sử dụng group_by để chuyển đổi các kết quả thành một nhóm băm. Sau đó, chỉ cần lấy ra các giá trị:

[0,1,2,3,4,5,6,7,8].group_by.with_index {|_,index| index % 3 }.values
# => [[0, 3, 6], [1, 4, 7], [2, 5, 8]]
0    3    6
1    4    7
2    5    8

Đoạn group_by trên truyền cả value và index vào khối mã. Không quan tâm đến value nên sử dụng _. Nếu chúng ta muốn sắp xếp từ trái sang phải trong các cột, đơn giản có thể làm điều này:

threes = (0..2).cycle
[0,1,2,3,4,5,6,7,8].slice_when { threes.next == 2 }.to_a
# => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]

threes enumerator là chu kỳ lặp vô tận từ 0-2:

0    1    2
3    4    5
6    7    8

Hoặc có thể sử dụng phương thức transpose:

x = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
x = x.transpose
# => [[0, 3, 6], [1, 4, 7], [2, 5, 8]]
x = x.transpose
# => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]

Folding

Chúng ta hãy xem xét cách kết hợp một bộ sưu tập với kết quả. Trong các ngôn ngữ khác, điều này thường được thực hiện với một phương thức có tên fold. Trong Ruby, nó đã được thực hiện với reduce và inject. Một bổ sung gần đây là dùng each_with_object. Ví dụ, tính tổng một tập các số nguyên:

[1,2,3].reduce(:+)
# => 6

[1,2,3].inject(:+)
# => 6

class AddStore
  def add(num)
    @value = @value.to_i + num
  end

  def inspect
    @value
  end
end

[1,2,3].each_with_object(AddStore.new) {|val, memo| memo.add(val) }
# => 6

# As of Ruby 2.4
[1,2,3].sum
# => 6

each_with_object cần một đối tượng có thể được cập nhật. Không thể thay đổi một đối tượng số nguyên từ chính nó, vì vậy chúng ta tạo một đối tượng AddStore. Nhưng phương pháp này sẽ được chứng minh tốt hơn bằng cách lấy dữ liệu từ một tập và đưa chúng vào một tập.

collection = [:a, 2, :p, :p, 6, 7, :l, :e]

collection.reduce("") { |memo, value|
  memo << value.to_s if value.is_a? Symbol
  memo # Note the return value needs to be the object/collection we're building
}
# => "apple"

collection.each_with_object("") { |value, memo|
  memo << value.to_s if value.is_a? Symbol
}
# => "apple"

Structs

Các đối tượng struct của Ruby cũng là các đối tượng có thể đếm được.

class Pair < Struct.new(:first, :second)
  def same?;    inject(:eql?)  end
  def add;      inject(:+)     end
  def subtract; inject(:-)     end
  def multiply; inject(:*)     end
  def divide;   inject(:/)     end

  def swap!
    members.zip(entries.reverse) {|a,b| self[a] = b}
  end

end

x = Pair.new(23, 42)
x.same?
# => false

x.first
# => 23

x.swap!

x.first
# => 42

x.multiply
# => 966

Cấu trúc thường không được sử dụng cho các tập dữ liệu lớn mà là các đối tượng dữ liệu hữu ích, một cách để truyền dữ liệu có tổ chức với nhau, cho phép sử dụng dữ liệu rõ ràng hơn là các cụm dữ liệu. Vì vậy, cấu trúc trong Ruby nói chung là tập dữ liệu nhỏ, nhưng bản thân dữ liệu đó có thể là các tập dữ liệu khác. Trong trường hợp này, một struct có thể là một cách để thực hiện các phép biến đổi đối với những tập dữ liệu đó, giống như một custom class.

Nguồn: https://blog.codeship.com/advanced-enumeration-with-ruby/

0