Babel Coder

พื้นฐาน funtional programming ใน JavaScript

beginner

โค๊ดที่คุณเขียนทุกวันนี้แอบเจือปนด้วย functional programming ไหม? แรกเริ่มที่การเขียนโปรแกรมแบบฟังก์ชันถือกำเนิด ยังไม่เป็นที่นิยมสำหรับเหล่ามนุษย์ลุงมนุษย์ป้าโปรแกรมเมอร์สมัยนั้นมากนัก แต่ทำไมการเขียนโปรแกรมแบบฟังก์ชันกลายเป็นเรื่องสำคัญในยุคนี้ และส่งอิทธิพลต่อมนุษย์เงินเดือนหาเช้ากินค่ำแบบพวกเราหละ? มาร่วมหาคำตอบและเรียนรู้การเขียนโปรแกรมแบบฟังก์ชันด้วย JavaScript ในบทความนี้กันครับ ลุย!

การเขียนโปรแกรมแบบฟังก์ชันคืออะไร?

ย้อนกลับไปสมัยเราเรียน ม.4 วิชาคณิตศาสตร์จะมีบทเรียนนึงชื่อ ความสัมพันธ์และฟังก์ชัน แหม พูดเรื่องอดีตแล้วมันช่างเป็นการเช็คอายุกันเสียจริง บทนิยามที่อธิบายความหมายของฟังก์ชันกล่าวไว้ว่า… ฟังก์ชันคือความสัมพันธ์จากโดเมน (input) ไปหาเรนจ์ (output) โดยทุกๆสมาชิกของโดเมนจะสัมพันธ์กับสมาชิกของเรนจ์เพียงค่าเดียว

นั่นไงหละ เจอเลขเข้าไปสตั้นกันไปคนละ 3 วินาที เราจะเลิกพูดถึงคณิตศาสตร์กันในบทความนี้เพราะผมเชื่อว่าหลายคนในที่นี้อาจเกลียดเข้าไส้

ฟังก์ชันคือการจับคู่ที่พิเศษนิดนึงตรงที่ว่าเราคู่ใครแล้วห้ามนอกใจไปมีคนอื่น ถ้าเราบอกว่า 1 ต้องคู่กับ 3 เสมอนะ นั่นหมายความว่าเราส่ง 1 เข้าไปในฟังก์ชันแล้วผลลัพธ์ต้องออกมาเป็น 3 เสมอ ห้ามนอกใจไปเป็นเลขอื่น

นั่นคือคอนเซ็ปต์ของฟังก์ชันครับ คำถามคือจะจับคู่ข้อมูลเข้าให้ตรงกับข้อมูลออกได้ทุกครั้งต้องทำอย่างไรบ้าง? นึกไปถึงเรื่องความรักของนายเอและนางบี เอและบีจะคบกันต่อไปถ้าคนใดคนหนึ่งไม่คิดนอกใจและไม่มีมือที่สามใช่ไหมครับ? การเขียนโปรแกรมแบบฟังก์ชันก็เช่นเดียวกัน จะใส่ข้อมูลเข้าแล้วได้ข้อมูลออกแบบเดิมทุกครั้งได้ต้อง

  • ห้ามมีการแก้ไขข้อมูล ถ้าเราบอกว่า var a = 3 แล้วเราจะเปลี่ยน a เป็น 4 ไม่ได้ เราเรียกสิ่งนี้ว่า immutable data (ห้ามคิดนอกใจนั่นเอง)
  • state ของโปรแกรมไม่เปลี่ยนแปลง (ไม่มีมือที่สามมาปั่นป่วนความสัมพันธ์)

พิจารณาตัวอย่างโปรแกรมต่อไปนี้

let init = 0

const addTo = (num) => {
  return init += num
}

console.log(addTo(1)) // 1
console.log(addTo(1)) // 2

จากตัวอย่างโปรแกรมข้างบนพบว่าเราเรียก addTo สองครั้งพร้อมส่ง 1 เข้าไปเหมือนกันแท้ๆแต่กลับได้ผลลัพธ์กลับมาไม่เหมือนกัน สาเหตุเป็นเพราะ num ในฟังก์ชัน addTo โดนมือที่สามคือ init เข้ามาแทรกกลางทำให้ผลลัพธ์ไม่เหมือนเดิม อีกทั้ง init ยังเปลี่ยนแปลงค่าได้ตลอดเวลา (คิดนอกใจได้เสมอ) จึงผิดหลักการของฟังก์ชันตามหลักคณิตศาสตร์เพราะส่งค่าเข้าไปเหมือนกันแต่ได้ผลลัพธ์ต่างกัน ทั้งนี้เราจะเรียกฟังก์ชันที่ถูกต้องตามนิยามทางคณิตศาสตร์ว่า pure function

สำหรับผู้อ่านคนไหนสนใจประวัติศาสตร์ Functional Programming ฉบับภาษาไทยหละก็ลองอ่านจากบทความนี้ดูนะครับ

ข้อดีของการเขียนโปรแกรมแบบฟังก์ชัน

ลองจินตนาการถึงการเขียนโปรแกรมครับ ถ้าเราเปลี่ยนแปลงค่าของตัวแปรไม่ได้อะไรจะเกิดขึ้น?

const a = 10
...
...
...
...
console.log(a) // 10

เราจะทราบทันทีว่าตัวแปรของเรามีค่าเป็นอะไร ไม่ว่าเราจะอ่านโค๊ดที่บรรทัดไหน นั่นเพราะค่าของตัวแปรไม่เคยเปลี่ยนแปลงเลย นอกจากนี้เมื่อฟังก์ชันของเราคืนค่ากลับเป็นค่าเดิมเสมอสำหรับค่าเข้าที่เหมือนกัน นั่นทำให้การทดสอบโปรแกรมของเราง่ายขึ้นตาม

let init = 0

const addTo = (num) => {
  return init += num
}

console.log(addTo(1)) // 1
console.log(addTo(1)) // 2

โปรแกรมเดิมตัวนี้ถ้าเราต้องการเขียนเทสสำหรับ addTo จะเริ่มลำบากเพราะเราต้องคิดแล้วหละว่าในขณะนั้น init มีค่าเป็นอะไร

Functional Programming ใน JavaScript อย่างรวดเร็ว

อวยไส้แตกกันซะขนาดนี้ลองมาดูกันซิครับว่าเราสามารถประยุกต์ให้ JavaScript น้อยๆของเรามีความเป็นฟังก์ชันมากขึ้นได้อย่างไรบ้าง

Map / Reduce

ส่วนนี้เป็นพื้นฐานเลยก็ว่าได้ครับคือการแปลงข้อมูลไปเป็นอีกชุดหนึ่งโดยไม่เปลี่ยนแปลงข้อมูลต้นฉบับ อย่าลืมนะครับว่าข้อมูลเราเป็น immutable คือแก้ไขค่าไม่ได้ ตัวอย่างเช่น

// ใช้ Object.freeze เพื่อป้องกันการแก้ไขข้อมูล
// การันตีว่าโค๊ดข้างล่างจะไม่แอบแก้ไขค่า arr
const arr = Object.freeze([1, 2, 3, 4])

// ได้อาร์เรย์ชุดใหม่ที่เกิดจากการเอาแต่ละค่าคูณสอง
arr.map(item => item * 2) // [2, 4, 6, 8]

// ได้ผลลัพธ์เป็นค่าใหม่คือผลรวมของทุกค่าในอาร์เรย์
arr.reduce((sum, item) => sum + item, 0) // 10

// เลือกค่าในอาร์เรย์ที่เป็นเลขคู่ออกมาเป็นอาร์เรย์ตัวใหม่
arr.filter(item => item % 2 === 0) // [2, 4]

// เนื่องจากการคืนค่าใหม่นี้เองจึงทำให้สามารถเรียกฟังก์ชันต่อไปได้ต่อเนื่อง
arr.map(item => item * 2)
   .filter(item => item % 2 === 0) // [2, 4, 6, 8]

สังเกตนะครับว่าการทำงานเหล่านี้จะได้ค่าใหม่ออกมา โดยไม่มีการเปลี่ยนแปลงค่าของข้อมูลเดิม

Higher-Order Functions

ในภาษา JavaScript ฟังก์ชันถือเป็นพลเมืองชั้นหนึ่งกล่าวคือเราสามารถโยนฟังก์ชันใส่ตัวแปร โยนฟังก์ชันเข้าไปในฟังก์ชัน หรือแม้กระทั่งโยนฟังก์ชันออกมาจากฟังก์ชันได้

// assign ฟังก์ชันให้ตัวแปร
const fn = () => { console.log('fn') }

// โยนฟังก์ชันเข้าไปในฟังก์ชัน
function print(fn) {
  fn()
}

print(fn) // fn

Currying

เรามีฟังก์ชันสำหรับพิมพ์รายชื่ออยู่ก่อนแล้วดังนี้

const print = (last, first) => {
  console.log(`${last} ${first}`)
}

print('Smith', 'Sophia') // Smith Sophia
print('Smith', 'Nevaeh') // Smith Nevaeh
print('Smith', 'Autumn') // Smith Autumn

สิ่งหนึ่งที่เราพบคือถ้าเราต้องการพิมพ์ชื่อคนในครอบครัวเรากลับต้องใส่นามสกุลทุกครั้งซึ่งถือเป็นการทำงานที่ซ้ำซ้อน เพื่อเป็นการประหยัดเวลาเราจึงแก้ไขด้วยการใช้ bind ผูกนามสกุลเข้ากับฟังก์ชัน print เรียกวิธีการนี้ว่า Function currying

const print = (last, first) => {
  console.log(`${last} ${first}`)
}

const printMemberInSmithFamily = print.bind(this, 'Smith')

printMemberInSmithFamily('Sophia') // Smith Sophia
printMemberInSmithFamily('Nevaeh') // Smith Nevaeh
printMemberInSmithFamily('Autumn') // Smith Autumn

ศึกษากรณีการใช้ bind เพิ่มเติมที่บทความข้อแตกต่างของ bind, apply และ call ใน JavaScript กับการใช้งาน

Composition

composition หรือที่ภาษาไทยเรียกฟังก์ชันประกอบสามารถเขียนนิยามเป็นภาษาโปรแกรมได้ดังนี้

// เราจะนำฟังก์ชัน f และ g มาประกอบร่างกัน
const compose = (f, g) => {
  // return ฟังก์ชันที่ได้จากการประกอบร่าง f และ g กลับไป
  return (x) => {
    // วิธีการประกอบร่างคือส่งค่า x เข้าไปในฟังก์ชัน g ก่อน
    // จากนั้นได้ค่าอะไรจึงนำผลลัพธ์นั้นส่งเข้าไปที่ f อีกที
    return f(g(x))
  }
}

ฟังก์ชันประกอบช่วยสร้างฟังก์ชันใหม่ที่สืบทอดความสามารถมาจากฟังก์ชันที่รวมร่างกัน

// สำหรับประกอบฟังก์ชัน f และ g เป็นฟังก์ชันใหม่
// ที่มีความสามารถมาจากทั้ง f และ g
const compose = (f, g) => {
  return (x) => {
    return f(g(x))
  }
}

// เปลี่ยนช่องว่างในข้อความให้เป็น -
const convertSpaceToDash = (text) => {
  return text.replace(' ', '-')
}

// แปลงข้อความให้เป็นอักษรตัวเล็ก
const lowercase = (text) => {
  return text.toLowerCase()
}

// รวมฟังก์ชันทั้งสองเข้าด้วยกัน ได้ฟังก์ชันใหม่
const convertSpaceToDashAndLowercase = compose(
  convertSpaceToDash, 
  lowercase
)

console.log(convertSpaceToDashAndLowercase('Hello World')) // hello-world

Recursion

จริงๆหัวข้อนี้ข้ามได้เลยนะครับเพราะเป็นพื้นฐานที่เชื่อว่าเพื่อนๆทุกคนได้เรียนมาหมดแล้ว สิ่งหนึ่งที่ผมชอบมากในหัวข้อนี้คือชื่อภาษาไทยเนี่ยหละ Recursive มีชื่อภาษาไทยสุดเก๋ว่าความสัมพันธ์เวียนบังเกิด อืม… ใครตั้งให้นะ

Lazy Evaluation

พิจารณาตัวอย่างต่อไปนี้ครับ

const array = Array.from(Array(100).keys()) // [0, 1, 2, ..., 99]

array.map(item => item * 2).slice(0, 3) // [0, 2, 4]

จากตัวอย่างเราสร้างอาร์เรย์ที่มีค่าตั้งแต่ 0 ถึง 99 จากนั้นจึงเรียก map เพื่อสร้างอาร์เรย์ใหม่ขึ้นมาโดยอาร์เรย์ใหม่นี้ให้มีค่าเป็นสองเท่าของเดิม จากนั้นใช้ slice เพื่อดึงข้อมูลแค่สามค่าแรก

สิ่งที่เราพบได้จากตัวอย่างนี้คือภายใต้คำสั่ง map ทุกๆค่าใน array จะโดนนำไปคูณสองทั้งหมด แม้ว่าภายหลังเราจะต้องการข้อมูลเพียงแค่สามค่าก็ตาม คำถามคือ ทำไมเราต้องคำนวณทุกๆค่าของอาร์เรย์ด้วย สุดท้ายเราต้องการแค่สามค่าก็คูณสองแค่สามค่าแรกซิ ไม่ได้หรอ? การคำนวณเมื่อต้องการใช้งานจริงนี่หละครับคือ Lazy Evaluation ผู้อ่านคนไหนสนใจเพิ่มเติมอ่านได้ที่การใช้ Lazy Evaluation ใน JavaScript

ทั้งหมดนี้เป็นเพียงพื้นฐานเริ่มต้นสำหรับมือใหม่ครับ เรื่องอื่นๆเกี่ยวกับ functional programming ใน JavaScript จะพยายามเขียนเรื่อยๆ ถ้าอย่างไรรบกวนเพื่อนๆช่วยติดตามเรื่อยๆนะครับ (ยิ้มกว้าง3เมตร)


แสดงความคิดเห็นของคุณ


Makkhawan Worabut8 เดือนที่ผ่านมา

ใน const ของ JavaScript ผมมองว่ามันคือ Pointer เหมือนในภาษา C เลยนะ ถึงแม้ว่าจะแก้ไขไม่ได้ตรง แต่ถ้าเราสามารถนำ instant อื่นไปรับค่าแล้วเปลี่ยนเป็นค่าอื่น ซึ่งก็ทำให้ตัวต้นฉบับเปลี่ยนตามไปด้วย เช่น

const foo = [1, 2]
const bar = foo
bar[0] = 2

console.log(foo) // [ 2, 2 ]
console.log(bar) // [ 2, 2 ]
ข้อความตอบกลับ
Nuttavut Thongjor8 เดือนที่ผ่านมา

การประกาศตัวแปรสำหรับอ็อบเจ็กต์ ตัวแปรไม่ได้เก็บอ็อบเจ็กต์โดยตรง แต่เก็บ memory address ที่ชี้ไปยังอ็อบเจ็กต์ในพื้นที่ส่วน Heap อีกที

การใส่ const นำหน้าตัวแปรที่เก็บอ็อบเจ็กต์ จึงเป็นการบอกเพียงว่า ห้ามเปลี่ยน memory address หรือก็คือห้ามชี้ไปยังอ็อบเจ็กต์อื่น แต่ไม่ได้การันตีว่าตัวอ็อบเจ็กต์เดิมจะเปลี่ยนค่าไม่ได้ครับ

เราจึงต้องใช้ Object.freeze เพื่อแช่แข็งอ็อบเจ็กต์ไว้ แต่ก็ทำได้แค่ระดับเดียว ถ้าเป็นอ็อบเจ็กต์ซ้อนอ็อบเจ็กต์ อ็อบเจ็กต์ตัวในก็เปลี่ยนค่าได้เช่นเดิมครับ 😃


nattatorn10 เดือนที่ผ่านมา

แชร์ครับ ใช้ rest กับ Composition รับ function ได้ไม่จำกัดครับ

let compose = (...funcs) => funcs.reduce((f, g) => (...args) => f(g(...args))) 

const  fn = compose( a, b, c) 
ข้อความตอบกลับ
Nuttavut Thongjor10 เดือนที่ผ่านมา

👍


Nuttavut Thongjorปีที่แล้ว

@saknarak ขอบคุณที่แจ้งข้อผิดพลาดครับ

อาร์เรย์เป็นอ็อบเจ็กต์ชนิดหนึ่ง

การใส่ const เป็นเพียงการบอกว่าเราจะเปลี่ยน reference หรือยัดอาร์เรย์ตัวใหม่ไม่ได้

แต่เรายังคงแก้ไขเปลี่ยนแปลงข้อมูลภายในอาร์เรย์ได้

const arr = [1, 2]
arr = [3, 4] // ทำไม่ได้

แก้ไขเรียบร้อยครับ ขอบคุณที่แจ้งเข้ามาครับ 😃


saknarakปีที่แล้ว

// ใช้ const เพื่อป้องกันการแก้ไขข้อมูล // การันตีว่าโค๊ดข้างล่างจะไม่แอบแก้ไขค่า arr const arr = [1, 2, 3, 4] arr[0] = 100; console.log(arr); // [100, 2, 3, 4]