8 กระบวนท่า ‘Optional’ ในภาษา Swift
บังเอิญว่าผมอาจจะต้องเขียน mobile application เลยมาจับ Swift ซะหน่อย บังเอิญว่าทั่น Erik Meijer (ผู้สร้าง LINQ และ Rx) แกบอกว่า
ถ้าเฮียจะพูดขนาดนี้ ไหนขอลอง functional Swift ซะหน่อยซิ
ลองไปลองมา ก็ไปสะดุดกับ Optional (หนึ่งในวิชามารของ functional programming) เพราะว่าหน้าตามันประหลาดกว่าชาวบ้าน อย่าง Scala, Java 8 อยู่พอสมควร แล้วมีวิธีจะใช้งานมันแบบประหลาดๆ เยอะมากเลย ไม่แน่ใจว่าดีหรือไม่ดีเหมือนกัน เดี๋ยวเรามาดูกัน
แล้ว Optional มันคืออะไร?
สำหรับใครที่ไม่รู้ว่า Optional มันคืออะไร ถ้าให้แปล Optional
แล้วล่ะก็ คงจะแปลเป็นไทยได้ว่า type นี้มันอาจจะมีหรือไม่มีตัวตนอยู่ก็ได้
เรื่องนี้เกี่ยวข้องกับ null reference (ใน Swift คือnil
) ซึ่งหลายคนอาจจะเคยได้ยินแล้วว่า Tony Hoare คนที่คิดค้น null reference ขึ้นมาเพื่อใช้ในภาษา ALGOL W (และเป็นคนเดียวกับคนสร้าง CSP ซึ่งเป็น formal language เบื้องหลัง actor model และ go routine ด้วย) แกบอกว่า null reference เนี่ย
I call it my billion-dollar mistake
แกอดไม่ได้ที่จะใส่ null reference ลงไปเพราะมัน implement ง่าย แต่ผลที่ตามมาคือ error มากมาย, ช่องโหว่, crash ซึ่งคงจะทำให้สูญเสียกันเป็นพันล้านดอลล่า และผู้คนต้องปวดใจกันมาตลอด 40 ปี…
แล้วชาว functional แก้ปัญหานี้ยังไง?
อะไรปัญหาเยอะ ก็เอามันไป “ห่อ” ซะสิ
พอเราห่อมันแล้ว ชีวิตก็ไม่ต้องมาคอย if value == nil
อยู่ตลอดเวลา ก็ใช้มันแบบที่มันห่อๆ ไว้แล้ว แล้วค่อยไปจัดการกับมันตอนปลายทางแล้วกัน
*จากนี้ไป แนะนำให้เปิด playground มาพิมพ์เล่นตามไปด้วยเพื่อเพิ่มอรรถรส!
หน้าตามันเป็นอย่างงี้ครับ
let optionalNumber: Int? = 1
let optionalString: String? = nil
ตอนแรกที่ผมเห็นผมก็งงๆ หน่อยว่ามันทำอะไรของมัน
สุดท้ายผมพบว่ามันเป็น syntactic sugar เฉยๆ ความจริงมันมีค่าเท่ากับแบบนี้
let optionalNumber: Optional<Int> = Optional(1)
let optionalString: Optional<String> = Optional.none
จะเห็นได้ว่าเจ้า ?
เนี่ยมันไปแอบห่อเจ้าก้อนหลัง =
ให้ด้วย Optional
ซึ่งก็คิดซะว่า ไอที่ประกาศไว้นี่จะเป็น Int
String
หรือไม่มีอะไรอยู่ในกอไผ่ก็เป็นได้ ซึ่งทำให้มันเห็นได้ชัดมากๆ ว่าตรงไหนที่มันมีโอกาสจะไม่มีอะไรเลยได้บ้าง จะได้จัดการกับมันถูก
แถมถ้าลองแบบนี้ดู เราจะ assign nil
ให้กับ type String
let optionalString: String = nil// => error: nil cannot initialize specified type 'String'
let optionalString: String = nil
โดนด่าเลย :(
เราเลยมั่นใจได้ว่า ไม่ว่ายังไง ถ้ามันไม่ได้เขียน type แบบมี ?
มา แม่งไม่ nil
แน่ๆ
นอกจากเราจะแอบห่อตัวแปรได้แล้ว เรายังแอบห่อผลลัพธ์ของทั้ง function ได้อีกด้วย
func half(number: Int) -> Int? {
return number % 2 == 0 ? number / 2 : nil
}half(number: 10) // => Optional(2)
half(number: 1) // => Optional.none
จะเห็นได้ว่า แม้ว่าเรา return
ตัวเลข หรือ nil
เปล่าๆ ไว้ แต่ return type บอกว่าเป็น Int?
มันเลยห่อผลลัพธ์ด้วย Optional
ให้เรียบร้อย
ห่อเอาไว้แล้วจะใช้งานมันได้ยังไง
โดยไอเดียแล้ว Optional
มีไว้เพื่อให้ใช้งานแบบห่อๆ แล้วส่งต่อห่อไปเรื่อยๆ ตาม call stack แล้วค่อยไปแกะที่ปลายทาง ก่อนที่จะมาพูดเรื่องแกะห่อ เรามาพูดถึงเรื่องการแปลงร่างของในห่อโดยไม่ต้องแกะกันก่อนดีกว่า
อันนี้เป็นท่ามาตรฐานของบรรดาของที่ถูกห่อ บางคนคงเคยใช้กับพวก array มาแล้ว ซึ่งก็ไม่ต่างกันเท่าไหร่ นั่นก็คือ map
และ flatMap
นั่นเอง
1) map
สมมติว่าเรามี name: String?
อยู่ เราสามารถทำแบบนี้ได้
name.map { s in s.characters.count }
จะเห็นได้ว่า function ที่เราใส่เข้าไปใน map
รับ argument เป็น String
ธรรมดา ถ้า name: String? = "Boss"
map แล้วจะได้ Optional(4)
ที่มี type เป็น Int?
สิ่งที่มันทำคือ มันแกะห่อ แล้วเอาของจากในห่อ ไปยัดใส่ function ให้ evaluate จากนั้นก็ยัดกลับเข้าไปในห่ออีกรอบ สุดท้ายแล้ว ผลลัพธ์ของ map
จะกลายเป็นอะไรก็ตามที่เป็น return type ของ function ที่ pass ไป แล้วห่อด้วย Optional
อีกทีนั่นเอง
แต่ถ้าเป็น name: String? = nil
แล้ว name
มีจะค่าเท่ากับ Optional.none
พอเรา map แล้วจะได้ Optional.none
เหมือนเดิม เพราะเมื่อเราจะทำอะไรกับความว่างเปล่า เราก็จะได้ความว่างเปล่ากลับมาเสมอ
2) flatMap
สมมติว่าเรามีเราอยากจะแบ่งครึ่ง 12 ซักสามรอบด้วยเจ้า half
ที่เขียนไว้ข้างบน ทำยังไงดี
func half(number: Int) -> Int? {
return number % 2 == 0 ? number / 2 : nil
}
ลอง map ดูแล้วกัน
half(number: 12)
.map(half)
.map(half)// error: cannot convert value of type '(Int) -> Int?' to expected argument type '(Int?) -> _'
ปรากฏว่า โค้ดข้างบนนี้ใช้ไม่ได้ เพราะผลลัพธ์ของ half(number: 12).map(half)
จะกลายเป็น typeInt??
เนื่องจาก ผลลัพธ์จาก type ของ half
คือ Int -> Int?
พอเรา .map(half)
ผลลัพธ์ที่เป็น Int?
อยู่แล้วจะถูกห่อด้วย Optional
อีกที กลายเป็น Int??
ก็เลยเอาไป .map(half)
ต่อไม่ได้ เพราะพอแกะห่อ Int??
จะได้ Int?
แต่ half
ต้องการ Int
เปล่าๆ เป็น argument
ในกรณีนี้ flatMap
ช่วยคุณได้
half(number: 12) // => Optional(6)
.flatMap(half) // => Optional(3)
.flatMap(half) // => Optional.none
สิ่งที่มันทำก็เหมือน map
เลยครับ แค่ตอนที่หลังจาก evaluate function ที่ใส่เข้าไปแล้ว มันจะเอาผลลัพธ์ที่ได้มาเนี่ย ไปสลายห่อทิ้ง (flatten) ทำให้ type หลังจาก flatmap
ผลลัพธ์จบด้วยห่อชั้นเดียวเสมอ
อันนี้แถม โดยปกติแล้ว Optional
จะมี function filter
มาให้ โดย pass function ที่ return Bool
(predicate function) คือถ้าผ่านเงื่อนไขก็โอเค return ตัวเดิม ไม่ผ่านก็จะกลายเป็น Optional.none
แต่ใน Swift ดันไม่มี!
แต่เราก็ทำให้มันมีได้ไม่ยากเพราะมันมี extension
ให้เราใช้ implement เพิ่มเติมจากของที่มีอยู่แล้วได้ (เหมือน Open class ของ Ruby เลย)
extension Optional {
func filter(_ predicate: (Wrapped) throws -> Bool) rethrows -> Wrapped? {
switch self {
case .some(let wrapped):
return try predicate(wrapped) ? wrapped : nil
case .none:
return nil
}
}
}
แล้วเราจะได้ของแบบนี้
let evenNum: Int? = 12
let oddNum: Int? = 3
let noNum: Int? = nilfunc isEven(num: Int) -> Bool {
return num % 2 == 0
}evenNum.filter(isEven) // => Optional(12)
oddNum.filter(isEven) // => Optional.none
noNum.filter(isEven) // => Optional.none
ทั้งนี้ทั้งนั้นmap
flatMap
และ filter
ไม่ได้ไปแก้ค่าของตัวแปร แต่จะ return Optional
ตัวใหม่ออกมาทุกครั้ง เพื่อรักษาความ immutable เอาไว้
3) Optional Chaining
สมมติว่าเราต้องการอ้างอิงถึง attribute หรือ function ของของที่ถูกห่อ เราทำอะไรได้บ้าง ตัวอย่างเช่น
class Person {
let name: String init(name: String) {
self.name = name
}
}class Dog {
let owner: Person? init(owner: Person?) {
self.owner = owner
}
}let ownedDog = Dog(owner: Person(name: "I'Boss"))
let strayDog = Dog(owner: nil)
เราสามารถไปถึงชื่อเจ้าของได้ด้วย map
ownedDog.owner.map { person in person.name }
// => Optional("I'Boss")
แต่ถ้าใน map
ไม่ได้จำเป็นต้องมีอะไรเพิ่มเติมนอกจากเรียก name
แล้ว เราสามารถใช้ Optional Chaining แทนได้
ownedDog.owner?.name // => Optional("I'Boss")
strayDog.owner?.name // => Optional.none
ถึงเวลาต้องใช้แล้ว มาแกะห่อกันเถอะ!
เราไม่สามารถให้มันอยู่ในห่อตลอดไปได้ วันนึงเราก็ต้องแกะมันออกมาใช้อยู่ดี ในภาษา Swift มีท่าแกะห่อให้เลือกใช้เยอะมาก ลองไปดูกัน
4) การแกะห่อแบบไม่ปลอดภัย
ถ้าเรามี
let optionalNumber: Int? = 1
let optionalString: String? = nil
เราสามารถทำแบบนี้ได้ครับ
optionalNumber! // => 1
แต่ๆ ถ้าสมมติว่ามันเป็น nil
ล่ะ
optionalString!// fatal error: unexpectedly found nil while unwrapping an Optional value
แน่นอนว่ามันเป็น runtime error ไม่ใช่ compile time มันเลยน่ากลัวมาก มันอาจจะทำให้เกิดพฤติกรรมที่เราไม่คาดคิดได้ ถ้าจัดการกับมันไม่ดี
จริงๆ แล้วเจ้า !
นี่มี function ที่ทำหน้าที่เหมือนกัน แล้วชื่อมันสื่อสารออกมาได้ดีมาก นั่นคือ .unsafelyUnwrapped
ชื่อก็บอกแล้วว่าไม่ปลอดภัย ถ้าจำเป็นต้องใช้ก็ใช้อย่างระมัดระวัง เลือกใช้ตัวเลือกถัดๆ ไปดีกว่านะครับ
optionalString.unsafelyUnwrapped// fatal error: unexpectedly found nil while unwrapping an Optional value
5) การแกะห่อโดยปริยาย
เราสามารถเขียน !
ต่อท้ายจาก type ได้แบบนี้
let number: Int! = optionalNumber
number // => 1
มันก็ดูคล้ายๆ .unsafelyUnwrapped
เนอะ แต่…
let string: String! = optionalString
string // => nil
มันไม่ error? อะไรฟระ?
จริงๆ ผมหลอกคุณ เพราะค่าที่แท้จริงของ number
และ string
มันเป็นแบบนี้
number // => ImplicitlyUnwrappedOptional(1)
string // => ImplicitlyUnwrappedOptional<String>(nil)
ครับ เจ้า type ที่ตามด้วย !
อย่าง Int
เนี่ย มันคือ ImplicitlyUnwrappedOptional<Int>
นั่นเอง
แล้วมันมีประโยชน์อะไร?
เรียกได้ว่า มันคือ optional type ที่จะแกะห่อ (เหมือนที่เราเรียก !
ต่อท้าย) แทนเราให้เองตอนที่พยายามใช้มัน เช่น
optionalNumber! + 1 // => 2
number + 1 // => 2optionalString!.capitalized // fatal error: unexpectedly found nil while unwrapping an Optional valuestring.capitalized
// fatal error: unexpectedly found nil while unwrapping an Optional value
ซึ่งเจ้า ImplicitlyUnwrappedOptional
เนี่ย มี use cases ที่น่าสนใจหลายอย่างเวลาที่เรา develop application จริงๆ ลองดูเพิ่มเติมได้ที่นี่
6) nil-coalescing operator `??
`
อันนี้ง่ายมาก ถ้าเป็น Java กับ Scala พวก Optional
จะมี function orElse
หรือ getOrElse
อยู่ แปลว่าเราจะเอาค่ามันออกมาจากห่อนั่นแหละ แต่ถ้าไม่มี ก็ขอ default value ให้หน่อย
สำหรับ swift มี syntax น่ารักๆ แบบนี้
let optionalString: String? = nil
optionalString ?? "string is missing" // => "string is missing"
ซึ่งใน official documentation เขาบอกว่า มันคือตัวย่อของ
optionalString != nil ? optionalString! : "string is missing"
ซึ่งวิธีนี้ปลอดภัยในการใช้งาน เพราะยังไงก็ไม่ได้ nil
ออกมาให้ fatal error
แน่ๆ
7) Optional Binding
แบบแรก if let
“ถ้าไม่ nil
ขอเอาค่าไปใช้หน่อย”
if let string = optionalString {
print(string.capitalized)
}
ถ้าค่าใน optionalString
เป็น nil
ก็จะไม่ทำอะไรเลย แต่ถ้า มีค่าอยู่ block นี้ก็จะทำงาน เจ้า string
(แกะห่อแล้ว และเปลี่ยนตัวแรกเป็นอักษรตัวใหญ่) ก็จะถูก print
ออกมาทาง console
เราสามารถเพิ่ม condition ได้ด้วย เช่น
if let string = optionalString, string.characters.count > 3 {
print(string.capitalized)
} else {
print("not long enough!")
}
ขา functional (เหมือนผม)อาจจะหงุดหงิด อาจจะหงุดหงิดนิดหน่อยเพราะ if
ไม่ใช่ expression ทำให้ เราเอาเจ้าก้อนนี้ไป assign หรือ เอาค่าไปใช้ต่อเลยไม่ได้ (เป็นสาเหตุที่ทำให้ผมใช้ตัวอย่างที่มี side-effect อย่าง print
)
แน่นอนว่าเราก็ใช้ guard
แบบนี้ได้เหมือนกัน วิธีคิดแบบ guard
คือเคลียร์อันที่ไม่อยากได้ออกไปก่อน แล้วค่อยทำอันที่สนใจ ซึ่งเอาไว้ใช้กับ loop หรือ function
func guardShortString(optionalString: String?) -> String {
guard let string = optionalString,
string.characters.count > 3 else
{
return "not long enough"
}
return string.capitalized
}guardShortString(optionalString: "boss") // => "Boss"
guardShortString(optionalString: "me") // => "not long enough"
8) switch และ pattern matching
เราสามารถใช้ switch
เพื่อแกะห่อ Optional
ออกมาได้แบบนี้
switch optionalString {
case .some(let string):
"the wrapped string is \(string)"
case .none:
"there's nothing here"
}
บางคนก็อาจจะรู้สึกว่า อ๊ะ นี่มัน pattern matching นี่หว่า
บางคนอาจจะพอใจแล้วว่า เอ้อเราสามารถ switch
แบบนี้ได้นะ แต่สำหรับขา functional อย่างผมจะหงุดหงิดนิดนึง
ถ้าใครเคยเขียน Scala มาจะเจอของแบบนี้
val s = optionalString match {
case Some(string) => s"the wrapped string is $string"
case None => "there's nothing here"
}
จะเห็นได้ว่าผมสามารถเอาไปใส่ตัวแปรได้เลย if
ก็ทำแบบนี้ได้เหมือนกันแต่ Swift ทำไม่ได้ เพราะฉะนั้น โค้ด Swift ข้างบนจะไม่มีประโยชน์อะไรเลย เพราะในแต่ละ case
ไม่ได้เอาไปใช้อะไรเลย
จะทำยังไงให้มีประโยชน์โดยไม่ต้องไปแก้ค่าตัวแปร หรือสร้าง side-effect อื่นๆ ? เพราะอยากจะเขียนให้มัน functional
ถ้าเราเขียนโค้ดโดยยึด Single Responsibility Principle อยู่แล้ว สุดท้ายเราคงหนีไม่ค่อยพ้น function เล็กๆ หน้าตาแบบนี้
func describe(optionalString: String?) -> String {
switch optionalString {
case .some(let string):
return "the wrapped string is \(string)"
case .none:
return "there's nothing here"
}
}
ซึ่งสำหรับผมก็พอใจแล้วนะ แต่ถ้าอยาก treat มันเป็น expression แบบที่ Scala ทำข้างบน ก็อาจจะใช้ closure เข้ามาช่วย
let s: String = {
switch optionalString {
case .some(let string):
return "the wrapped string is \(string)"
case .none:
return "there's nothing here"
}
}()
ซึ่งไอเดียคือเราสร้าง anonymous function ที่ในที่นี้มี type () -> String
ขึ้นมา แล้วเรียกใช้มันทันที ทุกอย่างจะจบในนั้น แล้วคืนค่าที่เราต้องการออกมาได้ ซึ่งเราก็เอาไปใช้กับ if
ข้างบนได้เช่นกัน
บทสรุป
จบไปแล้วสำหรับ 8 กระบวนท่า Optional
สำหรับผมแล้ว มันมีท่าให้ใช้เยอะ เหมือนจะสะดวก แต่แอบรู้สึกว่ามันซับซ้อนเกินความจำเป็นไปนิดนึง เพราะต้องทำความเข้าใจ syntax ที่มันไม่ค่อย consistent เท่าไหร่ ซึ่งบางคนก็อาจจะชอบ บางคนอาจจะไม่ชอบ
ในแง่ความสะดวกในการเขียน Swift ใน functional style ผมว่าก็ใช้ได้เลย ถึงแม้จะมีข้อจำกัดบางอย่าง แต่โดยรวมแล้วก็น่าสนใจ
ส่วนใครสงสัยว่า Optional มันเกี่ยวกับ functional programming ยังไง Optional คือ Maybe monad ใน Haskell ถ้าใครสนใจเรื่องนี้ ลองไปดู Functors, Applicatives, And Monads In Pictures ได้ หรือเวอร์ชั่นแปลเป็น swift ก็มีครับ ลองไปดูกัน
ขอบคุณที่รับชม
functional programming FTW!