บทความชุด: Functional Programming
รวมเนื้อหาเกี่ยวกับการเขียนโปรแกรมแนว Functional และหัวข้ออื่นๆ ที่เกี่ยวข้อง
- ตอนที่ 1 Pure, First-Class, High-Order, พื้นฐานแห่ง Functional Programming
- ตอนที่ 2 Lambda Function and Closure ฟังก์ชันทันใจและพื้นที่ปิดล้อม!
- ตอนที่ 3 map filter reduce และเพื่อนๆ พระเอกแห่งโลก Functional
- ตอนที่ 4 โครสร้างแบบ Pair และ Either กับ List Comprehension การสร้างลิสต์ฉบับฟังก์ชันนอล
- ตอนที่ 5 Lazy Evaluation ขี้เกียจไว้ก่อนแล้วดีเอง?!
- ตอนที่ 6 Recursive Function ฟังก์ชันเวียนเกิด, เขียนลูปไม่ได้ทำยังไง? มาเขียนฟังก์ชันเรียกตัวเองแทนกันเถอะ!
- ตอนที่ 7 Curry, Partial Function
- ตอนที่ 8 Functor, Monad
- [บทความปี 2015] มารู้จักกับ Functional Programming สิ่งที่คุณต้องรู้ในตอนนี้กันเถอะ
- Function ทำงานยังไง?, ในมุมมองของโปรแกรมเมอร์สาย Imperative
Category Theory & Lambda Calculus
บทความนี้จะเน้นที่ List Comprehension เป็นหลัก เพราะแนวคิดของโครงสร้างแบบ Pair และ Either muitsfriday เขียแบบละเอียดไปเรียบร้อยแล้ว ในหัวข้อของ Product (Pair) และ Co-Product (Either) ซึ่งแนะนำให้อ่านก่อนอย่างยิ่งเลยนะ จะเขียนใจวิธีการออกแบบภาษาแนว FP เลย
เนื้อหา Pair และ Either ในบทความนี้จะเกริ่นให้พอเข้าใจเพื่อปูเรื่องต่อไปยัง List เท่านั้น)
ในบทที่แล้วเราพูดถึงการจัดการข้อมูลในโครงสร้างแบบลิสต์กับไปแล้ว แต่เรายังไม่ได้อธิบายเลยว่าในโลก FP นั้นมีแนวคิดในการสร้างตัวแปรแบบลิสต์ขึ้นมายังไงกัน รวมถึงตัวแปรโครงสร้างข้อมูล (Data Structure) ที่ใช้งานใน FP ด้วย
Pair: 2 ค่าอยู่ในตัวแปรตัวเดียว
ในบทที่แล้วเราพูดถึงมิติของตัวแปรที่เรียกว่า Tensor โดยตัวแปรที่เก็บค่าธรรมดาเรียกว่า Tensor Rank 0 หรือ Scala
แล้วเราก็ได้เรียนรู้ว่าถ้าต้องการจะเก็บตัวแปร 2 ค่าขึ้นไปจะต้องเปลี่ยนไปใช้ Tensor ที่ rank มากกว่าเดิม เช่น List
แต่ก่อนที่จะไปเรื่องต่อไป เราขอพูดถึงโครงสร้างที่เป็นต้นกำเนิดของลิสต์กันก่อน ซึ่งถือว่าเป็นชนิดตัวแปรแบบพื้นฐานสุดๆ ใน FP เลย นั่นคือตัวแปรที่เรียกว่า "Pair" แพร์ หรือก็คือตัวแปรคู่ เก็บได้ 2 ค่า!
Pair คือตัวแปรที่เก็บค่าได้ 2 ค่า (หมายเหตุ แต่ละภาษาจะมี syntax วิธีการเขียนต่างกันไปนะ)
var x = (123, "ABC")
ถ้าถามว่า แค่นี้เหรอ?
ใช่แล้วครับ แค่นี้แหละ คอนเซ็ปของ Pair คือตัวแปรที่เก็บค่าได้เพิ่มอีก 1 ค่า (กลายเป็น 2 ค่า เท่านั้นแหละ!!)
แล้วสร้าง Pair ยังไงล่ะ?
ถ้าคุณเป็นสาย OOP การจะสร้างตัวแปรที่เก็บค่าคู่ได้ ก็ไม่มีอะไรยาก คือเราสามารถสร้างคลาสที่เก็บตัวแปร 2 ตัวเอาไว้ (สมมุติว่าชื่อ first
และ second
)
class Pair<F,S>{
F first
S second
Pair(first, second){
this.first = first
this.second = second
}
}
var entry = Pair(123, "ABC")
print(entry.first) //123
print(entry.second) //ABC
แต่ไหนๆ แล้วเราก็กำลังศึกษาการเขียนโปรแกรมแบบ FP อยู่ ไหนลองมาดูวิธีสไตล์ FP หน่อย
function pair(first, second){
return [first, second]
}
function first(pair){
return pair[0]
}
function second(pair){
return pair[1]
}
var entry = pair(123, "ABC")
print(first(entry)) //123
print(second(entry)) //ABC
จะสังเกตว่า FP นั้นเวลาเราออกแบบ เราจะเริ่มจากการคิดว่าสร้างฟังก์ชันขึ้นมาเพื่อ evaluate ข้อมูลอะไรบางอย่างออกมาเป็นอีกค่าหนึ่ง
เช่นในกรณีนี้ เราสร้างฟังก์ชัน pair()
สำหรับสร้างแพร์ขึ้นมา จากนั้นถ้าเราต้องการดึงค่าออกมา ก็เหมือนเดิมคือต้องทำผ่านฟังก์ชันนั่นแหละ ก็สร้างขึ้นมาอีก 2 ตัว คือ first()
และ second()
ที่เมื่อโยนแพร์ลงไป จะได้ค่าตัวแรกหรือตัวที่สองออกมา
จุดสังเกตอีกจุดคือ เราสร้าง pair()
โดยซ่อนโครงสร้างจริงๆ ไว้ข้างใน เช่นในตัวอย่างนี้เราสร้างมันด้วย Array ... แต่ความจรืง เราจะสร้างมันด้วยอะไรก็ได้นะ ตราบใดที่ first()
, second()
สามารถดึงค่าออกมาให้เราได้ถูกต้อง
การเอา Pair ไปใช้งาน
ส่วนใหญ่เราจะใช้งานแพร์เมื่อต้องการเก็บค่ามากกว่า 1 ตัว ตัวอย่างที่เห็นชัดที่สุดก็เช่นการรีเทิร์นค่าจากฟังก์ชัน (เพราะฟังก์ชันรีเทิร์นค่ากลับได้แค่ 1 ค่า ถ้าอยากจะรีเทิร์นกลับมากกว่านั้นก็ต้องใช้ Pair นี่ล่ะ)
function getWickedData(){
var status = ...
var data = ...
return pair(status, data)
}
var entry = getWickedData()
var status = first(entry)
var data = second(entry)
แต่ส่วนใหญ่ภาษาที่มีโครงสร้างแบบ Pair มาให้ จะมีฟีเจอร์ที่เรียกว่า destruct มาให้ด้วย นั่นคือการแตก pair ออกเป็นค่า 2 ค่าให้เลย แบบนี้
var (status, data) = getWickedData()
ถ้าเรามีโครงสร้างแบบ Pair ซึ่งสามารถกลายเป็นค่าได้ 2 ค่าต่อไป มันก็จะมีโครงสร้างอีกแบบหนึ่งที่ตรงข้ามกับ Pair เลยเพราะนี่เป็นโครงสร้างที่อนุญาตให้คุณเก็บค่าได้สองชนิดเลยนะ แค่เก็บได้ทีละ 1 ค่าเท่านั้น
Either: ตัวแปรตัวเดียว แต่เป็นได้ 2 ค่า
ในบางภาษา (เช่นตัวอย่างนี้ใช้ภาษา TypeScript) จะอนุญาตให้เราสร้าง type ที่เป็นไทป์ผสมระหว่าง 2 (หรือมากกว่านั้น) ได้ เช่น
type EitherNumberOrString = number | string
นั่นคือเราสามารถกำหนดค่าใส่ตัวแปรชนิดนี้ได้ทั้งตัวเลขและตัวอักษร แบบนี้
let data: EitherNumberOrString
data = 1
data = "A"
การเอา Either ไปใช้งาน
ส่วนใหญ่ Either จะใช้กับกรณีค่าที่รีเทิร์นกลับนั้นมี 2 state(หรือมากกว่านั้น) เช่นฟังก์ชันโหลดข้อมูลของเรา อาจจะตอบกลับเป็น Error ก็ได้
type MayError = Data | Error
function getWickedData(): MayError{
if(...) {
return Data(...)
} else {
return Error(...)
}
}
let res = getWickedData()
if(res instanceof Data){ ... }
else if(res instanceof Error){ ... }
เอาล่ะ จบโครงสร้างแบบพื้นฐานที่เราเจอได้ใน FP กันไปแล้ว ต่อไปจะเป็นการพูดถึงโครงสร้างแบบ List ในมุทมอง FP กันต่อ
Pair as List
จริงๆ แล้วโครงสร้างข้อมูลใน FP นั้นจบแค่ Pair เท่านั้นแหละ ทีนี้ ถ้าเราต้องการเก็บข้อมูลมากกว่า 2 ตัวเราจะทำยังไง?
คำตอบคือ จับ Pair ซ้อน Pair ๆๆๆ เข้าไปเรื่อยๆ ไงล่ะ
var pair = pair("A", pair("B", pair("C", "D")))
ซึ่งแนวคิดของเอา Pair มาต่อๆ กันเนี่ยแหละที่มันจะกลายไปเป็นโครงสร้างแบบ List ต่อไป
หรือสรุปง่ายๆ คือ List เกิดจากการนำโครงสร้างที่เป็นหน่อยย่อยที่สุดอย่าง Pair มาประกอบเข้าด้วยกันไงล่ะ!
ทีนี้ถ้าเราจะหยิบข้อมูลแต่ละตัวออกมา เราจะต้องทำยังไง?
var A = first(pair)
var B = first(second(pair))
var C = first(second(second(pair)))
var D = second(second(second(pair)))
ก็ค่อยๆ เรียกทีละชั้นๆ เริ่มจากข้างในออกข้างนอกนะ
อาจจะมองว่าวิธีการเรียกแบบนี้ยากกว่าการกำหนด index ตรงๆ แบบที่เราเคยทำ เช่น arr[2]
หรือ arr[8]
อะไรแบบนั้น แต่นั่นไม่ใช่ปัญหาสำหรับ FP เพราะการโปรเซสลิสต์ส่วนใหญ่ใน FP จะไม่อ้างไอเทมแบบเจาะจงเป็นตัวๆ แบบนั้นอยู่แล้ว
นอกจาก map, filter, reduce ที่เราพูดถึงกันในบทที่แล้ว ยังมีฟังก์ชันที่เอาไว้จัดการลิสต์ในเชิงการ getElement หรือ sublist ให้ใช้อีกเยอะ เช่น
fn | return type | Note |
---|---|---|
first |
element | ค่าตัวแรกในลิสต์ เหมือน arr[0] |
last |
element | ค่าตัวสุดท้ายในลิสต์ เหมือน arr[n-1]] |
tail |
list | sublist ตั้งแต่ตัวที่ 1 ถึงตัวสุดท้าย (ไม่รวมแค่ตัวแรก) |
init |
list | sublist ตั้งแต่ตัวถึงตัวรองสุดท้าย (ไม่รวมแค่ตัวสุดท้าย) |
take(x) |
list | เลือกตั้งแต่ตัวแรก ไป x ตัว |
skip(x) |
list | ข้าม x ตัวแรกไปจนถึงตัวสุดท้าย |
เช่น ถ้าเราอยากหยิบค่าตำแหน่งที่ 2
ออกมา ก็เขียนได้ว่า
var C = first(skip(2, list))
แต่เอาจริงๆ แล้ว ภาษาโปรแกรมที่พวกเราใช้ๆ กันอยู่จะแปลงรูปนี้ให้อยู่ในเชิง method มากกว่า ก็จะได้แบบข้างล่างแทนนะ (ส่วนตัวคิดว่าอ่านและเขียนง่ายกว่าแบบฟังก์ชันเยอะ)
var C = list.skip(2).first
//or
var C = list.take(3).last
List Comprehension
หลังจากเข้าใจโครงสร้างแบบลิสต์กันแล้ว ลองมาดูฟีเจอร์การสร้างลิสต์สไลต์ FP กันต่อเลย
ตามปกติแล้ว เราสามารถกำหนดลิสต์ที่มีสมาชิกข้างในตรงๆ ได้
var list = [1, 2, 3, 4]
แต่ถ้าเราต้องการลิสต์ที่มีซัก 1-100 ล่ะ จะต้องนั่งไล่พิมพ์เลขทีละตัวเหรอ? ก็คงไม่ใช่เรื่องล่ะ
หรือจะเขียนแบบนี้
var list = [1,2 .. 100]
แน่นอนว่าคนอ่านออกและพอจะเดาได้เองว่า ..
ที่เว้นไว้น่ะต้องเติมเลขไปเรื่อยๆ จนถึง 100
แต่คอมพิวเตอร์มันจะไปรู้เรื่องได้ยังไงกัน?
ได้ยังไงกัน...จริงเหรอ??
นั่นเพราะเรากำลังคิดแบบ imperative อยู่ยังไงล่ะ! สำหรับ FP แล้วการทำคำความเข้าใจลิสต์ที่โปรแกรมเมอร์ต้องการสร้างน่ะ มันเป็นไปได้นะ!
Comprehension แปลว่า "ความเข้าใจ/ความหยั่งรู้" ก็ตามนั้นเลย List Comprehension เลยแปลว่าการที่คอมพิวเตอร์จะเข้าใจลิสต์แบบที่มนุษย์กำหนดขึ้นมาได้ยังไงล่ะ
โดยขอแบ่งเป็น 2 ฟีเจอร์ นั่นคือ Range และ List Comprehension with Loop
Range
คำสั่ง range
เป็นคำสั่งที่เอาไว้ "สร้างช่วงของค่า ตั้งแต่ค่าหนึ่ง ไปจนถึงอีกค่าหนึ่ง" ส่วนใหญ่จะต้องเป็นค่าที่สามารถเป็น sequence หรือเรียงค่ากันได้ เช่น number หรือ character อะไรแบบนั้น
และสำหรับภาษาสาย FP แท้ๆ แบบภาษา Haskell (รู้จักมั้ย?) สามารถทำความเข้าใจลิสต์ได้แบบเทพมากๆ เช่น
[1, 2 .. 100]
ตัวภาษาสามารถเข้าใจว่า เราต้องการสร้างลิสต์ ที่นับเพิ่มทีละ 1 ค่า เริ่มตั้งแต่ 1-100 ก็จะได้ค่าออกมา 1, 2, 3, 4, 5, 6 ไปเรื่อยๆ เลยจนถึง 98, 99, 100
แต่ยังไม่หมดแค่นั้น เราสามารถสร้างเลขที่กระโดดกันได้ด้วย เช่นต้องการสร้างลิสต์ของตัวเลขทั้งหมดที่หารด้วย 3 ลงตัวตั้งแต่ 0 ถึง 100 ก็เขียนได้แบบนี้
[0, 3 .. 100]
haskell ก็จะคิดว่าเราต้องการเลขในช่วง 0-100 แต่เดี๋ยวก่อน! ตัวต่อจาก 0 นั้นเป็น 3 แฮะ แสดงว่าโปรแกรมเมอร์ไม่ได้ต้องการเลขเรียงต่อกันแบบ 0, 1, 2 แล้ว มันต้องกระโดดทีละ 3 สินะ โอเค งั้นหลังจาก 0, 3 แล้วตัวต่อไปก็ต้องเป็น 6, 9, 12, ไปเรื่อยๆ ล่ะ
ความสามารถอีกอย่างชอง range
ใน haskell คือมันสามารถสร้างลิสต์แบบ"ปลายเปิด"ได้ เช่น [2, 4 ..]
นั่นคือสร้างลิสต์ 2 4 6 8 .. ไปเรื่อยๆ แต่ไม่ได้บอกจุดสิ้นสุด (เดี๋ยวเรื่องนี้เราจะไปขยายความต่อในบทของ Lazy Evaluation นะ)
จะเห็นว่ามันเป็นการสร้างลิสต์ที่เจ๋งมาก แทนที่โปรแกรมเมอร์จะมาพยายามทำความเข้าใจว่าเราจะกำหนดค่ายังไง (อาจจะต้องคิด logic หรือวนลูป) การเขียนแบบ FP จะเป็นการเอาใจฝั่มนุษย์ แล้วให้คอมพิวเตอร์เข้าใจเราแทน
แน่นอนว่าฟีเจอร์นี้มันดีสุดๆ ภาษาใหม่ๆ เลยชอบจับฟีเจอร์นี้ใส่เข้าไปในภาษาของตัวเอง ในที่นี้เลือกภาษาหลักๆ ที่มีฟีเจอร์นี้แบบตรงๆ มาให้ดูกันนะ
เช่นถ้าเราต้องการลิสต์ตามเงื่อนไขแบบนี้
Language | 1,2,3 | 1,2 (ไม่รวม 3) | 1,3,5,7 (กระโดดทีละ2) |
---|---|---|---|
Haskell | [1..3] |
init [1..3] |
[1,3..7] |
Python | - | range(1,3) |
range(1,7,2) |
PHP | range(1,3) |
- | range(1,3,2) |
Kotlin | 1..3 |
1 until 3 |
1..7 step 2 |
Swift | 1...3 |
1..<3 |
- |
Ruby | 1..3 |
1...3 |
(1..7).step(2) |
สำหรับ haskell นั้นถ้าไม่ต้องการตัวสุดท้าย ก็ใช้ฟังก์ชัน init
ที่สอนไปแล้วมาตัดเฉพาะตัวหน้า
ส่วนตัวชอบแบบภาษา Swift ที่สุด ความหมายสื่อชัดดี ส่วนตัวที่ใช้แล้วสับสนทุกครั้งคือ Kotlin เพราะคำที่ใช้คือ until
เช่น 1 until 3
มันแปลได้ว่า 1 ถึง 3
แต่ดันไม่รวมเลข 3 เข้าไปด้วย (แต่ Kotlin ก็ยังเป็นภาษาอันดับ 1 ในใจอยู่นะ ฮา)
หากใครเขียนได้หลายภาษาเชื่อว่าจะต้องมีความมึนงงเวลาใช้แน่นอน เพราะแต่ละภาษานั้นเขียนไม่เหมือนกัน หรือถึงเขียนเหมือนกันแต่ค่าที่ได้อาจจะไม่เท่ากันก็ได้ เช่น range
ใน Python และ PHP ใช้ตัวเดียวกัน แต่ผลออกมาไม่เหมือนกัน
Note:
range
ของภาษา Haskell เป็น Lazy Evaluation โดยตัวภาษาที่เป็น FP อยู่แล้ว, แต่ Python3range
จะเป็น Lazy เทียบได้กับxrange
ใน Python2 (Lazy Evaluation คืออะไร เดี๋ยวเราจะพูดกันต่อในบทหลังๆ นะ)
List Comprehension with Loop
หลังจากเราเข้าใจวิธีการสร้างลิสต์ด้วยการใช้ range
ไปแล้ว อาจจะมีข้อสงสัยว่า แล้วถ้าลิสต์ที่เราต้องการ มันเป็นตัวเลขที่ไม่ได้เรียงกันด้วย logic ง่ายๆ ล่ะ?
เช่นต้องการลิสต์ของตัวเลขตั้งแต่ 0-100 เฉพาะตัวที่หารด้วย 3 หรือ หารด้วย 5 ลงตัว เราจะสร้างกันยังไง?
แน่นอนว่า range
นั้นรับมือกับเคสนี้ไม่อยู่แล้ว ขั้นแรกลองมาคิดแบบ imperative ที่เราคุ้นเคยกันก่อนดีกว่า
for(i=0; i<=100; i++){
if(i % 3 == 0 || i % 5 == 0){
print(i)
}
}
สำหรับ imperative ตามโจทย์ข้างบน เราก็จะเขียนโค้ดได้ประมาณนี้แหละ แต่เดี๋ยวสิ อันนี้มันไม่ใช่การสร้างลิสต์ซะหน่อย นี่มันแค่ปริ้นค่าเลขออกมาธรรมดานะ!?
แต่ลองคิดดูนะ เลขที่เราปริ้นออกมาพวกนั้น คือตัวเลขที่เราต้องการจะเอามาสร้างเป็นลิสต์นี่นา
ถ้าอย่างนั้นเราเอาสัญลักษณ์ของลสิต์ คือ [
และ ]
ครอบโค้ดนี้ไป แล้วเอา print()
ออกจะได้มั้ย แบบนี้...
var list = [
for(i=0; i<=100; i++){
if(i % 3 == 0 || i % 5 == 0){
i
}
}
]
เอ๊ะ ถามอะไรอย่างนั้น คอมพิวเตอร์มันจะไปเข้าใจได้ยังไงกัน!?
ผู้ที่อ่านอยู่บางคนอาจจะมีความคิดแบบนี้ แล้วก็ต่อด้วยความคิดที่ว่าเมื่อกี้ตอน range
ฉันก็โดนหลอกไปแล้วรอบนึง ก็จะมีความลังเลว่าถ้ามันไม่ได้แล้วคนเขียนมันจะยกตัวอย่างนี้ขึ้นมาทำไม แล้วมันทำได้จริงๆ เหรอ?
ถูกแล้วครับ! นี่คือขั้นสูงสุดของ List Comprehension เพราะมันสามารถเข้าใจลิสต์ของเราได้ยังไงล่ะ! (เฉพาะภาษาที่มีฟีเจอร์นี้นะ)
คอนเซ็ปของ List Comprehension ก็คือเราสามารถวนลูปข้างในลิสต์เพื่อกำหนดค่าให้แต่ละ element ของลิสต์ได้เลย
โดยภาษาที่จะยกมาสอนคือ Haskell (อีกแล้ว) และ Python กับ Dart (ภาษา Python น่าจะรู้จักกันดีอยู่แล้ว / ภาษา Dart เป็นภาษาสำหรับเขียนแอพแบบ cross-platform หากสนใจตามไปอ่านได้ที่ Dart 101: ทำความรู้จักภาษาDartฉบับโปรแกรมเมอร์ .. ขายของเฉยเลย ฮา)
มาเริ่มกันด้วย Haskell กับสไตล์ภาษา FP แท้ๆ กันก่อน
โครงสร้าง Loop สำหรับทำ List Comprehension จะแบ่งได้หลักๆ 3 ส่วน นั่นคือ..
- Output Item: ผลลัพธ์ที่ต้องการ
- Loop: ลูปที่จะวนสร้างไอเทม
- Condition: เงื่อนไขว่าต้องการไอเทมไหนบ้าง
เช่นตัวอย่างข้างบนคือสร้างลิสต์ของตัวเลขตั้งแต่ 1-10 แต่เลือกเฉพาะตัวที่เป็นเลขคู่ (หาร 2 ลงตัว) เท่านั้น
output:
[2,4,6,8,10]
แต่ใน haskell เรายังสามารถกำหนดลิสต์ขึ้นมาจากลิสต์อื่นๆ กี่ตัวก็ได้ แถมมีเงื่อนไขกี่ตัวก็ได้เช่นกัน
เช่น ต้องการหาว่าตัวเลขตั้งแต่ 1-10 มีกี่คู่ที่สามารถบวกกันได้ 10 พอดี ก็เขียนลิสต์ได้แบบนี้
output:
[(5,5),(6,4),(7,3),(8,2),(9,1)]
อธิบาย:
- เวลาอ่านโค้ดพวกนี้ให้เริ่มจาก Loop -> Condition -> Output นะ จะทำให้เข้าใจได้ง่ายขึ้น
- เริ่มจากการบอกว่าเราจะสร้างลิสต์จาก ลิสต์ 2 ตัวซึ่งประกอบด้วยตัวเลข 1-10 ทั้งคู่
- ดึงเลขแต่ละตัวออกมาจากลิสต์ 2 ตัวนั้น ขอเรียกว่า
a
กับb
- เลือกเฉพาะตัวที่
a
+b
ได้ 10 - เพื่อป้องกันได้คู่ซ้ำ เช่น (6,4) กับ (4,6) เลยใส่ลงไปอีกเงื่อนไขหนึ่งคือ
b <= a
(ทำให้ผลออกมาแต่ (6,4) ไงล่ะ) - สุดท้าย คำตอบจัดอยู่ในรูป pair(a,b)
สังเกตว่าการเขียนแบบนี้เป็นการเขียนสไตล์ declarative คือการกำหนดเงื่อนไขเฉยๆ เลย ไม่มีการนำคำสั่งมาเรียกกันเป็น logic แบบ imperative เลย เวลาเห็นโค้ดก็เข้าใจกว่าการเขียนลูปเยอะมาก เพราะแทบจะเป็นภาษาคนแล้ว (ใครยังไม่คล่อง อาจจะต้องฝึกซักพัก)
Cartesian Product
สำหรับการทำ List Comprehension ถ้าลิสต์ต้นฉบับ (Source List) มีมากกว่าหนึ่งตัว มันจะจับคู่ไอเทมแต่ละตัวเข้าด้วยกัน แบบกระจายทุกความเป็นไปได้ หรือที่เราเรียกว่า ผลคูณคาร์เทเชียน (Cartesion Product)
ขอพักภาษา haskell ไว้แค่นี้ก่อน ลองกลับมาดูภาษาที่เราคุ้นเคยกันบ้าง
# python
[ x for x in range(1, 10+1) if x % 2 == 0 ]
│ │ │
│ └────── loop └─ condition
└─ output
//dart
[ for(var x=0; x<=10; x++) if(x % 2 == 0) x ]
│ │ │
└────── loop └─ condition output
และนั่นละครับท่านผู้อ่าน! ความมึนงงของแต่ละภาษามันก็เกิดขึ้นตรงนี้ เพราะลำดับ output, loop, condition ของแต่ละภาษามันดันเรียงไม่เหมือนกัน! (จุดนี้ก็ตัวใครตัวมันละกันนะ คอนเซ็ปมันเหมือนกัน แต่เขียนไม่เหมือนกัน จำกันเองนะ)
Quiz!
ก่อนจะจบบท ลองมาทำโจทย์ประยุกต์ใช้งาน List Comprehension ในการหาคำตอบกันหน่อย
โจทย์ก็ง่ายๆ ไม่มีอะไรมาก
กำหนดให้แบบรูปข้างบนนี่แหละ จงหาค่าของ rabbit, dog, และ pig
โจทย์พวกนี้หลายๆ คนเห็นมักจะคันไม้คันมืออยากหาคำตอบ ... เอาจริงๆ โจทย์พวกนี้คือโจทย์ สมการหลายตัวแปรธรรมดาเลย แต่ถ้าเปลี่ยนเป็นตัวแปร x, y, z ดูนะ จะไม่มีใครอยากเล่นเลย
x + x + x = 9
x + x - y = 7
x * z = 20
หมดความน่าเล่นไปในทันใด (ฮา)
เอาล่ะ คุณผู้อ่านจะลองคิดเล่นๆ ด้วยตัวเองก่อนก็ได้นะว่าคำตอบเป็นเท่าไหร่ จากนั้นลองไปดูว่า List Comprehension หาคำตอบพวกนี้ได้ยังไง
ยกตัวอย่างเป็นภาษา Dart นะ
answers = [
for(var rabbit = 1; rabbit <= 20; rabbit++)
for(var dog = 1; dog <= 20; dog++)
for(var pig = 1; pig <= 20; pig++)
if(
rabbit + rabbit + rabbit == 9 &&
dog + dog -rabbit == 7 &&
dog * pig == 20
)
[rabbit, dog, pig]
];
print(answers.first);
output:
[3, 5, 4]
อธิบาย
- เริ่มจากเราต้องกะเอาว่า คำตอบของ rabbit dog pig 3 ตัวนี้น่าจะอยู่ในช่วง 1-20 ไม่เกินนี้แน่ๆ (เดาเอาจากตัวเลขในโจทย์)
- เราก็เขียนลูปสร้างคู่คำตอบทุกความเป็นไปได้ เริ่มตั้งแต่กำหนดให้สัตว์ทุกตัวเป็น 1-20 แล้วลูปซ้อนๆ กัน
- สร้างเงื่อนไขตามโจทย์เลย ถ้าคู่คำตอบในรอบนั้นตรงเงื่อนไข ก็เก็บเอาไว้ในลิสต์
- สังเกตว่าคำตอบจะออกมาเป็น List เสมอ แต่สมการนี้เราต้องการคำตอบเดียว เลยใช้
first
ในการดึงเฉพาะตัวแรกออกมาก็พอ - ได้คำตอบเป็น rabbit=3 dog=5 pig=4 (ทำไมหมูเบากว่าหมา ไม่ต้องสงสัยนะ ฮา)