การใช้ Lazy Evaluation ใน JavaScript
ศิลปะของความขี้เกียจ
กาลครั้งหนึ่งนานมาแล้ว มีครูประจำชั้นสายโหดท่านหนึ่ง เรียกเด็กชายเอให้ยืนขึ้นพร้อมท่อง ก-ฮ เวลาผ่านไปสามนาทีกว่า บทสวดพยัญชนะไทยจึงจบลงพร้อมกับคำถามจากคุณครูวัยเกษียณว่า ที่เธอท่องมาทั้งหมด อักษรไทยสามตัวแรก มีอะไรบ้าง?
นี่เป็นสถานการณ์ที่ CPU จะอึดอัดในหัวใจมาก หากคุณเขียนโค๊ดให้โปรแกรมคำนวณชุดตัวเลขยาวๆขึ้นมา แล้วบอกมันว่า ขอแค่สามตัวแรกเองหวะแก
ดังนี้
1(1..Float::INFINITY).collect { |n| n*n }.first(3)
ตัวอย่างข้างบนเป็นภาษา Ruby เราสร้างชุดตัวเลขที่มีค่าตั้งแต่ 1 ถึง Infinity โดยแต่ละตัวเลขให้ยกกำลังสองก่อน เมื่อเสร็จขั้นตอนจึงขอแค่สามตัวบน... เดี๋ยว ไม่ใช่หวย ปัดโถ่!
แน่นอนว่าโปรแกรมนี้จนชาติหน้าก็รันไม่เสร็จ เพราะไม่สามารถคำนวณเลขไปถึง Infinity ได้ เมื่อเราดูคำสั่งหลังสุดคือ first
พบว่าเราต้องการแค่สามตัวแท้ๆ ทำไมเราไม่เลือกตัวเลขมาเลยหละ? ทำไมเราต้องรอให้ประโยคก่อนหน้าสร้างเลขมากมายที่ไม่ได้ใช้มาด้วย?
ตรรกะคนสันหลังยาว
เรารู้ถึงความไม่แน่นอนของโค๊ดที่เราเขียน ว่าผลลัพธ์ควรเป็นเช่นไร จนกว่าจะอ่านถึงข้อคำสั่งตัวสุดท้าย เหตุนี้เราจึงยังไม่ควรคำนวณตัวเลขออกมาตั้งแต่แรก แต่รอดูอย่างขี้เกียจไปก่อนว่าสุดท้ายต้องการอะไร มากน้อยแค่นี้ หลักการนี้เรียกว่า Lazy Evaluation โชคดีหน่อยที่หลายๆภาษามีคุณสมบัติข้อนี้อยู่ในตัวภาษาเองเลย เช่น Stream ใน Elixir
กลับมาที่โค๊ด Ruby ของเรากัน เพียงแค่คุณเติม lazy เข้าไป สันหลังคุณก็จะยาวขึ้นทันที 1 นิ้ว
1# ได้ผลลัพธ์เป็น [1, 4, 9] โดยไม่ต้องคำนวณไปถึง Infinity2(1..Float::INFINITY).lazy.collect { |n| n*n }.first(3)
สร้างความขี้เกียจฉบับ JavaScript
แม้โลกความจริงจะไม่มีใครรันตัวเลขไปถึง Infinity แต่ถ้าเราสามารถลดปริมาณการคำนวณได้ โดยดูว่าต้องการผลลัพธ์ถึงแค่ไหน ก็จะทำให้โปรแกรมเราทำงานได้เร็วขึ้น บทความนี้เน้นการใช้งาน Lazy Evaluation บนฐานของภาษา JavaScript ที่ไม่ได้มี Syntax อะไรพิเศษเอื้อต่อการทำงาน
1const upto = (from, to) => {2 return Array(to)3 .fill()4 .map((_, i) => i + from)5}67console.log(upto(0, 1000).slice(0, 3))
ตัวอย่างข้างบนให้ผลลัพธ์เป็น [0, 1, 2] แต่กว่าจะได้เลขสามตัวนี้ออกมา โปรแกรมของเราต้องสร้าง Array ตั้ง 1,000 จำนวนขึ้นมาก่อน ลองเปลี่ยนโค๊ดนี้นิดหน่อยดังนี้ครับ
1const upto = (from, to) => {2 return [from, () => upto(from + 1, to)]3}45const iterate = (stream, n, cb) => {6 while (n-- > 0) {7 cb(stream.shift())8 stream = stream[0]()9 }10}1112const display = (stream, n) => {13 iterate(stream, n, (item) => {14 console.log(item)15 })16}1718console.log(display(upto(0, 1000), 3))
โค๊ดนี้อาศัยความเข้าใจที่ว่า Function จะไม่ทำงานทันทีถ้าเราไม่เรียก เมื่อเราเรียกฟังก์ชัน upto
มันจะคืนค่าเป็นอาร์เรย์สองช่อง ช่องแรกคือค่าก่อนหน้า ส่วนช่องหลังคือฟังก์ชัน สมมติเราเรียก upto(0, 1000) เราจะได้อาร์เรย์ลักษณะนี้ [0, () => [1, () => [2, () => {...}]]]
อาร์เรย์ชั้นนอกสุดมี2ช่องคือช่องแรกเก็บค่า0 ช่องหลังเก็บฟังก์ชัน () => [1, ...]
ตัวข้างในก็ซ้อนกันไปเรื่อยๆ ทีนี้เมื่อเราบอกว่า display
แค่สามค่านะ โปรแกรมนี้ก็จะไปแกะเอาเฉพาะอาร์เรย์ช่องแรกจนกว่าจะครบสามตัว เมื่อครบแล้วก็หยุดเพราะขี้เกียจ
โปรแกรมนี้ดูสมบูรณ์แบบขึ้นมาแล้ว แต่จริงๆมันยังมีข้อผิดพลาดนะครับ ไม่เชื่อลองใส่ display(upto(0, 1), 5)
ดูซิ ทำไมมันคืนค่ามา5ตัว ทั้งๆที่เราบอกในประโยคแรกว่าให้สร้างอาร์เรย์แค่1ตัว จะเห็นว่ายากพอควรเหมือนกันถ้าเราจะเขียน Lazy Evaluation ด้วยตนเองให้ทั้งถูกต้องและครอบคลุม
เพิ่มความเกียจคร้านให้ถึงขีดสุด
ไหนๆก็ไหนๆแล้ว ถ้าคิดจะขี้เกียจก็ต้องทำให้ถึงที่สุด เราจะมานั่งเขียนฟังก์ชันเองอยู่ทำไม ในเมื่อ Library ก็มีให้ใช้ เอาหละ เรามาลองเล่น lazy.js กันเถอะ
1var result = Lazy(people)2 .pluck('lastName')3 .filter(function (name) {4 return name.startsWith('Smith')5 })6 .take(5)
ตัวอย่างข้างบนนี้คัดลอกมาจากตัวอย่างของ Lazy.js ครับ ถ้าเรามีลิสต์ของก้อนอ็อบเจ็กต์ person จำนวนมาก ต้องการเฉพาะนามสกุลโดยมีเงื่อนไขว่านามสกุลต้องขึ้นต้นด้วย Smith สุดท้ายต้องการแค่5อ็อบเจ็กต์เท่านั้น เราสามารถใช้ Lazy.js ช่วยทำ Lazy Evaluation โดยไม่ต้องแงะหานามสกุลของคนทั้งหมด
สารบัญ
- ศิลปะของความขี้เกียจ
- ตรรกะคนสันหลังยาว
- สร้างความขี้เกียจฉบับ JavaScript
- เพิ่มความเกียจคร้านให้ถึงขีดสุด