Higher-Order Functions in Swift แบบเข้าใจง่ายๆ

Higher Order Functions In Swift Background

Hello Swifty, ฉลองบทความแรกของ swift 🎉 มาเขียนภาษาที่ผมถนัดสะที 😂 ไหนๆเริ่มต้น ขอเริ่มด้วย HOF (higher order function) เลยละกัน

ในภาษา swift เราใช้ function กันเป็นว่าเล่น วันนี้เราเลยลองมาใช้ function ที่มัน advance ขึ้นมาอีกนิ้ดนึง

หลายคนอาจจะร้องอ๋อ เพราะเคยใช้มาแล้วแต่อาจจะไม่รู้ว่ามันคือ HOF ส่วนคนที่ไม่เคยใช้ ก็ไม่ต้องใช้หรอกคับ ผ่าม! ได้ใช้บ่อยแน่นอนไม่ต้องห่วงง 55

What is Higher-Order Function

ได้ยินคำว่า higher แปลว่า function นี้มันอยู่สูงจริงมั้ย ผมตอบเลยว่าจริง !

เพราะว่าผมวางโค้ดให้อยู่บรรทัดแรกๆ .. 🐸


เอาใหม่ ได้ยินแล้วอาจจะ งงๆ แต่ไม่ต้อง งง จริงๆแล้วมันแค่นี้เอง มันคือ function ที่

  1. รับ parameter เป็น function หรือ closure
  2. return ออกโดยใช้ function ที่เรารับเข้ามา

พูดง่ายๆคือมันเป็น function ที่ทำหน้าที่ส่งต่อให้อีก function ทำต่ออีกที แค่นี้เอ๊งง

(T คือ Generic แปลว่ามันจะเป็น type อะไรก็ได้)

ข้อดี

  • flexible (ลองดูตัว custom function ข้างล่าง)
  • clean เพราะมันเขียนสั้นลงมากก
  • ประหยัดเวลา swift มี buit-in functions ให้ใช้เพียบ

ตัว Higher order function มันเป็นหัวใจหลักของ functional programming เลย (คือแนวคิด การเขียน program โดยใช้ function เป็นหลัก)

ซึ่ง swift เนี่ยภาษามันเป็น multi paradigm แปลว่าใช้ได้ทั้ง OOP, functional อย่างเฟี้ยว เวลาทำงานจริงเราใช้ผสมกันเป็นโกโก้เลย 55

Swift Built-In Higher-Order Functions

ปูมาขนาดนี้เพราะภาษา swift มีมาใช้เพียบเลย บทความนี้บีมจะมาแนะนำ 10 อันที่ใช้กันบ่อยๆ และ อันที่น่าใช้ (ก่อนเรียกใช้อย่าลืมม import foundation นะค้าบบ 😂)

  1. map
  2. compactMap
  3. flatMap
  4. reduce
  5. sorted
  6. filter
  7. contains
  8. allSatisfy
  9. first(where)
  10. dropWhile

Before we start ต้องเข้าใจ $0 ก่อน

ไม่ต้องห่วงเห็นครั้งแรก งง ทุกคน $0 คือตัวแทน parameter ตัวแรกใน closure, function ซึ่งของใน built-in ส่วนใหญ่ มันก็คือตัวแทน ของค่าของ index ที่มันโดนวนอยู่ – ถ้ามีหลาย parameter ก็จะใช้ $1, $2 ไปเรื่อยๆ

ตัวอย่าง

let result = [1, 2, 3, 4].map { number in
    return number * 2
}

อย่างแบบนี้ $0 ก็คือ number นั่นแหละ เขียนได้เหมือนกันทั้งสองแบบ ถ้าเข้าใจแล้วก็ลุยกันเลย

let result = [1, 2, 3, 4].map { $0 * 2 }

Map

  • เป็นการวนใน collection ของเรา เพื่อให้เราเอาไปทำอะไรสักอย่างต่อ
  • เหมาะเอาไปใช้แปลงข้อมูล จากของเดิม
  • จะได้ result เป็น array

ตัวอย่าง:

  • ยกกำลัง วนแล้วคูณเลขตัวเอง (square root)
let squareRoot: [Int] = [1, 2, 3, 4]
let toPrintSquareRoot: [Int] = squareRoot.map { $0 * $0 }

// output: [1, 4, 9, 16]

CompactMap

  • เหมือน map ทุกอย่างแต่ไม่เอา result ที่ได้ nil ❌
  • เหมาะเอาไปใช้แปลงข้อมูล ที่ไม่มี nil
  • จะได้ result เป็น array

ตัวอย่าง:

  • วนแล้วเปลี่ยน string ตัวเลขทุกตัวเป็น type int
  • “impostor” เปลี่ยนเป็น type int ไม่ได้เลยได้ nil
let numberWithNil: [Int?] = [
    "1", 
    "2",
    "impostor", 
    "3"
]
let toPrintCompactMap: [String] = numberWithNil.compactMap { Int($0) }

// output: [1, 2, 3]

FlatMap

  • เอาไว้ลบ nested array [[1, 2, 3]] -> [1, 2, 3]
  • จะได้ result เป็น array
  • ลบ array ได้ทีละชั้น !
  • ลบได้มากสุด ยังไงก็จะเหลือ array 1 ชั้นเสมอ !!

ตัวอย่าง 1 (one layer nested array):

  • ใช้ flatMap ลบ array ชั้นนอกสุดออก
let twoLayerNestedArray: [[[String]]] = [
    ["1"], 
    ["2"], 
    ["3"]
]
let simpleArray: [String] = twoLayerNestedArray.flatMap { $0 }

// output: ["1", "2", "3"]

ตัวอย่าง 2 (two layer nested array):

  • flatMap มันลบได้ทีละชั้น
  • ถ้าจะลบ 3 ชั้นก็ต้องใช้ flatMap อีกครั้ง 😎
let threeLayerNestedArray: [[[String]]] = [[["1"], ["2"], ["3"]]]
let simpleArray: [String] = threeLayerNestedArray.flatMap { $0 }.flatMap { $0 }

// output: ["1", "2", "3"]

Reduce

  • จำว่า reduce คือการ ทำให้ array ลด(รวม) เหลือแค่ค่าเดียว
  • parameter ของ reduce(initialValue) ข้างในวงเล็บอะ คือค่าตั้งต้น
  • type ของ result คือ type เรา set ไว้อยู่แล้วตอนแรก เช่น reduce(0)เราจะได้ค่า int

ตัวอย่าง:

  • process ของมันเริ่มจาก เอา initialValue ที่ตั้งไว้คือ 0 มาบวก 1 (array index แรก) ก็จะได้ 1
  • เสร็จแล้ว ตัวต่อไปคือ 2 มาบวกกับ 1 ตัวก่อน จะได้ 3
  • ทำแบบนี้ต่อไปเรื่อยๆจนครบใน array
  • ใน reduce $0 คือค่าที่ถูกเก็บไว้ $1 คือค่าของ index ปัจจุบัน
let numbers: [Int] = [1, 2, 3, 4]
let sumNumber: Int = numbers.reduce(0) { $0 + $1 }

// output: 10

Sorted

  • เอาไว้เรียงค่าตามตามที่เราตั้ง
  • จะได้ result เป็น array

ตัวอย่าง:

  • เรียงจากน้อยไปมากเพราะใช้ <
  • จะเรียงแบบอื่นก็เปลี่ยนตอนดัก
let unorderedNumbers: [Int] = [44, 55, 22, 33]
let orderNumbers: [Int] = unorderedNumbers.sorted { $0 < $1 }

// output: [22, 33, 44, 55]

Filter

  • เอาไว้กรอง
  • อันนี้ใช้บ่อยมากก ส่วนใหญ่เราต้องคัดของใน array ตลอดอยู่แล้ว
  • จะได้ result เป็น array

ตัวอย่าง:

  • กรองเอาเลขคู่ (ตัวที่หาร 2 ลงตัว)
let numbers: [Int] = [1, 2, 3, 4, 5, 6]
let evenNumbers: [Int] = numbers.filter { $0 % 2 == 0 }

// output: [2, 4, 6]

Contains

  • เช็คใน collection ว่ามี condition ที่ match มั้ย ถ้ามีจะได้ true ทันที
  • เหมาะเอาไว้เช็คว่าข้างในมีของที่เราหามั้ย
  • จะได้ result เป็น boolean

ตัวอย่าง:

  • เช็คว่าคะแนนสอบของเด็กมีใครได้ F มั้ย 🥲
let studentsGrades: [String] = [
    "A",
    "B",
    "C",
    "D",
    "E",
    "F"
]

let isContainFailStudent: Bool = studentsGrade.contains { $0 == "F" }

// output: true

AllSatisfy

  • จะได้ result เป็น boolean
  • เช็คว่าใน collection match กับ condition ของเราทั้งหมดมั้ย
  • ถ้าเช็คแล้วเป็น true หมด result ก็จะได้ true

ตัวอย่าง:

  • เช็คว่าใน array เป็นเลขคู่ทั้งหมดมั้ย
let numbers: [Int] = [2, 4, 6]
let isAllEven: Bool = evenNumbers.allSatisfy { $0 % 2 == 0 }

// output: true

First (where)

  • เอาไว้หาตัวที่ตรงกับเงื่อนไขเราตัวแรก
  • ได้ result type เดียวกับ type ข้างใน array

ตัวอย่าง:

  • หาผลไม้ชื่อแรกใน array ที่ขึ้นต้นด้วย B
  • ในนี้มีทั้ง Banana กับ Blueberry ที่ขึ้นต้นด้วย B แต่เราจะได้ Banana 🍌 เพราะลำดับมาก่อน
let fruits: [String] = [
    "Apple 🍎", 
    "Banana 🍌", 
    "Cherry🍒",
    "Blueberry 🫐", 
    "Mango 🥭"
]

let letterBfruit: String = fruits.first(
    where: { $0.hasPrefix("B") }
) ?? "No fruit found! 🍏"

// output: Banana 🍌

DropWhile

  • จำว่า drop คือลบออก
  • เอาไว้ลบทุกตัว ก่อนที่จะ match กับ condition
  • หลังจากมันเจอ condition ที่ได้ true แล้วมันถึงหยุด

ตัวอย่าง:

  • รวมคะแนนของนักเรียน ที่ได้มากกว่า 50 🤓
  • ใช้ dropWhile อันไหนน้อยกว่า 50 ก็โดนลบออกหมด
let scores: [Int] = [30, 40, 45, 50, 60, 40, 70, 80]
let filteredScores: [Int] = scores.drop(while: { $0 < 50 })

// output: [50, 60, 40, 70, 80]

Custom Higher-Order Function

นอกจากที่เค้าให้มา เรามาสร้างเองก็ทำได้ชิวๆ

ตัวอย่าง:

  • ฟังชั่น modifyTextInput เอาไว้แปลง string text ที่ส่งเข้าไป
  • customOperation คือ ฟังชั่นที่เรา เอาไว้สั่งให้ text มันเปลี่ยนเป็นอะไร
  • customOperation รับ function หรือ closure type (string) -> (string) เท่านั้น
// Higher-Order Function ตัวตั้งต้นของเรา

func modifyTextInput(text: String, using customOperation: (String) -> String) -> String {
    return customOperation(text)
}

Function

สังเกตดูว่ามัน dynamic มาก เพราะจะเอา function อันไหนไปใช้ก็ได้ อยากใช้แบบอื่นก็สร้างเพิ่มได้เรื่อยๆ

อะเข้า concept SOLID ละ 1 จุด Open-Closed Principle 🤫

ในนี้ผมสร้างมา 3 function เอาไปใช้ใน modifyTextInput ได้หมดเลย ผลลัพท์ก็ได้ตามใน output คับ

1.

2.

3.

Closure

ท่าที่ใช้ closure ก็คล้ายๆ function แค่เราไม่ต้องสร้าง function เพิ่ม

1.

let trimmedText = modifyTextInput(
    text: "  Hello World  ",
    using: { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
)

// output: "Hello World"

2.

let replacedText = modifyTextInput(
    text: "Hello World",
    using: { $0.replacingOccurrences(of: " ", with: "-") }
)

// output: "Hello-World"

3.

let happyText = modifyTextInput(
    text: "Hello World",
    using: { "🥳\($0)🥳" }
) 

// output: "🥳Hello World🥳"

Tip: จริงๆถ้า parameter ตัวสุดท้ายของ function หรือเป็น closure เราไม่ต้องเขียน ชื่อ parameter ตอนเรียกใช้ก็ได้ เช่น ข้างบนถ้าจะตัด using ไปเลยก็ได้ แล้วต่อด้วย block code เหมือนเดิม แต่ในนี้บีมจะยังใส่ไว้ให้เข้าใจได้ง่ายๆค้าบ


จบแล้วลองเอาไปฝึกใช้ดูนะครับ ใช้คล่องๆได้นี่เก่งมากเลย ประหยัดเวลา dev แถมอ่านง่ายทีม review code สบาย ใครสนใจไปดูเพิ่มได้ใน

แต่ขอเตือนว่า ระวังอย่าไปใช้ตอนสอบสัมภาษณ์งานนะคร้าบบ เพราะมันเขียนสั้นโกงเกินไปปไม่ดีๆ 555 😂


Happy Coding Swift คร้าบบ

ผมเขียนเรื่องแบบนี้เรื่อยๆ ถ้าเพื่อนๆชอบบทความที่อ่านช้าๆและมีสาระแบบนี้ ฝากกดติดตาม 🍌

Leave a Reply

Discover more from Journey of beamtan

Subscribe now to keep reading and get access to the full archive.

Continue reading