FP(03): Map Filter Reduce และเพื่อนๆ พระเอกแห่งโลก Functional

developer

บทความชุด: Functional Programming

รวมเนื้อหาเกี่ยวกับการเขียนโปรแกรมแนว Functional และหัวข้ออื่นๆ ที่เกี่ยวข้อง


Category Theory & Lambda Calculus


เนื้อหาในบทนี้น่าจะเป็นสิ่งที่เราเอาไปใช้เขียนโปรแกรมในชีวิตประจำวันมากที่สุดเรื่องหนึ่งเลย พอๆ กับเรื่อง pure function ในบทที่แล้ว

สำหรับภาษาโปรแกรมกระแสหลักที่เป็นแนว imperative หรือ OOP ซะเป็นส่วนใหญ่ ฟีเจอร์ของ FP ที่ได้รับความนิยมเอามาใช้กันมากที่สุดน่าจะเป็นเรื่องการโปรเซส List ด้วยคำสั่ง map, filter, และ reduce แทนการวน loop แบบปกติ

TL;DR (ยาวไป;ไม่อ่าน) - ในบทความนี้จะเน้นหลักการใช้ map, filter, reduce และอื่นๆ แบบเจาะลึกในมุมมองของ FP (ฉบับ insight!)

ถ้าอยากอ่านวิธีการใช้ helper function พวกนี้เลย ข้ามไปส่วนปูพื้นฐาน ข้ามไปอ่านข้างล่างได้เลยนะ

Operation การดำเนินการกับตัวแปร

ข้อมูลในมุมมองการเขียนโปรแกรมนั้นมีหลายชนิด เริ่มจากแบบมาตราฐานมากๆ เช่น

int x = 1;
double d = 12.5;
string s = "FP";

แต่ก็ยังมีตัวแปรอีกประเภท ที่ถือตัวแปรชนิดอื่นไว้ในตัวเองอีกทีหนึ่ง นึกออกมั้ยว่าคืออะไร?

คำตอบคือตัวแปรประเภท "Collection" เช่น

int[] arr = [1, 2, 3, 4]
  • Array หรือ List: ที่เก็บตัวแปรหลายๆ ตัวเรียงกัน ซึ่งเป็นตัวหลักที่เราจะพูดถึงกันในบทนี้!

ชนิดตัวแปรอื่นๆ ที่เข้าข่ายก็ เช่น

  • Map หรือ Dict: ที่เก็บตัวแปรหลายๆ ตัวโดยมี key เป็นตัวอ้างอิง
  • Pair หรือ Tuple: เป็นการจัดกลุ่มตัวแปรมากกว่าหนึ่งตัวไว้ด้วยกัน

Note: เราสามารถแบ่งประเภทตัวแปรออกตาม dimension (มิติที่เก็บข้อมูลได้) โดยจะเรียกว่า Tensor เริ่มตั้งแต่ rank 0, 1, 2 ไปเรื่อยๆ

  • rank 0 Scala: คือตัวแปรทั่วไป เก็บ value ได้ค่าเดียว เช่น int, doudle, string หรือพวก object ต่างๆ ก็ถือว่าใช่ได้ด้วย
  • rank 1 Vector: หรือในการเขียนโปรแกรม เรามักจะชินกันคำว่า Array หรือ List กันมากกว่า
  • rank มากกว่านั้นคือพวก array-2D, array-3D ไปเรื่อยๆ

ทีนี้, มีปัญหาอะไรล่ะ ที่เราต้องมาแบ่งตัวแปรออกเป็นกลุ่มตาม dimension ของมันแบบนี้?

สมมุติว่าเราจะ increment ค่าให้ตัวแปรหนึ่ง ถ้าจะทำให้มุมมอง imperative ก็อาจจะเขียนโค้ดแบบนี้

int x = 1
x = x + 1
print(x) // 2

แต่ไหนๆ ก็อยู่ในโลกแห่ง FP แล้ว แทนที่จะแก้ไขค่าตัวแปรตรงๆ ขอทำเป็นฟังก์ชันแทนละกัน

function inc(x){
    return x + 1
}

int x = 1
print(inc(x)) // 2

เทียบได้กับรูปข้อล่างนี่

นั่นคือถ้าข้อมูลของเราเป็น Scala การโยนข้อมูลนี้เข้าฟังก์ชันก็ไม่มีปัญหาอะไรติดขัด

แต่ทว่า !

แล้วถ้าข้อมูลของเราอยู่ในรูปของ Tensor rank 1 หรือ Array ล่ะ? จะเกิดอะไรขึ้น?

function inc(x){
    return x + 1
}

int[] x = [1, 2, 3, 4]
print(inc(x)) // ???


แน่นอนว่าโค้ดนี้ ทำงานไม่ได้!

นั่นเพราะว่าถ้าเราโยน Array เข้าฟังก์ชัน จะได้แบบนี้

[1, 2, 3, 4] + 1 ???

Array (rank 1) จะถูกนำไปบวกกับ int ซึ่งเป็นค่าแบบ Scala (rank 0) ค่าทั้งสองอยู่คนละ dimension เลยนำมาบวกกันไม่ได้

วิธีการแก้ก็คือ ... ฟังก์ชันที่โปรเซส Scala ก็ต้องรับค่าเป็น Scala

วิธีมาตราฐานแบบ imperative เราก็จะแก้มันด้วยการ วน loop ยังไงล่ะ! แหม่..คิดไม่ถึงเลย (ฮา)

function inc(x){
    return x + 1
}
int[] arr = [1, 2, 3, 4]

for(i=0; i<x.length; i++){
  arr[i] = inc(arr[i])
}

print(arr) // [2, 3, 4, 5]

ปัญหาคือ แล้วเราจะทำให้ inc() นั้นสามารถใช้ได้กับ Array ได้ยังไงกัน?

คอนเซ็ปของฟังก์ชันควรจะใส่ชนิดตัวแปรได้ทั้งหมดไม่ใช่เหรอไง? ... ลองดูโค้ดนี้ต่อ

function incArray(arr){
  int[] output = []
  for(i=0; i<x.length; i++){
    output[i] = inc(x[i])
  }
  return output
}

int[] arr1 = [1, 2, 3, 4]
int[] arr2 = incArray(arr1)

print(arr2) // [2, 3, 4, 5]

อืมม... ก็ดูดีไม่ใช่เหรอ? (ดูดีกะ***น่ะสิ!)

แค่จับโค้ดทั้งหมดที่เป็น imperative ไปยัดในฟังก์ชัน ไม่ได้แปลว่าคุณเขียน FP แล้วนะ เพราะนี่มันแนวคิดแบบ imperative ชัดๆ

(เหมือนกับที่แค่สร้าง class สร้าง object ไม่ได้แปลว่าคุณเขียน OOP แล้วนะ ถ้าคุณไม่ได้ใช้คอนเซ็ปของ OOP อย่างเช่น Abstraction, Inheritance, หรือ Polymorphism ... อุ๊ปส์ มีใครทำแบบนั้นอยู่รึเปล่า?)


เอาล่ะ ปูพื้นฐานกันมาพอล่ะ มาเข้าเรื่องกันดีกว่า

ในเมื่อการวนลูป ไม่ใช่สไตล์ของ FP, แต่เมื่อ FP เจอ Array เข้าไปมันก็ต้องมีวิธีแก้ปัญหาของมัน (โดยไม่ใช้ลูป)

นั่นคือภาษาสาย FP จะมี helper function แถมมาให้ในตัว ซึ่งทำให้เราสามารถใช้ฟังก์ชันแบบนี้

function inc(x){
    return x + 1
}

ฟังก์ชัน Scala สามารถเอาไปใช้กับ Array ได้

มาเริ่มกับฟังก์ชันแรก...

map

ฟังก์ชัน map เอาไว้ mapping หรือก็คือการ "แปลง" (transform) ข้อมูลจากอย่างหนึ่งไปเป็นอีกอย่างหนึ่ง เช่น ใส่ border ให้กับไอเทมทุกชิ้น หรือ x 10 กับตัวเลขทุกตัว

ซึ่ง map จะรับฟังก์ชันที่แปลงค่า Scala ธรรมดาเข้าไป แล้วทำการโปรเซสกันไอเทมทุกๆ ตัวในลิสต์

ตัวอย่างเช่น ถ้าเรามีฟังก์ชัน mul10 แบบนี้

function mul10(x){
    return x * 10
}
var x = 1
var y = mul10(x)

แทนที่จะเอาไปวนลูปเองแบบนี้

function mul10(x){
    return x * 10
}
var arr1 = [1, 2, 3]
var arr2 = []

for(var item in arr1){
  arr2[i] = mul10(arr1)
}

เราก็จะใช้ฟังก์ชัน map แทน

ก็จะกลายเป็นแบบนี้

function mul10(item){
  return item * 10
}
var arr1 = [1, 2, 3]

// ถ้าภาษานั้นใช้ map ในรูปแบบ method
var arr2 = arr1.map(mul10)

// ถ้าภาษานั้นใช้ map ในรูปแบบ function
var arr2 = map(arr1, mul10)

หรือถ้าเราอยากให้โค้ดสั้นลง โดยการใส่ "lambda" หรือฟังก์ชันนิรนาม (ในบางภาษาอาจจะมี syntax การเขียนที่ไม่เหมือนกันนะ และบางภาษาอาจจะเรียกว่า anonymous function) โค้ดข้างบนก็จะดูสั้น กระชับ กรุบกริบดี!

// method
var arr2 = arr1.map(item -> item * 10)

// function
var arr2 = map(arr1, item -> item * 10)

แปลว่า นำไอเทมทุกตัวใน arr1 ไป x 10 แล้วได้ผลลัพธ์มา ใส่ไว้ใน arr2

filter

ฟิลเตอร์เป็น helper function สำหรับเลือกไอเทมในลิสต์ว่าจะเอา/หรือไม่เอาตัวนี้ เช่น เลือกรูปทรงที่มี"มุม"เท่านั้น หรือ เลือกตัวเลขที่ ≤ 2 เท่านั้น

ในการใช้งาน filter เราจะต้องสร้างฟังก์ชันสำหรับระบุว่าของชิ้นไหนบ้างที่เราอยากได้ (ของที่ผ่านเงื่อนไข จะถูกเลือกมา) ฟังก์ชันที่ทำหน้าที่เลือกนี้จะถูกเรียกอีกอย่างว่า Predicate ซึ่งจะรับไอเทมที่อยากเช็กเข้ามา แล้วรีเทิร์น true - ถ้าจะเอาไอเทมตัวนั้น / false - ถ้าไม่เอาไอเทมตัวนั้น

ลองมาดูโค้ดแบบ imperative กันก่อน

function lessThanOrEqualsTwo(item){
  return x <= 2
}
var arr1 = [1, 2, 3]
var arr2 = []

for(var item in arr1){
  if(lessThanOrEqualsTwo(item)){
    arr2.push(item)
  }
}

ก็คือการวนลูป แล้วเช็กค่าที่ละตัว ถ้าผ่านเงื่อนไข (ตามฟังก์ชัน predicate) ก็ push ลงลิสต์ที่เป็นคำตอบเก็บไว้

ทีนี้ลองมาดูวิธีแบบฟังก์ชันนอลกันบ้าง

function lessThanOrEqualsTwo(item){
  return x <= 2
}
var arr1 = [1, 2, 3]
var arr2 = []

// ถ้าภาษานั้นใช้ filter ในรูปแบบ method
var arr2 = arr1.filter(lessThanOrEqualsTwo)

// ถ้าภาษานั้นใช้ filter ในรูปแบบ function
var arr2 = filter(arr1, lessThanOrEqualsTwo)

และก็เหมือน map ล่ะนะ คือถ้าเปลี่ยนมาเขียนด้วย lambda โค้ดก็จะสวยและสั้นลง

// method
var arr2 = arr1.filter(item -> item <= 2)

// function
var arr2 = filter(arr1, item -> item <= 2)

reduce

สำหรับ reduce ในบางภาษาอาจจะแยกออกเป็น 2 ฟังก์ชันคือ reduce กับ fold นะ

reduce น่าจะเป็นตัวที่เข้าใจยากที่สุดในสามสหาย map, filter, reduce ดังนั้นก่อนจะอธิบาย reduce ขอยกตัวอย่างด้วยฟังก์ชันที่เข้าใจง่ายกว่าคือ sum และ join

sum

ฟังก์ชันที่นำเลขทุกตัวในลิสต์มาบวกกัน จนได้เป็นคำตอบสุดท้ายเพียงคำตอบเดียว

var arr = [1, 2, 3, 4]
var x = sum(arr)
// x = 1+2+3+4 = 10

join

ฟังก์ชันที่ทำสตริงทุกตัวมาเชื่อมต่อกันด้วยสิ่งที่เรียกว่า delimiter ที่เรากำหนดเองได้ เช่น เชื่อมด้วย " ","-",หรือจะเป็นคำว่า " and " ก็ยังได้

var arr = ["A", "B", "C"]
var x = join(arr, ", ")    // x = "A, B, C"
var y = join(arr, "-")     // y = "A-B-C"
var z = join(arr, " and ") // z = "A and B and C"

แต่สังเกตอะไรมั้ย?

ว่า ... ฟังก์ชันทั้ง 2 ตัวนี้มีอะไรบางอย่างที่เหมือนกัน นั่นคือฟังก์ชันทั้งสองตัวจะทำการรวม item ทุกตัวในลิสต์ด้วย "ด้วยวิธีการอะไรอย่างหนึ่ง" ให้ค่าทั้งหมดรวมกันกลายเป็น "ค่าๆ เดียว"

ฟังก์ชันพวกนี้แหละ ที่เราเรียกว่า reduce วิธีการสร้าง lambda ที่เอาไว้กำหนดวิธีรวมจะยากนิดนึง เพราะ lambda ของ reduce จะต้องเป็นฟังก์ชัน 2 parameters

เช่นถ้าเราจะสร้าง reduce แบบการบวก (เหมือน sum)

var arr = [1, 2, 3, 4]
var x = arr.reduce((a, b) -> a + b)

สร้าง (a, b) -> a + b เพื่อกำหนดว่าวิธีการที่จะเอาไอเทมทุกตัวในลิสต์ตัวนี้มารวมกันคือ ถ้ามีไอเทม 2 ตัวคือ a, b ให้นำสองตัวนี้มา + กัน นั่นเอง

จริงๆ lambda ที่เราต้องสร้างให้ reduce ประกอบด้วย 2 parameters ก็จริง แต่มีความหมายต่างกันอยู่

นั่นคือตัวแรกจะเป็น "accumulator" หรือ "ค่าสะสม" ในขณะนั้น ส่วนตัวที่สองจะเป็น current item ในการวนรอบนั้นๆ

มาดูตัวอย่างการใช้ reduce กันต่อ

เราสามารถใช้ reduce ในการหาค่ามากสุด/น้อยสุดในลิสต์ได้เช่นกัน เพราะ min/max คือการยุบลิสต์ให้เหลือแค่ค่าเดียว ก็ใช้ reduce ได้ดังนี้

var maxItem = arr.reduce((a, b) -> max(a, b))
var minItem = arr.reduce((a, b) -> min(a, b))

แต่ถ้าเรามองว่า lambda ของเรากับฟังก์ชัน max, min มันก็เหมือนๆ กันนั้นแหละ (parameter เท่ากันเลย) ก็จะย่อได้อีก

var maxItem = arr.reduce(max)
var minItem = arr.reduce(min)

//หรือถ้าเป็นภาษาเชิง OOP อาจจะต้องเขียนจาก
var maxItem = arr.reduce(a, b -> Math.max(a, b))
//เป็นแบบนี้
var maxItem = arr.reduce(Math::max)

ทีนี้! บางครั้งการ reduce ก็ไม่ได้ง่ายและตรงไปตรงมาขนาดนั้น เพราะทิศทางหรือ direction ของการ reduce จะมีผลกับคำตอบเสมอ!

เช่น ถ้าเราเลือกที่จะ reduce ค่าด้วยการ - บ้างล่ะ จะเกิดอะไรขึ้น?

จะเห็นว่า ถ้าเรา reduce จาก ซ้าย->ขวา ผลที่ได้จะต่างจาก ซ้าย<-ขวา

ค่าดีฟอลต์ของ reduce ส่วนใหญ่จะเป็น left-to-right ถ้าอยากให้มันเริ่มจากฝั่งขวา ในภาษาส่วนใหญ่จะมีคำสั่ง reduceRight (reduce จากทางขวา) หรือซักอย่างที่ชื่อประมาณนี้ให้ใช้งาน

var ans1 = arr.reduce((a, b) -> a - b)
var ans2 = arr.reduceRight((a, b) -> a - b)

แต่ถึงไม่มี reduceRight เราก็อาจจะเลี่ยงไปใช้งานคำสั่งนี้ผสมเข้ามาช่วยได้

var ans1 = arr.reverse().reduce((a, b) -> a - b)

คือกลับหัวลิสต์ซะก่อน แล้วค่อย reduce ไงล่ะ (หืม? ถ้าภาษานั้นไม่มี reverse อีกจะทำยังไง? ... นั่นสิ 555 อ่านต่อไปก่อนละกัน ฮา)

มีอีกเคสหนึ่ง ที่reduceจะมีปัญหา นั่นคือถ้าเราต้องการกำหนดค่าเริ่มต้นจะทำยังไง? (ปกติแล้วเวลา reduce ค่าaccumulatorจะเริ่มต้นด้วยไอเทมแรกสุดในลิสต์)

เช่นอยาก sum เลขทุกตัวเหมือนเดิม แต่ไม่อยากให้ accumulator เริ่มต้นที่ 0 แต่เริ่มจาก 100 แทน

var arr = [1, 2, 3, 4]
var x = ([100] + arr).reduce((a, b) -> a + b)

วิธีแก้แบบง่ายที่สุดคือ ... ก็เติมไอเทมเข้าไป 1 ตัวก่อน reduce สิ

เออ มันก็เวิร์คแหละ แต่ส่วนใหญ่ฟังก์ชัน reduce มักจะมี parameter ที่2ให้เรากำหนด initial value ได้นะ

แต่บางภาษาก็ไม่มีอ๊อบชันนี้ให้ใช้ แต่แยกคำสั่งออกไปอีกชื่อนึงเลย คือ fold = reduce ที่ต้องกำหนด initial value เสมอ

var arr = [1, 2, 3, 4]
var x = arr.reduce((a, b) -> a + b, 100)

// หรือใช้ fold
var x = arr.reduce((a, b) -> a + b)
var y = arr.fold(100, (a, b) -> a + b)

และแน่นอน เมื่อ reduce มี reduceRight, fold ก็มี foldRight เช่นกันนะ


ขั้นเวลา! ตอนนี้เราผ่านฟังก์ชันหลัก 3 ตัวไปแล้ว แต่ก่อนจะไปดูฟังก์ชันตัวต่อไป เราลองมาคิดอะไรเล่นๆ กันดีกว่า กับคำถาม..

map ปะทะ filter ปะทะ reduce

ตอนนี้เรารู้จักฟังก์ชัน 3 ตัวกันไปแล้ว ถ้าถามว่าตัวไหนเจ๋งสุด คุณคิดว่าคำตอบคืออะไรเอ่ย?

ลองคิดดูเล่นๆ ก่อนนะแล้วค่อยอ่านต่อไป

...

คำตอบคือ reduce ครับ!

ถ้าใครเคยเรียนวิชา Digital System มาก่อนน่าจะรู้จักกับ circuit gate เช่น AND OR NOT ในจำนวนพวกนี้เกต NAND เป็นเกตที่เรียกว่า universal gate หรือเกตครบจักรวาล นั่นหมายความว่าเราสามารถเอา NAND มาต่อกันเป็นเกตตัวอื่นเช่น AND หรือ OR ได้ทั้งหมด

reduce ก็เหมือนกันครับ เราสามารถเอา reduce มาประยุกต์ใช้งานแทน map กับ filter ได้ทั้งหมด!

คุณอาจจะสงสัยว่าเป็นไปได้ยังไง เพราะทั้ง map, filter ให้ค่าออกมาเป็นอะเรยเสมอ แต่reduceจะให้ค่าออกมาเป็นแค่ค่าเดียว

ใช่ครับ, reduce ให้ค่าออกมาแค่ค่าเดียว แต่ไม่จำเป็นว่าค่านั้นต้องเป็น scala นี่นา จะตอบค่ากลับมาเป็น array (list) ก็ได้นะ แบบนี้..

// map: (item) -> item * 10
var x = arr.reduce((arr, item) -> {
  arr.push(item * 10)
  return arr
}, [])

// filter: (item) -> item <= 2
var x = arr.reduce((arr, item) -> {
  if(item <= 2){
    arr.push(item)
  }
  return arr
}, [])

หรือฟังก์ชัน reverse ที่เราพูดค้างไว้ตอนต้นก็นำมาเขียนด้วย reduce ได้เช่นกัน

var arr = [1, 2, 3, 4]
var reversed = arr.reduce((arr, item) -> {
  arr.insert(0, item) //add ลงไปในตำแหน่งแรกสุด (index=0) แทน (บางภาษาอาจจะใช้คำสั่ง unshift)
  return arr
}, [])

แต่แบบนี้ map, filter ก็ไม่จำเป็นน่ะสิ เพราะเราใช้ reduce แทนได้ทั้งหมด ... ก็ใช่ล่ะนะ แต่การใช้แบบนั้น ชื่อมันไม่สื่อ ครับ อ่านไปจะงงเองว่าฉันเขียนอะไรลงไป


กลับมาต่อกับฟังก์ชันตัวต่อไป...

flatMap

่ย่อมาจาก "Flatten Map" บางภาษาอาจจะใช้ในชื่อของ flatten

ถ้าเราลองวิเคราะห์ความสามารถของ map, filter, reduce แล้วก็จะพบว่า

  • map: จำนวนไอเทมจะเท่าเดิม (แต่ข้อมูลจะเปลี่ยนไป)
  • filter: จำนวนอาจจะน้อยลงจากเดิมจนถึง 0 ตัวเลยก็ได้ (แต่ข้อมูลจะไม่เปลี่ยน)
  • reduce: ผลลัพธ์ 1 ตัวเท่านั้น

นั่นแปลว่าเราขาดฟังก์ชันที่สร้างไอเทมมากกว่า input ที่ใส่เข้าไป นั่นแหละคือความสามารถของ flatMap ... ลองดูรูปข้างล่างประกอบ

ถ้าลิสต์มีจำนวนไอเทมทั้งหมด N ตัว เราจะได้ชาร์ตตามข้างบนนี่

var arr1 = [1, 2, 3, 4]

var arr2 = arr1.flatMap(item -> [item, item * 10])
// arr2 is [1, 10, 2, 20, 3, 30, 4, 40]

จากตัวอย่างข้างบน เราสามารถใช้ flatMap ในการขยายลิสต์ได้ เช่นในกรณีนี้เรากำหนดว่า item -> [item, item * 10] คือ 1 ไอเทมของลิสต์ ให้ขยายเป็น 2 ไอเทมคือitemตัวเดิม และ itemอีกตัวที่x10

หรืออีกวิธีหนึ่ง คือเราสามารถแกะ nested array ออกมาให้เหลือชั้นเดียวได้

var arr1 = [[1, 2], [3], [], [4, 5, 6]

var arr2 = arr1.flatMap(item -> item)
// arr2 is [1, 2, 3, 4, 5, 6]

นั่นคือถ้าเรารีเทิร์นค่ากลับเป็น item เลย จะเป็นการเอาอะเรย์ที่ครอบมันอยู่ออก

ส่วนใหญ่ เราไม่ค่อยจะเจอโอกาสจะได้ใช้ flatMap เท่าไหร่ แต่เดี๋ยวในบทหลังๆ เราจะได้ใช้ flatMap กันเยอะมาก .. ไม่ได้ใช้ด้วยการแกะชั้นอะเรย์ออก แต่ใช้ในเรื่องของ "Monad"

zip

ชื่อของ zip มาจากคำว่า "ซิป" นั่นแหละ เพราะสิ่งที่มันทำคือการจับคู่ลิสต์ 2 ตัว (หรืออาจจะมากกว่า 2 ก็ได้) เข้าด้วยกัน

var arr1 = [1, 2, 3]
var arr2 = ["A", "B", "C"]

var arr3 = zip(arr1, arr2)
// arr3 is [(1, "A"), (2, "B"), (3, "C")]

ตัวอย่างการใช้งาน เช่นถ้าเราต้องการจะวนลูปลิสต์ 2 ตัวพร้อมๆ กัน จากเดิมที่เราใช้ foreach ไม่ได้เพราะ foreach วนได้ทีละลิสต์ ทำให้เราต้องไปวนลูปแบบใช้ตัวนับ i

var arr1 = [1, 2, 3]
var arr2 = ["A", "B", "C"]

var n = min(arr1.length, arr2.length)
for(i=0; i<n; i++){
  var a = arr1[i]
  var b = arr2[i]
  ...
}

// แต่ถ้าเรา zip ลิสต์ 2 ตัวนั้นเข้าด้วยกัน ก็จะสามารถวนลูปพร้อมกันได้
for(a, b in zip(arr1, arr2)){
  ...
}

forEach

ในฟังก์ชันทั้งหมดในบทความนี้ ส่วนใหญ่จะมีรูปแบบการใช้งานแบบฟังก์ชันนอลทั้งหมด คือพยายามทำให้โค้ดเป็น pure function ไม่มีการเปลี่ยนค่าตัวแปรหรือยุ่งกับ I/O ภายนอก .. ยกเว้น forEach นี่แหละที่นับว่าเป็นฟังก์ชันที่มักจะยุ่งกับ I/O หรือ global variable ตรงๆ

วิธีการใช้งานของ forEach ถือว่าเข้าใจง่ายมาก เหมือนการวนลูป for แบบปกติ

var arr = [1, 2, 3]

for(item in arr){
  print(item)
}

// เปลี่ยนไปใช้ฟังก์ชันแทน
arr.forEach(item -> {
  print(item)
})

หรืออีกตัวอย่าง.. เราต้องการวนลูปเพื่อรวมค่า sum ของอะเรย์

var arr = [1, 2, 3]

var sum = 0
for(item in arr){
  sum += item
}

// เปลี่ยนไปใช้ฟังก์ชันแทน
var sum = 0
arr.forEach(item -> {
  sum += item
})

สังเกตว่า forEach มักจะยุ่งกับค่าภายนอกเสมอ (ไม่เป็น pure function) เพราะโดยหลักการของมันคือจะให้ item มา1ตัว แต่ไม่ต้องรีเทิร์นค่าอะไรกลับไปเลย ถ้าเราไม่เอาค่านั้นมาใช้ก็ไม่รูปจะวนลูปทำไม

สรุป

ในบทนี้เราได้เรียนรู้ helper function ตัวสำคัญๆ ใน FP ที่ทำให้การทำงานกับ List, Array ของเรานั้นง่ายขึ้นเยอะมาก ไม่ต้องมาวนลูปแบบเดิมๆ

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

var arr = [1, 25, 20, 4, ...]

var sum = 0
for(item in arr){
  item = item * 10
  if(item > 100){
    sum += item
  }
}

print(sum)

โค้ดข้างบนนี่ เขียนด้วยสไตล์ imperative แบบเดิมๆ ถ้าเราไม่เคยรู้มาก่อนว่าจุดประสงค์ของโค้ดต้องการจะทำอะไร เราจะต้องใช้เวลาสักพักหนึ่งเลยในการแกะโค้ดให้ออกว่ามันทำหน้าที่อะไร

แต่ถ้าเราเปลี่ยนเป็นโค้ดสไตล์ FP จะได้แบบนี้

var arr = [1, 25, 20, 4, ...]

var sum = arr.map(item -> item * 10)
             .filter(item -> item > 100)
             .reduce(a,b -> a + b)
print(sum)

นั่นคืออ่านได้ง่ายมากว่า

  1. (map) นำตัวเลขทุกตัวมา x10
  2. (filter) จากนั้นเลือกเฉพาะเลขที่ >100
  3. (reduce) สุดท้ายเอาตัวเลขทั้งหมดมา + กัน
222 Total Views 3 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...