Immutable data ลดปัญหาในการเขียนโปรแกรมของเราได้อย่างไร

ทุกอย่างในโลกการเขียนโปรแกรมมีทั้งข้อดีและข้อเสีย ในโพสนี้ผมจะขอโฟกัสเฉพาะข้อดีของ immutable data นะครับ

มาเริ่มจากการดูโค้ดที่ไม่ได้ใช้ immutable data กันก่อน

def method1(obj1) # ตำแหน่งที่ 1
  # ...
  method2()
  obj1 # ตำแหน่งที่ 2
  # ...
end

โค้ดนี้มันมีความเป็นไปได้ที่ obj1 ณ ตำแหน่งที่ 2 จะถูกเปลี่ยนแปลงไปจากตอนที่เมท็อดรับมันมา ณ​ ตำแหน่งที่ 1 เรื่องนี้มันเป็นสาเหตุหนึ่งที่ทำให้เราต้องพยายามจำโค้ดของเราทั้งหมดว่ามีอะไรเกิดขึ้น ณ จุดใดบ้าง เราต้องรู้ว่าโค้ดก่อนหน้า อย่างเช่น method2 หรือก่อนหน้านั้น ไม่ได้ไปทำอะไรกับ obj1 ของเราในทางอ้อม ถ้าเราเขียนโค้ดดีก็อาจจะไม่มีการเปลี่ยนแปลงให้น่าปวดหัวแบบนั้นเกิดขึ้น แต่มันไม่มีอะไรรับประกันและป้องกันความผิดพลาดนี้ การเขียนโค้ดผิดพลาดมันเป็นเรื่องธรรมดาที่เกิดขึ้นได้อยู่แล้ว

เมื่อเราต้องการ debug โค้ดนี้ เราก็ต้องพยายามใส่ print ให้ใกล้กับตำแหน่งที่เราต้องการเช็คค่ามากที่สุด เพื่อที่เราจะได้มั่นใจได้ว่าเราได้เห็นค่าที่ถูกต้องจริง ๆ แต่มันก็ยังอาจจะไม่จริงเสมอไปเมื่อโปรแกรมของเรามี concurrency เช่น multi thread เข้ามาเกี่ยวข้อง ค่าที่ print ออกมาก็อาจจะยังไม่ใช่ค่าจริง ๆ ณ เวลานั้น ถึงเราจะใช้ debugger ช่วย มันก็มีปัญหาลักษณะเดียวกันอยู่ดี แถม debugger อาจจะไปรบกวนการทำงานของโปรแกรม ทำให้เราไม่เห็นค่าที่ถูกต้องได้อีกด้วย

Immutable data เข้ามาช่วยลดภาระของสมองของเราตรงนี้

(defn func1 [data1] ; ตำแหน่งที่ 3
  ; ...
  (func2)
  data1 ; ตำแหน่งที่ 4
  (let [data1 "something else"] ; ตำแหน่งที่ 5
    data1 ; ตำแหน่งที่ 6
    )
  ; ...
  )

เมื่อเรามาเขียนภาษาที่ data เป็น immutable ปัญหาที่ผมกล่าวมาก็จะหมดไป ค่าของ data1 ในตำแหน่งที่ 4 จะเท่ากับตำแหน่งที่ 3 เสมอ* ถึงแม้ว่าเราอาจจะสามารถเปลี่ยนให้ data1 ไปชี้ที่ค่าอื่นได้ (shadow) ดังเช่นในตำแหน่งที่ 5 ที่ทำให้ค่าของ data1 ในตำแหน่งที่ 6 ไม่เหมือนตำแหน่งที่ 4 แต่มันเป็นเพียงแค่การเปลี่ยน reference โดยที่ค่าของ data1 จากตำแหน่งที่ 3 และ 4 จะไม่ได้ถูกแตะต้อง เพียงแค่เราไม่สามารถเข้าถึงมันได้ในฟังก์ชันนี้แล้วเท่านั้น สังเกตว่าเราสามารถเห็นการเปลี่ยนแปลงได้ชัดเจนเสมอ เพราะมันไม่ได้ถูกซ่อนเอาไว้ เมื่อเป็นเช่นนี้ เราจะได้รับการการันตีว่าไม่ว่าเราจะตรวจค่าของ data1 ณ จุดใด มันจะเป็นค่าเดียวกับที่ถูกส่งมาให้ หรือเป็นค่าที่เราเป็นคนเปลี่ยนเองในฟังก์ชันนี้เสมอ

ผมมีตัวอย่างคลาสิคอีกหนึ่งอัน

def method3
  obj2 = SomeClass.new # ตำแหน่งที่ 7
  method4(obj2)
  obj2 # ตำแหน่งที่ 8
end

เช่นเดียวกันกับตัวอย่างแรก ในตัวอย่างนี้ไม่มีอะไรที่การันตีเราได้ว่า obj2 ในตำแหน่งที่ 8 มีค่าเหมือนกับ obj2 ในตำแหน่งที่ 7 ถ้าเราเขียนโค้ดไม่ดีก็มีโอกาสเกิดขึ้นได้เสมอ

(defn func3 []
  (let [data2 {:key "value"}] ; ตำแหน่งที่ 9
    (func4 data2)
    data2)) ; ตำแหน่งที่ 10

ในขณะที่โค้ดในตัวอย่างสุดท้ายนี้ data2 ในตำแหน่งที่ 9 และ 10 จะมีค่าเหมือนกันเสมอเพราะ data2 มันชี้ไปที่ immutable data ({:key "value"})

จากตั้งอย่างทั้งหมดจะพบว่าการที่เราเขียนโปรแกรมโดยใช้ immutable data ทำให้เราโฟกัสกับฟังก์ชันหรือเมท็อดที่เรากำลังอ่านและเขียนอยู่ได้ สามารถโยนความกังวลว่า object หรือ data ที่อยู่ตรงนี้จะถูกเปลี่ยนแปลงโดยไม่รู้ตัวหรือเปล่าทิ้งไป ลดภาระของสมองของเราเอาไปไว้คิดและจำส่ิงอื่น และลดความผิดพลาดในโปรแกรมของเราไปได้เยอะเลย

*ยกเว้น data1 จะเป็น Java object ที่ mutate ได้ ซึ่งในการเขียนโปรแกรมโดยปกติเราไม่ต้องใช้

Leave a comment