7. Handling Exception in Scala - Gaoey/scala-diary GitHub Wiki

The good and bad aspects of exceptions

ทำไม Exception ถึงเป็น side effect เราจะมาพิสูจน์กัน Tips: กฏของ RT คือเมื่อเราแทนค่าใดๆ แล้วจะได้ค่ากลับมาเป็นค่าเดิมเสมอ see more https://github.com/Gaoey/scala-diary/wiki/2.-Pure-Function

Prove :: y is not RT (referentially transparent)

  1. ลอง run program นี้
def failingFn(i: Int): Int = {
    val y: Int = throw new Exception("fail!")
    try {
       val x = 42 + 5
       x + y
    }
    catch { case e: Exception => 43 }
}
scala> failingFn(12)
java.lang.Exception: fail! 
    at .failingFn(<console>:8)
    ...

result: (1r) failingFn(12) throw exception ออกมา

  1. ลองแทน y ที่ x+y
def failingFn(i: Int): Int = {
    try {
       val x = 42 + 5
       x + (throw new Exception("fail!"))
    }
    catch { case e: Exception => 43 }
}
scala> failingFn2(12)
res1: Int = 43

result: (2r) failingFn(12) return 43 ออกมา

จาก RT คือเมื่อเราแทนค่าใดๆ แล้วจะได้ค่ากลับมาเป็นค่าเดิมเสมอ แต่ (1r) ไม่เท่ากับ (2r)

ดังนั้น: y is not RT is true


เราอาจจะบอกได้ว่า

RT Expression จะไม่ขึ้นกับบริบทของโปรแกรม และเป็นเหตุเป็นผลในระดับ local ยกตัวอย่างคือ 20 + 3 ก็จะเท่ากับ 23 ตลอดไป

non RT Expression จะมีความเกี่ยวเนื่องกับบริบทของโปรแกรม(context-dependent) และเป็นเหตุเป็นผลในระดับ global ยกตัวอย่าง throw new Exception("fail") นั้น context-dependent เพราะ ผลลัพธ์ที่จะ return ออกมา จะตามคำสั่งใน try block ซึ่งจะเป็นอะไร เราไม่สามารถคาดเดาได้

ดังนั้น Exception จึงมีปัญหาอยู่ 2 อย่าง

  1. Exception ไม่ใช่ RT จึงไม่มีคุณสมบัติของ Pure function สำหรับ Exception นั้นควรจะจัดการแค่ข้อผิดพลาดเท่านั้น ไม่ควรควบคุมการทำงานแบบต่างๆของโปรแกรมได้ (ใช้ try ครอบเพื่อควบคุมอะไรบางอย่าง)

  2. Exception are not Type-safe จากตัวอย่างข้างบน function failingFn() เป็น Int => Int ซึ่งเราไม่สามารถรู้ได้เลยว่า function นี้จะ return exception ตอนไหน ถ้าเราไม่ได้ทำ case exception ไว้ ปัญหาที่เกิดจะเกิดตอน runtime ซึ่งไม่ควร


ใน scala ใช้วิธีการจัดการ Error ที่เรียกว่า completely type-safe นั่นคือการบังคับให้ data สามารถ defined value หรือ กำหนดค่าให้ data ได้

Possible alternatives to exceptions

def mean(xs: Seq[Double]): Double =
    if (xs.isEmpty)
        throw new ArithmeticException("mean of empty list!")
    else xs.sum / xs.length

function mean เป็นตัวอย่างที่เราเรียกว่า partial function(ฟังก์ชั่นไม่สมบูรณ์) คือ function ที่ไม่ได้กำหนด input ทั้งหมดที่เป็นไปได้ (มีบาง input ที่ทำให้โปรแกรมไม่ถูกต้อง) และมีบางทีที่ input เข้ามา, แทนที่ function นี้จะจบด้วยตัวมันเอง(terminate) แต่กลับ ไม่จบ(infinite cycle) หรืออาจจะ throw exception หรือ crash ไปเลย

ยกตัวอย่าง mean สมมุติว่า xs.sum = 0.0 และ xs.length = 0.0 ผลลัพธ์ของการที่เราไม่ได้กำหนด input นั้นมีอะไรบ้างที่ทำให้โปรแกรมนี้ถูกต้อง อาจจะทำให้เกิดเคส 0.0/0.0 ก็เป็นได้ จะสังเกตุได้ว่าผลลัพธ์นี้จะเป็น input empty หรือ Double.Nan หรือ ชนิดข้อมูลอื่นๆอีกมากมาย ซึ่งจะลำบากเราในการ Dev ที่ต้องมาดัก Exception ที่ละอย่างทีละเคส ทีละ input ท่ี่ไม่ถูกต้อง และทำให้เราสับสนว่าจะ Return type อะไรกลับไปดีสำหรับ function นี้ถ้า handle ทุกเคสแล้ว

เราจะมาทำให้ function mean เป็น total function(ฟังก์ชั่นสมบูรณ์) คือ function ที่กำหนด input ทั้งหมดเอาไว้ และสามารถจบได้ด้วยตัวมันเอง และคืนค่าบางอย่างกลับมาด้วย

ด้วยการใช้ Option

sealed trait Option[+A]
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
def mean(xs: Seq[Double]): Option[Double] =
if (xs.isEmpty) None
else Some(xs.sum / xs.length)

function mean กลายเป็น total function แล้ว ทีนี้ไม่ว่าเกิดอะไรขึ้นจาก input type แต่ละแบบที่เราคาดเดาไม่ได้ แต่เราสามาถกำหนดให้ function return เป็น Some หรือ None เท่านั้น นี่จึงเป็นวิธีการที่เรียกว่า Completely type safe คือบังคับให้ return เป็น type ที่เราต้องการและควบคุมได้

ข้อดีของการใช้ Option แทน exception-handling แบบปกติ

  • เราสามารถแยกแยะองค์ประกอบแบบต่างๆของการจัดการ error ได้ ด้วย HOF
  • สามารถเขียน exception-handling ได้อย่างอิสระ (เพราะจากข้อดีข้อหนึ่ง) โดยไม่ส่งผลกระทบ error handling ของตัวอื่น
⚠️ **GitHub.com Fallback** ⚠️