พื้นฐาน funtional programming ใน JavaScript
โค๊ดที่คุณเขียนทุกวันนี้แอบเจือปนด้วย functional programming ไหม? แรกเริ่มที่การเขียนโปรแกรมแบบฟังก์ชันถือกำเนิด ยังไม่เป็นที่นิยมสำหรับเหล่ามนุษย์ลุงมนุษย์ป้าโปรแกรมเมอร์สมัยนั้นมากนัก แต่ทำไมการเขียนโปรแกรมแบบฟังก์ชันกลายเป็นเรื่องสำคัญในยุคนี้ และส่งอิทธิพลต่อมนุษย์เงินเดือนหาเช้ากินค่ำแบบพวกเราหละ? มาร่วมหาคำตอบและเรียนรู้การเขียนโปรแกรมแบบฟังก์ชันด้วย JavaScript ในบทความนี้กันครับ ลุย!
การเขียนโปรแกรมแบบฟังก์ชันคืออะไร?
ย้อนกลับไปสมัยเราเรียน ม.4 วิชาคณิตศาสตร์จะมีบทเรียนนึงชื่อ ความสัมพันธ์และฟังก์ชัน
แหม พูดเรื่องอดีตแล้วมันช่างเป็นการเช็คอายุกันเสียจริง บทนิยามที่อธิบายความหมายของฟังก์ชันกล่าวไว้ว่า.. ฟังก์ชันคือความสัมพันธ์จากโดเมน (input) ไปหาเรนจ์ (output) โดยทุกๆสมาชิกของโดเมนจะสัมพันธ์กับสมาชิกของเรนจ์เพียงค่าเดียว
นั่นไงหละ เจอเลขเข้าไปสตั้นกันไปคนละ 3 วินาที เราจะเลิกพูดถึงคณิตศาสตร์กันในบทความนี้เพราะผมเชื่อว่าหลายคนในที่นี้อาจเกลียดเข้าไส้
ฟังก์ชันคือการจับคู่ที่พิเศษนิดนึงตรงที่ว่าเราคู่ใครแล้วห้ามนอกใจไปมีคนอื่น ถ้าเราบอกว่า 1 ต้องคู่กับ 3 เสมอนะ นั่นหมายความว่าเราส่ง 1 เข้าไปในฟังก์ชันแล้วผลลัพธ์ต้องออกมาเป็น 3 เสมอ ห้ามนอกใจไปเป็นเลขอื่น
นั่นคือคอนเซ็ปต์ของฟังก์ชันครับ คำถามคือจะจับคู่ข้อมูลเข้าให้ตรงกับข้อมูลออกได้ทุกครั้งต้องทำอย่างไรบ้าง? นึกไปถึงเรื่องความรักของนายเอและนางบี เอและบีจะคบกันต่อไปถ้าคนใดคนหนึ่งไม่คิดนอกใจและไม่มีมือที่สามใช่ไหมครับ? การเขียนโปรแกรมแบบฟังก์ชันก็เช่นเดียวกัน จะใส่ข้อมูลเข้าแล้วได้ข้อมูลออกแบบเดิมทุกครั้งได้ต้อง
- ห้ามมีการแก้ไขข้อมูล ถ้าเราบอกว่า var a = 3 แล้วเราจะเปลี่ยน a เป็น 4 ไม่ได้ เราเรียกสิ่งนี้ว่า immutable data (ห้ามคิดนอกใจนั่นเอง)
- state ของโปรแกรมไม่เปลี่ยนแปลง (ไม่มีมือที่สามมาปั่นป่วนความสัมพันธ์)
พิจารณาตัวอย่างโปรแกรมต่อไปนี้
1let init = 023const addTo = (num) => {4 return (init += num)5}67console.log(addTo(1)) // 18console.log(addTo(1)) // 2
จากตัวอย่างโปรแกรมข้างบนพบว่าเราเรียก addTo สองครั้งพร้อมส่ง 1 เข้าไปเหมือนกันแท้ๆแต่กลับได้ผลลัพธ์กลับมาไม่เหมือนกัน สาเหตุเป็นเพราะ num ในฟังก์ชัน addTo โดนมือที่สามคือ init เข้ามาแทรกกลางทำให้ผลลัพธ์ไม่เหมือนเดิม อีกทั้ง init ยังเปลี่ยนแปลงค่าได้ตลอดเวลา (คิดนอกใจได้เสมอ) จึงผิดหลักการของฟังก์ชันตามหลักคณิตศาสตร์เพราะส่งค่าเข้าไปเหมือนกันแต่ได้ผลลัพธ์ต่างกัน ทั้งนี้เราจะเรียกฟังก์ชันที่ถูกต้องตามนิยามทางคณิตศาสตร์ว่า pure function
สำหรับผู้อ่านคนไหนสนใจประวัติศาสตร์ Functional Programming ฉบับภาษาไทยหละก็ลองอ่านจากบทความนี้ดูนะครับ
ข้อดีของการเขียนโปรแกรมแบบฟังก์ชัน
ลองจินตนาการถึงการเขียนโปรแกรมครับ ถ้าเราเปลี่ยนแปลงค่าของตัวแปรไม่ได้อะไรจะเกิดขึ้น?
1const a = 102...3...4...5...6console.log(a) // 10
เราจะทราบทันทีว่าตัวแปรของเรามีค่าเป็นอะไร ไม่ว่าเราจะอ่านโค๊ดที่บรรทัดไหน นั่นเพราะค่าของตัวแปรไม่เคยเปลี่ยนแปลงเลย นอกจากนี้เมื่อฟังก์ชันของเราคืนค่ากลับเป็นค่าเดิมเสมอสำหรับค่าเข้าที่เหมือนกัน นั่นทำให้การทดสอบโปรแกรมของเราง่ายขึ้นตาม
1let init = 023const addTo = (num) => {4 return (init += num)5}67console.log(addTo(1)) // 18console.log(addTo(1)) // 2
โปรแกรมเดิมตัวนี้ถ้าเราต้องการเขียนเทสสำหรับ addTo จะเริ่มลำบากเพราะเราต้องคิดแล้วหละว่าในขณะนั้น init มีค่าเป็นอะไร
Functional Programming ใน JavaScript อย่างรวดเร็ว
อวยไส้แตกกันซะขนาดนี้ลองมาดูกันซิครับว่าเราสามารถประยุกต์ให้ JavaScript น้อยๆของเรามีความเป็นฟังก์ชันมากขึ้นได้อย่างไรบ้าง
Map / Reduce
ส่วนนี้เป็นพื้นฐานเลยก็ว่าได้ครับคือการแปลงข้อมูลไปเป็นอีกชุดหนึ่งโดยไม่เปลี่ยนแปลงข้อมูลต้นฉบับ อย่าลืมนะครับว่าข้อมูลเราเป็น immutable คือแก้ไขค่าไม่ได้ ตัวอย่างเช่น
1// ใช้ Object.freeze เพื่อป้องกันการแก้ไขข้อมูล2// การันตีว่าโค๊ดข้างล่างจะไม่แอบแก้ไขค่า arr3const arr = Object.freeze([1, 2, 3, 4])45// ได้อาร์เรย์ชุดใหม่ที่เกิดจากการเอาแต่ละค่าคูณสอง6arr.map((item) => item * 2) // [2, 4, 6, 8]78// ได้ผลลัพธ์เป็นค่าใหม่คือผลรวมของทุกค่าในอาร์เรย์9arr.reduce((sum, item) => sum + item, 0) // 101011// เลือกค่าในอาร์เรย์ที่เป็นเลขคู่ออกมาเป็นอาร์เรย์ตัวใหม่12arr.filter((item) => item % 2 === 0) // [2, 4]1314// เนื่องจากการคืนค่าใหม่นี้เองจึงทำให้สามารถเรียกฟังก์ชันต่อไปได้ต่อเนื่อง15arr.map((item) => item * 2).filter((item) => item % 2 === 0) // [2, 4, 6, 8]
สังเกตนะครับว่าการทำงานเหล่านี้จะได้ค่าใหม่ออกมา โดยไม่มีการเปลี่ยนแปลงค่าของข้อมูลเดิม
Higher-Order Functions
ในภาษา JavaScript ฟังก์ชันถือเป็นพลเมืองชั้นหนึ่งกล่าวคือเราสามารถโยนฟังก์ชันใส่ตัวแปร โยนฟังก์ชันเข้าไปในฟังก์ชัน หรือแม้กระทั่งโยนฟังก์ชันออกมาจากฟังก์ชันได้
1// assign ฟังก์ชันให้ตัวแปร2const fn = () => {3 console.log('fn')4}56// โยนฟังก์ชันเข้าไปในฟังก์ชัน7function print(fn) {8 fn()9}1011print(fn) // fn
Currying
เรามีฟังก์ชันสำหรับพิมพ์รายชื่ออยู่ก่อนแล้วดังนี้
1const print = (last, first) => {2 console.log(`${last} ${first}`)3}45print('Smith', 'Sophia') // Smith Sophia6print('Smith', 'Nevaeh') // Smith Nevaeh7print('Smith', 'Autumn') // Smith Autumn
สิ่งหนึ่งที่เราพบคือถ้าเราต้องการพิมพ์ชื่อคนในครอบครัวเรากลับต้องใส่นามสกุลทุกครั้งซึ่งถือเป็นการทำงานที่ซ้ำซ้อน เพื่อเป็นการประหยัดเวลาเราจึงแก้ไขด้วยการใช้ bind ผูกนามสกุลเข้ากับฟังก์ชัน print เรียกวิธีการนี้ว่า Function currying
1const print = (last, first) => {2 console.log(`${last} ${first}`)3}45const printMemberInSmithFamily = print.bind(this, 'Smith')67printMemberInSmithFamily('Sophia') // Smith Sophia8printMemberInSmithFamily('Nevaeh') // Smith Nevaeh9printMemberInSmithFamily('Autumn') // Smith Autumn
ศึกษากรณีการใช้ bind เพิ่มเติมที่บทความข้อแตกต่างของ bind, apply และ call ใน JavaScript กับการใช้งาน
Composition
composition หรือที่ภาษาไทยเรียกฟังก์ชันประกอบสามารถเขียนนิยามเป็นภาษาโปรแกรมได้ดังนี้
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}
ฟังก์ชันประกอบช่วยสร้างฟังก์ชันใหม่ที่สืบทอดความสามารถมาจากฟังก์ชันที่รวมร่างกัน
1// สำหรับประกอบฟังก์ชัน f และ g เป็นฟังก์ชันใหม่2// ที่มีความสามารถมาจากทั้ง f และ g3const compose = (f, g) => {4 return (x) => {5 return f(g(x))6 }7}89// เปลี่ยนช่องว่างในข้อความให้เป็น -10const convertSpaceToDash = (text) => {11 return text.replace(' ', '-')12}1314// แปลงข้อความให้เป็นอักษรตัวเล็ก15const lowercase = (text) => {16 return text.toLowerCase()17}1819// รวมฟังก์ชันทั้งสองเข้าด้วยกัน ได้ฟังก์ชันใหม่20const convertSpaceToDashAndLowercase = compose(convertSpaceToDash, lowercase)2122console.log(convertSpaceToDashAndLowercase('Hello World')) // hello-world
Recursion
จริงๆหัวข้อนี้ข้ามได้เลยนะครับเพราะเป็นพื้นฐานที่เชื่อว่าเพื่อนๆทุกคนได้เรียนมาหมดแล้ว สิ่งหนึ่งที่ผมชอบมากในหัวข้อนี้คือชื่อภาษาไทยเนี่ยหละ Recursive มีชื่อภาษาไทยสุดเก๋ว่าความสัมพันธ์เวียนบังเกิด อืม.. ใครตั้งให้นะ
Lazy Evaluation
พิจารณาตัวอย่างต่อไปนี้ครับ
1const array = Array.from(Array(100).keys()) // [0, 1, 2, ..., 99]23array.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 อย่างรวดเร็ว