Functional Programming บน Swift

เดือนที่แล้วได้มีโอกาสได้พูดเรื่อง Functional Programming ด้วยภาษา Swift และอยากจะลองเขียนบทความ ไว้เตือนความจำ วันนี้เลยถือโอกาสบันทึกความรู้ที่ได้จากการศึกษาจากหลายๆบทความ ให้ท่านที่สนใจเกี่ยวกับเรื่อง Functional Programming ได้เป็นอีกช่องทางในการศึกษา

Functional Programming คืออะไร

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

แล้วใช้ Swift เขียนแบบ Functional Programming ได้ไหม ?

Swift ไม่ใช่ Pure Functional Language แต่ Functional Programming ไม่ต้องการ Purl Functional Language แล้วต้องการอะไร ?

  • Pure Functions จริงๆ มันก็คือฟังก์ชั่นธรรมดาที่รับตัวแปรเข้ามาประมวลผลแล้วส่งค่ากลับไป เพียงแต่เราจะไม่ให้ตัวแปรภายนอกเข้ามามีผลต่อการทำงานของฟังก์ชั่น พูดง่ายๆคือเป็นอิสระจากสภาวะแวดล้อมภายนอก ซึ่งถ้าเราส่งค่าเดิมกลับมาก็จะได้ผลลัพแบบเดิมอย่างแน่นอน ซึ่งด้วยหลักการนี้ทำให้เราสามารถทดสอบการทำงานของฟังก์ชั่นได้ง่ายและ ด้วยหลักการนี้มันทำให้ code เราสามารถนำกลับมาใช้ได้ใหม่ คือมันไม่ผูกติดกับส่วนอื่นๆ พูดง่ายๆคือมัน แทบจะเป็นหน่วยการทำงานที่เล็กที่สุดแล้ว และฟังก์ชั่นที่ดีก็ควรทำงานแค่อย่างเดียว
  • First-Class Functions คือการที่เราสามารถทำให้ฟังก์ชั่นอยู่ในรูปตัวแปรได้ ซึ่งเมื่อเราสามารถเก็บมันไว้เหมือนการจับยัดใส่กล่องแล้ว เราก็สามารถส่งมันไปให้คนอื่นๆ หรือ เก็บมันไว้ใช้ทีหลังก็ยังได้
  • High-Order Function การที่ฟังก์ชั่นสามารถเป็น Parameter ของฟังก์ชั่นหรือเป็นค่าที่ return จากฟังก์ชั่นได้
  • Closures คือการที่เราสามารถนำตัวแปรภายนอกฟังก์ชั่นเข้ามาใช้ในฟังก์ชั่นได้ และ เมื่อตัวแปรภายนอกมีการเปลี่ยนแปลงก็จะไม่ส่งผลต่อตัวแปรที่เรานำเข้ามา
  • Immutable Stat คือการที่เราจะไม่เปลี่ยนแปลงค่าของตัวแปร ฟังดูแล้วอาจจะสงสัยว่ามันดียังไง แต่นี้เป็นส่วนนึงของข้อดีที่ทำให้เราสามารถทำงานแบบการประมวลผลแบบคู่ขนานได้อย่างดี

Function Composition

ในการทำงานบางอย่างเราต้องทำงานร่วมกันกับหลายฟังก์ชั่น ซึ่งเราต้องนำผลลัพธ์ของฟังก์ชั่นนึงไปใส่อีกฟังก์ชั่นนึง

ตัวอย่างเช่น เราต้องการฟิลเตอร์รูปภาพด้วย 4 ฟิลเตอร์

func filter1(image: CIImage) -> CIImage
func filter2(image: CIImage) -> CIImage
func filter3(image: CIImage) -> CIImage
func filter4(image: CIImage) -> CIImage

เราต้องส่งรูปภาพเข้าไปที่ละฟิลเตอร์แล้วเอาผลลัพธ์ไปใส่ฟิลเตอร์ถัดไป

let output1 = filter(image: image)
let output2 = filter2(image: output1)
let output3 = filter3(image: output2)
let output4 = filter4(image: output3)

ซึ่งดูแล้วยุ่งยาก เรามาลองใช้แนวคิดของ Function Composition กันดู

ขั้นแรกเราจะกำหนดฟังก์ชั่นที่รับค่าเป็น CIImage และ return ค่าเป็น CIImage ว่า Filter

typealias Filter = (CIImage) -> CIImage

จากนั้นสร้างฟังก์ชั่นสำหรับรวมฟิลเตอร์เข้าด้วยกัน

func composeFilter(filter1: Filter, filter2: Filter) -> Filter {
 return { image in
  return filter1(filter2(image))
 }
}

และ เรามาลองใช้ฟังก์ชั่น composeFilter กันดู

let filter = composeFilter(filter1: composeFilter(filter1: composeFilter(filter1: filter1, filter2: filter2), filter2: filter3), filter2: filter4)
let output = filter(image: image)

จะเห็นได้ว่าดูดีขึ้น แต่ก็ยังดูซับซ้อนอยู่นิดหน่อย เราจึงต้องมีตัวช่วยที่จะทำให้โค้ดเราดูอ่านง่าย นั้นคือ Infix operator

สร้างเครื่องหมาย >>> ให้เป็น Infix operator

precedencegroup RightGroup {
associativity: right
}
infix operator >>> : RightGroup

นำไปใส่ให้กับฟังก์ชั่น composeFilter

func >>>(filter1: Filter, filter2: Filter) -> Filter {
return composeFilter(filter1: filter1, filter2: filter2)
}

จากสิ่งที่เราทำมาทั้งหมดสามารถปรับวิธีการเรียกใช้งานฟิลเตอร์ได้ดังนี้

let filter = fiter1 >>> filter2 >>> filter3 >> filter4
let output = filter(image: image)

จะเห็นได้ว่าโค้ดดูสวยงาม และอ่านง่าย จะเพิ่ม หรือจะเอาออกก็ง่าย

Curry

คือการทำให้ฟังก์ชั่นที่มี Parameter หลายตัวให้เหลือแค่ตัวเดียวและ ส่งผลลัพธ์เป็นฟังก์ชั่นที่รับ Parameter ที่เหลือ

ตัวอย่างเช่น เรามีฟังก์ชั่นในการต่อ String ซึ่งฟังก์ชั่นดังกล่าวรับค่าเป็น String และ Array ของ String แล้ว return เป็น String

func joinBy(separator: String, array: [String]) -> String

สมมุติเราต้องการต่อ [“A”,”B”,”C”,”D”], [“ก”,”ข”,”ค”,”ง”], [“1”,”2”,”3”,”4”] ด้วย “ : ” เราจะเรียกใช้ฟังก์ชั่นได้ประมานนี้

let result1 = joinBy(separator: “:”, array: [“A”,”B”,”C”,”D”])
let result2 = joinBy(separator: “:”, array: [“ก”,”ข”,”ค”,”ง”])
let result3 = joinBy(separator: “:”, array: [“1”,”2”,”3”,”4”])

คราวนี้เราจะปรับฟังก์ชั่นให้เหลือ Paramter แค่ตัวเดียวตามแนวคิดของ Curry

func joinBy(separator: String) -> ([String]) -> String {
return { strings: [String] -> String in
...
}
}

ผลที่ได้ก็คือ

let joinByColon = joinBy(separator: ":")
let result1 = joinByColon(array: [“A”,”B”,”C”,”D”])
let result2 = joinByColon(array: [“ก”,”ข”,”ค”,”ง”])
let result3 = joinByColon(array: [“1”,”2”,”3”,”4”])

โค้ดของเราในตอนนี้ดูอ่านง่าย สวยงาม และแก้ไขง่ายมากขึ้น

สุดท้ายเราจะเห็นประโยชน์ของ Functional Programming นั้นก็คือ โค้ดของเราจะอ่าน แก้ไข ดูแล ทดสอบ ได้ง่ายและ สามารถนำกลับไปใช้ใหม่ได้ นั้นเอง