ทุกอย่างในโลกการเขียนโปรแกรมมีทั้งข้อดีและข้อเสีย ในโพสนี้ผมจะขอโฟกัสเฉพาะข้อดีของ 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 ได้ ซึ่งในการเขียนโปรแกรมโดยปกติเราไม่ต้องใช้