การใช้ Lazy Evaluation ใน JavaScript

Nuttavut Thongjor

ศิลปะของความขี้เกียจ

กาลครั้งหนึ่งนานมาแล้ว มีครูประจำชั้นสายโหดท่านหนึ่ง เรียกเด็กชายเอให้ยืนขึ้นพร้อมท่อง ก-ฮ เวลาผ่านไปสามนาทีกว่า บทสวดพยัญชนะไทยจึงจบลงพร้อมกับคำถามจากคุณครูวัยเกษียณว่า ที่เธอท่องมาทั้งหมด อักษรไทยสามตัวแรก มีอะไรบ้าง?

นี่เป็นสถานการณ์ที่ CPU จะอึดอัดในหัวใจมาก หากคุณเขียนโค๊ดให้โปรแกรมคำนวณชุดตัวเลขยาวๆขึ้นมา แล้วบอกมันว่า ขอแค่สามตัวแรกเองหวะแก ดังนี้

Ruby
1(1..Float::INFINITY).collect { |n| n*n }.first(3)

ตัวอย่างข้างบนเป็นภาษา Ruby เราสร้างชุดตัวเลขที่มีค่าตั้งแต่ 1 ถึง Infinity โดยแต่ละตัวเลขให้ยกกำลังสองก่อน เมื่อเสร็จขั้นตอนจึงขอแค่สามตัวบน... เดี๋ยว ไม่ใช่หวย ปัดโถ่!

แน่นอนว่าโปรแกรมนี้จนชาติหน้าก็รันไม่เสร็จ เพราะไม่สามารถคำนวณเลขไปถึง Infinity ได้ เมื่อเราดูคำสั่งหลังสุดคือ first พบว่าเราต้องการแค่สามตัวแท้ๆ ทำไมเราไม่เลือกตัวเลขมาเลยหละ? ทำไมเราต้องรอให้ประโยคก่อนหน้าสร้างเลขมากมายที่ไม่ได้ใช้มาด้วย?

ตรรกะคนสันหลังยาว

เรารู้ถึงความไม่แน่นอนของโค๊ดที่เราเขียน ว่าผลลัพธ์ควรเป็นเช่นไร จนกว่าจะอ่านถึงข้อคำสั่งตัวสุดท้าย เหตุนี้เราจึงยังไม่ควรคำนวณตัวเลขออกมาตั้งแต่แรก แต่รอดูอย่างขี้เกียจไปก่อนว่าสุดท้ายต้องการอะไร มากน้อยแค่นี้ หลักการนี้เรียกว่า Lazy Evaluation โชคดีหน่อยที่หลายๆภาษามีคุณสมบัติข้อนี้อยู่ในตัวภาษาเองเลย เช่น Stream ใน Elixir

กลับมาที่โค๊ด Ruby ของเรากัน เพียงแค่คุณเติม lazy เข้าไป สันหลังคุณก็จะยาวขึ้นทันที 1 นิ้ว

Ruby
1# ได้ผลลัพธ์เป็น [1, 4, 9] โดยไม่ต้องคำนวณไปถึง Infinity
2(1..Float::INFINITY).lazy.collect { |n| n*n }.first(3)

สร้างความขี้เกียจฉบับ JavaScript

แม้โลกความจริงจะไม่มีใครรันตัวเลขไปถึง Infinity แต่ถ้าเราสามารถลดปริมาณการคำนวณได้ โดยดูว่าต้องการผลลัพธ์ถึงแค่ไหน ก็จะทำให้โปรแกรมเราทำงานได้เร็วขึ้น บทความนี้เน้นการใช้งาน Lazy Evaluation บนฐานของภาษา JavaScript ที่ไม่ได้มี Syntax อะไรพิเศษเอื้อต่อการทำงาน

JavaScript
1const upto = (from, to) => {
2 return Array(to)
3 .fill()
4 .map((_, i) => i + from)
5}
6
7console.log(upto(0, 1000).slice(0, 3))

ตัวอย่างข้างบนให้ผลลัพธ์เป็น [0, 1, 2] แต่กว่าจะได้เลขสามตัวนี้ออกมา โปรแกรมของเราต้องสร้าง Array ตั้ง 1,000 จำนวนขึ้นมาก่อน ลองเปลี่ยนโค๊ดนี้นิดหน่อยดังนี้ครับ

JavaScript
1const upto = (from, to) => {
2 return [from, () => upto(from + 1, to)]
3}
4
5const iterate = (stream, n, cb) => {
6 while (n-- > 0) {
7 cb(stream.shift())
8 stream = stream[0]()
9 }
10}
11
12const display = (stream, n) => {
13 iterate(stream, n, (item) => {
14 console.log(item)
15 })
16}
17
18console.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 กันเถอะ

JavaScript
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
  • เพิ่มความเกียจคร้านให้ถึงขีดสุด