Photo credit — Alexandre Perotto

Hello, Functional Programming!

I'Boss Potiwarakorn
FUNKTIONAL
Published in
6 min readJan 6, 2016

--

พักหลัง ๆ นี้ หลายคนคงจะได้ยินคนพูดถึงภาษาที่ยังไม่ค่อยจะเป็นกระแสหลักอย่างพวก Clojure, Scala หรือ Haskell กันมากขึ้น ซึ่งภาษาเหล่านี้เป็น functional language (หรือ object-functional ในกรณีของ Scala) ในด้านของ front-end web development เองก็พูดถึง architecture อย่าง Redux ที่ใช้ functional programming ในการจัดการกับ state และได้รับความสนใจอย่างมาก (10k stars บน Github) หรือแม้แต่ Java 8 เองก็มี lambda expression แล้ว(จะกล่าวถึง lambda expression ต่อไป) ทำให้เห็นว่า functional programming กำลังเป็นที่ได้รับความสนใจมากขึ้นเรื่อยๆ

แต่ถ้าลองหาข้อมูลดูสักหน่อยจะพบความจริงว่า จริงๆแล้ว FP (Functional Programming) เนี่ยแก่กว่า OOP เสียอีก ถ้ามันเจ๋งก็น่าจะเป็นกระแสหลักมานานแล้วสิ ทำไมเพิ่งจะมาสนใจกันนะ?

เพื่อที่จะตอบคำถามข้างบน เราคงต้องรู้จักกับ Functional Programming กันก่อน

Functional Programming คืออะไร?

functional programming เป็น programming paradigm หรือรูปแบบวิธีคิดในการเขียนโปรแกรมแบบหนึ่ง ซึ่งหลายๆคนอาจจะคุ้นเคย paradigm อื่นอย่าง object-oriented programming หรือที่ไม่ค่อยเห็นกันอย่าง logic programming (เช่นในภาษา Prolog)

จริง ๆ แล้ว functional programming มีพื้นฐานมาจาก Lambda calculus ซึ่งเป็นคณิตศาสตร์แขนงหนึ่งซึ่งคิดค้นโดย Alonzo Church ผู้เป็นอาจารย์ของ Alan Turing เจ้าของ Turing machine นั่นเอง (คนเดียวกับตัวละครในภาพยนตร์เรื่อง The Imitation Game) ถ้าใครไม่รู้จัก Turing machine อธิบายสั้น ๆ ก็คือ เป็นแบบจำลองทางคณิตศาสตร์ที่มีความสามารถระดับเดียวกับคอมพิวเตอร์ในปัจจุบัน แต่มี memory ไม่จำกัด นั่นแปลว่าเราสามารถอธิบาย algorithm ใดๆก็ตามด้วย Turing machine ได้ ซึ่งในภายหลัง Turing เองก็ได้ทำการพิสูจน์ว่า Lambda calculus กับ Turing machine นั้นสมมูลกัน สามารถใช้แทนกันได้ทุกกรณี(ไม่พูดถึงความยากลำบากในการนำไปใช้ และทำความเข้าใจ) ซึ่งแปลว่า เราสามารถใช้ functional programming มา implement algorithm ใดก็ได้ที่ computable หรือพูดง่ายๆก็คือ โปรแกรมที่เราเคยเขียนได้ สามารถแปลงมาเป็นแบบ FP ได้หมด

ถามว่าสิ่งที่เห็นได้ชัดเจนว่าแตกต่างจาก code ที่เขียนกันโดยทั่วไปกันอย่างไร? คงต้องตอบว่า โดยปกติแล้ว code ที่เขียนกันในปัจจุบัน มักจะถูกเขียนในลักษณะ imperativeในแต่ละบรรทัดของ code นั้น อธิบายขั้นตอนของการทำงานว่าต้องทำงานอย่างไร หลักๆจะเป็นการติดตามดูและคอยเปลี่ยนแปลง state หรือ “สถานะ” ของ program ซึ่งสามารถแทนได้ด้วยตัวแปรต่างๆหรือถ้าเป็น OOP ก็มักแทนด้วย object ในขณะที่ FP (รวมถึง logic programming ด้วย)นั้นจะมีลักษณะแบบ declarative คือเป็นการประกาศว่าอะไรเป็นอะไร แล้วจึงทำการประเมินค่าเมื่อ runtime ซึ่งก็คล้ายพีชคณิตที่เรียนกันตอนเด็กๆ

อาจจะไม่ค่อยเห็นภาพ จะขอยกตัวอย่างเป็นภาษา javascript (ES2015) ให้ดูนะครับ ในที่นี้จะเขียนโปรแกรมที่คัดเฉพาะเลขคู่ออกมาจาก Array ที่เก็บเลข 1- 10 เอาไว้ แล้วนำมายกกำลังสอง

imperative style

let numbers = [1, 2, 3, 4, 5, 6 ,7, 8, 9, 10];
let squaredEvens = [];
for(let i = 0; i < numbers.length; i++) {
let currentNumber = numbers[i];
if(currentNumber % 2 === 0) {
squaredEvens.push(currentNumber * currentNumber)
}
}

ในส่วนนี้ state ของ program จะถูกอธิบายด้วย arrays ชื่อ numbers และ squaredEvens สิ่งที่เราทำคือเราเปลี่ยนแปลง state ของ program โดยการ push สิ่งที่เราเลือกไปใส่ใน squaredEvens หากสังเกตดูจะเห็นว่า code ชุดนี้ทำการอธิบายลำดับขั้นตอนการทำงาน ติดตามดูและเปลี่ยนแปลง state ของ program

declarative style (functional)

const numbers = [1, 2, 3, 4, 5, 6 ,7, 8, 9, 10];const isEven = n => n % 2 === 0;
const square = n => n * n;
numbers.filter(isEven).map(square);

ในขณะที่แบบ imperative นั้นเน้นไปที่การบอกลำดับการทำงาน แบบ functional นั้นจะมองคนละมุมกันเลยครับ คือจะไม่บอกลำดับการทำงาน แค่ประกาศสิ่งต่าง ๆ ตามที่ต้องการลงไป เดี๋ยวมันจะหยิบสิ่งที่เราประกาศมาแปลผลให้เองครับ

หลายคนอาจจะไม่คุ้นกับหน้าตาแบบนี้ อย่าง isEven และ square จริงๆแล้วเป็น function ที่สามารถเขียนแบบนี้ได้ครับ(ถึงแม้จริงๆแล้วจะต่างกันเล็กน้อย แต่เป็นเรื่องของภาษา javascript จะไม่ขอกล่าวถึงในที่นี้นะครับ)

function isEven(n) { return n % 2 === 0; }
function square(n) { return n * n; }

อย่างที่บอกไว้ว่าไอเดียมันคล้าย ๆ กับพีชคณิต ในพีชคณิตมีสิ่งที่เรียกว่า expression ซึ่งมีหน้าตาแบบนี้

x = 5
y = 2 - x
ƒ(x, y) = x + (y × 3)

ถ้าเราอยากจะรู้ว่า ƒ(x, y) มีค่าเท่าไหร่ เราก็คงจะต้องทำประมาณนี้

ƒ(x, y)
= ƒ(x, 2 - x) // แทนค่า y
= ƒ(5, 2 - 5) // แทนค่า x
= ƒ(5, -3)
= 5 + (-3 × 3)
= 5 + (-9)
= -4

กระบวนการนี้เรียกว่า expression evaluation เป็นการประเมินค่า expression นั้นๆ

กลับมาดู code ของเรา ผลลัพธ์ที่เราสนใจคือผลจากการ evaluate expression ดังที่จะแสดงให้เห็นต่อไปนี้ครับ

numbers.filter(isEven).map(square);// แทนค่า numbers
[1, 2, 3, 4, 5, 6 ,7, 8, 9, 10].filter(isEven).map(square);
// แทนค่า isEven
[1, 2, 3, 4, 5, 6 ,7, 8, 9, 10]
.filter(n => n % 2 === 0).map(square);
// เลือกมาเฉพาะค่าที่ mod 2 แล้วได้ 0
[2, 4, 6 ,8, 10].map(square);
// แทนค่า square
[2, 4, 6 ,8, 10].map(n => n * n);
// นำแต่ละสมาชิกแต่ละตัวใน array มาคูณกับตัวเอง
[4, 16, 36, 64, 100]

ด้วยวิธีนี้ เราจะไม่แก้ไข state เดิม แต่จะสร้าง state ใหม่ขึ้นมาแทน กล่าวได้ว่า ถึงแม้ FP จะไม่เปลี่ยนแปลง state แต่ก็ไม่ได้จำเป็นต้อง stateless เพียงแต่มีวิธีจัดการกับ state ที่ต่างกัน

มาถึงตรงนี้คงรู้สึกได้ว่า วิธีคิดแตกต่างจากปกติพอสมควร แต่สำหรับตัวอย่างนี้ พอเข้าใจ FP ขึ้นมาบ้างแล้ว กลับไปอ่าน รู้สึกว่าแบบหลังอ่านง่ายกว่ามั้ยครับ?

หัวใจสำคัญของ Functional Programming

สิ่งที่สำคัญที่สุดของ functional programming นั้นก็คือ function จะต้องหลีกเลี่ยง side-effect หรือผลข้างเคียงที่จะเกิดต่อ function อื่นและต่อตัวเอง

  • เมื่อมี input ค่าหนึ่ง จะได้ต้องได้ output เท่าเดิมเสมอ มีชื่อเท่ ๆ ว่า referential transparency เนื่องจากเราจะไม่อ้างอิง(reference) อะไรโดยที่ไม่ประกาศลงไปใน parameter ทำให้ผลลัพธ์นั้นสามารถคาดการณ์ได้
  • ต้องไม่ mutate state หรือก็คือไม่ไปเปลี่ยนแปลงค่าของตัวแปรจำพวก global variable หรือ static variable

function ในลักษณะดังกล่าวเรียกว่า pure function เพราะมันไม่ถูกเจือปน และไม่ไปเปื้อนชาวบ้านด้วยเช่นกัน เป็นลักษณะเดียวกับ function ในคณิตศาสตร์ ซึ่งจะมีประโยชน์อย่างมากในการทำ parallel computing

ในทางตรงกันข้าม หาก function หนึ่งๆ มี side-effect จะเรียกว่า function นั้นว่า impure function

let state = [0, 0];function pureAdd(arr, a, b) { 
return [arr[0] + a, arr[1] + b];
}
function impureAdd(a, b) {
state[0]++;
state[1]++;
return [state[0] + a, state[1] + b];
}

จากตัวอย่าง เรากำหนดค่า state ไว้ แต่ pureAdd ไม่ได้ยุ่งอะไรกับมันเลย หรือถ้าจะยุ่งก็ต้อง pass เข้ามาเป็น argument ในขณะที่ impureAdd นั้นทำทุกอย่างทั้งแก้ค่า และใช้เป็นส่วนหนึ่งของ return value ด้วย ถ้าเราเห็น pureAdd([0, 0], 1, 2) ก็คงตอบได้เลยว่า [1, 2] แต่ impureAdd(1, 2) เราตอบไม่ได้ว่ามันจะมีค่าเป็นเท่าไหร่

นี่เป็นเหตุผลที่ทำให้ภาษาบางภาษา อย่างเช่น Haskell, Scala มาพร้อมกับ immutable data structure เลยเพื่อให้เราไม่สามารถแก้ค่าข้างในได้ ต้องสร้างใหม่อย่างเดียว

Language features ที่จำเป็นสำหรับ Functional Programming

การหลีกเลี่ยง side-effect คือหัวใจหลักของ functional programming เราจะเขียนยังไงก็ได้ ถ้ายังเป็นไปตาม 2 ข้อที่บอกไว้ด้านบนก็เรียกว่า functional programming ได้ แต่ถ้าภาษาที่ใช้ขาด feature ที่จะกล่าวถึงต่อไปนี้ ก็จะ apply functional programming ได้ลำบากครับ

First-class function & Higher-order function

ตอนแรกๆที่ศึกษา FP ผมสับสนกับสองคำนี้มาก เพราะมันมีเรื่องที่ซ้อนทับกันอยู่เยอะ แต่จริงๆแล้วไม่เหมือนกันเลย

First-class function คือ feature ของ programming language ที่อนุญาตให้ function นั้นเป็น first-class citizen ประชากรอันดับหนึ่งของภาษา เทียบเท่ากับ value อื่น ๆ อย่างตัวเลข หรือ string เป็นต้น ซึ่งเราสามารถ assign function ให้กับตัวแปรได้ เป็น argument หรือ return value ของ function อื่นๆก็ได้

function ในลักษณะที่รับ argument เป็น function หรือ return function ออกมามีชื่อเรียกว่า Higher-order function ที่มีชื่ออย่างนี้เข้าใจว่าเพราะเป็น function ที่เหนือกว่า function ทั่วไป เป็นตัวที่จัดการกับ function อีกที จะเห็นได้ว่า First-class function นั้นพูดถึงการปฏิบัติต่อ function ของภาษา ในขณะที่ Higher-order function นั้นพูดถึง function ที่มีลักษณะพิเศษ ซึ่งในตัวอย่างที่เคยยกไว้ก่อนหน้านี้ก็มี Higher-order function ซึ่งรับ function เป็น argument อย่าง map และ filter ซึ่งใช้บ่อยมากๆ

const isEven = n => n % 2 === 0;
const square = n => n * n;
numbers.filter(isEven).map(square);

Lambda expression & Closure

นี่ก็เป็นอีกสอง term ที่ผมสับสนมากๆในตอนแรก เพราะมักจะถูกพูดถึงพร้อมๆกัน แต่ไม่ใช่สิ่งที่คล้ายกันเลย เพียงแต่มักถูกใช้ด้วยกันบ่อยๆ

สำหรับ Lambda expression หรือบางคนก็เรียกสั้นๆว่า Lambda ซึ่งLambda expression มักถูกเข้าใจว่าเป็น anonymous function หน้าตาแบบนี้

lambda { |a, b| a + b } # rubylambda x, y: x + y # python(a, b) => a + b; // javascript(ES2015)(a: Double, b: Double) => a + b // scala(a, b) -> a + b; // java

เมื่อพูดถึง Lambda expression ใน Lambda calculus จริงๆแล้ว เป็นได้ทั้ง

  • ตัวแปร (ในเชิงคณิตศาสตร์ ซึ่งเป็นตัวแทนของค่า หรือ argument ของ function ไม่ใช่ mutable variable หรือตัวแปรแบบที่เรา update ค่าได้เรื่อยๆเวลาเขียนโปรแกรมแบบ imperative)
  • function ซึ่งใน Lambda calculus นั้น function ไม่มีชื่อ
  • application ([function][argument]) ถ้าเปรียบเทียบกับพีชคณิตก็คล้ายๆกับแทนค่า parameter ใน function เช่น ƒ(3), g(21)

ส่วน Closure เป็นสิ่งที่ผมสงสัยอยู่นานมาก เพราะมักจะเจอตัวอย่างแบบนี้

function ticker() {
let count = 0;
return () => count++;
}
let t = ticker();t(); // 0
t(); // 1
t(); // 2
t(); // 3

สำหรับตัวอย่างนี้ticker เป็น function ที่สร้าง function ที่ใช้นับเลขอีกทีหนึ่ง ทุกครั้งที่ function t ถูกเรียก ก็จะ return ตัวเลขปัจจุบัน แล้ว +1 ซึ่งจะเป็นค่าที่ถูก return ในรอบต่อไป

จะเห็นว่า ticker เป็น Higher-order function เพราะ return value เป็น function แต่นั่นไม่ได้แปลว่าจะเป็น FP หากลองสังเกตดีๆ จะเห็นว่า t ไม่ใช่ pure function แน่ๆ เพราะเมื่อเรียกแต่ละครั้ง ไม่มี input เหมือนกัน แต่ output ไม่เท่ากัน ผมจนปัญญา เลยไปถามพี่แอมป์แห่ง Proteus ว่าตกลงมันยังไงกันแน่

ผมก็พบว่า สิ่งที่ทำให้ผมไม่เข้าใจคือ ผมคิดว่า Closure เป็นส่วนหนึ่งของ FP แต่จริงๆแล้วไม่ใช่ Closure เป็นเพียงเทคนิคหนึ่งที่ใช้กับ paradigm อื่นๆก็ได้ ซึ่ง MDN (Mozilla Developer Network) ได้ให้นิยาม Closure ไว้ได้น่าสนใจดังนี้

Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure ‘remembers’ the environment in which it was created.

เขาบอกว่า Closures คือ functions ที่อ้างอิง free variables เจ้า free variables เนี่ย มันคือตัวแปรที่ไม่ได้ถูกประกาศไว้ใน parameter จากตัวอย่างด้านบน จะเห็นได้ว่า () => count++ ไม่มี parameter เลย แต่มีตัวแปร count ซึ่งโดยปกติแล้วเมื่อ execute ticker เสร็จ เจ้า count ควรจะถูก garbage collector เก็บไป แต่ในกรณีนี้ เมื่อเราบอกว่า let t = ticker(); แล้ว t จะกลายเป็น closure ซึ่งเป็น function ที่จำสภาพแวดล้อมที่สร้างมันขึ้นมา ทำให้ผลลัพธ์เป็นอย่างที่เห็น แต่ไม่เป็น FP

แล้วถ้าเราจะใช้กับ FP ล่ะ? ลองมาดูตัวอย่างกัน

function makeAdder(a) {
return b => a + b;
}
const addFive = makeAdder(5);
const addTen = makeAdder(10);
addFive(20); // 5 + 20 => 25
addTen(9); // 10 + 9 => 19

คราวนี้จะเห็นได้ว่าทั้ง makeAdder, addFive และ addTen ล้วนเป็น pure function หรือตัวอย่างที่ดูฉลาดขึ้นหน่อย

function makeSum(transFunc) {
return (a, b) => transFunc(a) + transFunc(b);
}
const sumSquared = makeSum(n => n * n);
const sumCubed = makeSum(n => n * n * n);
sumSquared(1, 2); // 1^2 + 2^2 => 5
sumCubed(1, 2); // 1^3 + 2^3 => 9

หรือเราอาจจะได้ closure เพื่อสร้าง curried function ก็ได้ แต่ขอเก็บไว้กล่าวถึงในคราวหน้านะครับ

ประโยชน์ของ Functional Programming

เนื่องจากหัวใจสำคัญของ functional programming นั้นคือการหลีกเลี่ยง side-effect เพราะฉะนั้น ประโยชน์ส่วนใหญ่ของมันก็มาจากจากจุดนี้ครับ ลองมาดูกัน

Testability — อย่างแรกเลยคือมัน test ง่าย เนื่องจากพฤติกรรมของ function นั้นคาดเดาได้ง่าย เพราะการหลีกเลี่ยง side-effect ทำให้เกิด deterministic function ถ้าใส่ input a ได้ output b แล้วผลลัพธ์จะเป็นเช่นนั้นเสมอ ทำให้เราไม่ต้องสนใจ factor อื่นๆนอกจาก argument เวลาที่เราจะ test

Parallel Computing — สำหรับ functional programming นั้น ผลจากการหลีกเลี่ยง side-effect ทำให้ function ใดๆ ไม่สามารถไปเปลี่ยนแปลง state ไม่สร้างผลกระทบใดๆ ต่อ function ที่ทำงานคู่ขนานกันได้ ทำให้เราจัดการกับการทำงานแบบคู่ขนานได้ง่ายกว่าการปล่อยให้มีการ mutate state

Readability — ประเด็นนี้ค่อนข้างจะเป็นที่ถกเถียง บางคนอาจจะอ่าน imperative code ได้เข้าใจกว่าเพราะบอกทุกขั้นตอนที่จะทำ แต่สำหรับผมแล้ว functional programming ทำให้ code อ่านง่ายกว่ามาก แต่ก็มีมีบางกรณีที่ทำให้ code อ่านเข้าใจยากมากๆเช่นกัน

เราไม่จำเป็นต้องเขียน code ให้เป็น pure functional ก็ได้ แม้ว่าเราจะเขียนแบบ imperative อยู่ เราก็สามารถยืมบางส่วนที่น่าสนใจไปใช้ได้ (ถ้าภาษา support)

ทำไมมาสนใจกันเอาป่านนี้?

อย่างที่บอกว่า functional programming นั้นแก่กว่า OOP ซะอีก แต่เนื่องจาก hardware สมัยก่อนมีประสิทธิภาพไม่ดีนัก การเขียนแบบ imperative จะทำให้ประสิทธิภาพของ program ดีกว่า แต่เมื่อถึงจุดหนึ่งที่ hardware มันโหดมากจนเราสามารถละเลยเรื่องประสิทธิภาพไปได้บ้าง

และเป็นเพราะการเกิด muti-core CPU, การคำนวณบน GPU ไปจนถึง Cloud ก็อย่างที่บอกไปว่า functional programming นั้นทำให้ parallel programming สะดวกขึ้น ถึงแม้ว่าจะต้องแลกกับประสิทธิภาพเล็กๆน้อยก็เป็นการแลกเปลี่ยนที่คุ้ม

ส่งท้าย

คงจะได้เห็นกันแล้วว่า functional programming คืออะไร เจ๋งแค่ไหน! ใครที่สนใจ functional programming ก็เข้ามาคุยแลกเปลี่ยนความคิดเห็นกันได้นะครับ ทั้งที่นี่และบน facebook เลย

สุดท้ายอย่าลืม ”ติดตามกันบน facebook” ด้วยนะครับ แล้วเจอกันคราวหน้า~

--

--

I'Boss Potiwarakorn
FUNKTIONAL

CTO @ █████; EX-ThoughtWorker; FP, Math, Stats, Blockchain & Human Enthusiast