March 06, 2016

Swift Lazy: เรามาขี้เกียจกันเถอะ

บทความนี้จะใช้ความขี้เกียจแสดงวิธีการเขียนโค้ดให้ดูดีมีประสิทธิภาพกันมากขึ้น โดยภาพรวมเราจะพูดถึง lazy variable และ lazy sequence

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



ปัญหาจากโค้ดข้างบนก็คือ เราจำเป็นต้องกำหนด property ทุกๆ ตัวตั้งแต่ตอนสร้าง instance เลย ไม่อย่างนั้นจะ compile ไม่ผ่าน ซึ่งถึงแม้ smallImage จะไม่ถูกใช้ แต่เราก็ยังต้อง init มันอยู่ดี ทำให้เปลืองโดยใช่เหตุใช่ไหม?

การแก้ปัญหา
เราจะแก้ปัญหาด้วยการ init ตัวแปรเฉพาะตอนที่เราจะใช้มันเท่านั้น แบบนี้


ด้วยวิธีนี้เราจำเป็นต้องกำหนดค่าให้ _smallImage เป็น optional และเมื่อมีการ access property ชื่อ smallImage โดยที่ยังไม่เคยกำหนดค่าอะไรให้เลยมาก่อน เราถึงจะเริ่มย่อรูปและส่งกลับออกไปให้ และนี่ดูเหมือนจะเป็นอะไรที่เราอยากจะได้แล้ว แต่ก็ดูเหมือนว่าโค้ดที่ต้องเขียนมันดูเยอะๆ ยังไงชอบกล เรามาทำให้มันสั้นลงกันดีกว่า ด้วยการใช้ lazy initialization แบบนี้


สั้นลงเยอะมาก พฤติกรรมของโค้ดชุดนี้ กับโค้ดชุดก่อนหน้าเหมือนกันเป๊ะเลย อธิบายได้แบบนี้
  • ถ้า access smallImage โดยไม่ได้กำหนดค่าอะไรให้มันมาก่อน มันจะถูกคำนวนและเก็บไว้ใช้งาน และคำนวนเพียงครั้งเดียวเท่านั้น ณ ตอนที่เรากำลังอ้างถึงมันครั้งแรก จากนั้นเก็บไว้ให้เราใช้งานต่อไป โดยที่จะไม่คำนวนซ้ำอีกรอบตอนที่เราอ้างถึงมันใหม่หลังจากนี้
  • ถ้าเราได้กำหนดค่าให้ smallImage ไปแล้ว แล้วเราอ้างถึง smallimage เพื่อนำไปใช้งาน ค่า smallImage จะไม่ถูกคำนวนอีก
  • ถ้าเราไม่เคยอ้างถึง smallImage เลย... ค่า default ของ smallImage จะไม่ถูกกำหนด และจะไม่มีการคำนวนอะไรเกิดขึ้นเลย

เห็นไหมว่ามันเจ๋งมาก มันทำให้เราสามารถหลีกเลี่ยง useless initialization ได้

Initialization with a closure
เหมือนกับ property ทั่วๆ ไป ที่นอกจากจะกำหนดค่าตั้งต้นให้กับ property ได้จากโค้ดคำสั่ง 1 บรรทัดแล้ว เรายังสามารถกำหนดค่าแบบหลายๆ บรรทัดได้ด้วย แบบนี้


จากโค้ดจะเห็นว่า เราสามารถอ้างถึง self ได้ในตอนที่กำหนดค่าให้ lazy property ในขณะปกติแล้วถ้าไม่ใช่ lazy property เราจะไม่สามารถอ้างถึง self ได้ในขั้นตอนนี้

เนื่องจากตาม concept แล้ว lazy variable จะต้องถูกกำหนดค่าในภายหลังจากที่ object ถูกสร้างขึ้นมาสมบูรณ์แล้ว ดังนั้นนั่นคือเหตุผลที่ว่าทำไมเราถึงอ้าง self ได้นั่นเอง ในอีกทางหนึ่ง หากไม่ใช่ lazy เราจำเป็นต้องกำหนดค่าตั้งต้นให้กับตัวแปรในขึ้นตอนการ init ตามปกติ

สิ่งที่สังเกตได้จากโค้อีกอย่างหนึ่งก็คือ การใช้ closure เพื่อกำหนดค่าเริ่มต้นให้ attribute แบบที่ทำให้ดูนั้น จะเป็นการใช้ @noescape ให้เราโดยอัตโนมัติ นั่นแปลว่าเราไม่จำเป็นต้องใช้ [unowned self] เพื่อหลีกเลี่ยง reference cycle ก็ได้

การใช้ lazy let
เราไม่สามารถใช้ lazy let กับ instance property ได้ เนื่องจาก lazy นั้นจำเป็นต้องใช้กับ property ที่สามารถถูกกำหนดค่าได้ในภายหลัง เพราะเหตุผลที่ว่าถ้าใช้ lazy แล้วหลังจากที่เราอ้างถึง ตัวแปรถึงจะถูกกำหนดค่าให้ยังไงล่ะ


พอพูดถึง let อย่างนึงที่น่าสนใจก็คือ ค่าคงที่ที่ถูกประกาศเป็น let constants ที่ถูกประกาศที่ global scpoe หรือถูกประกาศเป็น class property (ใช้ static let) จะเป็น lazy และ thread-safe โดยอัตโนมัติอยู่แล้ว

ลองดูโค้ดนี้

ถ้าเรารันโปรแกรมเราก็จะเห็นว่ามัน print Hello มาก่อน จากนั้น Global constant initialized และ 42 และ Type constant initialized และ Felix และ Bye เป็นลำดับสุดท้าย ซึ่งนั่นทำให้เราเห็นว่า Cat.defaultName constants ถูกกำหนดค่าให้ ณ ตอนที่เราอ้างถึงมันจริงๆ

**
อย่าสับสนกับ instance properties ที่ถูกประกาศไว้ใน struct หรือ class นะ เช่นถ้าเราประกาศว่า struct Foo { let bar = Bar() } แบบนี้ bar จะถูกกำหนดค่าให้เลยตั้งแต่ตอนที่ instance ถูกสร้างขึ้นเลยทันที

เวลาจะสร้าง singleton object เราควรใช้ static let (ถึงแม้ว่าเราควรจะหลีกเลี่ยงการใช้ singleton ก็เถอะ) เนื่องจากมันทั้ง lazy และทั้ง thread-safe และยังถูกสร้างได้แค่ครั้งเดียวเท่านั้นด้วย

อีกตัวอย่างเกี่ยวกับ Lazy Sequences


เมื่อไรก็ตามที่เรา access incArray แล้ว ค่าทุกค่าจะถูกคำนวนเสมอ จากโค้ดเราจะเห็นว่า map ถูก apply ถึง 1,000 ครั้งตามจำนวน element ใน array แม้ว่าเราจะไม่ได้ใช้ทุกๆ ค่าก็ตาม แม้ว่าเราจะใช้แค่ค่าที่ 0 กับ 4 เท่านั้น... ซึ่งลองคิดดูสิว่า ถ้าหากว่าเป็นการคำนวนที่ซับซ้อนกว่านี้ และจำนวนข้อมูลก็มากกว่านี้ มันคงจะเปลือง (ไม่) น่าดู


ทีนี้ถ้าเรานำ lazy มาใช้ดูล่ะ?? จะแก้ไขปัญญานี้ได้ไหม? ใช่สิ มันได้


Result:
Computing next value of 0…
Computing next value of 4…

1 5

ลองดูผลลลัพธ์ที่ print ออกมา เราจะพบว่า มีแค่ 0 กับ 4 เท่านั้นที่จะถูกคำนวน ไม่ใช่กับข้อมูลทั้ง 1,000 ชุดเหมือนกับโค้ดตัวอย่างก่อนหน้านี้ และถูกคำนวนเฉพาะตอนที่เราจะ access ด้วยเท่านั้น ไม่ได้คำนวณไว้ก่อนล่วงหน้า มันเยี่ยมมากเลยใช่ไหม


Chaining Lazy Sequence
เทคนิคสุดท้ายสำหรับการนำ lazy มาใช้กับ sequence


จากโค้ดก็จะพบว่า double(increment(array[3])) ถูกทำงานแค่ครั้งเดียวเท่านั้น และถูกคำนวนเฉพาะตอนที่เราอ้างถึงมันด้วย และทำแค่ครั้งเดียวเท่านั้นเหมือนกัน!!