JavaScript ฉบับมือใหม่ – ตอนที่ 3 Js มองในมุมของ Functional Programming

developer

บทความชุด: JavaScript/Fundamental for beginner

รวมเนื้อหาการใช้ภาษา JavaScript สำหรับมือใหม่ ตั้งแต่หลักการ แนวคิด การทำงานกับเว็บทั้งฝั่งclient-server library&frameworkที่น่าสนใจ จนถึงมาตราฐานการเขียนแบบใหม่ ES6 (ECMAScript6)

บทความนี้จะมาพูดถึงเรื่องของ JavaScript ต่อกันอีกเรื่องกับหัวข้อของ Functional Programming ซึ่งเป็นรูปแบบการเขียนโปรแกรมแบบหนึ่ง ซึ่งเจ้า JavaScript นี่ก็มีลูกเล่นกับ function เยอะซะด้วย

ทบทวนการสร้าง Function ใน JavaScript

การสร้างฟังก์ชันในภาษา JavaScript สามารถทำได้ตั้งแต่การสร้างแบบพื้นฐานด้วยคำสั่ง

let add = new Function('a', 'b', 'return a + b');

ความหมายคือ "สร้างฟังก์ชันที่รับ parameter 2 ตัวคือ a กับ b แล้วรีเทิร์นค่าผลบวกกลับมา" ซึ่งแน่นอนว่าเราไม่แนะนำให้เขียนแบบนี้นะ! แต่โค้ดนี้ทำให้เราเห็นได้อย่างหนึ่งว่า function ใน JavaScript นั้นเป็น Object ชนิดหนึ่ง!

ต่อไปมาดูวิธีแบบปกติกันบ้าง

function add(a, b){
    return a + b
}

หรือตั้งแต่ JavaScript เวอร์ชั่น ES6 เป็นต้นไป (ตอนนี้น่าจะใช้ได้ทุกระบบไม่มีปัญหาแล้วล่ะ) สามารถสร้าง function ในรูปแบบของ Arrow Function ซึ่งเดี๋ยวเราจะพูดถึงรายละเอียดกันต่อข้างล่างนะ

const add = (a, b) => {
    return a + b
}

//แต่เนื่องจาก body มีแค่ 1 return statement เราเลยย่อได้เป็น

const add = (a, b) => a + b

 

เอาล่ะ ทบทวนกันแค่นี้ ถ้าใครยังสร้าง function ไม่เป็น แนะนำให้อ่านบทความก่อนๆ เอานะ

ต่อไปเราจะขึ้นเรื่องของ functional กันแล้ว

 

Functional Programming เบื้องต้นกันหน่อย

คอมพิวเตอร์เกิดขึ้นมาบนพื้นฐานของวิชาคณิตศาสตร์ (เห็นคำว่าคณิตศาสตร์แล้วอย่าเพิ่งกดปิดบทความหนีไปนะ! บทความนี้ไม่พูดเรื่องคณิตศาสตร์ยากๆ หรอก เพราะคนเขียนก็ไม่ค่อยเก่งเลข ฮา) ว่ากันตามตรงคอนเซ็ปของการเขียนโปรแกรมแบบฟังก์ชันนั้นมีมาก่อน imperative (การเขียนแบบเป็นขั้นตอนเช่นภาษา C) ซะอีกนะ

นิยายของฟังก์ชัน

ไม่ต้องไปยกคำอธิบายของใครมาหรอก ใช้คำง่ายๆ เลยละกันนะ

"function คือกล่องดำปริศนาที่ใส่ค่าเข้าไปแล้วให้ผลลัพธ์เป็นคำตอบออกมา"

เป็นสิ่งที่คนเขียนโปรแกรมแบบเราๆ รู้จักกันมาตั้งแต่คอร์สการเขียนโปรแกรมพื้นฐานแล้วล่ะ (ว่าแต่ ใช้กันเป็นรึเปล่า?)

(อันนี้เคยเขียนบทความแปลเรื่อง functional programming มาครั้งนึงแล้วใน มารู้จักกับ Functional Programming สิ่งที่คุณต้องรู้ในตอนนี้กันเถอะ ขอยกหัวข้อสำคัญๆ มาอีกรอบเพื่อทบทวนความจำ)

1. Pure Functions

คือฟังก์ชันแบบมาตราฐาน น่าจะเป็นฟังก์ชันแบบแรกที่ทุกคนเรียนกัน โดยกฎของ Pure Function (ภาษาไทยเรียก "ฟังก์ชันพิสุทธิ์") มีอยู่คอนเซ็ปหลักๆ เลยคือ "Pure Function จะรับค่า Input (ที่เรียกว่า parameter) ไปจำนวนหนึ่ง แล้วส่งผลลัพท์ออกมาเป็น output 1 ค่า" ... อ่ะ ดูตัวอย่าง

//ฟังก์ชันแบบรับ parameter 1 ตัว
function plusOne(x){
    return x + 1
}

//ฟังก์ชันแบบรับ parameter 2 ตัว
function add(a, b){
    return a + b
}

//ฟังก์ชั่นแบบไม่รับ parameter แบบนี้ไม่ค่อยมีประโยชน์เท่าไหร่
function gravity(){
    return 9.8
}

//ฟังก์ชันที่ไม่มีการ return ค่ากลับมา แบบนี้ไม่ถือว่าเขียนตามคอนเซ็ปของ pure function
function display(name){
    console.log(name)
}

ซึ่งโดยส่วนใหญ่แล้วฟังก์ชันที่รับค่า input เดิมมักจะให้ค่า output เหมือนเดิม เช่น add(1,2) = 3 ถ้าเราเรียกใช้ add(1,2) อีกครั้งคำตอบที่ได้ก็สมควรจะได้ 3 เท่าเดิม

แต่ก็ไม่จำเป็นนะว่ามันจะต้องเป็นแบบนี้ทุกฟังก์ชัน เช่น ฟังก์ชันสร้าง hash-key หรือ token แบบ "one-time use" และฟังก์ชันที่อิงกับการ random

สำหรับภาษา JavaScript เราอนุญาตให้ function หยิบตัวแปรข้างนอก (global) เข้ามาใช้ได้ เช่น

let sum = 0;
function accumulate(x){
    sum = sum + x;
    return sum;
}

สังเกตว่าฟังก์ชันนี้ถึงจะใส่ input เป็นค่าเดิมก็อาจจะได้ output ที่เปลี่ยนไปทุกรอบ เพราะการทำงานของฟังก์ชันได้รับผลจากค่าภายนอกฟังก์ชัน หรือ External Factor

แปลว่าค่า output ของฟังก์ชันเราอันนี้ ไม่ได้ขึ้นอยู่กับตัวเอง (คือโปรเซสภายใน หรือ input) แต่อิงตามค่าภายนอกด้วย แบบนี้ไม่เรียกว่า pure function นะ!

มาถึงตอนนี้อาจจะสงสัยว่า แต่มันเขียนได้ไม่ใช่เหรอ?

ใช่แล้ว มันเขียนได้ แต่ถือว่าไม่ใช่ pure function ไงล่ะ

ถ้าเลื่ยงได้ ก็ควรจะเลียงกัน แล้วเขียนในรูปแบบ pure function จะดีกว่านะ

2. First-Class Functions

First-Class หมายถึงการเป็นประชากรชั้นหนึ่ง (คล้ายๆ คำว่า First-Class Citizen ที่หมายถึงพลเมืองชั้นหนึ่งของประเทศ ไม่ใช่คนต่างด้าว)

หมายถึงการที่เราสามารถจับฟังก์ชันยัดลงไปเป็นตัวแปรได้ (ปกติเวลาเราสร้างตัวแปรก็มีแค่ int double string object อะไรประมาณนั้นใช่มั้ยล่ะ) ทีนี้ถ้าเรามองว่าฟังก์ชันก็เป็นตัวแปรได้ล่ะ จะออกมาเป็นยังไง

function add(a, b){
    return a + b
}

อันนี้คือฟังก์ชันจากข้อที่แล้ว ปกติเวลาเราจะใช้งานฟังก์ชัน add เราก็ต้องเรียกผ่าน add(x,y) งั้นลองมาดูขั้นต่อไป

function add(a, b){
    return a + b
}

let addIt = add

add(1,2) //3
addIt(1,2) //3

นั่นคือเราสามารถสร้างตัวแปร addIt เอามาเก็บค่า add (ความจริงคือไม่ได้เก็บค่า แต่เป็นการ pointer ไปที่ฟังก์ชั่นนั้น) สังเกตดีๆ ว่าเราไม่ได้สั่งว่า addIt = add() เพราะแบบนั้นจะเป็นการสั่งให้ฟังก์ชั่นทำงาน addIt จะไม่ได้เก็บค่าฟังก์ชัน แต่เก็บผลลัพธ์แทน

ส่วนการใช้งาน เราสามารถสั่งให้ addIt ทำงานได้เช่นเดียวกับการสั่ง add() นั่นคือการเติม () ลงไปข้างหลัง

หรือแบบนี้เลยก็ได้

const addIt = function add(a, b){
    return a + b
}

add(1,2) //3
addIt(1,2) //3

อันนี้ เราสร้างตัวแปร addIt มารับฟังก์ชันเมื่อกี้อีกที แล้วเราก็สามารถเรียกใช้งาน addIt(1,2) แบบนี้ได้เลย! ว้าว!

หรือแม้แต่ลดรูปให้เหลือแค่นี้ก็ได้ เพราะชื่อฟังก์ชัน add ไม่ได้ใช้งานเลย เลยไม่ต้องไปตั้งชื่อให้มันก็ได้

const add = function(a, b){
    return a + b;
}

//เลยเป็นที่มาของการเขียนให้อยู่ในรูปของ Arrow Function แบบนี้แทน

const add = (a, b) => a + b;

การสร้างฟังก์ชันแบบนี้เราเรียกว่า anonymous function หรือฟังก์ชันนิรนาม และเพราะมันไม่มีชื่อ เราเลยต้องจับมันใส่ลงในตัวแปรยังไงล่ะ และใช้ตัวแปรนั้นแทนฟังก์ชันตัวนี้เลย

(และเนื่องจากฟังก์ชันนั้นไม่ควรเปลี่ยนวิธีการทำงานได้ ดังนั้นเขาเลยใช้ const แทน let นะ)

การมองฟังก์ชันเป็นตัวแปร สามารถเปลี่ยนค่าได้แบบนี้เราเรียกว่าคุณสมบัติแบบ First-Class ซึ่งเป็นการล้อกับคำว่า First-Class Citizen หรือประชากรชั้นหนึ่งของสังคม ถ้าฟังก์ชันเป็นประชากรชั้นหนึ่ง มันก็สามารถเก็บลงในตัวแปรได้เหมือนยังประมาณนั้นล่ะ

3. High-Order Function

หมายถึงฟังก์ชันที่รับค่า input เข้ามา (parameter) เป็น function หรือไม่ก็ให้ค่า output ออกมาเป็น function

ซึ่งเป็นการใช้ประโยชน์จากคุณสมบัติ First-Class ที่มองฟังก์ชันเป็นตัวแปรได้ และในเมื่อมันเป็นตัวแปร เราก็ส่งมันผ่าน parameter เข้าไปได้

function add(a, b){
    return a + b
}

function operate(x, y, op){
    return op(x, y)
}

let result = operate(1, 2, add)

ในตัวอย่างนี้เรามีฟังก์ชันอยู่ 2 ตัว คือ add กับ operate ซึ่ง add ก็ทำงานง่ายมาก ก็แค่บวกเลขนั่นเอง

แต่ในฟังก์ชันที่สองคือ operate นั้นจะมีการรับตัวแปร op เข้าไปด้วย ซึ่งในบรรทัดที่เรา call function เราก็ส่ง add เข้าไป นั่นหมายความว่าในฟังก์ชัน operate นั้นตัวแปร op ก็จะทำงานแบบ add ที่ใส่เข้าไปนั่นเอง
แน่นอนว่าเราสามารถใส่ฟังก์ชันอะไรเข้าไปก็ได้ แต่ตัว parameter จะต้องเท่ากับเวลา call function
function sub(a, b){
    return a + b
}

function operate(x, y, op){
    return op(x, y)
}

let result = operate(1, 2, sub)

หรืออีกวิธีก็คือใส่ Anonymous Function เข้าไปเลยก็ได้ (ในตัวอย่างนี้ใช้รูปแบบ Arrow Function)

function operate(x, y, op){
    return op(x, y)
}

let result = operate(1, 2, (a, b) => a + b)

ต่อไป คือเราสามารถให้ฟังก์ชัน return ฟังก์ชันออกมาอีกทีก็ได้ แบบนี้

function getFunction(){
    return y => y + 1
}

let f = getFunction()
let result = f(2) //3
โค้ดข้างบนนี้ getFunction นั้นตอบฟังก์ชันกลับมาหนึ่งตัว (เขียนอยู่ในรูปแบบ Arrow Function นะ) แล้วก็เอาตัวแปร f ไปรับ
ดังนั้น f ก็จะมีค่าเท่ากับฟังก์ชันที่ getFunction รีเทิร์นกลับออกมาให้นั่นเอง (getFunction ตอบฟังก์ชันที่รับ parameter y เข้าไปแล้วเอาไป +1)

หรือเราสามารถให้ getFunction รับ parameter เข้าไปด้วยก็ได้เช่น

function getFunction(x){
    return y => x + y
}

let f = getFunction(1)
let result = f(2) //3

ในตัวอย่างนี้ เราให้ getFunction รับ parameter เข้าไปได้หนึ่งตัว แล้วเอาไปใช้ต่อใน function ที่รีเทิร์นกลับมา

ซึ่งคุณสมบัตินี้เราจะพูดต่อกันในหัวข้อถัดไป

4. Closures

มาจาก close หรือก็คือคุณสมบัติการปิดล้อม ในเรื่องนี้เราจะต้องทำความเข้าใจกับเรื่องของ global และ local กันซะก่อน

ใน JavaScript เราสามารถสร้าง function ซ้อนๆ กันได้ (ไม่ใช่ทำได้ในทุกภาษา แต่ JavaScript ทำได้ล่ะ!) เช่น

function a(){
    let x = 10
    function b(){
        let y
        function c(){
            let z
            z = x + y // Ok! ฟังก์ชั c สาสารถเรียกใช้ตัวแปรได้หมดเลย
        }
        y = x // Ok! ฟังก์ชัน b สามารถเรียกใช้ x ได้
        y = z // Error! z undefined ในฐานะ b เราไม่รู้จักตัวแปร z เพราะมันถูกสร้างใน c
    }
    x = y + z // Error! y undefined ในฐานะ a เราไม่รู้จักทั้งตัวแปร y และ z เพราะมันถูกสร้างภายในฟังก์ชันข้างในทั้งคู่เลย
}

ในตัวอย่างนี้เรามีฟังก์ชัน a, b และ c และมีการสร้างตัวแปร x, y และ z ขึ้นมาด้วย

แต่ขอบเขตของการเรียกใช้ตัวแปร (เรียกว่า scope นะ) ไม่ใช่ว่าทุกตัวจะเรียกใช้กันได้ ในที่นี้ จะขออธิบายในฐานะฟังก์ชัน b นะ

  • local variable - ตัวแปรที่ประกาศภายในพื้นที่ของฟังก์ชัน คือ y
  • global variable - ตัวแปรภายนอกฟังก์ชัน คือ x
  • variable in inner function - ตัวแปรที่สร้างอยู่ภายในฟังก์ชันที่สร้างอยู่ภายในฟังก์ชันเรา (งงมั้ย?) คือ z เพราะในฐานะฟังก์ชัน b แล้ว มีการสร้างฟังก์ชัน c อยู่ภายใน และ z เป็นตัวแปรใน c (ไม่ถือเป็น local ของ b นะ แต่ถือเป็น local ของ c)

ตามปกติแล้ว การเรียกใช้งานตัวแปร จะเรียกได้แค่สโคปของ local variable และ global variable เท่านั้น

ส่วน variable in inner function นั้นจะไม่สามารถเรียกใช้งานได้เลย เพราะตัวแปรนั้น "โดนปิดล้อมโดยฟังก์ชันอยู่" เราเรียกสถานการณ์แบบนี้ว่า "Closure" นั่นเอง

ซึ่ง Closure นั้นสามารถใช้ร่วมกับ High-Order ได้เช่นในตัวอย่างเมื่อกี้ (แต่ของเปลี่ยนเป็นรูปแบบ function ธรรมดานะ จะได้ดูง่ายขึ้น)

function getFunction(x){
    return function(y){
        return x + y
    }
}

เพราะแบบนี้ สำหรับฟังก์ชันตัวใน เลยสามารถหยิบตัวแปร x (ในฐานะฟังก์ชันตัวใน x คือ global variable) เข้ามาใช้งานได้เลย

ถ้าดูง่ายๆ Closure นั้นก็เหมือนการกางสโคปให้ตัวแปรนั่นเอง แต่จริงๆ มันมีอะไรเยอะกว่านั้น

ดูโค้ดต่อไปนี้นะ (โค้ดเดิมนั่นแหละ แต่เราอาจจะลืมคิดอะไรบ้างอย่างไป!)

function getFunction(x){
    return function(y){
        return x + y
    }
}
let f = getFunction(1) 
//ตรงบรรทัดนี้ ฟังก์ชัน getFunction ทำงานเสร็จแล้ว แปลว่า getFunction จะถูกทำลายทิ้ง (ตามกฎการ call function ฟังก์ชันตัวไหนทำงานเสร็จแล้วจะถูกลบทิ้งไป) แปลว่าตัวแปร x ที่สร้างในฟังก์ชันนี้ก็ต้องหายไปด้วยน่ะสิ?
let result = f(2) //3
//แต่ทำไมตรงนี้ก็ยังคำนวณค่า x+y ออกมาได้ถูกต้องอยู่ดีล่ะ?

โค้ดนี้เรามีการเรียกใช้งาน getFunction ซึ่งมีตัวแปร x เกิดขึ้นมาด้วย แล้วก็มีฟังก์ชันไร้ชื่อข้างใน (ขอเรียกว่า "inner" ละกัน) ที่ต้องมีการเรียกใช้ค่า x ไปคำนวณผลบวกด้วย

บรรทัดที่เราสั่งให้ getFunction ทำงาน มันจะตอบ inner กลับมา ซึ่งเจ้า inner นี้ยังไม่ถูกเรียกให้ทำงานก็จริง แต่มันมีการเรียกใช้งานตัวแปร x ข้างใน

คุณสมบัติของ Closure อีกอย่างคือถ้าฟังก์ชันต้องมีการเรียกใช้งาน global variable ภายนอก เช่นในเคสนี้ inner จะต้องใช้ตัวแปร x

แม้ว่า getFunction จะทำงานเสร็จจบถูกทำลายทิ้งไปแล้วก็ตาม ตัวแปร x ก็จะยังคงอยู่ต่อไป เพราะ inner ต้องการใช้งานนั่นเอง (จะถูกทำลายตามไปเมื่อ inner ทำงานเสร็จเลย)

555 Total Views 3 Views Today
Ta

Ta

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

You may also like...

1 Response

  1. 6 เมษายน 2020

    […] JavaScript ฉบับมือใหม่ ตอนที่ 3 Js มองในมุมของ Functional Programming อยู่นิดหน่อยนะ … […]

ใส่ความเห็น

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