FP(05): Lazy Evaluation ขี้เกียจไว้ก่อนแล้วดีเอง?!
Tags: beginners, functional, programming
Published: 2020-05-18 13:22:48

> ติดตามบทความอื่นๆ ของเราได้ที่ [TAMEMO.com](https://www.tamemo.com)

ในบทนี้เราจะแบ่งออกเป็น 2 พาร์ท นั่นคือคอนเซ็ปของการใช้ Lazy ในภาษาสาย FP แท้ๆ กับการนำคอนเซ็ปนี้ไปใช้ในภาษาทั่วๆ ไปนะ ... ซึ่งช่วงแรก เราจะขออธิบายกันด้วยภาษา haskell (อ่านไม่ยากหรอก ฮา)

## Laziness Evaluation
คำนี้ถ้าแปลเป็นภาษาไทยก็คงประมาณ "การประมวลผลแบบขี้เกียจ" ซึ่งฟังดูไม่ดีเท่าไหร่นะ ขอแปลแบบเข้าใจง่ายๆ ว่า "การประมวลผลเมื่อต้องใช้งานตอนนั้น" แทนละกัน

ในบทก่อนเราสอนวิธีการสร้างลิสต์ในรูปแบบ List Comprehension ไปแล้ว

เช่นถ้าเราต้องการลิสต์ของตัวเลข 1-1000 เราก็อาจจะเขียนได้แบบนี้ 
ในฐานะที่เรากำลังคุยเรื่อง FP กันอยู่ ขอยกตัวอย่างด้วยภาษาสายฟังก์ชันนอลแท้ๆ อย่าง haskell กันก่อนละกัน

```hs
evenNumbers = [1,2..1000]
```
แต่ใน haskell เราสามารถสร้างลิสต์แบบนี้ได้ด้วย
```hs
evenNumbers = [1,2..]
```
ในลิสต์ตัวแรก เราอ่านออกว่าต้องการสร้างตัวเลข 1-1000 ขึ้นมา

แต่ในลิสต์ตัวที่สอง เราตัดค่าตัวสุดท้ายทิ้งไป ... แล้วแบบนี้มันจะสร้างลิสต์ไปจนถึงตัวไหนกันล่ะ?

คำตอบก็คือ .. **ไม่มีตัวสิ้นสุดยังไงล่ะ!!**

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

สำหรับมนุษย์น่ะ อาจจะไม่มีปัญหา เราสามารถทำความเข้าใจคำว่าลิสต์ของตัวเลข 1, 2, 3, 4, ... ไปเรื่อยๆ ได้ เราสามารถทำความเข้าใจได้ว่านี่คือลิสต์ที่มีตัวเลขไล่ไปเรื่อยๆ เป็นล้านๆ ตัวได้เลย **โดยที่เราไม่ได้จำตัวเลขทั้งหมดนั้นไว้ทั้งหมด แต่เราจำว่ามันคือลิสต์ที่เป็นเลขต่อๆ กัน** แล้ว**เมื่อจะใช้งาน ค่อยคิดว่ามันจะต้องเป็นเลขอะไร**

และนั่นแหละ มันคือสิ่งที่เรียกว่า "Lazy Evaluation"

![](https://2.bp.blogspot.com/-V-qJZLnHfhU/XrrVtmkNgEI/AAAAAAAASWE/z7bBoiOoWHUfdJRnMwQg-3gMTxZGsb80wCNcBGAsYHQ/s1600/fp-05-2.png)

ลิสต์แบบปกติเราจะเรียกว่ามันเป็น Static List นั่นคือลิสต์ที่มีข้อมูลพร้อมทุกตัวตั้งแต่แรกที่สร้างลิสต์ขึ้นมา แต่ถ้าเป็นลิสต์แบบ Lazy มันจะสร้างไอเทมแค่บางตัวไว้ตอนแรกเท่านั้น ส่วนตัวที่เหลือ**ถือว่ามี**นะ **แค่ยังไม่ได้สร้าง**ขึ้นมาเท่านั้นเอง

ลองดูตัวอย่างเวลาเราใช้งาน Lazy List กัน เริ่มต้นด้วยลิสต์ `[1,2..]` ที่มีเลขแค่สองตัว

ต่อมา ถ้าเรามีการ access ข้อมูลตัวไหนก็ตาม ลิสต์จะเช็กเองว่าข้อมูลตัวนั้นเคยสร้างขึ้นมารึยัง ถ้ายังไม่ได้สร้าง ก็จัดการสร้างมันซะจังหวะนี้แหละ

![](https://4.bp.blogspot.com/-TVvBUeldRSs/XrrVt6wERGI/AAAAAAAASWI/SVrokLbzQTwRENeL6vn_jpEVJZbjkGktwCNcBGAsYHQ/s1600/fp-05-3.png)

ข้อสังเกตอีกอย่างนึงคือ ตอนที่เรา access ไปถึงข้อมูล `index 4` นั้น เราข้าม `index 3` ไปนะ แต่ลิสต์มันก็สร้างตัวที่ `index 3` ให้เราด้วยนะ เพราะ Lazy List จะสร้างไอเทมเรียงกัน ไม่กระโดดข้าม ถ้าต้องการไอเทมตัวท้ายๆ ก็ต้องสร้างตัวข้างหน้ามันให้เสร็จซะก่อน

เมื่อ FP มีฟีเจอร์แบบนี้ ทำให้เราสามารถสร้าง **"ลิสต์ปลายเปิด"** ที่ไม่ได้บอกว่าจะไปจบที่เลขเท่าไหร่ได้ยังไงล่ะ

ก่อนจะจบส่วนของเนื้อหา ลองมาดูตัวอยากการประยุกต์ใช้ Lazy กันหน่อย

### Fibonacci Number 

![](https://4.bp.blogspot.com/-NeZN7cN0R9w/XrrRi-Wm4OI/AAAAAAAASV4/EVeWYAOb3ho_6VuCqe2Ev3R1TPF6xDuDQCNcBGAsYHQ/s1600/fp-05-1.png)

*ลำดับฟีโบนัชชีคืออะไร ถ้ายังไม่รู้อ่านก่อนได้ที่ [https://th.wikipedia.org/wiki/จำนวนฟีโบนัชชี](https://th.wikipedia.org/wiki/%E0%B8%88%E0%B8%B3%E0%B8%99%E0%B8%A7%E0%B8%99%E0%B8%9F%E0%B8%B5%E0%B9%82%E0%B8%9A%E0%B8%99%E0%B8%B1%E0%B8%8A%E0%B8%8A%E0%B8%B5)*

ถ้าสรุปง่ายๆ มันคือลำดับตัวเลขที่มาจากการ**บวกตัวเลข 2 ตัวข้างหน้า**
```
Fibonacci Number:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
```
ซึ่งเราสามารถสร้างลิสต์ `fibo` ในภาษา haskell ได้แบบนี้
```hs
fibo = 1 : 1 : [a + b | (a, b) <- zip fibo (tail fibo) ]
```
อาจจะอ่านไม่รู้เรื่องว่ามันคืออะไรกัน (ฮา) ไม่เป็นไร เรากำลังจะอธิบายต่อ

- สำหรับ fibo นั้นต้องเริ่มด้วยตัวเลขคู่แรก กำหนดมาก่อนเป็น `1` และ `1` (บางครั้งก็นับว่าเริ่มด้วย `0` และ `1` แต่ในเคสนี้ขอเริ่มด้วยคู่แลขแบบแรกละกัน)
- เมื่อเรามีเลขคู่แรกเป็นตัวตั้งต้นแล้ว เราก็เลยกำหนดว่าชุดตัวเลขหลังจากนี้ (ลิสต์) เราจะสร้างมาจากลิสต์อีก 2 ตัว นั่นคือ `fibo` และ `tail fibo`
- เราจะเอาลิสต์ 2 ตัวนั้นมา `zip` กัน แล้ว + เป็นค่าตัวใหม่

*ใครจำว่า `zip` และ `tail` คืออะไรอ่านทวนได้ใน [บท 3](https://www.tamemo.com/post/159/fp-03-map-filter-reduce/) และ [บทที่ 4](https://www.tamemo.com/post/160/fp-04-pair-either-list-comprehension/) ได้เลยนะ*

ดังนั้น ตอนเริ่มเราจะได้ลิสต์ fibo ที่มีหน้าตาแบบนี้ [1, 1, ...] 

คำถามคือตัวต่อไปจะมาจากไหนล่ะ? ในโค้ดบอกว่าให้จับลิสต์ fibo ก็คือลิสต์ของตัวมันเองนั่นแหละ ที่ตอนนี้มีแค่ [1, 1] มา zip กัน (แต่ตัวนึง สั่งให้ `tail` ด้วย ก็คือตัดตัวแรกสุดทิ้งไป)

![](https://2.bp.blogspot.com/-2usWv980K48/Xr1E-zLMhGI/AAAAAAAASXU/YehG2fi9M40TDNXKQXqfuIJpoP3Pw6ZAACNcBGAsYHQ/s1600/fp-05-4.png)

นั่นก็คือเราจะหยิบตัวเลขตัวแรกสุดของลิสต์ทั้งสองตัวออกมา คือ (1) และ (1) แล้วเอามาบวกกัน = 2 

แล้วใส่เลขตัวนั้นต่อลิสต์ fibo ลงไป (สรุปคือตอนนี้ fibo = [1, 1, 2, ...] นะ)

ถึงตอนนี้อาจจะสงสัยว่า งั้นตัวเลขของเราก็หมดแล้วน่ะสิ !?

อย่าลืมว่า ลิสต์ที่เราเอาไว้ดึงตัวเลขตัวถัดไปของเรา มันไม่ใช่ static list หรอกนะ มันคือ lazy list ที่ชื่อว่า **fibo** นะ

![](https://2.bp.blogspot.com/-HyZlnc8_5gQ/Xr1E_DpATdI/AAAAAAAASXQ/oqXH39Ntw7UXp0uPfoIkP-9b07tuYvc1QCNcBGAsYHQ/s1600/fp-05-5.png)

เพราะลิสต์ที่เรากำลังสร้างอยู่ชื่อว่า fibo แถมลิสต์ที่เอาไว้ดึงค่าตัวต่อไปก็ดันอ่านมาจากลิสต์ fibo อีก!

ก็แสดงว่าเมื่อเรามีไอเทมเพิ่มเข้ามาใน fibo ลิสต์ทางขวา (ดูในรูป) ก็จะได้ไอเทมเพิ่มไปด้วยโดยอัตโนมัติ (เพราะจริงๆ มันคือลิสต์เดียวกัน!!)

![](https://2.bp.blogspot.com/-bKuAr2_KfK0/Xr1E_Ja7C8I/AAAAAAAASXY/uZb_uF_d-pEvihvHfyEa5p8lgVRzAFAyACNcBGAsYHQ/s1600/fp-05-6.png)

หลังจากนั้นก็จะใช้คอนเซ็ปเดิม คือ
- เราจะสร้างตัวเลขต่อไปโดยการดึงค่าตัวแรกออกมาจากลิสต์ 
- เอามาบวกกัน แล้ว append ต่อเข้าไปในลิสต์ตัวหลัก
- และในจังหวะนั้นเอง เมื่อลิสต์ตัวหลักมีการเปลี่ยนแปลง ลิสต์คู่ที่ใช้ดึงข้อมูลก็จะได้ไอเทมไปเพิ่มทันที
- มันก็จะวนต่อๆๆ แบบนี้ไปเรื่อยๆ
  
แต่ถ้ามองว่าแบบนี้มันก็วนไปไม่มีสิ้นสุดน่ะสิ -> ใช้แล้วครับ มันสามารถวนไปได้เรื่อยๆ เลย แต่เนื่องจากมันเป็น lazy list กระบวนการทั้งหมดนี้จะยังไม่ถูกโปรเซสอะไรทั้งนั้น จนกว่าเราจะมีการดึงข้อมูลออกมา เช่นสั่งว่า `take 10 fibo` (ขอไอเทม 10 ตัวแรกจากลิสต์)

ผลลัพธ์ก็จะรันได้แบบด้านล่างนี่
```
$ ghci
GHCi, version 8.0.2

> fibo = 1 : 1 : [a + b | (a, b) <- zip fibo (tail fibo) ]
> take 10 fibo
=> [1,1,2,3,5,8,13,21,34,55]
```

ปัญหาคือ...

ภาษาทั่วๆ ไปที่เราเขียนกัน มันไม่ได้มีฟีเจอร์นี้แบบ haskell น่ะสิ! ดังนั้นเราลองมาดูกันว่าภาษาทั่วไปที่เราเขียนกันถ้าอยากใช้ Lazy Evaluation จะต้องเขียนยังไง

---

## แล้วภาษาสาย Imperative จะทำยังไง?

ขอยกตัวอย่างด้วยสองภาษาคือ 
- **JavaScript**: ตัวแทนของภาษาที่ฟีเจอร์ Generator 
- **Java**: ตัวแทนของภาษาตระกูล OOP

### yield มันซะ!, สำหรับภาษาที่มี Generator

ในภาษาเขียนโปรแกรมยุคใหม่ๆ ส่วนใหญ่จะมีฟีเจอร์ที่เรียกว่า **"Generator"** ใส่มาให้ด้วย (จะใช้คีย์เวิร์ดคำว่า `yield`) ซึ่งทำให้เราสามารถเขียนฟังก์ชันที่ทำงานแบบ lazy ได้

ลองมาดูตัวอย่างฟังก์ชัน `range()` กันก่อน

```js
function range(start, end) {
    let numbers = []
    for(let i=start; i<=end; i++){
        numbers.push(i)
    }
    return numbers
}
```
ฟังก์ชันนี้มีการทำงานคือรับตัวเลขเริ่มต้นและจุดสิ้นสุดเข้าไป แล้วสร้างลิสต์ของตัวเลขออกมาให้ เช่น `range(2,5)` ก็จะได้ `[2, 3, 4, 5]`

แต่หลังจากเรารู้จัก lazy evaluation กันไปแล้ว เราอาจจะมองว่าฟังก์ชันนี้มันทำงานไม่ดีเลยแฮะ เพราะจะต้องสร้างลิสต์ของตัวเลขทั้งหดมขึ้นมาเลยเหรอ ทั้งที่ยังไม่รู้ว่าจะใช้เลขครบทุกตัวจริงๆ มั้ย

สิ่งที่เราต้องการก็คือเราอยากจะ `return` เลขทีละตัว

```js
function range(start, end) {
    for(let i=start; i<=end; i++){
        return i 
        //return 1 value, then finish!
    }
}
```

แน่นอนว่าเราสั่งแบบนี้ไม่ได้ ถ้าเราสั่ง`return`ล่ะก็ หลังจากมันรีเทิร์นค่าแรกเสร็จแล้วโปรแกรมก็จะหยุดทำงานทันที

วิธีการแก้คือเปลี่ยนจากการใช้ `return` ไปใช้คำสั่ง `yield` แทน

```js
function* range(start, end) {
    for(let i=start; i<=end; i++){
        yield i
    }
}
```
ฟังก์ชันที่สามารถ `yield` ได้ เราจะเรียกมันว่าฟังก์ชัน **"Generator"**  ซึ่งมีความสามารถในการตอบค่าได้หลายครั้ง (คล้ายรีเทิร์นได้หลายค่า) โค้ดจะรันมาถึงจุดที่มีการสั่ง `yield` แล้วค้างอยู่ตรงนั้น จนกว่าจะมีการขอค่าครั้งแต่ไป 

เอาง่ายๆ มันคือฟังก์ชันที่สามารถหยุดทำงานชั่วคราวได้ และสามารรีเทิร์นได้หลายครั้ง

เวลาใช้งานก็เอามาวนลูปแบบนี้
```js
for(let x of range(1, 10)){
    console.log(x)
}
```
**ข้อควรระวังเวลาใช้ Generator คือถ้ามันเป็นแบบปลายเปิด คือสามารถสร้างเลขไปได้เรื่อยๆ เราจะต้องมีเงื่อนไขสำหรับหยุดลูปเสมอ!**
```js
function* range(start) {
    let i = start
    while(true) {
        yield i
        i = i + 1
    }
}
```
เช่นในเคสนี้ เราสร้างGeneratorที่สร้างเลขมาเรื่อยๆ ไม่มีจุดสิ้นสุด ถ้าเราเอาไปวนลูปตรงๆ มันจะกลายเป็น Infinity Loop ไม่มีวันจบ
```js
for(let i of range(1)) {
    //Infinity Loop!!
}
```
ในกรณีนี้ต้องมีเงื่อนไขที่เรียกว่า "Terminate Condition" เพื่อหยุดลูปเสมอ
```js
for(let i of range(1)) {
    //Terminate Condition
    if(...) break
}
```
หรือเราอาจจะสร้างฟังก์ชันมาดึงไอเทมออกมาตามจำนวนที่ต้องการก็ได้ แบบนี้
```js
function take(n, generator) {
    let elements = []
    let i = 0
    for(let element of generator) {
        if(i >= n) break
        elements.push(element)
    }
    return elements
}

let list = take(10, range(1))
//1, 2, 3, 4, 5, 6, 7, 8, 9, 10
```

### Fibonacci with yield

เราสามารถลองเอาฟังก์ชัน fibo มาเขียนใหม่ด้วยการใช้ `yield` ได้

ซึ่งส่วนใหญ่การเขียน Generator จะประกอบด้วยส่วนต่างๆ ประมาณนี้
```js
function fibo(){
    // 1.init
    let n = pair(1, 1)

    // 2.infinity loop
    while(true) {

        // 3.yield first number
        yield n.first

        // 4.update current pair numbers
        n = pair(
            n.second, 
            n.first + n.second
        )
    }
}
```

### ขี้เกียจสไตล์ OOP, เขียนเยอะกว่าเดิม บอกเลย!

สำหรับภาษาที่เคร่งเรื่อง OOP มากๆ แบบ Java การจะสร้าง Generator ขึ้นมาจะต้องสร้างในรูปแบบของ object แทน

เราต้องการให้อ็อบเจคตัวไหนสามารถเป็น Generator ได้เราจะต้องสั่งให้มัน implements `Iterable` ซึ่งจะบังคับให้เราเขียนเมธอด `iterator`

```java
class Persons implements Iterable<People> {

    List<Person> persons = new ArrayList<>();
    
    //ถ้าข้อมูลของเราเป็น Iterable อยู่แล้ว
    public Iterator<Person> iterator() {
        return persons.iterator();
    }
    
    //ถ้าข้อมูลของเรา ไม่ได้เป็น Iterable (ต้องสร้างเอง)
    public Iterator<Person> iterator() {
        return new Iterator<Type>() {

            //ตัวนับ index
            private int currentIndex = 0;

            //เช็กว่าเรายังเหลือข้อมูลให้ตอบมั้ย
            @Override
            public boolean hasNext() {
                return currentIndex < persons.size();
            }

            //ดึงข้อมูลตัวถัดไป
            @Override
            public Type next() {
                return persons.get(currentIndex++);
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
}
```

## Lazy ใน Real-World

ก่อนจบบทนี้ ลองมาดูตัวอย่างเล็กๆ น้อยๆ กับการประยุกค์ใช้ที่เจอได้ในการเขียนโค้ดประจำวัน

### Collections vs Sequences 
ในบางภาษา เช่น Kotlin จะมีชนิดตัวแปรแบบ List 2 คือลิสต์แบบ static ธรรมดากับแบบ lazy ที่เรียกว่า "Sequence"

ลองดูตัวอย่างข้างล่างนี้เมื่อเราใช้คำสั่ง `map` กับ `first`
```kt
val numbers = listOf(1, 2, 3, 4, 5)

// Collections
val n = numbers.map {
    it * it
}.first {
    it > 10
}
// 1. นำเลขทุกตัวไปยกกำลังสอง  = [1, 4, 9, 16, 25]
// 2. เลือกเลขตัวแรกที่มากกว่า10 = 16

// Sequences
val n = numbers.asSequence().map {
    it * it
}.first {
    it > 10
}
// 1. นำเลขทุกตัวไปยกกำลังสอง (แต่ยังไม่ทำ เพราะยังไม่มีการขอตัวเลขไปใช้)
// 2. เลือกเลขตัวแรกที่มากกว่า10 (ในจังหวะนี้ค่อยนำตัวเลขจากสเต็ปที่แล้วไปยกกำลังสองทีละตัว ถ้าเจอตัวที่ตรงเงื่อนไขก็หยุดได้ทันที) = [1, 4, 9, 16, ...]
```

สำหรับรายละเอียดเพิ่มเติมลองไปอ่านที่บทความนี้ดู [Collections and sequences in Kotlin](https://medium.com/androiddevelopers/collections-and-sequences-in-kotlin-55db18283aca)
![](https://miro.medium.com/max/1000/0*hdSVv06ug45AzTTr)


### Singleton

Singleton เป็น Design Pattern ยอดนิยมหนึ่งตัวที่คนใช้กันเยอะมาก (แต่คำแนะนำของเราคือเราไม่ควรใช้แพทเทิร์นนี้เยอะๆ นะ)

คอนเซ็ปคือถ้าเรามีคลาสสไตล์ helper ที่ทั้งโปรแกรมสร้างอ็อบเจคแค่ตัวเดียวก็พอ เช่น

```java
class Calculator {

    public static Calculator instance = new Calculator();

    public int add(int x, int y){
        return x + y;
    }
}

int ans = Calculator.instance.add(1, 2);
```

เราสร้างตัวแปรแบบ `static` ชื่อ **instance** เอาไว้ จากนั้นเวลาจะใช้งานจะได้เรียกจากตัวแปรนี้อย่างเดียวได้ ไม่ต้องคอย `new` อ็อบเจคเรื่อยๆ ให้เปลือง

แต่ก็มีปัญหาคือถ้าเราเขียนโค้ดแบบนี้ instance จะถูกสร้างทันทีที่โปรแกรมเริ่มทำงาน **แม้ว่าจะยังไม่มีโค้ดส่วนไหน เรียกเมธอด `add()` เลยก็ตาม**

ดังนั้นวิธีแก้ของแพทเทิร์นส่วนใหญ่จะนิยมให้เรียกผ่านเมธอด `getInstance()` แทน ซึ่งจะมีการเช็กว่าตัวแปรนั้นถ้ายังไม่เคยถูกสร้าง ให้สร้างใหม่ตอนนั้น (แปลว่าถ้าไม่มีใครเรียกใช้งานเลย ก็ไม่ต้องสร้าง = สร้างเมื่อใช้งานตามคอนเซ็ป lazy)
```java
class Calculator {

    private static Calculator instance = null;

    public static Calculator getInstance(){
        if(instance == null){
            instance = new Calculator();
        }
        return instance;
    }

    public int add(int x, int y){
        return x + y;
    }
}

int ans = Calculator.getInstance().add(1, 2);
```

ในภาษายุคใหม่หลังๆ ส่วนใหญ่จะมีการใส่การใช้ lazy ลงไปใน syntax ของภาษาเลย

เช่น Kotlin (อีกแล้ว)
```kt
val instance: Calculator by lazy {
    Calculator()
}
```
 
จริงๆ มีตัวอย่างอีกมากมายที่เราสามารถนำหลักการ lazy มาใช้เพื่อเพิ่มประสิทธิภาพของโปรแกรมได้ เช่น ORM (ถ้าสนใจ ลองเสิร์จดูได้ หรือถ้ามีโอกาสในอนาคตเราจะพูดถึงเรื่องนี้กันต่อ)