DEV Community

armariya
armariya

Posted on • Originally published at Medium on

ทำความรู้จักกับ Lazy Evaluation

ไม่รู้ว่าหลาย ๆ คนเคยได้ยินคำนี้มาก่อนรึเปล่า แต่ว่าผมเพิ่งเคยรู้จักและได้ยินมาจากการอ่านหนังสือ “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)