บทความจาก https://www.tamemo.com/post/157/fp-01-fundamental/
ในโลกของ FP นั้นมีหลายสิ่งที่ต้องเรียนรู้กัน แต่เราจะมาเริ่มกับสิ่งที่เป็นพื้นฐานที่สุดกันก่อน นั่นคือ
- Pure Function
- First-Class Function
- High-Order Function
Pure Function
ความจริงแล้วคุณสมบัติที่สำคัญที่สุดใน FP คือ First-Class Function แต่เรื่องของ Pure Function นั้นเข้าใจง่ายกว่าเลยขอยกมาเล่าก่อนละกันนะ (แต่หากใครยังไม่แม่นเรื่องฟังก์ชันคืออะไรให้ไปอ่านที่นี่ก่อนนะ)
สำหรับวิชาคณิตศาสตร์แล้ว ฟังก์ชันคือสิ่งที่ รับค่าจำนวนหนึ่งเข้าไป (input) แล้วทำการคำนวณให้ได้ผลลัพธ์ 1 ค่าแล้วตอบกลับมา (output)
เช่น
f(x) = x + 10
นั่นหมายความว่าเราสร้างฟังก์ชัน f
ขึ้นมาซึ่งจะรับค่าเข้าไปแล้วเอาไป +10
เวลาจะเรียกใช้งานก็เช่น f(20)
ก็จะได้ค่าเท่ากับ 20+10
= 30
...ตรงไปตรงมามาก ซึ่งถ้าเราจะเขียนแบบภาษาโปรแกรม เราก็จะได้โค้ดแบบนี้
function plusTen(x){
return x + 10
}
แต่การเขียนฟังก์ชันในการเขียนโปรแกรมนั้น เราสามารถสร้างฟังก์ชันที่ไม่รับ input (หรือก็คือ parameter นั่นแหละ) อะไรเลย มีแต่ return ค่ากลับมาเลย หรือแม้แต่สร้างฟังก์ชันที่ไม่ตอบค่าอะไรกลับมาเลยก็ยังไงนะ
// ตอบ 10 ทุกรอบที่มีการเรียกใช้งาน ไม่ค่อยมีประโยชน์เท่าไหร่
function justTen(){
return 10
}
// รับค่าเข้าไปคำนวณเล่นๆ ไม่ตอบอะไรกลับมาเลย ดูไร้ประโยชน์มาก
function justReceive(x){
x = x + 10
}
📢 Note: ดังนั้นการสร้าง Pure Function ที่ดี ต้องรับ input เข้าไปอย่างน้อย 1 ตัว แล้ว return ค่ากลับ 1 ค่าเสมอ
ฟังก์ชันนั้นถึงจะเอามาใช้งานได้อย่างมีประโยชน์นะ
Side-Effect
แต่! สำหรับการเขียนโปรแกรมเราสามารถเขียนอะไรได้มากกว่านั้น นั่นคือการทำให้ฟังก์ชันนั้นมี "Side-Effect" คือการที่ฟังก์ชันสามารถยุ่งกับค่าตัวแปรภายนอกได้ เช่น
function displayPlusTen(x){
print(x + 10)
}
นั่นคือเราเปลี่ยนจากการรีเทิร์นค่ากลับเป็นเอาค่าที่คำนวณได้ print ค่าออกมาแทน
นอกจากนี้ Side-Effect ยังรวมถึงการรับ input เข้ามาจากผู้ใช้ หรือการเปลี่ยนแปลงค่าตัวแปรระดับ global ด้วย
// Side-Effect จากการรับค่าจากผู้ใช้เข้ามา
function readX(){
var x = input('enter x: ')
...
}
// Side-Effect จากการอ่านหรือเขียนค่าตัวแปรภายนอก
var count = 0
function increment(){
count = count + 1
}
Side-Effect จะเกิดขึ้นเมื่อฟังก์ชันมีการอ่าน/เขียนตัวแปรนอกฟังก์ชัน หรือมีการติดต่อกับ I/O (input หรือ output) ภายนอก ไม่ว่าจะเป็นการอ่านค่าจากผู้ใช้, การแสดงผลทางหน้าจอ, การอ่าน/เขียน Database, หรือแม้แต่การเชื่อมต่อ API
ปัญหาของ Side-Effect คือมันทำให้ฟังก์ชันไม่เพียว! พอฟังก์ชันนั้นไม่ใช่ Pure Function สิ่งที่ตามมาก็คือ state ที่มากมาย ยากต่อการเทสโปรแกรม
จากตัวอย่างข้างบน เราสร้างฟังก์ชัน countIt()
ขึ้นมาเพื่อนับเลขไปเรื่อยๆ ทุกครั้งที่มีการเรียกใช้งาน ทีนี้ให้ลองดูที่โปรแกรมบรรทัด (1), (2), และ (3) เราจะพบว่าผลลัพธ์จากฟังก์ชัน countIt()
นั้นเปลี่ยนไปเรื่อยๆ ตามตัวแปรภายนอกของโปรแกรม หรือที่เราเรียกว่า State ของโปรแกรมนั่นเอง
ถ้า state ของโปรแกรมในขณะนี้รันมาถึงจังหวะที่ count=1
ฟังก์ชัน countIt()
ก็จะเปลี่ยน state เป็น count=2
แล้วตอบคำตอบเป็น 2
นั่นแสดงว่าเราไม่สามารถคาดเดาผลลัพธ์ที่ countIt()
จะตอบกลับมาได้เลย ซึ่งมีโอกาสที่จะทำให้เกิดบั๊กตอนเขียนโปรแกรมได้สูงมาก ยิ่งถ้าโค้ดของเราไม่ค่อยเป็นระเบียบด้วยนะ
แล้ว method ใน OOP ล่ะ?
สำหรับภาษายุคใหม่ๆ ที่ขาดไม่ได้เลยก็คือฟีเจอร์การเขียนโปรแกรมแบบ Object-Oriented นั่นเอง แล้วสำหรับภาษา OOP แท้ๆ เช่น Java เนี่ยจะกำหนดว่าทุกสิ่งทุกอย่างจะต้องอยู่ในรูปของ class เท่านั้น ไม่สามารถสร้างฟังก์ชันลอยๆ อยู่ข้างนอกได้
ใน OOP ไม่มีฟังก์ชัน แต่จะเรียกว่า method แทน
ส่วนความแตกต่างระหว่าง function vs method ก็คือเมธอดสามารถใช้งานตัวแปรภายในของคลาสที่เราเรียกว่า properties ได้นั่นเอง
class People {
var name
function setName(name){
this.name = name
}
function sayHi(){
return 'my name is ${name}'
}
}
โดยส่วนใหญ่แล้ว method มักจะมีการ"ยุ่งเกี่ยว"กับตัวแปรภายนอกแบบนี้เสมอ ซึ่งไม่ใช่เรื่องผิดอะไรในโลกของ OOP แต่ถ้าในมุมของ FP แล้วนั้น ทำให้เมธอดส่วนใหญ่ไม่มีคุณสบมัติของ pure function ยังไงล่ะ
แล้วปัญหาของการที่เราไม่เขียนเมธอดให้เป็น pure function จะทำให้เกิดปัญหาอะไร?
ลองดูตัวอย่างต่อไป
class WickedService {
var x = 1
function foo(){
...
}
function displayX(){
print(x) // 1
foo()
print(x) // ???
}
}
ลองดูที่เมธอด displayX()
จะเห็นว่ามีคำสั่งปริ๊นค่า x
อยู่ สมมุติว่าครั้งแรกที่คำสั่งนี้ทำงานค่า x
มีค่าเป็น 1
ดังนั้นความคาดหวังของเราคือถ้าเราปริ๊นค่า x
อีกทีนึง ค่าที่ได้ก็ควรจะเป็น 1 เหมือนเดิม เพราะในเมธอดนี้ไม่ได้มีการเซ็ตค่า x
ใหม่เลย
แต่ก็ไม่ได้เป็นแบบนั้นทุกครั้ง! เพราะในระหว่างที่ displayX()
ทำงานอยู่นั้น มีการเรียกใช้ foo()
ขั้นกลางด้วย และมีโอกาสที่เมธอด foo()
จะทำการเซ็ตค่า x
ใหม่ทำให้มันเปลี่ยนแปลงไประหว่างที่ displayX()
กำลังทำงานอยู่
เชื่อว่าคนที่เขียนโปรแกรมแบบ OOP มาแล้วน่าจะเคยเจอบั๊กที่เกิดการเหตุการณ์แบบนี้ คือเรากำลังเขียนโปรแกรมอยู่ในเมธอดหนึ่งอยู่ แล้วอยู่ๆ ค่าที่ชัวร์ว่ามันน่าจะเป็นค่านี้แน่ๆ ก็เปลี่ยนแปลงไปโดยไม่รู้สาเหตุ (ก่อนจะไปพบว่าจริงๆ มีเมธอดอีกตัวหนึ่งแอบเปลี่ยนค่ามัน) กว่าจะหาเจอก็เสียเวลาไปครึ่งวันซะแล้ว!
Purity คุณสมบัติแห่งความบริสุทธิ์
เพราะว่า pure function ไม่อนุญาตให้มี Side-Effect จากภายนอกมายุ่งกับโลจิคการทำงานภายในทั้นสิ้น มันเลยมีคุณสมบัติอีกอย่างนั่นคือ...
ไม่เรียกใช้ฟังก์ชันกี่ครั้ง ถ้าinputเหมือนเดิม outputก็ต้องเหมือนเดิมตามไปด้วย!
เช่น ถ้าเรามีฟังก์ชันตัวนึง ที่ใส่ค่า 6 เข้าไปแล้วมันตอบค่า 7 กลับมา แบบนี้
f(6) => 7
ไม่ว่าเราจะเรียก f(6)
อีกกี่ครั้ง มันก็จะต้องตอบ 7 อย่างแน่นอน
f(6) => 7
f(6) => 7 ไงล่ะ!
f(6) => ยังคงเป็น 7
f(6) => ก็ยัง 7 อยู่นะ
คุณสมบัตินี้เรานี่แหละที่เราเรียกว่า purity หรือการ memorize คำตอบของฟังก์ชันไว้ได้ (จำคำตอบไว้ได้ เพราะไม่ว่าจะเรียกฟังก์ชันกี่รอบ มันก็ต้องได้คำตอบเดิม)
ซึ่งถ้าฟังก์ชันของเราทำงานเยอะกว่าจะได้คำตอบมา จะเป็นอะไรที่ดีมาก เพราะหลังจากได้คำตอบมาแล้ว หากเราเรียกฟังก์ชันเดิมซ้ำพร้อมกับ input เดิมเราจะไม่ต้องรันฟังก์ชันนั้นอีก (เพราะมีการจำคำตอบหรือ memorize ไว้แล้วไงล่ะ)
Pure Function จะมีคุณสมบัติ Purity หรือการที่คำตอบจะต้องออกมาเหมือนเดิมทุกรอบ (ถ้า input เหมือนเดิม) ซึ่งจะนำมาสู่คุณสมบัติ Memorize ที่เป็นจดจำคำตอบไว้ ซึ่งจะมีประโยชน์มากหากฟังก์ชันนั้นมีการทำงานที่หนักกว่าจะได้คำตอบมา (เช่นฟังก์ชันที่
O(n)
เยอะๆ )📢 Note: ในวิชา Data Structure and Algorithm เรื่องนี้เป็นเรื่องเดียวกับแนวคิดของ Dynamic Programming นั่นเอง!
แต่ถ้าการเรียกใช้งานแต่ละครั้ง ให้ค่ากลับมาไม่เหมือนกัน แบบนี้
f(6) => 7 //ครั้งแรกตอบแบบนี้
f(6) => 3 //ครั้งที่สองตอบ 3 แทน
f(6) => 5 //ต่อไปเปลี่ยนไปตอบ 5 !
แบบนี้ไม่ใช่ pure function ก็จะทำให้ไม่มีคุณสบมัติ purity ไปด้วย
First-Class Function
ชื่อของหัวข้อนี้มาจากคำว่า first class citizen หรือประชากรชั้นหนึ่ง ไม่ใช่คนต่างชาติหรือต่างด้าว ก็คือประชาชนของประเทศนั่นแหละ ซึ่งก็จะมีสิทธิพื้นฐานคือทำได้ทุกอย่าง
ทีนี้ถ้าในมุมของการเขียนโปรแกรม การจะเป็นประชากรชั้นหนึ่งได้นั้นหมายความว่าเราจะต้องสร้าง type ชนิดนั้นจะต้องเซ็ตเป็นตัวแปรได้ = First-Class Function ก็คือการที่เราสามารถกำหนดให้ฟังก์ชันกลายเป็นตัวแปรได้นั่นเอง
function add(x,y){
return x + y
}
// call function: ผลลัพธ์ก็จะได้ 1 + 2 = 3
var a = add(1, 2)
// กำหนดตัวแปร f ให้เท่ากับฟังก์ชัน, แต่ไม่ได้เป็นการรันฟังก์ชันนะ สังเกตดูว่าไม่มี () ต่อหลัง
var f = add
// ตัวแปร f ในตอนนี้มีค่าเป็นตัวฟังก์ชัน add นั่นเอง
var b = f(1, 2)
การสร้างฟังก์ชันแบบปกติจะประกอบด้วยการ declare คือการกำหนดว่าฟังก์ชันจะทำงานยังไง และการ call หรือการเรียกใช้งานฟังก์ชันที่สร้างขึ้นมา
แต่ก็มีข้อแตกต่างระหว่างการ declare ฟังก์ชันกับการสร้างฟังก์ชันใส่ตัวแปร
นั่นคือการ declare ฟังก์ชันนั้น ส่วนใหญ่จะมีสโคปการใช้งานแบบ global แต่ถ้าใช้แบบตัวแปร ก็จะใช้กฎเดียวกับการสร้างตัวแปร นั่นคือก่อนเราสร้างตัวแปรเราจะใช้งานมันไม่ได้
// ตรงนี้สามารถเรียกใช้ f() ได้
// แต่เรียกใช้ g() ไม่ได้เพราะยังไม่ถึงบรรทัดที่ประกาศตัวแปร
function f(){ ... }
var g = function(){ ... }
// ตั้งแต่ส่วนนี้ไป ถึงจะเรียกใช้ g() ได้
High-Order Function
คุณสมบัตินี้จริงๆ เป็นคุณสมบัติที่ต่อเนื่องจาก First-Class Function นั่นคือเมื่อพอเรากำหนดฟังก์ชันเป็นตัวแปรได้ เราก็สามารถส่งฟังก์ชันผ่าน parameter ของฟังก์ชัน (อีกตัวหนึ่ง) หรือจะเป็นการรีเทิร์นฟังก์ชันกลับมาก็ยังได้
function calculate(a, b, operator){
return operator(a, b)
}
function add(x, y){
return x + y
}
function sub(x, y){
return x - y
}
calculate(2, 1, add) // 2 + 1 = 3
calculate(2, 1, sub) // 2 - 1 = 1
ตามตัวอย่างข้างบน เราสามารถสร้างฟังก์ชัน calculate
ที่รับฟังก์ชันอีกตัวหนึ่งเข้ามาเพื่อทำการโปรเซสค่า โดยที่ไม่ต้องรู้ว่ามันทำการคำนวณยังไง เราสามารถเปลี่ยนแปลงการคำนวณของ calculate
ได้ง่ายๆ โดยการสร้างฟังก์ชันใหม่แล้วใส่เข้าไปกี่ตัวก็ได้ เช่น add
, sub
(คอนเซ็ปของ encapsulation ของฟังก์ชันไงล่ะ!)
หรือจะใช้ในอีกรูปแบบหนึ่งคือการรีเทิร์นฟังก์ชันกลับมาก็ได้เช่นกัน แบบนี้
function priceCalculatorBuilder(type){
if(type == NORMAL) return function(unit){ return unit * 10 }
if(type == DISCOUNT) return function(unit){ return unit * 10 - 100 }
if(type == FREE) return function(unit){ return 0 }
}
สมมุติเราต้องการฟังก์ชันชื่อ priceCalculator
เอาไว้คำนวณจำนวนเงิน แต่ปัญหาคือมีสูตรการคำนวณหลายแบบเหลือเกิน
แทนที่จะสร้างฟังก์ชันตรงๆ ที่มีโลจิคการคำนวณมากมายอยู่ข้างใน เราก็เลือกที่จะสร้าง priceCalculatorBuilder
หรือก็คือฟังก์ชันที่เอาไว้สร้างเจ้า priceCalculator
อีกทีหนึ่งแทน
สังเกตดูว่าสิ่งที่ฟังก์ชันรีเทิร์นกลับคือฟังก์ชันอีกตัวหนึ่ง นั่นหมายความว่า การเรียนใช้ง่านในครั้งแรกจะยังไม่ได้ค่าจำนวนเงินออกมา แต่จะเป็นฟังก์ชันที่เอาไว้คำนวณเงินแทน (ตาม type ที่ใส่เข้าไปว่าจะให้คำนวณแบบไหน)
var priceCalculator = priceCalculatorBuilder(type)
print(priceCalculator(5))
อธิบายเพิ่มเติมเกี่ยวกับแนวคิดของฟังก์ชันในโค้ดนี้กันหน่อย
ตามปกติแล้ว ฟังก์ชันมีคุณสมบัติ encapsulation (หรือเรียกว่า การห่อหุ้ม ในภาษาไทย😳) แปลง่ายๆ คือการหุ้มโลจิคของการคำนวณเอาไว้ภายใน แต่ priceCalculator
นั้น ตัวมันไม่ได้หุ้มโลจิคการคำนวณไว้ตรงๆ เพราะจริงๆ มันเป็นแค่ทางผ่านเท่านั้น โลจิคอีกส่วนหนึ่งถูกหุ้มอยู่ใน priceCalculatorBuilder
นั่นเอง
สรุป
- ความจริงคอนเซ็ปของ Pure Function นั้นง่ายมาก นั่นคือ input --> process --> output โดยห้ามมีค่าภายนอกหรือ I/O มาเขียนข้องเลย...แม้แต่นิดเดียวกันไม่ได้ !
- ปัญหาคือตั้งแต่เราเริ่มเขียนโปรแกรมกัน เราไม่ค่อยได้สร้าง Pure Function กันเท่าไหร่ มักจะสร้างให้มันมี Side-Effect เล็กๆ น้อยๆ เสมอ (เอาง่ายๆ เช่นการ
print()
ไงล่ะ) - First Class Function คือการยอมให้เราเก็บฟังก์ชันลงในตัวแปรได้ ซึ่งถือว่าเป็นข้อสำคัญ ที่หากเราจะเขียน FP แล้ว ภาษาโปรแกรมนั้นจะต้องมีคุณสมบัติข้อนี้
- High Order Function ไม่มีอะไรมาก พอฟังก์ชันกลายเป็นตัวแปรได้แล้ว มันก็สามารถถูกส่งไปผ่าน parameter หรือจะรีเทิร์นค่ากลับมาก็ยังได้
Next~
ในบทต่อไป เราจะพูดกันต่อในเรื่องของการสร้างฟังก์ชันนิรนามและการปิดล้อม (Closure) ของฟังก์ชัน
Top comments (0)