FP(01) – Pure, First-Class, High-Order: พื้นฐานแห่ง Functional Programming

developer

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

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


Category Theory & Lambda Calculus

ในโลกของ FP นั้นมีหลายสิ่งที่ต้องเรียนรู้กัน แต่เราจะมาเริ่มกับสิ่งที่เป็นพื้นฐานที่สุดกันก่อน นั่นคือ

  1. Pure Function
  2. First-Class Function
  3. 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, add) // 2 + 1 = 3

ตามตัวอย่างข้างบน เราสามารถสร้างฟังก์ชัน 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 นั่นเอง

สรุป

  1. ความจริงคอนเซ็ปของ Pure Function นั้นง่ายมาก นั่นคือ input –> process –> output โดยห้ามมีค่าภายนอกหรือ I/O มาเขียนข้องเลย…แม้แต่นิดเดียวกันไม่ได้ !
  2. ปัญหาคือตั้งแต่เราเริ่มเขียนโปรแกรมกัน เราไม่ค่อยได้สร้าง Pure Function กันเท่าไหร่ มักจะสร้างให้มันมี Side-Effect เล็กๆ น้อยๆ เสมอ (เอาง่ายๆ เช่นการ print() ไงล่ะ)
  3. First Class Function คือการยอมให้เราเก็บฟังก์ชันลงในตัวแปรได้ ซึ่งถือว่าเป็นข้อสำคัญ ที่หากเราจะเขียน FP แล้ว ภาษาโปรแกรมนั้นจะต้องมีคุณสมบัติข้อนี้
  4. High Order Function ไม่มีอะไรมาก พอฟังก์ชันกลายเป็นตัวแปรได้แล้ว มันก็สามารถถูกส่งไปผ่าน parameter หรือจะรีเทิร์นค่ากลับมาก็ยังได้

ในบทต่อไป เราจะพูดกันต่อในเรื่องของการสร้างฟังก์ชันนิรนามและการปิดล้อม (Closure) ของฟังก์ชัน

648 Total Views 3 Views Today
Ta

Ta

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

You may also like...

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องที่ต้องการถูกทำเครื่องหมาย *