Thursday, August 25, 2016

ทำไงให้ GHC เร็วขึ้น

งานที่ผมทำอยู่ (ยังไม่เสร็จ) code ของ app จริง ๆ ตอนนี้แค่ 3 พันกว่าบรรทัด แต่มีใช้ template haskell กับ quasiquotation หลายที่ + lens นิดหน่อย  compile ทั้ง project ล่อไป 1 นาที ถ้าไปแก้ module ที่ module อื่นมายุ่งแล้ว compile ใหม่ ก็รอไปครึ่งนาที ไม่รู้มันช้าเอง เครื่องห่วย หรือผมเขียนอะไร noob ๆ ทำให้ compiler มันงงก็ไม่รู้

แต่จะนั่งขุดรายละเอียด GHC หาทางเอาใจ compiler ก็คงไม่ไหว.. เลยลอง google แล้วก็เจอคนเขียนเรื่องนี้ไว้พอดี เค้าลองปรับ options ต่าง ๆ  ของ GHC แล้วเอามาเทียบกันให้เราดู ว่าทำอะไรแล้วมันเร็วขึ้นแค่ไหน https://rybczak.net/2016/03/26/how-to-reduce-compilation-times-of-haskell-projects/

สำหรับใครขี้เกียจอ่าน ขอคำตอบแบบ stackoverflow:
  • ปิด optimizer (-O0) นอกจากเรื่อง correctness แล้ว GHC ยังใช้เวลาไม่น้อยไป optimize code อย่างหนักหน่วง เพื่อให้ code เราให้ทำงานได้เร็ว แต่เวลาเรา develop เราไม่ได้ต้องการเร็วอะไรขนาดนั้น ขอแค่เช็คว่า type ถูกมั้ย test พอได้ก็พอ ดังนั้นก็ปิด optimizer มันซะ
  • เพิ่ม heap (+RTS -A128m -n2m -RTS) เพราะ GHC เสียเวลาไปกับ garbage collection (GC) ตอน compile เยอะมาก ประมาณ 25 วิ จาก 65 ถ้าเพิ่ม heap ให้เยอะ ๆ GC ก็จะวิ่งน้อยลง เป็นการแลก memory กับ time
  • parallel compilation (-j) ใส่แล้ว GHC จะหาเองว่า module ไหนไม่มี dependency ก็จะ compile พร้อม ๆ กันไปเลย

ผลคือเร็วขึ้น เยอะมาก จาก 65 วิ เหลือแค่ 23 วิ... สบายละ

ปล: ใครคิดว่าเขียน haskell แล้ววงจร code feedback มันจะช้า จะขาดตอน ใจเย็น ๆ อย่าตื่นตูม แค่ลง intero (http://commercialhaskell.github.io/intero/) ตอนนี้มีแต่เป็น emacs plugin คุณก็จะได้ editor ที่ขีดเส้นตัวแดง ๆ ตรงที่ code ผิด เราเอา cursor ไปแหย่ก็รู้ทันทีว่าต้องแก้เพราะอะไร โดยไม่ต้อง compile ใหม่แม้แต่ตัวเดียว โค้ดทั่วไปรอประมาณครึ่งวิ โค้ดหนืด ๆ อย่าง template haskell รอไม่เกิน 3 วิ  หรือจะลอง test บาง function ก็มี repl ให้ใช้ใน emacs  ที่ intero มันเร็วเพราะหลังบ้านมันใช้ GHCi ในการ interpret code เรา ก็เลยเร็วเหมือนภาษา dynamic แต่เป๊ะ เหมือนวิ่งผ่าน compiler

Tuesday, July 19, 2016

ตอบเรื่อง java thread safety

จาก post ในกลุ่ม THJUG บน facebook มีคนชวนคุยเรื่อง thread safety ใน java ตามนี้ครับ





เหมือนเดิมครับ ตอบยาว เลยเอามาแปะบน blog เพื่อไม่ให้มันหายไปกับกาลเวลา...


เรื่อง thread safety ผมเห็นว่าเป็นรูปแบบนึงของผลกระทบจาก mutability + concurrency ครับ ปัญหานี้มีทุกภาษา แต่จะชัดและปวดหัวมากกับภาษาที่ไม่มีวิธีบังคับ immutability

เรื่องนี้หลักการไม่ยาก ถ้าคิดถูกแต่แรกก็เขียนให้ถูกง่าย แต่การ test ว่าถูกรึเปล่าบางทีก็ไม่ง่ายครับ เพราะเวลาผิด มันไม่ได้ตายทุกครั้งเนี่ยแหละ  ... "ระบบผิด ไม่โหดเท่า ระบบผิดบางครั้ง"

การแก้ปัญหา มีหลายแบบ แต่ละแบบก็มีข้อดีข้อเสียและลักษณะปัญหาที่ถูกโฉลกต่างกันไป ทำให้ถูกอย่างเดียวไม่ยากครับ lock แหลก แต่ทำยังไงให้ ทั้งถูก ทั้งเร็ว ทำยังไงให้ระบบเรารับ request จำนวนมาก ๆ ที่อิงกับ resource ที่ต้องใช้ร่วมกันแล้วถูกเสมอ นี่คือปัญหา

การ lock หรือที่เรียกกันใน java ว่า synchronized จะที่ method หรือที่ instance ก็เป็นเรื่องห้ามใช้ ถ้าจะทำระบบที่ high-performant (ไม่ถูก grammar เนอะ) เพราะ thread มันแพง server 1 ตัว รองรับ os thread ได้จำกัด ถ้า thread ไหนโดน lock ปุ๊บ thread นั้นจะนอนนิ่ง ๆ ทันที ไม่มีใครเอาไปใช้งานได้ ถ้า app ไหนมี lock แบบนี้แล้ว thread มากองเยอะ ๆ นี่ app หนืดได้ในไม่กี่วินาที นอกจากนี้ยังต้องระวังเรื่อง deadlock อีก ต้องเล็งดี ๆ เมื่อไหร่ wait เมื่อไหร่ notify เกิด lock พลาดเจอ deadlock ระบบก็หลับยาว..

ส่วนเรื่องทำให้ immutable โดยไม่ให้มี setter อย่างเดียวก็อาจจะไม่รอดครับ เช่น ถ้า private field เป็น final list แล้วมีการเรียก list.add(..) ต้องเพิ่มนิดนึงว่า field ใต้ instance นั้นต้อง immutable ด้วย

จริง ๆ ต้องบอกว่า immutability แค่ทำให้เราวิเคราะห์พฤติกรรมของโปรแกรมเราได้ง่าย มันไม่ได้รับประกันว่าจะไม่เจอปัญหานะครับ เพราะ software ที่ใช้งานจริง ยังไงก็ต้องมีการ mutate ค่า ถึงเราทำชิ้นย่อย ๆ ของเราให้ immutable แต่ยังไง layer บน ๆ ก็ต้อง mutate อะไรซักอย่าง

แล้วทางแก้ race condition ดี ๆ แบบ lock-free ล่ะ ? เอาเท่าที่ผมเคยใช้นะครับ

1. actor ตัวนี้ต้นตอมาจากไหนไม่รู้ แต่ดังโดย erlang และลอกโดย scala ถ้าชาว java จะใช้อาจจะต้องดู lib ชื่อ akka จริง ๆ มันทำประโยชน์ได้หลายอย่างมาก แต่สำหรับเรื่อง race condition หลักการมีแค่ว่า มี resource ที่จำกัดกี่ตัว ก็ให้มี actor เป็นตัวคุม resource เท่านั้นตัว เช่น ระบบเรา read x พร้อมกันได้เยอะ ๆ แต่ห้าม mutate x พร้อมกัน เราก็ออกแบบให้มี mutator actor ตัวเดียวที่คอย mutate x ใครอยาก mutate x ให้บอก mutator actor ให้ทำให้ หรือ ระบบ legacy หลังบ้านเรารับ api ได้แค่ 5 concurrent เท่านั้น เราก็มี actor สำหรับคอยยิง api 5 ตัวแบ่งงานกันทำ แค่นี้ทำยังไงก็ไม่มีโอกาสที่ใครจะมา mutate x หรือยิง api > 5 ตัวพร้อมกันแล้ว ข้อเสียมีหลายอย่างเหมือนกัน แต่สำหรับชาว java ไม่ต้องสนครับ java มีข้อเสียพวกนั้นครบอยู่แล้วถึงไม่ได้ใช้ actor ก็ตาม

2. software transactional memory (stm) นึกถึง optimistic locking ใน hibernate ครับ ตอนเรา load row มา เราจะมี tag เก็บไว้ (version) พอเราจะ commit ถ้า tag ไม่ตรงก็คือมีคนอื่นชิง write ก่อนเรา เราเจ๊ง ต้อง load data สดใหม่มาทำขั้นตอนเดิมอีกรอบเอง แต่ stm คือเราเขียน code แล้วบอกว่า code นี้ให้เช็ค tag ให้ด้วย ถ้า tag ตอน load กับตอน commit ไม่ตรง มันจะ re-run code block ให้เราเองไปเรื่อย ๆ จนกว่าจะผ่าน ข้อดีคือเราเขียน code ได้ธรรมชาติมาก เหมาะใช้กับงานที่เรา "คิดว่าคง conflict ไม่บ่อย" เพราะถ้า conflict บ่อยจะเปลืองตรงการทำอีกครั้ง ข้อเสียคือ ใช้กับ IO ไม่ได้ เช่น เราเขียน socket เสร็จ แต่เจอว่าระหว่างนั้นมีใคร mutate ตัวแปรเรา เราดึงข้อความที่เราเขียนกลับมาไม่ได้แล้ว รู้สึกตัวนี้จะมีใน akka เหมือนกัน

3. channel ไม่กล้าเรียก communicating sequential processes (csp) เพราะผมไม่รู้ว่า implement/prove แบบไหนถึงเรียกว่า csp แค่ได้ยินว่า channel ใน golang เป็น csp พอดีผมใช้ channel ตอนเขียน haskell (เป็นแค่ lib ตัวนึง) ดู implementation แล้วก็ไม่ได้มีอะไรพิเศษ  (implementation จริงของ haskell โกงครับ ใช้ lock แต่เป็น lock บน green threads ก็เลยไม่กิน os thread)  การใช้งานก็คล้าย ๆ actor ครับ มีคนเขียน มีคนอ่าน มี fan-out ถ้ามี resource อะไรจำกัดก็ให้มีคนจัดการ resource นั้นเท่านั้นตัว ใครอยากยุ่งกับ resource นั้นก็ส่งข้อมูลผ่าน channel ข้ามไปข้ามมา  แต่อันนี้ไม่รู้มีใน akka รึเปล่า

Thursday, June 23, 2016

Functor คืออะไร

จากคำถามใน group "Thai Functional Enthusiasts" https://www.facebook.com/groups/310209089128699/ มีคนถามว่า..

ผมขอคำอธิบาย 3 คำนี้หน่อยครับ
Functor, Applicative Functors, Monad รู้สึกยัง งงๆ กับ 3 คำนี้
ตามที่ผมเข้าใจ คือ
1. Functor มันคือการจำกัด type เพื่อจะเช็คว่า type ที่ส่งไปถูกต้องหรือไม่ ถ้าสร้างไว้แต่แรกๆ จะไม่ต้องมาเช็คพวก null, nil อีก
2. Applicative Functors เหมือน Functor แต่ทำกับพวก function 3. Monad นี้ งง สุดเลยถ้าอธิบายแล้ว งง ขออภัยครับ งง เหมือนกัน
อีกคำถามครับ Monad, Monoid, Monadic มันคืออันเดียวกันใช่ไหมครับ

พิมพ์ตอบใน facebook ท่าจะยาวเกิน เลยเขียน blog ครับ เผื่อมีประโยชน์ต่อคนอื่นบ้าง

คือ functor, applicative และ monad เป็น typeclass แปลว่าสิ่งที่มันกำหนดจริง ๆ มีแต่ชื่อ function กับ type ที่ function ไปยุ่ง ซึ่งถือว่า abstract มาก ถ้าเราพยายามหาตัวอย่างที่มัน concrete เราอาจจะยิ่งงง เพราะเราจะไปยึดติดกับของที่มัน concrete ทำให้เอาไปใช้กับอย่างอื่นไม่ได้ ซึ่งผมว่านี่เป็นปัญหายอดฮิตของคนที่เขียนภาษาอื่นก่อนมาหัด haskell เลย

ผมว่าก่อนจะรู้ functor, applicative, monad และ monoid จริง ๆ ต้องรู้เรื่อง value/type/kind และ typeclass ก่อนครับ มันสัมพันธ์กันหมด อธิบายด้วยอะไรที่จับต้องไม่ได้ แต่ผมจะลองอธิบาย functor แบบบ้าน ๆ ดู โดยจะพยายามไม่ให้เสีย concept ครับ

วันนี้ขอเรื่อง functor ก่อน.. (เรื่องอื่นไว้ post หน้าครับ) functor คืออะไร  functor คือ "context ของ <อะไรซักอย่าง> ที่เราสามารถเปลี่ยนไอ้ <อะไรซักอย่าง> เป็น <อะไรอีกอย่าง> ได้ แต่มีข้อแม้ว่า <อะไรอีกอย่าง> ยังต้องอยู่ใน context เดิม" ยิ่งฟังยิ่งงงเนอะ 555 ไม่เป็นไร พักไว้ก่อน

ดู definition ของ Functor บ้าง..

class Functor f where
  fmap :: (a -> b) -> f a -> f b

อ่านออกเสียงเป็นภาษาไทยว่า ถ้า f เป็น context นึง และ f เป็น functor แล้ว เราสามารถแปลง (fmap) มัน โดยวิธีการแปลง จะให้คนอยากแปลงส่ง function 1 ตัว (a -> b) เพื่อเปลี่ยน "อะไร" (a) ใน context f (f a) ให้เป็น "อีกอย่าง" (b) ใน context f (f b) เหมือนเดิม เช่น

ถ้าผมมี "ขวดน้ำที่ใส่อะไรซักอย่าง" และผมรู้ว่าขวดน้ำผมเป็น functor แปลว่าผมสามารถเปลี่ยนของในขวดอย่างนึงไปเป็นอีกอย่างได้แน่นอน เช่น เปลี่ยนน้ำในขวดเป็นน้ำแข็ง แต่น้ำแข็งต้องอยู่ในขวดเหมือนเดิมนะครับ ถ้าบ้าจี้เขียน code ก็จะได้ประมาณนี้

in: fmap (\น้ำ -> แช่แข็ง น้ำ) (ขวด น้ำ)
out: (ขวด น้ำแข็ง)

เอาใหม่ให้มันเบลอ ๆ กว่าเดิม ถ้าผมมี "ความรู้สึกดี ๆ กับอะไรซักอย่าง" และผมรู้ว่า "ความรู้สึกดี ๆ" เป็น functor แปลว่าผมสามารถเปลี่ยนความรู้สึกดี ๆ ต่อนาย ก เป็นความรู้สึกดี ๆ ต่อนาย ข หรืออาจจะเปลี่ยนไปรู้สึกดี ๆ กับหมาข้างถนนก็ได้ จะเห็นว่าผมสามารถเปลี่ยน "อะไร" ที่ผมรู้สึกดี ๆ ต่อได้ แต่ผมไม่สามารถเปลี่ยน context จากความรู้สึกดี ๆ เป็นความรู้สึกเกลียดชังได้

กลับมาตัวอย่างที่ใช้จริง เช่น Maybe .. context ของ Maybe คือ "ความอาจจะมีหรือไม่มีก็ได้" และ Maybe เป็น functor แปลว่าเราสามารถแปลง (fmap) ความอาจจะมีหรือไม่มีอะไรซักอย่าง ให้เป็นความอาจจะมีหรือไม่มีอะไรอีกอย่างได้ เช่น Maybe String (อาจจะมี string) กลายเป็น Maybe Int (อาจจะมีตัวเลข) ถ้าเราจะเปลี่ยนอะไรมัน เราก็เปลี่ยนได้เฉพาะกรณีที่มีค่าเท่านั้น

in: fmap (\s -> length s) (Just "hello")
out: (Just 5)

in: fmap (\s -> length s) (Nothing)
out: (Nothing)

ส่วน list .. context ของมันคือ "ความเป็น collection" หรือ "ความเป็นไปได้หลายอย่าง" การที่เราจะเปลี่ยนอะไร เราก็ไล่เปลี่ยนทีละตัว ถ้าไม่มีอะไรเลย (list ว่าง) เราก็ไม่ต้องเปลี่ยน (คืน list ว่าง)

in: fmap (\n -> show (n * 2)) [1,2,3,4,5]
out: ["2", "4", "6", "8", "10"]

in: fmap (\n -> show (n * 2)) []
out: []

หรือ async (ถ้าไม่เคยใช้ใน haskell นึกถึง promise ของ javascript) .. context ของ async คือ "เดี๋ยวชั้นทำงานเสร็จแล้ว ชั้นจะคืนค่าอะไรซักอย่างให้แก" ถ้าเราจะแปลง async เราก็ต้องแปลงไอ้ค่าที่ async จะคืนมา ตอนมันทำงานเสร็จนั่นเอง

in: data Student = Student { name :: String }
in: do
in:   name <- async (plankFor10MinsAndGetStudentName)    -- เดิม เป็น Async String
in:   let student = fmap (\s -> Student s) name               -- fmap แล้วกลายเป็น Async Student
in:   wait student
out: IO (Student "John")


ทิ้งท้าย.. monad กับ monadic ถ้าไม่ซีเรียส มองว่ามันเหมือนกันก็ได้ครับ ภาษาไทยเราใช้ noun กับ adj ปนกันอยู่แล้ว

ส่วน monoid แปลเป็นไทยคร่าว ๆ ว่า "อะไรก็ได้ที่ถ้าเราเอามัน 2 ตัวที่เป็น type เดียวกันมารวมกัน มันจะรวมกันได้ และต้องได้ type เดิม" คำว่ารวมนี่ไม่ใช่การ + อย่างเดียวนะครับ ถ้า int monoid เราอาจจะบวกหรือคูณ ถ้า string หรือ list monoid เราก็เอามา concat ฯลฯ

จะสังเกตว่า monoid ไม่ใช่ context ของ "อะไรซักอย่าง" (kind: * -> *) แต่มันคือ ไอ้ตัว "อะไรซักอย่าง" เองเลยครับ (kind: *)

Monday, April 21, 2014

ยังไงถึงจะเป็น functional programming language

พอดีในกลุ่ม THJUG ใน Facebook มีการพูดคุยกันเกี่ยวกับ feature ใหม่ของ Java 8 หนึ่งในเรื่องที่ถูกพูดถึงบ่อยมากที่สุดเรื่อง Lambda ซึ่งนี่เป็นครั้งแรกของ Java ที่ developer สามารถส่ง function ไปมาได้โดยไม่ต้องสร้าง interface แล้ว implement ให้วุ่นวาย

หลายคนที่เขียน Java เดิมอยู่อาจสงสัยว่า ไอ้ Lambda นี่มันมีอะไรดี ทำไมมันดูฮิตติดลมบนกันจังช่วงนี้ ทำไมเค้าว่ากันว่าไอ้เจ้านี่มันทำให้ Java รองรับสไตล์การเขียนแบบ "Functional"

ตอนแรกผมกะจะเขียนเรื่องข้อดีของความเป็น functional แต่ขอเปลี่ยนใจเล่าเรื่องเกี่ยวกับนิยามของมันก่อน เพราะเดี๋ยวเวลาพูดถึงข้อดี อาจจะต้องอ้างอิงด้วยว่า ข้อดีแต่ละข้อ ฟังดูดีเวอร์ แต่มันจะเป็นไปได้ยังไง

นิยามของคำว่า functional ... เราจะแยก programming language ออกได้ยังไงว่าอะไรคือ functional อะไรคือไม่ใช่ นิยามส่วนตัวของผมเอง (ไม่อิงหลักวิชาการ) ภาษาจะเป็น functional ได้ ต้องมีคุณสมบัติหลัก ๆ ดังนี้

  1. function เป็น first class citizen แปลว่าอะไรที่ variable, value, object ฯลฯ ทำได้ function ต้องทำได้ด้วย เช่น เราส่งตัวแปร เข้า method นี้ แล้ว method return ตัวแปรอีกตัวออกมา  function ก็ต้องทำอย่างนั้นได้ด้วย เราต้องสร้าง function แล้วส่งมันเข้า method ให้ method คำนวณแล้ว return function ใหม่ออกมาได้ หรือต้องจับ function ยัดใส่ list ใส่ map ได้
  2. immutable data structures แปลว่า ภาษาต้องมีโครงสร้างข้อมูลที่การันตีว่าจะไม่ถูกแก้ไขค่าเตรียมไว้ให้ใช้ หรือถ้าไม่มีก็ต้องสามารถ implement ใช้เองได้โดยง่าย ด้วย syntax ที่ไม่เยิ่นเย้อ อาจจะนึกภาพไม่ออก ตัวอย่างเช่น array list ของ Java อันนี้ไม่ functional เพราะเป็น mutable data structure เราส่ง array list เข้าไปใน method ลึกลับ เราไม่มีโอกาสรู้เลยว่า list ของเราจะถูกแก้ไขค่าหรือไม่ ต่างกับภาษา functional ที่จะมี list แบบที่สามารถการันตีว่ามันจะไม่ถูกแก้ไขค่าแน่นอน ไม่ว่าจะผ่าน function ลึกลับกี่ครั้งก็ตาม
  3. partial application ผมก็ไม่ค่อยแม่นนิยามความแตกต่างกับ currying แต่ผมว่าไม่ค่อยสำคัญ ขอแปลเป็นภาษาบ้าน ๆ ว่ามันคือการเรียก function แบบใส่ parameter ไม่ครบ แล้วได้ function ใหม่ออกมา ถ้าเราใส่ parameter ให้ครบ มันก็จะ return ค่าให้เรา เช่น function "plus" (บวก) ซึ่งรับ parameter 2 ตัว ถ้าเราใช้มันในภาษาปกติ เราต้องเรียก plus(2, 3) มันจะคืน 5 ให้เรา ถ้าเราเรียก plus(2) จะเจอ compilation error แต่ในภาษา functional เราสามารถเรียก plusWith2 = plus(2) แล้วตามด้วย result = plusWith2(3) ได้ โดย plusWith2 เป็น function ใหม่ที่ถูกสร้างกลางอากาศ
  4. lazy evaluation เอาไว้เวลาเราต้องการนิยามค่าบางอย่างที่ยาวไม่สิ้นสุด เช่น ลำดับ fibonacci หรือ list ที่มีค่า [0,1,2,3,0,1,2,3,0,1,2,3, ... ไปเรื่อย ๆ]
  5. pattern matching เปรียบเหมือน switch/case ที่เก่งกว่ามาก ๆ อธิบายไม่ถูก ดูตัวอย่างเลยดีกว่า
1.| simpleZipWith :: (a → b → c)[a][b][c]
2.| simpleZipWith _ [] _ = []
3.| simpleZipWith _ _ [] = []
4.| simpleZipWith ƒ (a:as) (b:bs) = ƒ a b : simpleZipWith f as bs
หน้าตาอาจจะดูแปลก ๆ มั่ว ๆ ซักหน่อยถ้าใครไม่เคยเห็นภาษานี้ ผมใส่สีไว้เพื่อให้รู้ว่าอะไรคืออะไร เจ้า simpleZipWith เป็น function ที่รับ parameter 3 ตัว ได้แก่ function, list ตัวแรก และ list ตัวที่สอง โดยมันจะหยิบ element แรกของ list ทั้งสองตัวมาผ่าน function แล้วเอาไปลง list ใหม่ ไปเรื่อย ๆ จนกว่า list ตัวใดตัวหนึ่งจะหมด มันก็จะหยุด
บรรทัดแรก ไม่มีอะไร เป็นการประกาศ type ของ function เฉย ๆ คิดซะว่า (a -> b -> c) แปลว่า function ที่รับ parameter 2 ตัว (a และ b) แล้ว return ค่านึงออกมา (c) ส่วน [a] [b] [c] แปลว่า list
บรรทัดที่ 2 บอกว่า parameter ตัวแรก (function) และตัวที่ 3 (list) เป็นอะไรไม่สนใจ ('_' แปลว่า "ไม่สน") ถ้า parameter ตัวที่ 2 เป็น list ว่าง ([] คือ element ใน list หมดแล้ว) ให้ return list ว่าง
บรรทัดที่ 3 บอกว่า parameter ตัวแรก กับ ตัวที่สอง เป็นอะไรไม่สน ถ้าตัวที่ 3 เป็น list ว่าง ให้ return list ว่าง
บรรทัดที่ 4 บอกว่า ถ้าไม่ใช่ทั้งกรณีในบรรทัดที่ 2 และบรรทัดที่ 3 ให้ถอดหัว list ตัวแรก (a) และ list ตัวที่สอง (b) ออกมา แล้วเรียก f(a, b) ได้ผลอะไรก็ตาม วางเอาไว้หน้า list แล้วขยับไปทำกับ list ส่วนที่เหลือ (as, bs) ด้วยหลักการเดียวกันไปเรื่อย ๆ (recursion) จน list ตัวใดตัวหนึ่งหมด โปรแกรมก็จะตกเคส บรรทัดที่ 2 หรือ 3 แล้วก็จะหยุด

เท่าที่นึกออกก็น่าจะมีเท่านี้ครับ ครั้งต่อไปผมจะเขียนเกี่ยวกับข้อดีของภาษาหรือสไตล์การเขียนแบบ functional 

Saturday, September 21, 2013

ทำยังไงให้ไม่ (ค่อย) เจอ exception

จากที่โพสไปในคราวที่แล้ว ว่าคนเขียน scala มักจะไม่ค่อยโดน NullPointerException, NoSuchElementException หรืออะไรทำนองนี้เล่นงาน วิธีที่ทำให้ไม่โดนเล่นงาน ก็คือไม่ต้องไปใช้มัน เพราะ scala (และหลาย ๆ ภาษา) มีวิธีอื่นที่ทำให้โปรแกรมเราสื่อความหมายได้ดีและปลอดภัยกว่าการใช้ null หรือการโยน exception ออกมาดื้อ ๆ แต่ถ้าใครใช้ scala แต่ยังใช้ null หรือ throw exception อันนี้ก็ตัวใครตัวมันครับ (รุมด่ามัน !)

โครงสร้างที่ว่ามีหลายแบบ ตัวอย่างแรกแบบที่คนเพิ่งหัดใช้ scala ใช้แล้วได้ผลประโยชน์ทันทีก็คือ Option/Some/None วันนี้เรามาดูกันว่า Option/Some/None มันช่วยทำให้โค้ดเรามีคุณภาพดีขึ้นกว่าการใช้ null/exception ยังไง

อ้อ ลืมบอกไปครับ Option นี่ไม่ใช่ keyword นะครับ เป็น class บ้าน ๆ ที่ออกลูกออกหลานได้ 2 ตัวคือ Some และ None ซึ่งแน่นอน ก็คือ class บ้าน ๆ เหมือนกัน

วิธีใช้

ปกติ สมมุติผมจะเขียน DAO ซักตัว ผมก็ร่าง interface มาก่อน เอาแค่ method เดียว สำหรับหา customer (คิดว่า trait = interface)

trait CustomerDao {
  def getCustomerById(id: Long): Customer
}

เสร็จละ ก็มา implement กัน

class JpaBackedCustomerDao extends CustomerDao {
  ...
  def getCusto ...
  ...
}

ยังไม่ทันเขียน method เสร็จ แว้บแรกที่ต้องเข้ามาในหัวแน่ ๆ คือ แล้วถ้ามันหาไม่เจอล่ะ ?? return null ก็ไม่ควร ปิ๊ง ใช้ Option สิ เพราะ Option มันสื่อว่า "อาจจะมีหรือไม่มีส่งกลับไปให้ก็ได้" คิดได้เราก็แก้ interface (trait) เราทันที

trait CustomerDao {
  def getCustomerById(id: Long): Option[Customer]
}

สบายใจแล้วก็ implement ต่อได้ แต่พอดี customer ที่ JPA (java) มัน return มาอาจจะเป็น null ก็ได้ เลยต้องป้องกันซักหน่อย (สีเทา ๆ คือไม่ต้องเขียนครับ ใน scala จะไม่ใช้ keyword 'return')

class JpaBackedCustomerDao extends CustomerDao {
  ...
  def getCustomerById(id: Long): Option[Customer] = {
    val entityManager = ...
    val customer = entityManager.find(...)
    if (customer != null)
      return Some(customer)
    else
      return None
  }
  ...
}

ส่วนคนนำไปใช้ ก็ไม่ต้องกลัวลืมเช็ค if null อีก เพราะ compiler จะบังคับให้เราจัดการกับ Option ว่าถ้าออก Some จะทำยังไง ออก None จะทำยังไง

def whateverTheNameIs {
  ...
  val customer = customerDao.getCustomerById(theId)
  ...
  ...
  // แบบนี้ compiler ไม่ยอม ไม่ปล่อยผ่าน
  println(customer.name)
  ...
  ...
  // compile ผ่าน แต่ไม่ควรทำ
  // เพราะเจอ exception ตอน runtime ถ้า customer เป็น none
  println(customer.get.name)
  ...
  ...
  // ต้องแบบนี้
  customer match {
    case Some(c) =>
      ... // ทำไรก็ทำไป c คือ customer ในกรณีที่หาเจอ
    case None =>
      ... // หาไม่เจอทำยังไง
  }
  ...
  ...
}

อาจจะดูว่า syntax อะไร ทำไมยุ่งยาก อันนี้ผมโชว์ให้ดูว่าเค้าตั้งใจให้เรา handle กรณีต่าง ๆ ให้ครบ ถ้าเขียนจริง ๆ มันจะมี method อื่น ๆ ให้เราใช้แบบกระชับ ๆ เช่น

val young = customer.exists(c => c.age < 25)

อันนี้จะเป็น true เมื่อ customer เป็น some และไส้ข้างในมี field age ที่น้อยกว่า 25 ถ้าเป็น none หรือ age >= 25 ก็จะได้ false โดย exists คือใช้ test สิ่งที่อยู่ใน "กล่อง" ใบนี้ ส่วนจะ test ยังไง เราก็ส่ง function c.age < 25 รึเปล่าเข้าไป

อีกตัวอย่าง

val fullName = customer.map(c => c.firstName + " " + c.lastName).getOrElse("no customer")

ก็จะได้ค่าชื่อและนามสกุลต่อกัน ในกรณีที่ customer เป็น some และได้คำว่า "no customer" ในกรณีที่ customer เป็น none โดย map คือการหยิบของในกล่องขึ้นมา ทำ ๆ ๆ ๆ เสร็จแล้วก็จับยัดลงกล่อง ถ้าเป็นกล่องเปล่า map แล้วก็จะได้กล่องเปล่าเหมือนเดิม ส่วน getOrElse คือการ "เปิดกล่อง" แบบปลอดภัย เพราะถ้ามีของในกล่องมันก็จะคืนค่านั้น แต่ถ้าไม่มีมันก็จะเอาค่าที่เราให้มันเป็น default คืนไปแทน ซึ่งในที่นี้ก็คือ "no customer"

หรือสุดท้าย สมมุติเราจะโอนเงิน เรามี

def transfer(dest: Account, src: Account, amount: BigDecimal): TransferResult = { ... }
val amount = ...
val srcAccount = findAccount(...)
val destAccount = findAccount(...)

ซึ่งเป็น account เราเป็น option ทั้งคู่ เราต้องการโอนเงินเฉพาะกรณีที่ทั้ง src และ dest มีค่า เราสามารถเอามามันมาเชื่อมกันได้โดยใช้ flatMap แบบนี้

val result = srcAccount.flatMap { src =>
  destAccount.filter(_ != src).map { dest =>
    transfer(dest, src, amount)
  }
} getOrElse TransferFailureBadAccount

เราก็จะได้ result (enum) ที่มีค่าตามผลการ transfer ว่าสำเร็จหรือเจ๊งเพราะอะไร ส่วนถ้าหา account ใด account หนึ่งไม่เจอ ก็จะได้ TransferFailureBadAccount เอาไปจัดการต่อได้

ถ้าชอบเขียนพรืด ๆ ต่อกัน ก็แบบนี้

srcAccount flatMap (src => destAccount filter (_ != src) map (dest => transfer(src, dest, amount)) getOrElse TransferFailureBadAccount

หรือจะเขียนเป็น for comprehension เลยก็ได้ แล้วแต่ใครคิดว่าแบบไหนสวยงาม (เดี๋ยว compiler ก็แปลงไปเรียก flatMap + map + filter อยู่ดี)

(for {
  src <- srcAccount
  dest <- destAccount if dest != src
} yield {
  transfer(dest,src,amount)
}) getOrElse TransferFailureBadAccount

สุดท้าย ไม่ใช่แค่โค้ดที่เราเขียนกันเองเท่านั้นที่ใช้ Option แต่โค้ดของ library มาตรฐานของ scala ก็ใช้ด้วย เรียกว่าเป็น style ของภาษาที่โปรแกรมเมอร์นิสัยดีพึงทำ ตัวอย่าง data structure ยอดนิยม เช่น List และ Map

val myNonEmptyList = List(1,2,3,4,5)
val myEmptyList = List[Int]()
myNonEmptyList.head        // ได้ 1
myNonEmptyList.headOption  // ได้ Some(1)
myEmptyList.headOption     // ได้ None
myEmptyList.head           // ได้ runtime exception

val scoreMap = Map("john" -> 55, "eric" -> 57, "jessy" -> 62)
scoreMap("john")           // ได้ 55
scoreMap.get("john")       // ได้ Some(55)
scoreMap.get("rob")        // ได้ None
scoreMap("rob")            // ได้ runtime exception

ก็เป็นงานของเราที่ต้องเลือกใช้ให้เหมาะ แต่ใครจะเลือกแบบที่มี exception ล่ะ จริงมั้ยครับ ?

Wednesday, September 18, 2013

ครบรอบ 1 เดือน กับการใช้ Scala เต็ม ๆ

เนื่องจากนี่เป็นโพสแรก ขอเท้าความหน่อยละกัน ว่าทำไมผมถึงมาลงเอยกับ scala ได้

ถ้าไม่อยากอ่านเท้าความ (นอกเรื่องยาว) ให้ข้ามตัวอักษรสีเทา ๆ ไปครับ ผมพยายามจะทำ spoiler แต่มันไม่ยอม

ตั้งแต่จบใหม่ ๆ ผมอยู่กับ java มาราว ๆ 6-7 ปี ได้ลอง ejb1 (นรก), ejb2 (นรก), struts, spring, hibernate, seam, jsf ฯลฯ ชีวิตก็มีความสุขตามอัตภาพ จนวันนึงได้มาเจอ ruby on rails แล้วก็คิดว่า ลาก่อน java (555) แต่ด้วยงานประจำตอนนั้นใช้ java เลยอด นั่งเขียน ruby กะ python แบบ noob ๆ กะป๋องกะแป๋ง ทำ shell script ใช้เองไปวัน ๆ ทำไรไม่ได้ก็รอ ๆ ๆ จน grails ออก version 1.0 ผมก็เอาไปโม้ให้พี่ ๆ เพื่อน ๆ ฟัง จนเค้ายอมให้ผมใช้ ผมก็ใช้มา 4-5 ปีได้ (ปัจจุบันก็ยังใช้อยู่)

ฟ้ากลั่นแกล้ง.. จากแรก ๆ ที่ happy ดี แต่พอใช้ท่าพิศดารก็เริ่มเจอบั๊กแปลก ๆ มากขึ้นเรื่อย ๆ ไปดู issue tracker ของ grails ก็ละเหี่ยใจ มี bug มากมายที่รอคิวแก้ หลายข้อเป็นบั๊กจริง ๆ ที่คาอยู่หลายปีแต่ยังไม่โดนแก้ จากที่ชอบเพราะความเร็ว ก็มาเจอบั๊กสกัดดาวรุ่งซะ แถมบังเอิญงานหลักที่แก๊งผมทำ มีความจำเป็นต้องแก้ (change) ด่วน ๆ deploy ด่วน ๆ ทุกวัน หรือบางช่วง top form วันละหลายครั้ง การไม่มีเทส (บู่ ...) + deploy ด่วน และภาษาเป็น dynamic ทำให้การพิมพ์ผิดและเขียนอะไรผิดนิด ๆ หน่อย ๆ ที่ compiler ควรจัดการให้ กลายเป็นระเบิดเต็มหน้า user เวลา user ส่ง email หรือโทรมาทีไรก็แสนจะเขิน "แหะ ๆ เดี๋ยวแก้ให้นะค้าบ รอ 10 นาทีค้าบ" ผมก็เลยเริ่มมองหาทางเลือกอื่น โดยมีเงื่อนไข 3 ข้อ.. คือ (1) เป็น static typing ยิ่ง static ยิ่งดี (2) predictable คือ ไม่มี magic เช่น การ add method วิเศษ หรือ การทำอะไรก็ตามที่เราหาที่มาได้ยาก และ (3) ต้องกระชับ โดยข้อ (1) และ (2) ก็เพื่อให้ compiler ช่วยเรามากที่สุดและถ้าเจอบั๊กอะไร เราจะได้แก้โค้ดของ library เองได้ง่าย ๆ ส่วนข้อ (3) ก็เพื่อให้โค้ดของเราอ่านง่าย แก้ง่าย

สวรรค์มีตา.. ระหว่างที่ต่อสู้กับบั๊กของ grails และการแก้ + deploy ด่วนรายวัน ผมได้รู้จัก scala (2.7.5) พอดี แวบแรกก็เฉย ๆ เหมือน toy project ของใครซักคน แต่พอได้ลองเล่น ได้ดูโค้ดที่คนอื่นเขียน เอ้ย เจ๋งเว้ย เป็นภาษา static (กว่า java เยอะ) typing เมพ น่าจะช่วยในสถานการณ์ที่เราไม่ทำ automated regression tests ได้ แถม scala ยังลอกข้อดีภาษาอื่นมาใช้หลายเรื่อง โดยเฉพาะ functional programming แบบ haskell แต่ก็ยังเอามาใช้ไม่ได้ เพราะติดข้อจำกัดหลายเรื่อง โดยเฉพาะ IDE ... อนาถมาก อนาถจริง ๆ แค่เปิด intellij ขึ้นมาก็มี exception แล้ว -..-' highlight ผิด ๆ ถูก ๆ เรียกว่าเชื่อ IDE ไม่ได้เลยดีกว่า (การเขียนภาษาใหม่โดยไม่พึ่ง IDE เลยนี่..​ เหนื่อยไปนะ) ไล่หา implicit conversion (typeclass) ก็ไม่ได้ โหดพอกับ grails magic เลย ก็เลยได้แต่อ่านโน่นอ่านนี่ตามข่าวไปเรื่อย แต่ไม่ได้ลงมือใช้จริงซักเท่าไหร่ รอมันโต

2-3 ปีผ่านไป ตอนนี้ IDE เมพพอแล้ว ecosystem ของ scala ก็แข็งแกร่งมากกว่าแต่ก่อนเยอะ ได้เวลาใช้จริงซักที

... ชิ้ง ฟ้าประทาน project ใหม่มาให้ พร้อมเงื่อนไขต่าง ๆ ที่สามารถตัด grails ทิ้งและเลือก scala (+ lift) เข้ามาได้อย่างปลอดภัยโดย tools และ libraries ที่ผม cen 1 1 no rum na sadddd กับงานนี้คือ

Build tool: SBT 0.12
Programming language: Scala 2.10.2
Web framework: Lift 2.5.1 (stable)
Database access library: Slick 1.0.1
Test framework: Specs2 (ว่าจะใช้ ในอีกเดือนถัดไป)
IDE: (IntelliJ IDEA 12.0.4 + Scala plugin) & (Eclipse + Scala IDE) ตัวเดียวเอาไม่อยู่..

เข้าเรื่องซะที 555 หลังจากเกริ่นซะยาว..

ใน post นี้ ผมขอเล่าคร่าว ๆ ก่อนละกันครับ ว่า scala นี่มันมีข้อดีข้อเสียยังไงเมื่อเทียบกับภาษายอดนิยมอย่าง java, groovy และ haskell (ห๊ะ !?!) ไว้โพสหน้า ถ้าผมยังเขียนอยู่ จะเริ่มเจาะเรื่องที่ว่าเป็นข้อดีและข้อเสียทีละข้อ เพราะโพสหน้า ผมอาจจะกำลังหาทางเลือกอื่นอีกแล้วก็ได้ 555 (หวังว่าไม่นะ)

ซีเรียส ๆ หมัดเด็ดของ scala จริง ๆ เลย คือ
  1. เป็น static typing มัน static กว่า java เยอะ แต่ก็ไม่สุดเหมือน haskell แต่แค่นี้ก็เพียงพอทำให้เรื่องพิมพ์ผิด หรือการแก้ type ตัวแปร แยก class รวม class ถูก compiler จับได้หมด แถมบางทีเวลาเราเขียน handle เงื่อนไขต่าง ๆ ไม่ครบ (ตอน pattern matching) compiler ยังขึ้น warning เตือนเราได้อีก เหลือเรื่อง collection และ equality test ที่ยังสู้ haskell ไม่ได้ แต่ก็มี library ช่วยให้พอกล้อมแกล้มไปได้
    • ผมต้องแก้ structure ใน database mapping layer หลายครั้งมาก แต่ทุกครั้ง compiler จับให้หมด ว่าต้องตามไปแก้ตรงไหนบ้าง ถ้าเป็น groovy คงลำบาก java น่าจะสูสี ส่วน haskell มี newtype keyword ที่ทำให้ type มัน static เข้าไปอีก แต่ผมว่าเราไม่ได้จำเป็นต้องใช้ขนาดนั้น ซึ่งจริง ๆ scala ก็เริ่มลอกมาแล้ว (AnyVal) แต่ยังบั๊ก ๆ อยู่ บางทีก็ compile ไม่ผ่านซะงั้น
    • หลายครั้งที่พอ compile ผ่าน แล้วโปรแกรมให้ผลถูกต้องเลย เพราะ type ช่วยดักข้อผิดพลาดในการวางแผนเขียนได้ระดับนึง โดยเฉพาะ function ที่เกี่ยวกับ recursion และ data structure ที่ซับซ้อน
  2. รองรับ functional programming style คือเขียนแบบ functional (และ imperative) ได้
    • งานเกือบทุกประเภท ถ้าเขียนแบบ functional ได้ จะ clean กว่า imperative มาก และ scala มี language construct ที่ทำให้การเขียนแบบ functional ไหลลื่น โดยที่ java และ groovy ไม่มีเลย แต่ haskell ก็ยังเมพกว่า scala ในความ functional มาก
    • มี data structure บางอย่างทำให้เราหลีกเลี่ยง exception ยอดนิยมใน java เช่น NullPointerException, NoSuchElementException และอะไรทำนองนี้ได้ ผมเขียน scala เฉลี่ยวันละ 6-8 ชั่วโมงตลอด 1 เดือนที่ผ่านมา เจอ exception ตระกูลนี้รวมกันไม่เกิน 3 ครั้ง (scala ใช้ Option, haskell ใช้ Maybe, ส่วน java + groovy ตัวใครตัวมัน)
    • ไม่มี side-effects ทำให้อ่านง่าย เทสง่าย แก้ง่าย function จะมีขนาดเล็กโดยธรรมชาติ (ส่วนใหญ่ function จะยาวไม่เกิน 1 คืบ)
      • อันนี้ผมคิดเองว่า (ยังไม่ได้ลอง) น่าจะหมดปัญหาเรื่อง TDD + major changes = waste โดยการรอเขียนเทสหลังจากที่ระบบนิ่งระดับนึงแล้ว ... ถ้าจะล่มผมว่าน่าจะล่มที่มันไม่มีความเป็นวินัยมากกว่าเทคนิค
    • ทางทฤษฎี.. เราจะทำ concurrency ง่ายขึ้นและคาดเดาได้ง่ายขึ้น (ยังไม่มีโอกาสได้ทำ)
  3. รันอยู่บน JVM
    • ซึ่งเร็วและแข็งแกร่งมาก
    • ใช้ lib ของ java ได้ ผมใช้ Quartz และ Axis2 เพราะหาอะไรที่ดีกว่าในโลก scala ไม่ได้ ซึ่งก็ใช้ได้เนียนดี เขียน glue code นิดหน่อยตรง axis 2 ให้จัดการเปลี่ยน nullable property -> option และเปลี่ยน class + getter + setter -> case class เพื่อความสวยงาม
    • system engineer ของลูกค้าดูแลได้ เพราะเอาไปรันบน app server ที่เค้าคุ้นเคย เรา build เป็น .jar .war ได้เลย
  4. โค้ดกระชับกว่า java เยอะมาก (และน่าจะภาษา static อื่น ๆ ด้วย) ส่วนนึงเพราะตัวภาษา เช่น lambda function ที่ส่งไปส่งมาได้ ไม่ต้องประกาศ anonymous class และยังมี type inference + implicits (typeclass) ทำให้ลดการประกาศ type ไปได้เยอะ เรียกว่ากระชับใกล้เคียงภาษา dynamic อย่าง groovy แต่ type inference ติดปัญหาที่ต้องรองรับ OO ด้วย ทำให้มันเมพสู้ haskell ไม่ได้ และ noise เยอะกว่าพอสมควร
  5. มี stack trace .. java กับ groovy ก็มี แต่ haskell ไม่มี  *0* ถือว่าเป็น deal breaker ของ haskell อีกข้อ
  6. สุดท้าย ความเห็นส่วนตัว... scala เปิดโลกการเรียนรู้ โลก functional programming มีแนวคิดเด็ด ๆ เยอะมาก การไล่โค้ดของ framework และ library แต่ละครั้งได้เจออะไรดี ๆ มากมาย แบบที่ java + groovy ไม่มี เพราะ java มันมีแค่นั้น ส่วน haskell ก็ยังเมพสุดอยู่ดี ทั้งเรื่องแนวคิดและเรื่องความสวยงาม เพราะมันเป็น functional เนื้อ ๆ ไม่มี OO มาเจือปนเหมือน scala
ข้อเสียกันบ้าง
  1. Document ของตัว scala ถือว่าเยอะพอ แต่ของ library ที่ใช้ ถือว่าน้อยมาก เจอปัญหาอะไรต้องขุด source ตลอด ในที่นี้คือ Lift + Slick น่าจะเป็นปัญหาสำหรับโปรแกรมเมอร์ที่โดนบังคับใช้ เพราะไม่มีแรงใจ.. แต่ถ้าใจรักน่าจะชอบ เพราะขุดแล้วคุ้ม ขุดทีเดียวรู้ที่มาที่ไปทั้งหมด
  2. ระบบ Type ซับซ้อน ยิ่งเจอ framework ที่ใช้ monad + typeclass เยอะ ๆ อย่าง slick นี่ อ่าน doc ยังไงก็ไม่ช่วย ต้องใช้ IDE ช่วยหา implicit conversion (typeclass) และตามไปอ่านโค้ดสถานเดียว ก็เหมือนข้อ 1 คือเสียเวลาอ่านทีเดียวก็จะเข้าใจ และใช้ได้แบบพริ้ว ๆ
    • คนส่วนใหญ่จะไม่ต้องเจออะไร advanced ๆ แบบนี้ ยกเว้นคนที่จะเขียน framework และพวกบุกเบิกเรียนรู้ library ไปสอนคนอื่นเท่านั้น
    • แต่จริง ๆ ติดอะไรก็ไปโพสถามใน mailing list ก็ได้ ผมเคยไปโพสอยู่สองสามครั้ง พวก committer เค้าก็ตอบกันเร็วดี
  3. Compiler มีบั๊ก มันไม่ได้เยอะจนทำงานไม่ได้หรือผิดแบบไม่รู้เนื้อรู้ตัว จะออกแนว compile ไม่ผ่าน ต้อง clean ก่อนถึงจะผ่าน อะไรทำนองนี้ ส่วนมากจะเป็นกับ feature ใหม่ของภาษา หรือบาง class ที่ type มัน complex มาก ๆ .. ก็ทำไรไม่ได้ นอกจาก workaround กันไป
  4. Compile ช้ามาก ช้ากว่า java, groovy และ haskell เรื่องนี้พอจะมี workaround คือลดการใช้ trait และ mixin แต่ผมว่าช้าแต่สวยดีกว่า (ถ้าจำไม่ผิด รู้สึก linkedin จะทำ เพราะโค้ดเค้าใหญ่มาก)
  5. ถึก.. ด้วยความที่ scala community มักไม่นิยมอะไรที่เป็น magic ทำให้ไม่ค่อยมีอะไรวิเศษ ๆ ช่วย (เช่น Grails GORM) ส่วนมากต้องเขียนเอง ทำให้ไม่ค่อยเหมาะกับงาน quick and dirty
  6. ไม่ opinionated เลย งาน ๆ นึงมีวิธีทำได้หลายวิธี ถ้าไม่คุยกันดี ๆ ในทีม มีเฮแน่นอน
เอ๊ะ.. ทำไมเขียนแล้วมันเยอะเท่ากับข้อดี ไม่เป็นไร ถือว่าข้อดีเรารวมหลาย ๆ อันเป็นข้อเดียวละกัน 555

สรุป เดือนแรก ผมเสียเวลากับการขุด framework 2 ตัวคือ Lift กับ Slick ไป น่าจะเกินครึ่งของเวลาที่ใช้ แต่รู้สึกว่าคุ้มมาก เพราะส่วนที่ขุดไปเจอก็น่าจะเป็นกรณีส่วนใหญ่ที่งานทั่วไปต้องใช้ และเริ่มรู้สึกว่าทำได้เร็วขึ้นเรื่อย ๆ อีกส่วนที่เสียเวลาคือโค้ดประเภท helper (จากการไม่มี magic) แต่พอขุดโค้ด framework ไปเรื่อย ๆ ก็เจอว่า framework เค้ามีให้อยู่แล้ว แถมครบกว่าที่เราทำซะอีก -..-'

แล้วคุณล่ะ.. ซักหน่อยมะ ?

ปล: งานนี้ยังไม่ test first เพราะอยากทดสอบ test later ดูว่าจะรอดมั้ย