ไม่รู้ว่าหลาย ๆ คนเคยได้ยินคำนี้มาก่อนรึเปล่า แต่ว่าผมเพิ่งเคยรู้จักและได้ยินมาจากการอ่านหนังสือ “the book,” The Rust Programming Language มันเป็นหนังสือของภาษาโปรแกรม Rust ซึ่งเป็นหนังสือที่ดีมาก ๆ ผมแนะนำให้ทุกคนนั้นน ควรจะไปอ่านครับ บอกเลยว่าเด็ด!
Lazy Evaluation คืออะไร?
“Lazy” ก็คือแบบโคตรจะตรงตัว ขี้เกียจ นั่นละครับ Lazy Evaluation คือจะทำการคำนวณบางสิ่งบางอย่างในเวลาที่เราต้องการค่ามันเท่านั้น เน้นย้ำว่าเท่านั้นนะครับ ลองดูโค้ดชุดนี้ครับ
fn simulated\_expensive\_calculation(result: u32) -> u32 {
thread::sleep(Duration::from\_secs(2));
println!("Finished");
result
}
เป็นการจำลอง function ที่มีการคำนวณอย่างหนักหน่วง ใช้เวลาถึงสองวินาที ถ้าเราเรียกฟังก์ชันนี้บ่อย ๆ และเรียกแล้วไม่ได้ใช้ค่า ๆ นี้ จะส่งผลกระทบต่อ performance มาก ๆ
โดย Lazy Evaluation มักจะมาคู่กับ Memorization ซึ่งก็คือพอเราทำการคำนวณค่าแล้วเนี่ย เราจะทำการจำค่าไว้ด้วย ถ้า Parameter เข้ามาเหมือนเดิม เราก็จะ Return ค่าที่มีให้ไปเลย โดยไม่ต้องไปทำการคำนวณใหม่
วิธีการทำ Lazy Evaluation
คราวนี้เราจะมาปรับปรุงโค้ดชุดด้านบน ให้กลายเป็น Lazy Evaluation + Memorization กันนะครับ เริ่มต้นโดยการเปลี่ยนโค้ดชุดข้างบนจาก function ให้กลายเป็น closure ซะก่อน
let expensive\_closure = |result: u32| -> u32 {
thread::sleep(Duration::from\_secs(2));
println!("Finished");
result
};
ตรงนี้นี่คือตัวฟังก์ชันเราจะยังไม่ได้ทำงานนะครับ เป็นแค่การเก็บ function ไว้ในตัวแปรเฉย ๆ นะครับ ยังไม่ได้มีการคำนวณอะไร ที่นี้เราจะสร้าง struct ขึ้นมาที่มีชื่อว่า Cacher เพื่อใช้เป็นที่เก็บ closure กับตัวที่ cache result ไว้นะครับ
struct Cacher<T> where T: Fn(u32) -> u32 {
calculation: T,
values: HashMap<u32, u32>
}
impl<T> Cacher<T> where T: Fn(u32) -> u32 {
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value.get(&arg) {
Some(&v) => v,
None => {
let v = (self.calculation)(arg);
self.value.insert(arg, v);
v
}
}
}
}
จะเห็นได้ว่า Struct นี้มีตัวแปรสองตัวก็คือ calculation ที่เป็นตัวเก็บ closure การทำงาน กับตัว values ที่เอาไว้ map ระหว่าง argument กับ result นะครับ
จากนี้เนี่ยเวลาใช้งานใช่มั้ยครับมันจะกลายเป็น
let cacher = Cacher::new(|result: u32| -> u32 {
thread::sleep(Duration::from\_secs(2));
println!("Finished");
result
})
ทำการสร้าง cacher ขึ้นมาก่อน เสร็จปุ๊บเวลาเราจะใช้ก็จะใช้แค่
cacher.value(20)
แบบนี้เป็นต้น มันก็จะวิ่งไปเข้า function value ข้างบน ถ้าหากว่าเคยประมวลผลค่า argument นี้แล้วมันจะ return คำตอบให้ไปเลย แต่ถ้ายังไม่เคยมีจะค่อยทำการคำนวณเพื่อให้ฟังก์ชันถูกเรียกใช้งานน้อยที่สุดนะครับ
จะเห็นว่ามันมีประโยชน์เอามาก ๆ เลยนะครับ สามารถทำให้ performance ของโปรแกรมเราดีขึ้นได้มาก ๆ เลยครับ แต่จากที่เห็นข้อมูล implement ข้างบนเนี่ย เราก็จะเห็นข้อเสียของวิธีนี้อยู่เหมือนกัน นั่นก็คือถ้าเราทำการเรียกด้วย argument ที่ต่าง ๆ กันมากเกินไป จะทำให้ต้องคำนวณทุกรอบ จบแทบจะไม่ต่างจากการเขียนธรรมดา แถมยังกิน memory มากกว่าวิธีธรรมดาเสียอีก! เพราะฉะนั้นจะใช้ก็ตรวจสอบปัญหาของตัวเองดี ๆ กันก่อนนะครับ
ปล. เย่ ถึงช่วงเวลาโฆษณา ตอนนี้กำลังจะเปิด Podcast ใหม่ครับ เย่ ~ ชื่อว่า “Deep Magic” ครับ ที่มาของมันก็คือไปเจอคำ ๆ นี้ใน Wikipedia มาว่าแบบนี้
Deep magic refers to techniques that are not widely known, and may be deliberately kept secret.
เลยสนใจในคำ ๆ นี้ขึ้นมาครับ เลยจะทำ Podcast อันนี้ขึ้นมาเป็นตอนสั้น ๆ นะครับ ที่จะเอา Technique ที่ได้พบเจอมาระหว่างทำงานเนี่ย มาแชร์ให้รู้จักกันนะครับ สามารถไปติดตามได้ที่ https://anchor.fm/ariya-lawanitchanon นี่เลยนะครับ สำหรับช่องทางอื่น ๆ จะทยอยตามมานะครับ เพราะว่า ตอนไม่พอ submit ไม่ผ่านนะฮะ (ฮา) นั่นแหละครับ ฝากตัวด้วยนะครับ :) ใครจะมาแจมนี่หลังไมค์มาเลยนะ ยินดีมาก
Top comments (0)