Tạo các dãy vô hạn (Infinite Sequences) với Ruby
Bài đăng này đã không được cập nhật trong 8 năm
Các ngôn ngữ lập trình hàm (functional languages) như Clojure có tính năng là dãy - sequences
. Sqequences
có thể biến đổi các thuật toán thành các cấu trúc dữ liệu. Ta có thể gọi hàm trên dữ liệu được tạo ra bởi các thuật toán và có thể coi chúng là các collection kể cả khi độ dài của nó là vô tận.
Thư viện chuẩn của Ruby không có class hoặc module nào để thể hiện sequence
nhưng nó cung cấp module Enumerable
giúp ta có thể viết một sequence
. Dân Ruby thường biết tới Enumerable
thông qua các phương thức của Array
như map
hoặc select
. Một Array
như là [1, 2, 3, 4]
thường được coi là một sequence
hữu hạn và được tạo sẵn; chúng đã được khởi tạo với tất cả các thành viên mà ta có thể dùng các method để chạy trên chúng. Với Enumerable
ta có thể thêm vào các chức năng để biến nó thành một sequence
.
Enumerable Fibonacci
Trước tiên, ta sẽ xem xet một ví dụ với một chuỗi (sequence
) vô hạn là Fibonacci. Ta có thể viết một hàm mà khởi tạo n
số Fibonacci đầu tiên như sau:
def eager_fibonacci(n)
a = b = 1
result = []
loop do
break if result.size >= n
result << a
a, b = b, a + b
end
result
end
Đơn giản đúng không, tuy nhiên ta có thể nâng cấp nó lên đôi chút. Thay vì trả về một Array
với các phần tử đã được tính toán sẵn, ta có thể trả về một Enumerator
. Sau đó với Enumerator
này ta có thể khởi tạo các phần tử một cách lần lượt, gọi đến đâu tạo đến đó.
def fibonacci
Enumerator.new do |y|
a = b = 1
loop do
y << a
a, b = b, a + b
end
end
end
Enumerator
đơn giản chỉ là một đối tượng Enumerable
được dùng để duyệt các collection. Enumerable
là một module được include trong rất nhiều class như Array
hay Hash
để cung cấp tính năng duyệt phần tử cho các object của các class này. Hàm khởi tạo của Enumerator
nhận vào một block và đóng vai trò là một template cho một thuật toán liệt kê (thuật toán tạo ra các dãy số). Block này nhận vào một tham số, y
, là một instance của class Enumerator::Yielder
. Nó sẽ làm nhiệm vụ là đẩy ra giá trị ra cho các hàm gọi của Enumerable
. Nói đơn giản là ta có thể coi một Enumerator
tương tự như một Array
; có thể duyệt từng phần tử và gọi các hàm trên các phần tử đấy.
Để lấy 10 phần tử đầu tiên của dãy Fibonacci được implement bằng Enumerator
, ta dùng như sau:
fibonacci.take(10)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
10 phần tử đầu tiên của dãy Fibonacci sẽ được tính toán và trả ra một mảng từ đó ta có thể duyệt nó như bình thường:
fibonacci.take(10).each{|i| puts i}
Ta có thể kết hợp sử dụng hàm lazy
của Enumerator
để tránh việc phải tính toán toàn trước toàn bộ các phần tử muốn lấy ra. Thay vào đó, các phần tử sẽ được khởi tạo lần lượt và ta có thể tính toán trên từng phần tử một. Sau đây là ví dụ về việc lấy 10 phần tử chẵn và lẻ đầu tiên của dãy Fibonacci:
fibonacci.lazy.select(&:even?).first(10)
#=> [2, 8, 34, 144, 610, 2584, 10946, 46368, 196418, 832040]
fibonacci.lazy.select(&:odd?).first(10)
#=> [1, 1, 3, 5, 13, 21, 55, 89, 233, 377]
Nếu muốn xem phải mất bao nhiêu vòng lặp để có thể lấy được 10 phần tử chẵn hoặc lẻ đầu tiên của dãy, hãy thêm hàm with_index
của Enumerator
:
fibonacci.lazy.with_index.select { |n, i| n.odd? }.first(10)
#=> [[1, 0], [1, 1], [3, 3], [5, 4], [13, 6], [21, 7], [55, 9], [89, 10], [233, 12], [377, 13]]
fibonacci.lazy.with_index.select { |n, i| n.even? }.first(10)
#=> [[2, 2], [8, 5], [34, 8], [144, 11], [610, 14], [2584, 17], [10946, 20], [46368, 23], [196418, 26], [832040, 29]]
Nhìn vào đây, ta thấy là để lấy được 10 phần tử lẻ đầu tiên của dãy Fibonacci thì phải mất 13 vòng lặp và 29 vòng lặp là số lượng để lấy ra được 10 phần tử chẵn đầu tiên của dãy Fibonacci. Nếu ta sử dụng cách thông thường để tạo dãy Fibonacci để làm điều này thì sẽ rất khó vì ta cần phải truyền vào số lượng phần tử muốn lấy ra từ dãy Fibonacci trước khi thực hiện thuật toán.
Một ví dụ nữa là tạo một dãy các số giai thừa với Enumerator
:
factorial = Enumerator.new do |y|
a = b = 1
loop do
y << a
b = b + 1
a = a * b
end
end
factorial.take 5
#=> [1, 2, 6, 24, 120
Sequence Function
Clojure có một số hàm cho phép tạo ra các dãy sequences
từ các hàm khác. Hãy xem ví dụ với hàm repeatedly
. Hàm này sẽ gọi một function liên tục và đưa ra kết quả như một sequence
. Để lấy ra một chuỗi sequence
gồm 5 phần tử bất kỳ từ 0 đến 100, ta làm như sau:
(take 5 (repeatedly #(rand-int 100)))
Đoạn code trên chỉ đơn giản là lấy 5 kết quả đầu tiên của hàm repeatedly
trong đó goi một hàm lấy một giá trị bất kỳ từ 0 -> 100.
Ta cũng có thể sử dụng Enumerator
của Ruby để thực hiện điều tương tự. Ta sẽ tạo một phiên bản repeatedly
riêng trong Ruby. Hàm này sẽ nhận vào một block và chạy nó một cách liên tục. Ta sẽ implement theo cách đơn giản nhất là sử dụng loop:
def repeatedly_foo(n, &block)
result = []
loop do
break if result.size >= n
result << block.call
end
result
end
Với cách implement này, block luôn bị gọi n lần và trả ra kết quả trước rồi từ đó ta mới có thể sử dụng kết quả này để thực hiện tính toán tiếp.
Bây giờ, ta sẽ bao đoạn loop với một Enumerator
và ta có thể coi kết quả trả về như là một sequence
:
def repeatedly(&block)
Enumerator.new do |y|
loop do
y << block.call # "yield" the result to the Enumerator::Yielder
end
end
end
Bây giờ ta đã có một sequence
mà có thể chain với các hàm Enumerator
khác. Bây giờ ta đã có một phiên bản repeatedly
tương đương với Clojure:
repeatedly { rand(100) }.take 5
#=> [54, 43, 93, 5, 87] # Kết quả này sẽ khác nhau với mỗi lần chạy.
Kết luận
Ta có thể áp dụng kỹ thuật sequence
này vào các ứng dụng mà làm việc với các tập dữ liệu có độ dài không biết trước được như kết quả từ một truy vấn tìm kiếm, dữ liệu phân trang từ các API, dữ liệu từ các web crawl...
Tham khảo
All rights reserved