JavaScript #03: ในมิติ Functional Programming

เกริ่นนำ

บทความนี้เป็นบทความที่ผมตั้งใจเขียนเพื่อขยายความ functional programming (FP) ของบทความชุดก่อน ตอนแรกตอนที่กำลังเริ่มเขียน ผมก็มาไล่ดูว่าน่าจะเขียนอะไรบ้าง ยิ่งไล่ก็ยิ่งบาน มีเรื่องต้องเขียนและเรื่องที่ควรเขียนเพิ่มมากมาย จบในบทความเดียวไม่ได้ ก็เลยคิดไปคิดมา ขยายเนื้อหาในบทความขึ้นเป็นชุด และลามไปถึงกลายเป็นหมวดในที่สุด

ผมก็เขียนปูพี้นมาสองบทแล้ว จนบทนี้เป็นบทที่ 3 ซึ่งเป็นเนื้อหาหลักที่ผมอยากเขียนในเบื้องแรกสุด ลองอ่านดูครับ น่าจะสนุก

พื้นฐาน functional programming

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

ฟังก์ชันพิสุทธิ์ (pure function)

ก่อนจะเข้าเรื่อง ผมขอชี้แจงนะครับ ศัพท์ภาษาไทยส่วนมากผมคิดของผมเอง และไม่ได้หมายให้ใครเอาไปใช้ต่อ ผมเขียนเอามันส์เฉยๆ เข้าใจตรงกันนะครับ

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

ถ้าเอานิยามข้างต้นมาจับ ECMAScript  พบว่า ECMAScript ย่อมไม่ใช่ฟังก์ชันพิสุทธิ์แท้ๆ อย่างแน่นอน ลองดูตัวอย่างครับ

จะเห็นได้ว่า f(5) เหมือนกันแต่ได้คนละค่า เพราะ f() ไปพึ่งค่าภายนอกที่โลเล ถ้าเป็นภาษาที่เป็น FP แท้แบบนี้จะเกิดไม่ได้ครับ แต่ไม่เป็นไร ไม่แท้ก็ไม่แท้ ถ้าเราควบคุมดีๆ ก็ไม่มีปัญหาแต่อย่างใด หรือคนส่วนมากเมื่อใช่ FP จะไม่มีการอ้างอิงตัวแปรโลเลยนอกฟังก์ชันเลย แบบนี้มันก็จะได้ความสามารถของฟังก์ชันพิสุทธิ์ไปตัว

first-class function

คำนี้ล้อมาจากคำว่า first-class citizen หรือประชาชนเต็มขั้นนั่นเอง ในเชิงภาษาคอมพิวเตอร์นั้น เต็มขั้นไม่เต็มขั้นเขาวัดกันจากความสามารถในการกำหนดให้เป็นตัวแปรได้โดยตรง สำหรับ object-oriented ตัว object สามารถนิยามเป็นตัวแปรได้ ดังนั้น object จึงเป็นเต็มขั้น แต่ฟังก์ชันใน OO ไม่สามารถกำหนดให้เป็นตัวแปรได้ ฟังก์ชันใน OO จึงไม่ไม่นับว่าเป็นเต็มขั้นหรือ first-class นั่นเอง

ECMAScript นั้น สามารถนิยามให้ฟังก์ชันและ object เป็นตัวแปรได้ ดังนั้นฟังก์ชันและ object จึงถือว่าเป็นเต็มขั้นในภาษานี้ ลองดูตัวอย่างนี้ครับ

f1() เป็นฟังก์ชัน แต่ลองพุ่งสายตาไปยังบรรทัดที่ 5 เรามีการกำหนค่ มอง f1 เป็นตัวแปร กำหนดค่าใส่ตัวแปรอีกตัวหนึ่งได้ สำหรับ ECMAScript นั้นเข้าใจง่ายๆ ครับ ถ้าอ้างถึงฟังก์ชันไม่มีวงเล็บ ก็คือมองมันเป็นตัวแปร แต่ถ้ามีวงเล็บและอาจมีหรือไม่มีพารามิเตอร์ภายใน ก็เป็นการเรียกใช้ฟังก์ชัน

ลองดูตัวอย่างอีกซักตัวอย่างนึงครับ

code นี้ เป็นการสร้าง function expression อย่างที่เรียนมาในบทความก่อนหน้านั่นเอง แต่ที่อยากชี้ให้เห็นในรายละเอียดก็คือ f1 เป็นตัวแปร เราสร้าง function expression มาบรรจุใส่เข้าไป เป็นคำสั่ง assignment นะครับ สังเกตในบรรทัดที่ 3 ผมเติม ; เพราะเป็นการปิดท้ายคำสั่งที่ยาวมาตั้งแต่บรรทัดที่ 1 ทั้งสามบรรทัดคือคำสั่ง assigment คำสั่งเดียว เป็นการกำหนดค่า ไม่มีการเรียกใช้ฟังก์ชันแต่อย่างใด

จำกลยุทธ์จักจั่นลอกคราบในบทความที่แล้วได้ไหมครับ อันนั้นเรานิยามฟังก์ชันแบบ function declaration แต่ภายในก็ยังสามารถกำหนดใหม่ เปลี่ยนเนื้องานได้เฉยเลย ดังนั้นฟังก์ชันจึงเป็นแบบเต็มขั้น เห็นๆ ครับ

gotcha! มาเล่นทายชื่อฟังก์ชันกัน มาเล่นกันเถอะ

ขอนอกเรื่องหน่อยครับ มาเล่น gotcha! แก้เครียดกันหน่อยดีกว่า ลองดูตัวอย่าง  function declaration นี้ดูครับ

ฟังก์ชันนี้ตตรงไปตรงมาครับ ชื่อฟังก์ชันคือ f1 อย่างไม่ต้องสงสัย คราวนี้มาลองดู function expression ดูบ้าง

ฟังก์ชันนี้ชื่ออะไร ใครตอบถูกตั้งแต่ครั้งแรกบ้างครับ ใครตอบไม่ถูกลองรันโปรแกรมดูครับ ตอบถูกไหมครับ ผมว่าน่าจะไม่ค่อยถูกกันนะครับ ขึ้นป้าย gotcha! ดีกว่า

เชื่อว่าบางคนแม้รันโปรแกรมแล้วก็ยังตอบไม่ถูกครับ มันมาแปลก ไม่แสดงอะไรเลย เงียบไปเฉยๆ จะ undefined หรือเป็น null ก็ไม่แสดงออกมาครับ เราต้องตรวจสอบโดยใช้ typeof ดังนี้ครับ

สังเกตดีๆ นะครับ ผลลัพธ์ไม่ใช่ undefined นะครับ property name เป็น string ครับ แต่ไม่มีค่ามันจึงเป็น string เปล่า หรือ “” นี่ครับจึงเป็นที่มาของคำว่า anonymous function หรือฟังก์ชันนิรนามนั่นเอง

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

โค้ดข้างบนได้คำตอบอะไรครับ ลองคิดดูก่อนรันนะครับ

gotcha!  รันไม่ได้นะครับ property อ่านอย่างเดียว ดังนั้นวิธีนี้ใช้ไปตั้งชื่อไม่ได้ ต้องใช้วิธีนี้ครับ

เท่ไหมครับ เรียกใช้ฟังก์ชันชื่อ f1 แต่ชื่อจริงของฟังก์ชันคือ f2 ดังนั้นกล่าวโดยสรุปก็คือ ชื่อของฟังก์ชัน จะถูกกำหนดหลังจาก keyword function เท่านั้นครับ ถ้าหลัง keyword function ไม่ระบุชื่อมันจะเป็นชื่อเปล่าๆ “” หรือเรียกว่า anonymous function นั่นเอง

ฟังก์ชันมีระดับ (high-order function)

ฟังก์ชันหรูมีระดับนั้น ก็ต่อยอดมาจาก first-class function นั่นเอง ในเมื่อฟังก์ชันสามารถกลายร่างเป็นตัวแปรตรงๆ ได้แล้ว เราก็สามารถสร้างฟังก์ชันมีระดับ ซึ่งก็คือฟังก์ชันธรรมดานี่เอง แต่รู้สึกเหมือนใหญ่กว่าฟังก์ชันปกติ  (ประมาณว่าขับรถชนตายก็ไม่ติดคุก) เพราะ

  • สามารถรับฟังก์ชันอื่นเป็นพารามิเตอร์
  • นิยามฟังก์ชันอื่นซ้อนเข้าไปในตนเองได้
  • ส่งฟังก์ชันอื่นออกมาเป็นค่าส่งกลับ

การรับฟังก์ชันอื่นเป็นพารามิเตอร์

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

จำฟังก์ชัน map ได้รึเปล่าครับ ถ้าเราส่ง array เข้าไป และฟังก์ชันที่ต้องการให้กระทำ  มันจะนำเอาฟังก์ชันไปกระทำกับแต่ละค่าใน array นั้น ได้ผลลัพธ์ใหม่ที่เป็น array เช่นเดิม จำนวน element เท่าเดิม แต่ค่าเปลี่ยนไป ฟังก์ชัน map จึงเป็นฟังก์ชันมีระดับในรูปแบบหนึ่งนั่นเอง รองรับฟังก์ชันอื่นมาเป็นพารามิเตอร์ จากตัวอย่างจะเห็นว่า arr2 และ arr3 นั้นเกิดจากการกระทำของฟังก์ชันที่แตกต่างกัน นั่นคือ double และ triple ตามลำดับ

บ่อยครั้งที่เราเรียกใช้ฟังก์ชัน map() แบบเฉพาะกิจ ฟังก์ชันที่ส่งเป็นพารามิเตอร์นั้นใช้งานเพียงครั้งเดียว อาจจะดูวุ่นวายเกินไปถ้าเราต้องสร้างฟังก์ชันนั้นขึ้นมา เราสามารถรวบโดยการสร้าง function expression แบบ on-the-fly (คือสร้างแล้วใช้เลย ไม่ต้องสร้างตัวแปรมารับก่อน) ได้เช่น

บางท่านที่ชอบแบบนี้ ก็ว่ามันสะดวกดี ไม่ต้องสร้างตัวแปร แต่บางท่านไม่ชอบ ก็บอกว่าดูแล้ววุ่นวายงงๆ แต่ไม่ว่าท่านชอบหรือไม่ชอบก็ตามครับ การเขียนลักษณะนี้เป็นปกติวิสัยของการเขียน ECMAScript ใครๆ เขาก็เขียนกัน แม้ไม่ชอบก็ต้องทำใจให้ยอมรับครับ

callback function

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

ย้อนมาถึงการประวิงเวลาไม่ยอมทำงานทันที ฟังดูแล้วอาจจะงงๆ ถ้าไม่ทำงานทันที แล้วทำเมื่อไร  จะรีรออะไรกัน ว่ากันจริงๆ แล้วฟังก์ชันมีระดับที่รับ callback นั้น มักจะไม่ใช่เป็นตัวสั่งให้ทำงาน แต่หากทำหน้าที่เป็นตัวลงทะเบียน (register)  เฉยๆ แล้วค่อยทำทีหลัง ตัวที่สั่งงานจริงอาจจะเป็นฟังก์ชันอื่น ลองดูตัวอย่างนี้ครับ

โค้ดนี้รันไม่ได้นะ ไม่สมบูรณ์ ผมตัดมาให้ดูเฉพาะส่วนที่เกี่ยวข้องเท่านั้น นี่เป็นโค้ด node.js ครับ ฟังก์ชัน fs.watchFile() เป็นฟังก์ชันมีระดับ ที่เรารับพารามิเตอร์ 2 ตัว ตัวแรกคือชื่อแฟ้ม ตัวที่สองคือ callback function เวลารันคำสั่งนี้ callback function ยังไม่ได้ทำงานนะครับ มันเพียงแค่ลงทะเบียนเอาไว้ว่า ถ้าแฟ้ม ‘./hello.txt’ มีการแก้ไข มันก็จะมาเรียกใช้ callback function นี้ แต่ถ้ายังไม่มีการแก้ไข callback function จะไม่มีการทำงาน ถ้านึกดีๆ มันก็คือแนวคิดของ event ที่เราคุ้นเคยนั่นเอง

การลงทะเบียนนั้นไม่มีอะไรวุ่นวายมากมาย อาจจะบันทึกเอาไว้ใน data structure ซักตัวหนึ่งเช่น queue เป็นต้น เรามาลองไล่การทำงานดูครับ หลังจากที่ลงทะเบียนเสร็จสิ้น ฟังก์ชัน fs.watchFile() ก็จบการทำงานนะครับ ดังนั้นในบรรทัดต่อมา  f1() จะทำงานต่อทันที ในกรณีที่เป็น node.js นั้น เมื่อโค้ดของแฟ้มทำงานจนหมดครบแล้ว อย่าคิดว่าโปรแกรมจบการทำงานนะครับ มันยังมีของที่ลงทะเบียนค้างไว้  ตัว node.js จะดึงงานออกจาก queue เอามาทำ แต่ในกรณีนี้จะโด้ดดังกล่าวจะทำเมื่อเกิดเหตุการณ์แฟ้ม hello.txt มีการเปลี่ยนแปลงเกิดขึ้น นั่นก็คือแนวคิดของ event ดังที่ได้กล่าวมานั่นเอง การเกิดก่อนแต่ทำงานทีหลัง นี่แหละครับคือสิ่งที่เรียกว่า callback แนวคิดนี้ใช้เป็นปกติวิสัยใน web browser และถูกถ่ายทอดมายัง node.js อีกที

การทำงานลักษณะนี้เป็นแบบที่เรียกว่า asynchronous ทำให้เราเหมือนว่าเราสั่งให้ทำงานหลายงานได้ แต่ตัว engine นั้นปกติแล้วจะทำงานโดยใช้เพียง CPU core เดียว มันจึงทำงานได้ทีละงานเท่านั้น แต่เมื่อเราลงทะเบียนไว้แล้ว เวลา CPU ว่าง มันก็จะเอางานที่ลงทะเบียนไว้มาทำ และมีแนวคิด promise ทำให้ callback ของเราทำงานไประยะหนึ่ง จากนั้นก็จะยอมหยุดทำงานชั่วคราว กลับไปต่อคิวใหม่ ปล่อยให้ callback ตัวอื่นทำงานบ้าง ถ้าสลับไปเรื่อยๆ แบบนี้ก็กลายเป็น multitasking กลายๆ ซึ่งผมจะเขียนให้อ่านกันในโอกาสต่อไปครับ

กลับมาดูอีกตัวอย่างหนึ่งครับ ตัวอย่างยอดนิยมกันเลยทีเดียว

โค้ดนี้ทำงานได้ทั้งบน browser และ node.js ครับ setTimeout(callback, timeout)  เป็นฟังก์ชันมีระดับ มีหน้าที่ลงทะเบียน callback และจะเรียกใช้เมื่อเวลาผ่านไปตาม timout ในที่นี้คือ 3,000 ms หรือ 3 วินาที ดังนั้นเมื่อรันโปรแกรม ก็เพียงลงทะเบียนฟังก์ชัน และทำในบรรทัดที่ 4 จะได้ผลลัพธ์ ‘done’ ในทันทีที่รันโปรแกรม แต่รอไป 3 วินาทีก็จะแสดง ‘timeout’ ออกมา หวังว่าคงกระจ่างนะครับ

ES6: Arrow function

ใน ES6 มีการเพิ่ม syntax ใหม่เข้ามา นั่นคือ arrow function หรือเรียกกันแบบไม่เป็นทางการว่า Lambda function นั่นเอง ลองดูตัวอย่างนี้ครับ

โจทย์ง่ายๆ จะเห็นว่าสั้นกว่าการใช้แบบ on-the-fly ข้างต้น แต่ถ้าเนื้อในของ callback function เริ่มซับซ้อน ก็จะไม่ต่างกันมัน ยกตัวอย่างเช่น

arrow function จะสะดวกถ้างานที่ทำสั้นๆ เสร็จในหนึ่งบรรทัด ถ้ามีเกินหนึ่งบรรทัดก็ต้องใช้ คำสั่ง return จะละเสียไม่ได้ ทำไปทำมา ก็ไม่เห็นสั้นกว่าเดิมเท่าไหร่ ว่าไปแล้ว on-the-fly function ที่เราสร้างขึ้น มันก็คือ Lambda function อยู่แล้ว และที่ทาง TC39 ต้องเพิ่ม arrow function แบบนี้เข้ามา ก็เพื่อให้หน้าตามันเหมือนกับภาษายอดนิยมภาษาอื่นนั่นเอง

ถ้าเป็น setTimeout ก็ใช้ arrow ได้นะครับ ดังนี้

สังเกตว่า เราใช้ () ในตำแหน่งของพารามิเตอร์ บ่งบอกว่าไม่มีพารามิเตอร์นั่นเอง

TS: arrow function

เราสามารถเพิ่มชนิดตัวแปรให้แก่ arrow function ดังนี้

กลยุทธ์: สลับแขกเป็นเจ้าบ้าน

เราอาจจะประยุกต์การส่งฟังก์ชันเป็นพารามิเตอร์มาประยุกต์เป็นแนวคิด inversion of control (IoC) หรือผมตั้งชื่อเป็นไทยๆ เข้าใจง่ายๆ ว่า กลยุทธ์: สลับแขกเป็นเจ้าบ้าน เป็นแนวคิดที่ยอดนิยมมากในยุคปัจจุบัน เรามาดูกันดีกว่าครับ

เรื่มจากการนิยามคำว่าเจ้าบ้านกับแขกเสียก่อน เจ้าบ้านในที่นี้ก็คือโปรแกรมที่เราเขียนนั่นเอง เริ่มจาก main()  โปรแกรมของเรา อยากให้ทำอะไรก็ร่ายยาวกันไปเลย ส่วนแขกในที่นี้คือก็ libraries ต่างๆ ที่เราเรียกใช้ บทบาทแขกกับเจ้าบ้านมันเป็นเช่นนี้มาเมินนานแล้ว เวลาเราเขียนโปรแกรมภาษา C ปกติก็เป็นเช่นนี้  จนมีคนคิดค้นแนวคิดใช้หัวพุ่งลงดินเท้าชี้ฟ้า กลับหัวกลับหาง สลับแขกเป็นเจ้าบ้าน

ถ้าถามผมนะว่า IoC เจ้าแรกที่โด่งดังที่สุดคือตัวไหน ผมว่ามันน่าจะเป็น Visual Basic นะ ยุคนั้นเป็นยุคเปลี่ยนผ่านจากยุค text-mode มาเป็นยุคใช้ mouse เป็น windows อย่างทุกวันนี้ แนวคิดการเขียนโปรแกรมก็ต้องถูกกระทบอย่างรุนแรงอย่างเลี่ยงไม่ได้ ในอดีตนั้นเราเคยเขียน Hello, World ภาษา C ใช้ประมาณ 4-5 บรรทัด พอมาเป็น Windows กดปุ่มเพื่อ popup แสดงคำว่า Hello, World ต้องเขียนโปรแกรมยาวหลายร้อยบรรทัด เพราะต้องเข้าไปเรียก Windows API เรียงตามลำดับ และตบท้ายด้วย event loop เพื่อวนรอเหตุการณ์เกิดขึ้น

พอมาเป็น Visual Basic ทุกอย่างเปลี่ยนไป เพราะหันมาใช้กลยุทธ์สลับแขกมาเป็นเจ้าบ้าน VB นั้นยึดเอา main เรามองไม่เห็นว่าอยู่ไหน มันซ่อน event loop เอาไว้กับตัว ทำตัวเป็นเจ้าบ้านเฉยเลย ถ้าเราสร้างโปรเจคใหม่ขึ้นมา ไม่เขียนอะไรเลยก็รันได้แล้ว ขึ้นมาเป็นหน้า window เปล่าๆ และถ้าเราลากเอาปุ่มมาใส่ กดเข้าไปเขียนโปรแกรม เขียนเพียงบรรทัดเดียว (จากเดิมที่ต้องเขียนหลายร้อยบรรทัด) ก็สามารถแสดง Hello, World ได้  กลายเป็นส่วนโปรแกรมที่เราเขียนกลายเป็นแขกไปเสียนี่

ถ้าวิเคราะห์กันให้ลึกหน่อย นอกจากเขียนสั้นลงแล้ว มันยังเป็นการบีบการเขียนโปรแกรมให้ไปอยู่ในโครงเดียวกัน ดังนั้นรูปแบบหน้าตาผลลัพธ์ที่ได้จึงออกไปในทำนองเดียวกัน มีเอกภาพ การเปลี่ยนที่โครงจะส่งผลไปยังโปรแกรมทุกตัว ทำให้หน้าตาเปลี่ยนทั้งหมดในคราวเดียว ไม่ต้องตามไปแก้ในแต่ละแฟ้ม ดังนั้นแนวคิดการเขียนลักษณะที่มีโครงอยู่แล้วอย่างนี้จึงเรียกว่า framework นั่นเอง

ทุกวันนี้ framework มีบทบาทมาก AngularJS, Spring, django, Ruby on Rails อื่นๆ อีกมากมาย ล้วนแล้วแต่เป็น framework ด้วยกันทั้งสิ้น ล้วนแล้วแต่ใช้กลยุทธ์สลับแขกเป็นเจ้าบ้านด้วยกันทั้งสิ้น และในส่วนของโค้ดเราที่เป็นแขกของ framework ก็ลงทะเบียนโดยการส่งฟังก์ชันของเราให้เป็นพารามิเตอร์ไปยังฟังก์ชันมีระดับที่ทำหน้าที่เป็นตัวลงทะเบียนนั่นเอง

การนิยามฟังก์ชันซ้อนฟังก์ชัน

ฟังก์ชันมีระดับแบบที่สองก์คือ ฟังก์ชันมีระดับสามารถซ้อนอีกฟังก์ชันหนึ่งอยู่ภายในได้ ลองพิจารณาโค้ดข้างบนดูครับ โค้ดนี้เราสร้างฟังก์ชัน inner() อยู่ภายในฟังก์ชัน outer() ซึ่งโดยปกติแล้ว เราจะเข้าถึง inner() จากภายนอกไม่ได้ ดังนั้นจะเรียกใช้ outer.inner() แบบนี้ไม่ได้ครับ หรือถ้าเขียนดังนี้

จากโค้ดผมพยายามเปลี่ยนเนื้อหาของ inner โดยการกำหนด outer.inner ให้เป็น arrow function ตัวไหม่ ทำได้ครับ แต่ inner เป็นคนละตัวกัน สรุปได้ว่า outer.inner() เป็นเรื่องของ object-based และ inner() ที่นิยามในฟังก์ชันนั้นเป็นฟังก์ชันที่อยู่ในฟังก์ชัน ซึ่งเป็นคนละตัวกัน

สังเกตตัวแปร word ครับ ตัวแปรนี้นิยามใน outer ในเมื่อ inner นิยามภายใต้ outer ก็เฉกเช่นภาษาคอมพิวเตอร์ทั่วไป คือฟังก์ชัน inner() มองเห็นตัวแปร word ของ outer() แต่ในทางกลับกัน ถ้าเรานิยามตัวแปรในฟังก์ชัน inner() ตัวฟังก์ชัน outer() จะมองไม่เห็นตัวแปรนั้นนะครับ ซึ่งก็เป็นเรื่องของ scope ตัวแปรตามปกติ เหมือนภาษาอื่นทั่วไป ถ้าเทียบกับชีวิตประจำวันก็คือ ลูกสามารถใช้ทรัพยากรใดๆ ในบ้านของพ่อแม่ได้ แต่พ่อแม่ไม่อาจเข้าห้องนอนลูกได้ (หลายบ้านที่เป็นเช่นนี้)

ผมขอทิ้งประเด็นเรื่องฟังก์ชันซ้อนฟังก์ชันเอาไว้ก่อน เดี๋ยวเราไปดูการส่งฟังก์ชันกลับจากฟังก์ชัน แล้วจะวกกลับมาเรื่องนี้กันอีกครั้ง

ฟังก์ชันเป็นค่าส่งกลับของอีกฟังก์ชัน

เราสามารถส่งฟังก์ชันออกมาจากฟังก์ชันได้แบบธรรมชาติ เป็นปกติวิสัย จะส่งจากฟังก์ชันที่นิยามซ้อนอยู่ข้างใน หรือจะ on-the-fly ก็ยังได้ ลองดูตัวอย่าง

คงไม่ต้องอธิบายมากนะครับ น่าจะเข้าใจได้โดยไม่ยาก คราวนี้มาลองดู on-the-fly แบบ function expression กันบ้าง

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

gotcha!

มาเล่น gotcha! กันอีกหน่อยดีกว่าครับ โค้ดข้างบนนี้ก็เป็นโค้ดที่ใช้ในหัวข้อฟังก์ชันซ้อนฟังก์ชัน ปรับเปลี่ยนเล็กน้อยโดยการส่ง inner() เป็นค่าส่งกลับ โปรแกรมก็ทำงานได้อย่างที่คิด เออ… แล้วมัน gotcha! ตรงไหน

เพื่อให้ง่ายขึ้นผมขอเรียกฟังก์ชัน outer() ว่าเป็นแม่ ส่วน inner() ว่าเป็นลูกจะได้เห็นภาพ ผมขอให้พิจารณาบรรทัดที่ 9 ให้ดีนะครับ ถามว่า ก่อนบรรทัดที่ 9 นั้น แม่ ลูก เกิดรียังครับ คำตอบคือยังไม่เกิดใช่ไหมครับ แค่นิยามเฉยๆ ยังไม่ได้เรียกใช้ มันก็ยังไม่เกิด พูดในเชิงการรันโปรแกรมให้ถูกต้อง มันยังไม่มี stackframe สำหรับแม่หรือลูกเกิดขึ้นนั่นเอง

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

แต่ก่อนแม่ตายนิดเดียวในบรรทัดที่ 9 แม่ก็ได้คลอดลูกออกมา ผมเอาตัวแปร myInner ไปรับขวัญ เมื่อคลอดเสร็จแม่ก็ตายไป แต่ลูกก็ยังไม่มีชีวิตนะครับ มันจะมีชีวิตในบรรทัดที่ 10 เกิดและตายในบรรทัดนั้น ซึ่งในระหว่างการทำงานบรรทัดที่ 10 นั้นเอง ก็มีการสร้าง stackframe สำหรับลูกขึ้นมา เพื่อใช้ในการทำงาน เมื่อลูกทำงานเสร็จในบรรทัดที่ 10 stackframe นั้นก็จะถูกทำลายลงเช่นเดิม

พูดมายาวยืดก็ไม่เห็นว่า จะมี gotcha! แต่อย่างใด แต่อยากให้มองในมุมนี้ครับ ให้ดูบรรทัดที่ 10 ในระหว่างการรันตัวลูก จะเห็นว่าตัวลูกมีการเรียกใช้ตัวแปร word ของแม่ในบรรทัด 4 คำถามก็คือ ในบรรทัดที่ 10 นั้นแม่ตายไปแล้วอย่างสนิท ลูกไปเอาตัวแปร word มาใช้จากไหนกันครับ  gotcha!

เพื่อให้เห็นถึงปัญหา ผมขอวาดเป็นกระดานดำดังนี้ครับ
closure-word

แม่เกิดใหม่จะสร้างตัวแปรใหม่หรือไม่

ปรากฏการณ์ประหลาดเกิดขึ้นมาแล้ว ผมจะใช้วิธีทางวิทยาศาสตร์ในการแก้ปัญหา คือตั้งสมมุติฐาน จากนั้นทดลองแล้วสรุปผลในขึ้นตอนสุดท้าย ในหัวข้อนี้ผมสงสัยว่า ถ้าเรียกแม่ 2 ครั้ง ลูกแต่ละตัวจะได้แม่เดียวกันหรือไม่  ดังนั้นผมจึงทดลองโดยหลังลูกตัวแรกรันโปรแกรมแล้ว ก็แอบเปลี่ยนแปลงตัวแปร word ของแม่เลย แล้วค่อยให้ลูกตัวที่สองอ่าน ดังนี้ครับ

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

ก่อนแม่ตาย ลูกคัดลอกตัวแปรแม่มาเก็บไว้ใช่หรือไม่

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

โค้ดนี้ระหว่างที่เกิดแม่น้้น ลองดูในบรรทัด 7 ถึง 9 ครับ เป็น loop ใน loop นี้ก็วนสร้างลูกขึ้นมา เอาไปเก็บไว้ใน arr ดังนั้นในบรรทัดที่ 8 นั้น ลูกแต่ละตัวก็จะได้ค่า num ที่แตกต่างกันไปตั้งแต่ 0 – 4  ดังนั้นลูกแต่ละตัวจะคัดลอกค่า num ที่แตกต่างกันออกไปเป็นของตัวเองที่ไม่เหมือนกัน จากนั้นก็ส่ง array ของฟังก์ชันเหล่านั้นออกมา เพื่อวนรอบ ถามว่าเมื่อรันแล้วจะได้คำตอบอะไรครับ ลองเดาดู ตอบถูกไหมครับ gotcha!

มันไม่ได้เป็นอย่างที่พยายามอธิบายในย่อหน้าก่อนหน้า คำตอบที่แสดงออกมา เป็นเลข 5 ทั้ง 5 บรรทัด นั่นหมายความว่า ตัวแปรของแม่ไม่ได้มีการคัดลอกมายังลูกนะครับ  ลูกทุกตัวใช้ตัวแปรตัวเดียวกัน ว่าแต่ว่าตัวแปร num ตัวนั้นอยู่ที่ไหน อยู่ในแม่เหรอ ก็แม่ตายไปแล้วนี่ ยิ่งคิดก็ยิ่งงง ไปดูเฉลยๆ กันดีกว่าครับ

closure

สิ่งแปลกๆ ที่เกิดผมนำเสนอในหัวข้อที่ผ่านมา ไม่ใช่ปัญหานะครับ แต่หากเป็นความสามารถพิเศษอย่างหนึ่งของ FP ที่เรียกว่า closure หรือ ‘การปิดล้อม’ ซึ่งเป็นคุณสมบัติเป็นพื้นฐานของภาษาที่เป็น FP แท้ๆ ทั่วไปอยู่แล้ว ในเมื่อมันเป็นธรรมชาติอยู่แล้ว เลยไม่รู้จะเอาอะไรไปเล่าสู่กันฟัง แต่พอมาเป็น ECMAScript ซึ่งเป็นภาษาที่มีรากมาจาก imperative พอมาใช้ closure จึงกลายเป็นเรื่อง gotcha! ไป

พูดถึง ‘การปิดล้อม’ ทำให้นึกถึงเหตุการณ์หนึ่งครับ ผมเคยไปนั่งดูวงดนตรีลูกทุ่งเปิดการแสดงที่ว่างแถวใกล้ๆ บ้าน ร้องก็ไม่เห็นได้เรื่อง เต้นก็ไม่เอาอ่าว ไม่รู้ทนฟังอยู่ได้ยังไงเหมือนกัน แต่พอดึกๆ  คราวนี้มีการเอาผ้าใบมาขึงล้อมพื้นที่ทั้งหมด เก้าอี้ที่ผมนั่งก็อยู่ในวงล้อมนั้นด้วย แล้วเก็บตังคนละ 20 บาท ผมก็งง จ่ายก็จ่ายครับ 20 บาทเอง ไม่มีปัญหา พอเก็บตังเสร็จ นักร้องนักแสดงที่เป็นผู้หญิง ก็แก้ผ้าหมดบนเวทีครับ อืม.. กลายเป็นโชว์จ้ำบ๊ะไป วันนั้นทำให้ผมเข้าใจว่า closure มันเป็นเช่นนี้เอง (ตถตา)

นึกๆ แล้วเรื่อง closure มันก็เรื่องเดียวกันกับที่ผมเล่าเลยครับ คือมีการล้อมพื้นที่ตัวแปรที่อยู่นอก scope ของฟังก์ชัน ทำให้มันเป็นซอมบี้ ไม่ตาย  มันปิดล้อมแค่ไหน เงื่อนไขในการปิดล้อมเป็นอย่างไร เรามาดูกันครับ

ผมขอเฉลยอย่างนี้เลยครับ ตัวแปร word ที่นิยามในแม่นั้น มันก็คือตัวแปรของแม่นั่นเอง ยังอยู่ในแม่ ไม่ได้ถูกคัดลอกไปไหนเลย ตัวแปร word นี้มันก็อยู่ใน frame หนึ่งใน stackframe ตามปกติ  อ้าว! แล้วพอแม่ตาย frame นั้นไม่ตายเหรอ ผมบอกได้ว่ามันไม่เหมือนภาษาอื่น มันไม่ตายครับ แต่ frame ถูกถอดออกจาก stackframe นั้นแน่นอนครับ ถ้าไม่ถอดออกระบบรวนแน่ครับ การเรียกฟังก์ชันจะมีปัญหากลับไม่ถูก ตัวแปรต่างๆ พารามิเตอร์ ก็จะเพี้ยนหมด มันต้องเอาออกไป แต่มันไม่เหมือนกันภาษาอื่นตรงที่ frame เมื่อเอาออกจาก stackframe แล้ว มันยังไม่ตายครับ ไม่ตายแล้วอยู่ไหน งงจัง!

เพื่อตอบปัญหานี้เรามาเรียนรู้กฏขอบเขตการอ้างถึงตัวแปรอีกข้อหนึ่งของ ECMAScript ครับ เรียกว่า lexical scope คือเมื่อลูกเกิดมา แน่นอนครับตอนนั้น frame ของแม่ยังคงอยู่บน stackframe และ frame ของลูกก็จะเกิดตามมา ซึ่ง frame ของลูกที่เกิดใหม่นั้นไม่ได้อยู่บน stackframe นะครับ เพราะแค่เกิด ยังไม่ได้รัน มันจะมีตัวชี้ตัวหนึ่งจาก frame ของลูกที่เกิดใหม่ มาชี้ยัง frame ของแม่ กฏของ lexical scope บังคับให้มันเป็นเช่นนั้น ตามมาตรฐาน ECMAScript เรียกตัวชี้ที่สร้างในลูกเพื่อไปชี้แม่ตัวนี้ว่า [[Scope]]

[[Scope]] นั้นจะเกิดขึ้นกรณีเดียวเท่านั้น คือฟังก์ชันซ้อนฟังก์ชันครับ แม่ลูกนั่นเอง เมื่อลูกเกิด มันจะสร้าง [[Scope] ใน frame ของตนเพื่อชี้ไปยัง frame ของแม่ แม่ในที่นี้เป็นแม่โดยตรงเท่านั้นครับ จึงชี้ไปที่เดียว ไม่ชี้ขึ้นไปในระดับถัดไปในระดับยายระดับทวดแต่อย่างใด

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

ดังนั้นจากหัวข้อที่แล้ว ตัวลูกแต่ละตัวมี frame เป็นของตัวเอง และถูกคำสั่ง for ดึงเข้ามาทำงาน และเมื่อทำงาน ก็มีการเรียกหาค่า num ทอดตาไปดูทั้ง frame แล้ว ไม่ปรากฏค่า num ดังนั้นจึงมุดเข้าไป [[Scope]] ไปโผล่ที่ frame ของ แม่ ปรากฏว่ามีค่า num ก็ใช้ค่า num ที่นี่ ค่านี้แก้ได้นะครับ เพื่อให้เข้าใจตรงกัน ผมขอเขียนกระดานดำดังนี้ครับ

 

closure-full

ภาพนี้เป็นการแช่ภาพบรรทัดที่ 15 เมื่อ ลูกตัวที่ 3 นั้นทำงาน ดังนั้นมันจึงอยู่ใน stackframe เพียงตัวเดียว ลูกตัวอื่นอยู่นอก stackframe นะครับ  สังเกตดีๆ นะครับ  ลูกตัวอยู่ไม่ได้อยู่ใน stackแต่ก็ยังไม่ตาย เพราะมีตัวแปร arrx ของ Global ชี้อยู่  ทำให้ array ไม่ตาย และเเมื่อ array ไม่ตาย มันก็เลยทำให้ลูกแต่ละตัวไม่ตายไปด้วย และเพราะลูกแต่ละตัวไม่ตาย มันยังคงชี้แม่อยู่ ทำให้แม่ไม่ตายไปด้วย  ดังนั้น ถ้าเราเพิ่มบรรทัดสุดท้ายเข้าไปว่า

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

การประยุกต์ใช้ closure

การทำงานภายในของ closure ดูๆ แล้วซับซ้อนเอาเรื่อง นั่นก็ไม่ใช่เรื่องเปลกครับ เพราะการทำงานของ FP และ imperative นั้นแตกต่างกันค่อนข้างมาก อย่าง Haskell ที่เป็น FP เต็มรูปนั้น ถ้าจะมารองรับ imperative ก็ยุ่งยากเช่นกันต้องคิดค้น Monad ขึ้น  แต่เอาเถอะครับ ความซับซ้อนมันก็ซ่อนอยู่ภายใน แต่การประยุกต์ใช้นั้นไม่ได้ยุ่งยากอย่างที่คิดครับ ในหัวข้อนี้ผมขอเสนอการประยุกต์ใช้ closure ในบางมิติ ง่ายๆ ตรงไปตรงมา จำไว้นะครับ  closure ง่ายนิดเดียว

ซ่อนตัวแปร

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

ก็อย่างที่ว่าไป ทำงานได้ แต่ตัวแปร num ก็จะเป็นหมู่บ้านกระสุนตก เสี่ยงถูกยิงจากโค้ดส่วนอื่น ถ้าเป็นภาษา C/C++ ก็มีวิธีการจัดการปัญหานี้ง่ายๆ โดยใช้ตัวแปร static ดังนี้

โปรแกรมข้างบนนี้เขียนด้วย C++ แสดงให้เห็นถึงความสามารถในการ “อม” ตัวแปรเอาไว้ในฟังก์ชัน ตัวแปรนี้จะไม่ตายในเมื่อฟังก์ชันทำงานเสร็จนะครับ การทำงานภายในจริงๆ ก็คือ ตัวแปร static นั้นแท้จริงแล้วก็คือตัวแปร global ที่กำหนดขอบเขตให้ฟังก์ชัน f() เห็น เรื่องก็มีแค่นั้น

ในเมื่อ ECMAScript ไม่รองรับ static แต่ก็สามารถเลี่ยงมาใช้ความสามารถของ closure ได้ดังนี้

จำกลยุทธ์จักจั่นลอกคราบในบทก่อนได้ไหมครับ ผมนำกลับมาใช้อีกครั้ง แต่ที่อยากให้พิจารณาในหัวข้อนี้ก็คือตัวแปร num  ไม่ได้อยู่ใน global อีกต่อไป ปลอดภัยขึ้นครับ ไปนิยามอยู่ใน f() จึงเกิดการซ่อน และ f() ใหม่ที่ลอกคราบมาก็มองเห็นตามกฏของการปิดล้อมของ closure นั่นคือการประยุกต์อย่างง่ายๆ อย่างแรกครับ

จำลองการสร้าง class

การใช้งานลักษณะจักจั่นลอกคราบนั้น ดูเหมือนว่าจะดี แต่ในชีวิตจริงแล้วทำให้โปรแกรมอ่านยากครับ เรามาลองดูอีกวิธีที่ทำให้อ่านง่ายขึ้นกัน

โค้ต C++ นี้แสดงโปรแกรมเดิมแต่ปรับมาใช้ class แทน ประเด็นไม่ได้อยู่ที่การใช้ class นะครับ ผมตั้งใจให้มองแค่ใน main() เท่านั้น ดูลักษณะการใช้งานให้ดีนะครับ มันจะทำงานเป็น 2 ขยัก ก็คือบรรทัดที่ใช้สร้างเป็นบรรทัดแรก จากนั้นก็เอาของที่สร้างมานั้นเอาไปใช้งาน จะใช้กี่ครั้งก็ได้ แต่การสร้างนั้นครั้งเดียว จะมองว่า CreateF() นั้นเป็นโรงงาน (factory) นั่นเอง

การใช้งานแบบ 2 ขยักอย่างนี้ ทำให้การเขียนโปรแกรม ECMAScript เรียบง่ายขึ้น เรามาลองดูโค้ดนี้ครับ

ฟังก์ชัน create_f() จะสร้างฟังก์ชันมาให้เราเพื่อนำเอาไปใช้ครับ และฟังก์ชันที่สร้างมานั้น ก็เข้าถึง ตัวแปร num ของ create_f() ได้ ทำให้โค้ดนั้นอ่านง่ายขึ้น ดูเหมือนเป็นการสร้าง object จาก class แต่ object นั้นมี method เดียว วิธีนี้เป็นวิธียอดนิยมในภาษา ECMAScript

คราวนี้เรามาลองดูการใช้แนวคิดนี้มาประยุกต์หาค่า Fibonacci ดังในบทความที่ผ่านมาดูบ้างครับ

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

เพิ่มความสามารถให้แก่ callback function

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

โดยที่เมื่อรันแล้ว จะพิมพ์ ‘hello’ ทันที จากนั้นก็จะรอ 10 วินาที แล้วค่อยพิมพ์ ‘world’ และเรารู้ว่าการรอ 10 วินาที นั้นใช้

วัตถุดิบมีครบแล้ว ถามว่าท่านจะยำมาเป็นเป็นฟังก์ชัน wait() นี้อย่างไร บางท่านเขียนดังนี้

อันนี้ผิดนะครับ เพราะ setTimeout() จะแค่ลงทะเบียนฟังก์ชันเอาไว้ ยังไม่มีการทำงานเกิดขึ้นเหมือนภาษาอื่น ดังนั้นเมื่อรันโปรแกรมนี้ มันจะพิมพ์ ‘hello’ และ ‘world’ คนละบรรทัดทันที จากนั้นค่อยรอ 10 วินาที โปรแกรมจะจบการทำงาน  ซึ่งไม่ตรงนะครับ เราต้องการแสดง ‘world’ หลังรอไปแล้ว 10 วินาทีต่างหาก

ดังนั้น callback function ใน setTimeout เป็นสิ่งที่ละเลยไม่ได้ ‘world’ ต้องพิมพ์จากใน setTimeout() ดังนั้น ถ้าเขียนโปรแกรมดังนี้

ถ้า myCallback() อยู่นอก wait() ย่อมมองไม่เห็น msg2 และการเอา msg2  ออกไปสู่ global ก็ไม่ใช่วิธีทีดี ดังนั้น myCallback() จึงต้องนิยามใน wait() และอาศัยความสามารถของ closure ก็ทำให้สามารถเข้าถึง msg2 ได้ ดังนั้นโปรแกรมจึงกลายเป็น

แต่คนเขียน ECMAScript มักนิยมเขียนฟังก์ชันแบบ on-the-fly จะได้ไม่ต้องตั้งชื่อ เพราะอาชีพการเขียนโปรแกรมเป็นอาชีพที่ต้องตั้งชื่อบ่อยที่สุดมากกว่าสาขาอาชีพอื่น พวกเราต้องตั้งชื่อกันวันละเป็นหลายร้อยหน และมีการวิจัยมาแล้วว่าสิ่งที่ยากที่สุดในการเขียนโปรแกรมก็คือการตั้งชื่อนั่นเอง หลีกเลี่ยงการตั้งชื่อบ้างก็น่าจะดี ดังนั้นผลลัพธ์จึงได้เป็นดังนี้

มือใหม่อาจจะสับสนว่าไปเข้าใจว่า เรานิยามฟังก์ชัน on-the-fly ภายใต้ setTimeout() ทำให้เรา closure ตัวแปรภายในของ setTimeout() ได้ จริงๆ แล้วไม่ใช่นะครับ on-the-fly function นั้นเข้าถึงตัวแปรใดๆ ของ setTimeout() ไม่ได้เลย แต่เข้าถึงตัวแปรของฟังก์ชัน wait() ได้  นั่นก็เพราะ lexical scope ครอบคลุมเฉพาะการสร้างเท่านั้นนะครับ ไม่รวมถึงการใช้  ในกรณี setTimeout() เป็นการใช้ จึงไม่เกิด closure ขึ้น

จากตัวอย่างไร้สาระนี้ พอทำให้เราเห็นได้ว่า ลำพังcallback ฟังก์ชันนั้นถ้าอยู่ลำพังเลยมักจะมีข้อจำกัดในการทำงานมาก จำเป็นต้องใช้ข้อมูลภายนอก และ closure ก็เป็นวิธีหนึ่งที่ดีมากที่จะทำให้ฟังก์ชันเหล่านั้นเข้าถึงข้อมูลภายนอกได้โดยที่ไม่ต้องขยายขอบเขต (scope) ของตัวแปร

pyramid of doom

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

code นี้ผมเคยแสดงให้ดูครั้งหนึ่งแล้วครับ เป็น code ที่ใช้คอมไพล์ TypeScript ให้เห็น JavaScript แล้วถ้าสำเร็จแล้วก็รันโปรแกรม ทำให้เราสามารถรันโปรแกรม TypeScript ได้โดยไม่ต้องออกจาก IDE  ประเด็นที่อยากนำเสนอในหัวข้อนี้คือด้านมืดของ callback  บ้างครับ

จากงานข้างบน ถ้าเขียนแบบภาษา C ธรรมดา ก็เขียนเรียงต่อไปเลยครับ

  1. คอมไพล์โปรแกรม
  2. ถ้าคอมไพล์โปรแกรมสำเร็จ ก็จะรันโปรแกรม

แต่ลองอ่านโต้ดข้างบนดูครับ มันไม่ได้ทำอย่างนั้นเลย เราเรียกใช้ ฟังก์ชัน exec() เพื่อคอมไพล์โปรแกรม ในบรรทัดที่ 3 ถ้าคอมไพล์เสร็จ ก็จะเกิดการ callback และใน callback นี่เอง ในบรรทัดที่ 7 ถ้าคอมไพล์ผ่าน ก็จะ exec อีกครั้งเพื่อการรัน ซึ่งเกิด callback ซ้อนเข้าไปอีกชั้น พอมองออกแล้วใช่ไหมครับ อะไรที่เขียนเรียงเป็นขั้นตอน พอมาเป็นแบบ callback ก็ต้องเขียนซ้อนเข้าไปเป็นชั้นๆ แบบนี้ ลึกลงเรื่อยๆ ฝรั่งมองเป็นเหมือน 3 เหลี่ยมลึกลงไปเรื่อยๆ ก็เลยเรียกปัญหานี้ว่า “pyramid of doom” หรืออาจเรียกว่า ‘callback hell’ ก็ด้วย เหมือนกับการเขียนแบบที่ใช้ goto เยอะๆ ในสมัยก่อน เราก็เรียกว่า ‘spaghetti code’

ถามว่าแล้วเราไม่เขียนแบบข้างบน ไปเขียนแบบ C ได้ไหม คำตอบคือไม่ได้ครับ ก็เพราะ library ส่วนมากมันสร้างมาเป็นแบบ callback ถ้าจะใช้ของเขาก็ต้องตามธรรมเนียมของเขาครับเลี่ยงไม่ได้ ถ้าเขียน node.js โค้ดขนาดกลางๆ ไม่ใหญ่มาก ไม่เล็กไป การเรียก callback ซ้อนเข้าไป 6-7 ชั้น ถือเป็นเรื่องปกติ ถ้าใครมาทางสาย FP โดยตรงก็จะรู้สึกเฉยๆ แต่ถ้ามาจากสาย imperative นี่ก็คงพึ่งยาแก้ปวดหัวบ้างครับ

Lambda lifting และ closure conversion

การซ้อนฟังก์ชันหลายๆ ชั้นนั้น อ่านยากและสับสน จนเกิดเป็น pyramid of doom ดังที่กล่าวมา ดังนั้นจึงมีความพยายามในการคิดค้นการลดปัญหาดังกล่าว โดยการทำลายความลึกของตัวปิรามิด เราเรียกวิธีการทำลายนี้ว่าการ defunctionalization ซึ่งมีวิธีหลักอยู่สองวิธีคือ Lambda lifting และ closure conversion

Lambda ในที่นี้ก็คือ callback function นั่นเอง (หรือจะเป็น lambda ใน ES6 ก็ได้) ดังนั้น ถ้า Lambda เกิดขึ้นในฟังก์ชันใด มันก็จะ closure ตัวแปรทั้งหมดของฟังก์ชันนั้นมา ดังนั้น Lambda และ closure จึงเป็นของสองสิ่งที่พึ่งพาอาศัยกัน

การลดชั้นของ Lambda ก็คือดึง Lambda นั้นเข้าสู่ชั้นบนกว่า (หรือ global) ถ้าทำได้ ก็จะได้ผลพลอยได้ก็คือฟังก์ชันดังกล่าวจะเรียกใช้ได้จากฟังก์ชันอื่น ไม่เป็นของเฉพาะของฟังก์ชันใดฟังก์ชันหนึ่ง (แต่ก็เป็นการเพิ่มมลพิษให้แก่ชั้นบนกว่า) แต่การโยน Lambda นั้นเข้าสู่ชั้นบนกว่า นั้นทำตรงๆ ไม่ได้เพราะมันผูกพันกับ closure ของฟังก์ชันเดิม การปรับนั้นเราใช้หลักการปรับโค้ดหรือ refactoring  ซึ่งได้สองวิธีดังที่กล่าวข้างต้น เรามาดูตัวอย่างปัญหากันก่อน

ถ้าเราปรับโค้ดเป็น

สังเกตได้ว่า เราดึงเอาฟังก์ชัน show() ออกข้างนอก แต่มันทำงานไม่ได้เพราะต้องพึ่ง closure ตัวแปร msg ก็ส่งเป็นพารามิเตอร์เลย แบบนี้เรียกว่า Lambda lifting ซึ่งมีข้อเสียที่เห็นๆ นอกเหนือจากการเกิดมลพิษชื่อ show() ในชั้นบน (global) แล้ว  ถ้า show() เคยถูกเรียกจากที่อื่น (call sites) ก็ต้องแก้ให้ใส่พารามิเตอร์ทุกตัวไป  คราวนี้มาดูอีกวิธี

วิธีนี้เรียกว่า closure conversion ก็คือการแปลง closure ให้กลายเป็นตัวแปรอิสระ (free variable) เอาขึ้นไปอยู่ชั้นบนเลย ทำให้ show() เรียกใช้ได้ ข้อดีก็คือ call sites ต่างๆ ไม่ต้องเปลี่ยน ไม่ต้องรื้อโค้ด แต่ข้อเสียก็คือตัวแปรอิสระจะทำให้เกิดมลพิษเพิ่มขึ้น

ใช้วิธีไหนหรือพอใจการใช้ closure ก็พิจารณากันเองนะครับ มันไม่มีสูตรตายตัว หาจุดพอดีให้เจอครับ

currying

อย่างที่ผมเคยเขียนในบทความชุด FP currying ถือว่าเป็นอาวุธหนักของ Haskell เลยทีเดียว ECMAScript ก็ไม่น้อยหน้าครับ รองรับ currying ได้ค่อนข้างดี ผมเริ่มจากโค้ดพื้นๆ อย่างนี้ก็แล้วกัน

ฟังก์ชันนี้ก็พื้นๆ ครับ หาค่ายกกำลังที่เป็นจำนวนเต็ม 2 ^ 8 ก็คือ 256 คราวนี้ในงานของเราส่วนมากเมื่อใช้เลขยกกำลังก็มีฐานเป็น 2 เสียเป็นส่วนใหญ่ การที่จะใส่พารามิเตอร์สองตัวมันก็ดูกระไรอยู่ เราสามารถสร้าง pow2() ได้ดังนี้

หรือจะเขียนเป็น arrow function ก็ได้ เช่น

แต่อีกทางเลือกหนึ่งก็คือการใช้ currying ใน ECMAScript ทำได้โดยผ่าน method bind ดังนี้ ดังนี้

ง่ายกว่าไหมครับ ชอบแบบไหนใช้แบบนั้นเลยครับ

ในส่วนของ undefined ก็เหมือน กับ .call() และ .apply() ในบทความที่แล้ว ซึ่งผมก็ขอยกยอดเอาไว้คุยกันในบทต่อไป

Functor

คำนี้เป็นคำสับสน ใน C++ ก็ใช้ ใน FP ก็มี แล้วมันก็คนละความหมาย เพื่อให้เห็นความแตกต่าง ผมขอนำเสนอ C++ ก่อนดังโค้ดข้างล่างครับ

เพื่อความเข้าใจ ผมขอตัดส่วนของการนิยาม class ทิ้งไป เหลือเพียงฟังก์ชัน main() และ highOrder()  ฝากให้คิดหน่อยครับ ถ้าดูเท่านี้ หน้าตามันดูคุ้นเคยไหม

ใช่แล้วครับมันก็คือการส่งฟังก์ชันเป็นพารามิเตอร์นั่นเอง ว่าแต่ว่าส่งไปได้ยังไง ฟังก์ชันใน C++ ไม่ใช่ first-class นี่หน่า object สิจึงจะเป็น first class การส่งฟังก์ชันตรงๆ เข้าไปยังฟังก์ชันอื่นทำไม่ได้ ดังนั้นจึงต้องมีการใช้เทคนิคเล็กน้อย ทำให้ object มันดูหน้าตาเหมือนฟังก์ชัน นั่นจึงเป็นที่มาของ Functor ในภาษา C++ ผมคงไม่อธิบายอะไรมากนะครับ มันนอกเรื่อง

Functor ใน FP นั้นมีความหมายแตกต่างออกไป เพื่อป้องกันความสับสน ทางฝ่าย C++ ส่วนหนึ่งจึงเลี่ยงเทคนิคข้างบนไม่เรียกว่า Functor แต่เรียกว่า function object แทน เอาหละครับ เรามาดู Functor ในมิติของ FP กัน

อ้างอิงตาม http://en.wikipedia.org/wiki/Functor เป็นส่วนเล็กๆ ส่วนหนึ่งในคณิตศาสตร์แขนง category theory นิยามสั้นๆ ของ Functor คือ ‘homomorphisms between categories’ ลองแปลเล่นๆ ดูครับ จำคำว่า polymorphism ใน object-oriented ได้ไหมครับ morphism มีหลายหน้า แต่เลือกที่จะมองเพียงมุมเดียว จะเป็นอาหาร เป็นของเล่น เป็นเสื้อผ้า ถ้าอยู่ในห้างมันจะมองเป็นหน้าเดียวกันที่เรียกว่า ‘สินค้า’ นั่นเอง  ถ้าเราถอดเอา poly ออกไป เอาคำว่า homo มาใส่แทน ซึ่งจะได้ผลตรงกันข้ามเลยครับ homomorphism จึงหมายความว่าเป็นของชนิดเดียวกันหมด เป็นอาหารก็ต้องเป็นอาหารอย่างเดียว จะมีของเล่นปนไม่ได้ ถ้าเป็นตัวเลข ก็จะเป็นตัวเลขอย่างเดียว จะมี string ปนไม่ได้เช่นกัน  นึกออกหรือยังครับ มันคืออะไร   ใช่แล้วครับ มันก็คือ array นั่นเอง

มาดูคำว่า categories บ้างครับมันหมายถึงหมวดหมู่ประเภท ตีความตามการเขียนโปรแกรม มันก็คือชนิดตัวแปรนั่นเอง ดังนั้นเมื่อรวมความทั้งหมดแล้ว Functor ก็คือการกระทำที่ทำกับ array ที่มีชนิดเดียวกัน โดยกระทำกับทุกสมาชิกใน array นั้นอย่างทั่วถึง ถ้าใครอ่านบทความของผมเรื่อง FP มา ก็จะรู้ทันทีว่า มันก็พวก Filter-Map-Reduce นั่นเอง

จริงๆ แล้ว Functor ยังมีรายละเอียดอีกเยอะ โดยเฉพาะอย่างยิ่งเมื่อใช้ร่วมกับ Monad จะยิ่งสนุก แต่เนื้อหามันเกิดขอบเขตของบทความนี้ครับ บทความนี้ผมขอจำกัดเนื้อหาของ Functor เฉพาะที่เป็น array หรือที่เรียกว่า array Functor นั่นเอง

Array Comprehension

array comprehension (หรือในภาษาอื่นเรียกว่า list comprehension) นั้น ทางทีม TC39 เคยประกาศว่าจะบรรจุลงไปใน ES6 แต่ก็เลื่อนจะไปบรรจุลงใน ES7 แทน เพราะติดประเด็นเรื่อง laziness ที่ยังไม่ลงรอยกัน ก็น่าเสียดาย ที่จริงมันก็มี libraries ที่ทำเรื่อง array comprehension เหมือนกัน และมีความสามารถ laziness ด้วยแต่ผมขอข้ามดีกว่า มันไม่เป็นมาตรฐาน ถ้าใครใจร้อนก็มีอีกทางเลือกหนึ่ง ลองไปศึกษา JavaScript engine ของ Mozilla ที่ชื่อว่า SpiderMonkey ดูครับ เขาทำล้ำมาตรฐาน ECMAScript ไปนานแล้ว ถ้าใน browser ก็เป็น FireFox  นั่นเอง รองรับ array comprehension เป็นอย่างดี แต่สำหรับบทความนี้ผมขอข้ามไปก่อนครับ

Map-Reduce Pattern

MapReduce pattern เป็นการประยุกต์ใช้ array Functor ซึ่งเรื่องนี้ผมเคยอธิบายเอาไว้แล้วในบทความ Functional Programming #5: Filter-Map-Reduce มาหัวข้อนี้ทฤษฎีก็เหมือนเดิมทุกประการ เพียงแต่ปรับให้เป็น ECMAScript เท่านั้น ค่อนข้างตรงไปตรงมา ดังนี้ครับ

array เป็นชนิด Object ดังนั้นมันจึงมี properties/methods และใน methods เหล่านี้ก็มี methods ที่รองรับการทำงานแบบ FP ไว้ค่อนข้างจะกว้างขวาง ผมขอเริ่มที่ map และ reduce โดยการแก้โจทย์ project Euler เจ้าเก่า ในบทความ FP #5 ผมทำถึงข้อ 5 คราวนี้ผมขอทำต่อข้อ 6 เลย

โจทย์ project Euler ข้อ 6 ผลต่างของผลรวมกำลังสอง

ผลรวมของเลขยกกำลัง 2 ของเลขธรรมชาติสิบตัวแรกคือ

{1}^2 + {2}^2 + ...+ {10}^2 = 385

กำลังสองของผลบวกตัวเลขธรรมชาติสิบตัวแรกคือ

{(1+2+3+...+10)}^2={55}^2=3025

ความแตกต่างของทั้งสองวิธีคือ 3025 – 385 = 2640  จงหาความแตกต่างดังกล่าวถ้าเปลี่ยนโจทย์จากเลขธรรมชาติสิบตัวแรกเป็น 100 ตัวแรก

ใครอ่านไม่รู้เรื่อง ก็อ่านภาษาอังกฤษต้นฉบับได้จาก https://projecteuler.net/problem=6

วิธีทำ เราเริ่มจากการสร้าง array ที่มีค่า 1 ถึง 100 ดังนี้ครับ

ถ้าสามารถใช้ array comprehension ได้ ก็จะเขียนได้โดยใช้เพียงบรรทัดเดียว แต่ในเมื่อทำไม่ได้ก็ยุ่งยากหน่อย ต้องเขียนเป็นแบบ imperative แบบข้างบนครับ ส่วนโปรแกรมทั้งหมดเขียนดังนี้

map() นั้นเป็น array Functor ที่ให้ผลลัพธ์เป็น array และในเมื่อเป็น array จึงสามารถใช้ array Functor ตัวอื่น . ต่อไปได้เลย ซ้อนได้เรื่อยๆ ผมเคยอธิบายไปหมดแล้ว ไปย้อนอ่านในบทความที่ผม link มาให้ได้ครับ

array Functor ของ ECMAScript นั้นเริ่มเป็นรูปเป็นร่างเมื่อ ES5.1 นี่เองครับ ผมคงไม่นำเสนออะไรมากมิฉะนั้นมันจะกลายเป็นการเขียนแบบอ้างอิงไป ถ้าใครสนใจว่า array Functor มีอะไรให้ใช้บ้าง ก็อ่านเพิ่มเติมได้จาก Array.prototype ครับ

ES6: ประยุกต์ใช้ arrow function

ถ้าเราใช้ arrow function ใน ES6 ก็จะได้ผลลัพธ์ที่สั้นกว่า สั้นพอจะคำนวณทุกอย่างอยู่ในบรรทัดเดียวได้ ดังนี้

คงจะอ่านเข้าใจกันง่ายอยู่แล้วนะครับ

กลยุทธ์: ขึ้นบ้านชักกระได

ทิ้งท้ายเรื่องสุดท้ายก่อนจบ มาลองพิจารณาฟังก์ชันนี้ดูครับ

ไม่มีอะไรมากใช่ไหมครับ ก็สร้างฟังก์ชันธรรมดาแล้วก็รัน แต่ถ้าผมอยากเขียนให้สั้นกว่านี้ โดยการสร้างฟังก์ชัน on-the-fly แล้วรันเลย ฟังก์ชันนี้ก็ไม่จำเป็นต้องมีชื่อ ผมขอไล่ขั้นตอนในการทำให้ดูดังนี้ครับ เริ่มจากนิยามฟังก์ชันไม่มีชื่อก่อน

ถ้าจะรันมันเลย ก็ใช้วงเล็บปิดหลังเลย จะเขียนแบบนี้เลย

แบบนี้ทำงานไม่ได้นะครับ เพราะ () มีระดับการทำงานสูงกว่าคำว่า function ดังนั้นมันจะทำ () ก่อน ไม่ถูกต้องนะครับ เรามีวิธีแก้อยู่ 2 ทางคือทางแรกคือ

สังเกตว่า เราเพิ่ม () คร่อมนิยามฟังก์ชัน หรืออีกวิธีหนึ่ง ก็เอา () คร่อมหมด ดังนี้

การเขียนแบบนี้เรียกว่า immediately-invoked function expression (IFFY) หรือการเรียกใช้ฟังก์ชัน expression แบบทำงานทันที  ก็คือสร้างเสร็จใช้งานได้เลยในคำสั่งเดียวกัน ท่านอาจสงสัยว่า มันจะอะไรกันนักหนา มันก็แค่ประหยัดบรรทัดการเรียกไปบรรทัดหนึ่งเท่านั้น หรือใครคิดลึกกว่านั้น ก็ต้องบอกว่าโค้ดนี้เป็นโค้ดไร้สาระ จะเขียนอ้อมๆ แบบนี้ทำไม อยากพิมพ์ ‘hello’ ก็ console.log() ไปเลยสิ จะมาคร่อมด้วย IFFY ให้มันเมื่อยตุ้มทำไม

คำตอบของเรื่องนี้มันอยู่ที่ของเขตของตัวแปรอีกนั่นแหละครับ เรื่องนี้เป็นเรื่องละเอียดอ่อนมากสำหรับ ECMAScript เรารู้อยู่แล้วว่า ตัวแปรที่สร้างในฟังก์ชัน จะตายหมดเมื่อหมดการทำงานของฟังก์ชัน ดังนั้นใน IFFY สร้างตัวแปรอะไรไว้ มันก็จะตายหมดเมื่อหมดฟังก์ชันเช่นกัน เพราะ IFFY คือฟังก์ชัน และการที่ใช้ฟังก์ชันเป็นแบบ on-the-fly แบบนี้ไม่มีการตั้งชื่อ ก็ทำให้ตัว IFFY เองไม่ก่อมลพิษขึ้นเอง ลองดูตัวอย่างล่าสุดในหัวข้อที่แล้วกันครับ

ตัวแปร a อยู่ใน global ไม่มีปัญหาครับ เพราะต้องใช้ในส่วนอื่น (ผมไม่ได้แสดงให้เห็น) แต่ตัวแปร i เป็นตัวแปรเฉพาะกาลเท่านั้น แต่สร้างมามันไม่ตาย มันกลายเป็นตัวแปร global ซึ่งไม่ดีนะครับ เราสามารถแก้โดยการใช้ IFFY ไปคร่อมดังนี้

จะเห็นว่า เมื่อทำงานเสร็จ กำหนดค่าให้แก่ a ครบทุกรอบแล้ว ก็หมดฟังก์ชัน ตัวแปร i ก็จะตายไปด้วย วิธีนี้จึงเป็นวิธีมาตรฐานที่คนเขียน ECMAScript เกือบทุกคนใช้งานครับ  ยังมีอีกรูปแบบนี้ที่มีคนใช้บ้างคือ IFFY แบบส่งค่าพารามิเตอร์ ดังนี้

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

ใน ECMAScript นั้น เป็นธรรมเนียมปฏิบัติว่าเราจะรบกวน global ให้น้อยที่สุด ดังนั้นใน frameworks หรือ libraries แต่ละตัวของ ECMAScript ถ้าเราเอามาใช้ มันจะสร้างตัวแปรที่เป็น global ให้น้อยที่สุด มีไม่น้อยที่สร้างเพียงแต่ตัวเดียว อย่างเช่น angularJS มีตัวแปรชื่อ angular ใน global เพียงตัวเดียวเป็นต้น ดังนั้นจึงไม่น่าแปลกใจที่เราจะพบการใช้ IFFY นี้เป็นปกติวิสัย

ES6: let

ถ้าท่านสังเกตโค้ดข้างต้นดังนี้

ท่านอาจแปลกใจว่าของเขตตัวแปร i ที่นิยามใน for loop ถ้าเป็นภาษา C++ แล้ว ตัวแปร i จะเกิดและตายใน for loop นี้ แต่นี่ไม่ใช่  นี่คือ ECMAScript ครับ จำได้ไหมครับ เมื่อเรา var ตัวแปร ไม่ว่าที่ใดในฟังก์ชันก็ตาม มันก็จะ hoist ตัวแปรนั้นขึ้นไปอยู่ข้างบนสุด จากนั้นมันจะมีชีวิตอยู่ ยาวไปจนหมดฟังก์ชันเลยทีเดียว ขอบเขตของมันกว้างมาก กว้างเกินไป ดังนั้นใน ES6 จึงนิยามการสร้างตัวแปรใหม่โดยใช้ let ครับ ซึ่งถ้าเราใช้ let นั้นก็จะมีขอบเขตเหมือนกันภาษาตระกูล C ทั่วไป คือ มีชีวิตอยู่ { } ไม่ไหลออกมา และไม่มีการ hoist แต่อย่างใด ดังนั้น เราสามารถเขียนได้เป็น

หรือ

ใช้ได้ทั้งสองวิธี ไม่มีปัญหา เชื่อในอนาคต let จะมีบทบาทเพิ่มขึ้น และ var ก็จะลดบทบาทลง ถ้าเลือกใช้ก็ใช้ let ให้มากๆ เข้าไว้ครับ ปลอดภัยกว่า

ทิ้งท้าย

เนื้อหาในบทนี้น่าจะขยายความแนวคิดของ FP ได้พอสมควร ตามที่มุ่งหวังเอาไว้ตอนแรก หวังว่าคงสนุกในการอ่านครับ

 

[Total: 15    Average: 4.2/5]

You may also like...

4 Responses

  1. iNut says:

    ก่อนอื่นเลยต้องขอขอบคุณที่ช่วยนำเรื่องราวที่ดูยากๆ มาเขียนให้น่าติดตามครับผม
    บางส่วนผมอ่านแล้วก็ยังไม่ค่อยเข้าสักเท่าไร แต่จะพยายามทำความเข้าใจครับ

    • เนื้อหาบท FP ของ ECMAScript นั้นต่อเนื่องมาจากชุด Intro FP ลองอ่านทั้งชุดดูก่อนครับ น่าจะทำให้เข้าใจได้มากขึ้น

  2. ximunfroy says:

    เยี่ยมมาก เป็นการอธิบายที่เข้าใจได้ง่ายมาก

  3. Geek says:

    ขอบคุณมากครับ อธิบายได้ละเอียดมาก และยกตัวอย่างให้เข้าใจได้ดี

Leave a Reply

Your email address will not be published. Required fields are marked *