หกเดือนที่ผ่านมา ผมกำลังพัฒนา NES Emulator ด้วย Rust คุณคาดเดาได้ว่าผมเรียนรู้ Rust มาก และเข้าใจ NES Internals เพิ่มขึ้นอีก แต่ประสบการณ์นี้ยังเปลี่ยนมุมมองของผมต่อ Ruby อีกด้วย
โดยเฉพาะอย่างยิ่ง ทำให้ผมเป็น Paranoid (ระแวง) มากขึ้นเกี่ยวกับเมธอดที่คืนค่า nil
ถ้าคุณไม่ยืนหยัดเพื่อบางสิ่ง คุณจะล้มเหลวเพราะอะไรก็ได้
nil ใน Ruby หมายถึงอะไร? เกือบจะเป็นอะไรก็ได้ เมื่อเมธอดคืนค่า nil อาจหมายถึง:
- เมธอดไม่มีค่าคืน
- ปกติมีค่าคืน แต่ครั้งนี้ไม่มี
- คืนค่าจากฐานข้อมูล ซึ่งเป็น NULL
- เกิดเหตุการณ์ที่ไม่คาดคิด
สิ่งนี้ทำให้โค้ดยากต่อการอ่าน และเป็นสาเหตุหลักของ ข้อยกเว้นที่พบบ่อยที่สุด ใน Ruby: NoMethodError ในฐานะเจ้าของส่วนหนึ่งของบริการตรวจสอบข้อยกเว้น (Exception Monitoring Service) NoMethodError กำลังเลี้ยงลูกของผมให้จบการศึกษา
ดูโค้ดต่อไปนี้ มันคืนค่า nil ส่วนใหญ่ เพราะ if statements ประเมินผลเป็น nil เมื่อเงื่อนไขไม่ตรงและไม่มี else
def color_name(rgb)
if rgb == 0x000000
"black"
end
end
color_name("#FFFFFF").titleize
# => NoMethodError: undefined method `titleize' for nil:NilClass
ถ้าคุณเป็นนักพัฒนา Ruby มืออาชีพ คุณจะรู้ว่เข้าไปลึกกว่านี้มาก บางครั้งความหมายของ nil ที่แตกต่างกันเหล่านี้ทับซ้อนกัน بطرقแปลกๆ ทำให้ไม่ thểรู้ได้ว่า — ตัวอย่างเช่น — ค่าในฐานข้อมูลเป็น NULL หรือว่าไม่มีค่าในฐานข้อมูลเลย
วิธีที่ดีกว่า
ใน Rust ไม่มี nil เลย แทนที่จะเป็นเช่นนั้น เมื่อเราต้องการระบุว่าฟังก์ชันบางครั้งคืนค่า และบางครั้งคืน "ไม่มีค่า" เราใช้ Option
Option เป็นประเภทที่อาจมีค่าเฉพาะเจาะจง หรือไม่มีค่าเลย นี่คือรูปแบบในโค้ด:
Option::Some(42); // ห่อหุ้มตัวเลข 42 ใน Option
Option::None; // บ่งชี้ "ไม่มีผลลัพธ์"
สิ่งนี้ดูดีกว่าการใช้ nil แบบ Ad-hoc แล้ว แต่มันดีขึ้นอีก Rust Compiler บังคับให้คุณพิจารณากรณี None คุณไม่สามารถมองข้ามมันได้โดยบังเอิญ
match my_option {
Some(x) => do_something_with_x(x),
// ถ้าคุณลบ `None` match ด้านล่างออก โค้ดนี้จะไม่ Compile ผ่าน
None => do_the_default_thing()
}
ดังนั้นเราสามารถเขียนตัวอย่างการตั้งชื่อสีใน Rust ได้แบบนี้:
fn color_name(rgb: u32) -> Option {
if rgb == 0x000000 {
Some("black".to_owned())
} else {
None
}
}
ตอนนี้เราถูกบังคับให้จัดการทั้งกรณี Some และ None:
let name = color_name(0xFFFFFF);
let name = match color_name(0xFFFFFF) {
Some(value) => value,
None => "unknown".to_owned(),
}
แน่ใจว่านี้ดูค่อนข้าง Verbose และดูแปลก แต่ทำให้เป็นไปไม่ได้ที่จะมองข้ามกรณีที่ฟังก์ชันไม่คืนค่าที่มีประโยชน์ นั่นหมายถึงโค้ดที่เข้าใจง่ายและดูแลรักษาง่ายขึ้น
การนำ Option มาใช้ใน Ruby
สิ่งที่ Ruby ขาดในความเข้มงวด ชดเชยด้วยความยืดหยุ่น ผมคิดว่าน่าสนใจที่จะลองนำ Option มา Implement ใน Ruby
เราไม่สามารถสร้าง Compile-time errors ใน Ruby ได้ เนื่องจากเป็น Interpreted Language แต่เราสามารถทำให้โค้ดที่ผิด เสมอ โยน Exception แทนที่จะโยน Exception เฉพาะตอนเจอ Edge Case
ก่อนอื่น สร้างสองคลาส Some เก็บค่า Read-only None ว่างเปล่า พวกมันง่ายเหมือนที่คิด
class Some
attr_reader :value
def initialize(value)
@value = value
end
end
class None
end
ต่อไป สร้างคลาส Option ที่เก็บ Some หรือ None และอนุญาตให้เข้าถึงได้ก็ต่อเมื่อเราให้ Handler สำหรับทั้งสอง
class Option
def initialize(value)
@value = value
end
def self.some(value)
self.new(Some.new(value))
end
def self.none()
self.new(None.new)
end
def match(some_lambda, none_lambda)
if @value.is_a?(Some)
some_lambda.call(@value.value)
elsif @value.is_a?(None)
none_lambda.call()
else
raise "Option value must be either Some or None"
end
end
end
สุดท้าย เราสามารถเขียนตัวอย่างสีใหม่โดยใช้คลาส Option:
def color_name(rgb)
if rgb == 0x000000
Option.some("black")
else
Option.none()
end
end
puts color_name(0x000000).match(
-> value { value },
-> { "no match" }
) # Prints "black"
สรุป
ผมยังไม่ได้ลองเทคนิคนี้ในโปรเจกต์จริง ผมคิดว่ามันช่วยป้องกัน NoMethodErrors หลายอย่างที่มักเลอะเข้า Production ได้ แม้จะดูค่อนข้าง Cumbersome และไม่ค่อย Ruby-ish แต่ผมเชื่อว่า עםการปรับปรุงบ้าง ไวยากรณ์ที่น่ารักกว่าจะ浮现ออกมา