Các điều tra viên là trung tâm của điều khiến Ruby trở thành một ngôn ngữ năng động và mạnh mẽ như vậy. Và những người điều tra lười biếng sẽ tiến thêm một bước nữa bằng cách cho phép bạn làm việc hiệu quả với các bộ sưu tập cực kỳ lớn.
Tệp - hóa ra - chỉ là một bộ sưu tập lớn các dòng hoặc ký tự. Vì vậy, những người điều tra lười biếng có thể thực hiện một số điều rất thú vị và mạnh mẽ với họ.
Dù sao thì Điều tra viên là gì?
Mỗi khi bạn sử dụng một phương pháp như each
, bạn tạo một điều tra viên. Đây là lý do mà bạn có thể xâu chuỗi các phương thức lại với nhau như [1,2,3].map { ... }.reduce { ... }
. Bạn có thể thấy ý của tôi trong ví dụ dưới đây. Gọi each
trả về một điều tra viên mà tôi có thể sử dụng để thực hiện các thao tác lặp đi lặp lại khác.
# I swiped this code from Ruby's documentation https://ruby-doc.org/core-2.2.0/Enumerator.html
enumerator = %w(one two three).each
puts enumerator.class # => Enumerator
enumerator.each_with_object("foo") do |item, obj|
puts "#{obj}: #{item}"
end
# foo: one
# foo: two
# foo: three
Người liệt kê lười biếng dành cho các bộ sưu tập lớn
Điều tra viên bình thường gặp vấn đề với các bộ sưu tập lớn. Lý do là mỗi phương thức bạn gọi muốn lặp lại trên toàn bộ tập hợp. Bạn có thể tự mình thấy điều này bằng cách chạy mã sau:
# This code will "hang" and you'll have to ctrl-c to exit
(1..Float::INFINITY).reject { |i| i.odd? }.map { |i| i*i }.first(5)
reject
phương thức chạy mãi mãi, vì nó không bao giờ có thể hoàn tất việc lặp qua một tập hợp vô hạn.
Nhưng với một bổ sung nhỏ, mã chạy hoàn hảo. Nếu tôi chỉ gọi lazy
, Ruby thực hiện điều thông minh và chỉ thực hiện nhiều lần lặp lại nếu cần thiết cho việc tính toán. Đó chỉ là 10 hàng trong trường hợp này, nhỏ hơn đáng kể so với vô cực.
(1..Float::INFINITY).lazy.reject { |i| i.odd? }.map { |i| i*i }.first(5)
#=> [4, 16, 36, 64, 100]
Sáu nghìn bản Moby Dick
Để kiểm tra các thủ thuật tệp này, chúng tôi sẽ cần một tệp lớn. Một con số lớn đến mức hiển nhiên là "lười biếng".
Tôi đã tải xuống Moby Dick từ Project Gutenberg, sau đó tạo một tệp văn bản chứa 100 bản sao. Tuy nhiên, điều đó không đủ lớn. Vì vậy, tôi đã nâng nó lên khoảng 6.000. Điều đó có nghĩa là bây giờ tôi có lẽ là người duy nhất trên thế giới có một tệp văn bản chứa 6.000 bản sao của Moby Dick. Đó là một loại khiêm tốn. Nhưng tôi lạc đề.
Tôi đã tải xuống moby dick và sao chép nó vài nghìn lần để có một tệp lớn để chơi. Cú pháp không phải là sai lầm. Đó là vỏ cá. Tôi nghĩ tôi là người duy nhất còn lại sử dụng nó.
Cách lấy các điều tra viên cho một tệp
Đây là một thủ thuật Ruby thú vị mà bạn có thể đã sử dụng, ngay cả khi bạn không biết mình đang sử dụng nó. Hầu như bất kỳ phương thức nào trong Ruby lặp qua một tập hợp sẽ trả về một đối tượng Enumerator cho bạn nếu bạn gọi nó mà không chuyển vào một khối. Điều đó có nghĩa là gì?
Hãy xem xét ví dụ này. Tôi có thể mở một tệp, và sử dụng từng dòng để in ra từng dòng. Nhưng nếu tôi gọi nó mà không có khối, tôi sẽ có một điều tra viên. Các phương pháp được quan tâm là each_line
, each_char
và each_codepoint
.
File.open("moby.txt") do |f|
# Print out each line in the file
f.each_line do |l|
puts l
end
# Also prints out each line in the file. But it does it
# by calling `each` on the Enumerator returned by `each_line`
f.each_line.each do |l|
puts l
end
end
Hai ví dụ này trông gần như giống hệt nhau, nhưng ví dụ thứ hai giữ chìa khóa để mở khóa AMAZING POWERS .
Sử dụng liệt kê của tệp
Khi bạn có một bảng liệt kê "chứa" tất cả các dòng trong một tệp, bạn có thể cắt và chia nhỏ các dòng đó giống như bạn có thể làm với bất kỳ mảng ruby nào. Đây chỉ là một vài ví dụ.
file.each_line.each_with_index.map { |line, i| "Line #{ i }: #{ line }" }[3, 10]
file.each_line.select { |line| line.size == 9 }.first(10)
file.each_line.reject { |line| line.match /whale/i }
Điều này thực sự tuyệt vời, nhưng tất cả các ví dụ này đều có một vấn đề lớn. Tất cả đều tải toàn bộ tệp vào bộ nhớ trước khi lặp lại. Đối với một tệp chứa 6.000 bản sao của Moby Dick, độ trễ là đáng chú ý.
Tải chậm các dòng của tệp
Nếu chúng ta đang quét một tệp văn bản lớn cho 10 trường hợp đầu tiên của từ "cá voi" thì thực sự không cần phải tiếp tục tìm kiếm lần xuất hiện thứ 10. May mắn thay, thật dễ dàng để nói với các điều tra viên của Ruby làm điều này. Bạn chỉ cần sử dụng từ khóa "lười biếng".
Trong các ví dụ dưới đây, chúng tôi tận dụng tính năng tải chậm để thực hiện một số việc khá phức tạp.
File.open("moby.txt") do |f|
# Get the first 3 lines with the word "whale"
f.each_line.lazy.select { |line| line.match(/whale/i) }.first(3)
# Go back to the beginning of the file.
f.rewind
# Prepend the line number to the first three lines
f.each_line.lazy.each_with_index.map do |line, i|
"LINE #{ i }: #{ line }"
end.first(3)
f.rewind
# Get the first three lines containing "whale" along with their line numbers
f.each_line.lazy.each_with_index.map { |line, i| "LINE #{ i }: #{ line }" }.select { |line| line.match(/whale/i) }.first(3)
end
Nó không chỉ dành cho các tệp
Các ổ cắm, đường ống, cổng nối tiếp - chúng được biểu diễn trong Ruby bằng cách sử dụng lớp IO. Điều đó có nghĩa là tất cả chúng đều có each_line
, each_char
và each_codepoint
các phương pháp. Vì vậy, bạn có thể sử dụng thủ thuật này cho tất cả chúng. Khá gọn gàng!
Nó không phải là phép thuật
Thật không may, người liệt kê lười biếng chỉ tăng tốc mọi thứ nếu nhiệm vụ bạn đang cố gắng hoàn thành không yêu cầu đọc toàn bộ tệp. Nếu bạn đang tìm kiếm một từ chỉ xuất hiện ở trang cuối cùng của cuốn sách, bạn phải đọc toàn bộ cuốn sách để tìm từ đó. Nhưng trong trường hợp đó, cách tiếp cận này không được chậm hơn cách tiếp cận không điều tra viên.