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

Nuttavut Thongjor

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

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

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

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

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

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

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

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

JavaScript
1let init = 0
2
3const addTo = (num) => {
4 return (init += num)
5}
6
7console.log(addTo(1)) // 1
8console.log(addTo(1)) // 2

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

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

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

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

JavaScript
1const a = 10
2...
3...
4...
5...
6console.log(a) // 10

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

JavaScript
1let init = 0
2
3const addTo = (num) => {
4 return (init += num)
5}
6
7console.log(addTo(1)) // 1
8console.log(addTo(1)) // 2

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

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

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

Map / Reduce

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

JavaScript
1// ใช้ Object.freeze เพื่อป้องกันการแก้ไขข้อมูล
2// การันตีว่าโค๊ดข้างล่างจะไม่แอบแก้ไขค่า arr
3const arr = Object.freeze([1, 2, 3, 4])
4
5// ได้อาร์เรย์ชุดใหม่ที่เกิดจากการเอาแต่ละค่าคูณสอง
6arr.map((item) => item * 2) // [2, 4, 6, 8]
7
8// ได้ผลลัพธ์เป็นค่าใหม่คือผลรวมของทุกค่าในอาร์เรย์
9arr.reduce((sum, item) => sum + item, 0) // 10
10
11// เลือกค่าในอาร์เรย์ที่เป็นเลขคู่ออกมาเป็นอาร์เรย์ตัวใหม่
12arr.filter((item) => item % 2 === 0) // [2, 4]
13
14// เนื่องจากการคืนค่าใหม่นี้เองจึงทำให้สามารถเรียกฟังก์ชันต่อไปได้ต่อเนื่อง
15arr.map((item) => item * 2).filter((item) => item % 2 === 0) // [2, 4, 6, 8]

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

Higher-Order Functions

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

JavaScript
1// assign ฟังก์ชันให้ตัวแปร
2const fn = () => {
3 console.log('fn')
4}
5
6// โยนฟังก์ชันเข้าไปในฟังก์ชัน
7function print(fn) {
8 fn()
9}
10
11print(fn) // fn

Currying

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

JavaScript
1const print = (last, first) => {
2 console.log(`${last} ${first}`)
3}
4
5print('Smith', 'Sophia') // Smith Sophia
6print('Smith', 'Nevaeh') // Smith Nevaeh
7print('Smith', 'Autumn') // Smith Autumn

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

JavaScript
1const print = (last, first) => {
2 console.log(`${last} ${first}`)
3}
4
5const printMemberInSmithFamily = print.bind(this, 'Smith')
6
7printMemberInSmithFamily('Sophia') // Smith Sophia
8printMemberInSmithFamily('Nevaeh') // Smith Nevaeh
9printMemberInSmithFamily('Autumn') // Smith Autumn

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

Composition

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

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

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

JavaScript
1// สำหรับประกอบฟังก์ชัน f และ g เป็นฟังก์ชันใหม่
2// ที่มีความสามารถมาจากทั้ง f และ g
3const compose = (f, g) => {
4 return (x) => {
5 return f(g(x))
6 }
7}
8
9// เปลี่ยนช่องว่างในข้อความให้เป็น -
10const convertSpaceToDash = (text) => {
11 return text.replace(' ', '-')
12}
13
14// แปลงข้อความให้เป็นอักษรตัวเล็ก
15const lowercase = (text) => {
16 return text.toLowerCase()
17}
18
19// รวมฟังก์ชันทั้งสองเข้าด้วยกัน ได้ฟังก์ชันใหม่
20const convertSpaceToDashAndLowercase = compose(convertSpaceToDash, lowercase)
21
22console.log(convertSpaceToDashAndLowercase('Hello World')) // hello-world

Recursion

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

Lazy Evaluation

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

JavaScript
1const array = Array.from(Array(100).keys()) // [0, 1, 2, ..., 99]
2
3array.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เมตร)

สารบัญ

สารบัญ

  • การเขียนโปรแกรมแบบฟังก์ชันคืออะไร?
  • ข้อดีของการเขียนโปรแกรมแบบฟังก์ชัน
  • Functional Programming ใน JavaScript อย่างรวดเร็ว