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 ดูว่าจะรอดมั้ย