Enumerators อยู่ที่หัวใจของ Ruby ที่ทำให้ powerful และ dynamic Lazy enumerators ขึ้นอีกขั้น ให้ทำงานกับ extremely large collections ได้อย่างมีประสิทธิภาพ
Files กลายเป็น large collections ของ lines หรือ characters Lazy enumerators ทำให้ทำสิ่งน่าสนใจและ powerful กับ files ได้
Enumerator คืออะไร?
ทุกครั้งใช้ method เหมือน each สร้าง enumerator นั่นคือเหตุผล chain methods เหมือน [1,2,3].map { ... }.reduce { ... } ได้ ดูตัวอย่าง each คืน enumerator ใช้ทำ iterative operations อื่นๆ
# 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
Lazy Enumerators สำหรับ Large Collections
Normal enumerators มีปัญหา large collections เพราะแต่ละ method ที่เรียกอยาก iterate ทั้ง collection ลองรัน code นี้:
# 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 method รันนצח เพราะไม่มีวัน finish iterating infinite collection
แต่เพิ่มนิดหน่อย code รัน perfect ถ้าแค่เรียก lazy method Ruby ทำ smart thing ทำแค่ iterations ที่จำเป็นสำหรับ computation แค่ 10 rows น้อยกว่า infinity มาก
(1..Float::INFINITY).lazy.reject { |i| i.odd? }.map { |i| i*i }.first(5) #=> [4, 16, 36, 64, 100]
Six Thousand Copies of Moby Dick
Test file tricks ต้องการ big file ใหญ่จน "failure to be lazy" ชัดเจน
Download Moby Dick จาก Project Gutenberg ทำ text file 100 copies ไมพอ เพิ่มเป็น ~6,000 ตอนนี้ผมอาจคนเดียวที่มี text file 6,000 copies Moby Dick น่าลังเล แต่ขอขุดต่อ
Download moby dick duplicate หลายพันครั้งได้ large file เล่น syntax ไม่ใช่ bash เป็น fish shell คิดว่าผมคนสุดท้ายที่ใช้
How to Get Enumerators for a File
Cool Ruby trick ที่คุณน่าจะใช้ แม้ไม่รู้ว่ากำลังใช้ Almost any method ใน Ruby iterate over collection คืน Enumerator object ถ้าเรียกโดยไม่ส่ง block หมายถึงอะไร?
ตัวอย่าง เปิด file ใช้ each_line print แต่ละบรรทัด แต่ถ้าเรียกโดยไม่ส่ง block ได้ enumerator Methods สนใจ: each_line, each_char และ 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
ตัวอย่างทั้งคู่เหมือนกันแทบซ้ำ แต่ตัวที่สองมี key unlock AMAZING POWERS
Using a File's Enumerator
ได้ enumerator "contains" ทุก lines ใน file slice และ dice lines เหมือน ruby array ตัวอย่าง:
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 }
Cool แต่มันมีปัญหาใหญ่ โหลด entire file เข้า memory ก่อน iterate ไฟล์ 6,000 copies Moby Dick lag noticeable
Lazy-Loading the Lines of the File
ถ้า scan large text file หา first 10 instances ของ "whale" ไม่ต้อง look further หลัง 10th occurrence บอก Ruby enumerators ทำได้ง่าย ใช้ keyword "lazy"
ตัวอย่างใช้ lazy loading ทำ sophisticated things:
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
It's Not Just for Files
Sockets, pipes, serial ports represented ใน Ruby โดย IO class มี each_line, each_char, each_codepoint methods ใช้ trick นี้ได้ทั้งหมด Pretty neat!
It's Not Magic
เสียดาย lazy enumerators เร็วขึ้นก็ต่อเมื่อ task ไม่ require entire file to be read ถ้าค้นหาคำที่เกิดแค่ last page ต้อง read whole book หา แต่นั้น case นี้ approach ไม่ควร slower non-enumerator approach