Functional Programming #3 : ฟังก์ชันมีระดับ

เกริ่นนำ

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

ฟังก์ชันกลายเป็นพลเมืองชั้นหนึ่ง

พลเมืองชั้นหนึ่ง (first class citizen) เป็นคำที่หมู่พลเมืองชั้นสองมักจะพูดถึง ซึ่งโดยทั่วไปแล้ว เมื่อคนที่ไปอยู่บ้านเมืองอื่น มักมีปัญหาไม่ได้รับสิทธิเท่าเทียมกับพลเมืองของเจ้าของประเทศ เช่นสิทธิในการรักษาพยาบาลเป็นต้น แล้วในแง่การเขียนโปรแกรมหละอะไรที่เรียกว่าพลเมืองชั้นหนึ่งหรือชั้นสอง เรื่องนี้ดูไม่ยากครับ ดูกันที่อะไรก็ตามถ้าสามารถกำหนดเป็นตัวแปรได้โดยตรงถือว่าเป็นชั้นหนึ่ง ถ้ากำหนดได้แต่โดยอ้อม ๆ หรืออาจไม่ได้เลย เราถือว่าเป็นชั้นสอง แต่ถ้ากำหนดเป็นตัวแปรไม่ได้เลย อันนี้ตีตั๋วชั้นสาม ใครมาก่อนได้ก่อน มาหลังต้องยืน อันนี้ล้อเล่นครับชั้นสามไม่มีนะครับ มีแค่สองชั้นถ้าท่านยังงงอยู่มันเกี่ยวอะไรกับการเขียนโปรแกรม ไม่เป็นไรครับลองดูตัวอย่างนี้ครับ

code ข้างบนนี้จะเป็นภาษาอะไรก็ช่างครับละไว้ในฐานที่ไม่เข้าใจ แต่พอจะดูออกใช่ไหมครับว่าเป็นตระกูลภาษาที่ใช้ paradigm Object-Oriented จะเห็นได้ว่า new Dog() นั้นเป็นการสร้าง object ใหม่ซึ่ง object ตัวนี้ถูกกำหนดให้แก่ตัวแปรที่ชื่อว่า munoi พอเห็นภาพรึยังครับ munoi เป็นตัวแปร แต่รับค่าเป็น object ได้ แสดงว่า object เป็นพลเมืองชั้นหนึ่งของภาษานี้ หรือจริงๆ ก็เป็นพลเมืองชั้นหนึ่งจักรวาลของ Object-Oriented ทุกภาษานั่นเอง

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

ภาษา Python ไม่ต้องนิยามตัวแปรก่อนใช้นะครับนึกอยากใช้ใช้เลย เรารู้เป็นอันดีว่า sqrt(n) นั้นเป็นฟังก์ชันหารรากที่ 2 ของพารามิเตอร์ n ที่ป้อนไป ถ้าเราเรียก munoi = sqrt(2)  ตัวแปร munoi จะเก็บค่าตัวเลข 1.4142135623730951 ซึ่งอันนี้ก็เป็นเรื่องทั่ว ๆ ไปรู้ดีกันอยู่แล้ว แต่ตัวอย่างนี้ ในบรรทัดแรกเรากำหนดค่า sqrt ให้แก่ munoi  สังเกตให้ดีนะครับ ไม่มีการใช้วงเล็บ ไม่มีค่าพารามิเตอร์ การกระทำนี้คือ การกำหนดให้ฟังก์ชันให้ munoi ครับ เป็นฟังก์ชันที่ทำงานเดียวกันกับ sqrt หรือจะพูดอีกมุมหนึ่งว่า sqrt และ munoi ชี้ที่ฟังก์ชันเดียวกันคือฟังก์ชันหารากที่สอง ดังนั้นในบรรทัดที่สองจึงสามารถนำมา munoi มาใช้งานเป็นฟังก์ชันได้ “เฉยเลย”

ภาษาอย่างภาษา C สามารถสร้างตัวแปรฟังก์ชันได้ แต่ยุ่งยากพอสมควรต้องใช้งานตัวชี้ (pointer) ที่เรียกว่า pointer to function ซึ่งเราก็ประทับตราให้กลายเป็นพลเมืองชั้นสองไป มาถึงจุดนี้บางท่านก็ยังคงคาใจอยู่ กำหนดฟังก์ชันเป็นตัวแปรได้ก็เข้าใจ แต่ “และยังไง”สาระมันอยู่ตรงไหน อันนี้ต้องใจเย็น ๆ ครับคำตอบมันจะอยู่ในหัวข้อต่อไปนี้เอง แต่จุดนี้ผมอยากทำความเข้าใจก่อนนะครับว่า เราไม่สามารถพิสูจน์ว่าภาษาใดเป็น FP หรือไม่ โดยใช้วิธีการนี้ครับ เนื่องจากภาษาบางภาษาโดยเฉพาะอย่างยิ่งสาย Object-Oriented จำพวก C#, Java, Scala อะไรทำนองนี้มันมองอะไรเป็น object ไปหมด พวกวัตถุนิยมก็อย่างนี้เอง ยกเว้นฟังก์ชันที่มันกลับไม่ได้มองเป็น object มาตั้งแต่แรก ครั้นจะไปข่มเขาโคขืนให้กลืนหญ้ามันก็ใช่ที่ครับ เอาเป็นว่าภาษาเหล่านี้แม้ว่าจะกำหนดฟังก์ชันให้เป็นตัวแปรไม่ได้ คือขั้นที่หนึ่งไม่ผ่าน แต่มันมีกลยุทธ์ที่ทำขั้นที่สองได้โดยไม่ต้องผ่านขั้นแรก แบบนี้ก็ถือว่าเป็น FP ได้เหมือนกัน

ฟังก์ชันมีระดับ : High-order function

เจ้ายศเจ้าอย่างเสียจริงนะครับ FP นี่ แบ่งชนชั้นพลเมืองยังไม่พอ ยังมีการยกฟังก์ชันบางฟังก์ชันเป็นฟังก์ชันมีระดับ (High-order function) เหนือกว่าฟังก์ชันทั่วไปอีก เอาเข้าไป ว่าแต่ว่ามันมีระดับอย่างไรกันนะ

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

สองบรรทัดแรกเป็นการนิยามฟังก์ชันง่าย ๆ นั่นคือฟังก์ชัน add รับพารามิเตอร์สองตัวแล้วส่งค่ากลับเป็นการเอามาบวกกัน ฟังก์ชัน mul ก็ทำนองเดียวกันแต่เป็นการคูณเราบรรทัดที่ 4 และ 5 จะเห็นได้ว่าเป็นการใช้ฟังก์ชัน add และ mul อย่างปกติทั่วไปเหมือนกับภาษาอื่น หามีอันใดแตกต่างไม่ เมื่อนำมาใช้ในบรรทัดที่ 3 และที่ 4 ก็จะเห็นผลลัพธ์ตามที่คาดไว้ครับ

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

คราวนี้เรามาลองดูการใช้งานในบรรทัด 6 และ 7 ครับ ในบรรทัดที่ 6 การเรียกใช้ process โดยส่งฟังก์ชัน mul เข้าไปเป็นพารามิเตอร์ตัวแรก จำได้ไหมครับฟังก์ชันเป็นพลเมืองชั้นหนึ่งในจักรวาลของ FP สามารถกำหนดฟังก์ชันให้กลายเป็นตัวแปรได้ ดังนั้นมีการกำหนดค่า  f=mul, a = 2, b = 3 และแทนค่ากลายเป็น f (2*2) (3*3)  ในบรรทัดที่ 10 ก็เช่นกันมีการกำหนดค่า f=add, a =2, b = 3  และจะได้ f (2*2) (3*3) เช่นกัน  พอเห็นภาพแล้วใช่ไหมครับยังครับ ถ้ายังก็มาต่ออีกตัวอย่างดูครับ

โปรแกรมข้างบนก็ยังคงเป็นภาษา Haskell ครับ  สังเกตนะครับ if –then-else ก็ใช้ได้ เหมือนกับภาษาทาง Imperative ผมขอใช้ add กับ mul จากตัวอย่างที่แล้วนะครับ ไม่เขียนใหม่ให้เปลืองพื้นที่ ตัวอย่างนี้ do_it เป็นฟังก์ชันมีระดับครับแต่ต่างกันตรงที่ไม่ได้รับพารามิเตอร์เป็นฟังก์ชัน แต่หากส่งกลับออกมาเป็นฟังก์ชัน

do_it รับ n มาเป็นตัวเลขธรรมดา ดังนั้นในบรรทัดที่สอง เรา ส่ง 2 ไปเป็นพารามิเตอร์ตัวแรก ซึ่ง do_it ก็รับพารามิเตอร์เพียงตัวเดียว ก็น่าจะจบ ว่าแต่ว่า พารามิเตอร์อีกสองตัว 5 และ 10 ไว้ทำอะไร อย่าเข้าใจผิดนะครับ พารามิเตอร์ 5, 10 ไม่ใช่สำหรับ do_it เพราะ do_it ต้องการพารามิเตอร์แค่ตัวเดียว พารามิเตอร์ที่เหลือทั้งสองตัว เป็นของฟังก์ชันที่ฟังก์ชันdo_it ส่งกลับออกมานั่นเอง ไม่งงนะ ไม่งง

ลองดูเงื่อนไขใน if สิครับ ถ้า n ต่ำกว่า 10 จะ ส่ง add ออกมา มิฉะนั้นจะ ส่ง  mul ออกมา  ก็ add กับ mul มันเป็นฟังก์ชันนี่ครับ do_it เลยกลายเป็นฟังก์ชันที่ส่งค่ากลับมาเป็นฟังก์ชัน นั่นคือฟังก์ชันมีระดับนั่นเอง เรามาดูตอนใช้กันครับ

ในบรรทัดแรกเป็นบรรทัดเต็มในการใช้งาน จะเห็นว่า do_it 2 จะถูกประมวลผลก่อน โดยเข้าไปใน if  ซึ่ง ค่าที่ส่งคือ 2 นั้นน้อยกว่า 10 ดังนั้นจึงส่งค่า add กลับมา อาจจะพูดได้ว่า do_it 2  ถูกแทนที่โดย add นี่เป็นกลไกพื้นฐานของ Lambda Calculus ที่เรียกว่าการแทนที่ (substitution) เมื่อ do_it 2 ถูกแทนที่กลายเป็น add แล้ว มันก็กลายเป็น add  “เฉยเลย” แทนที่แล้วกลายเป็นบรรทัดที่สองครับ  จากบรรทัดที่สองก็เป็นฟังก์ชัน add ธรรมดา บวกกันได้ 15 พอเข้าใจแล้วนะครับ

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

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

แลมด้าฟังก์ชัน : ขอพรพระเจ้าทันใจ

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

ผมเอา process จากหัวข้อที่แล้วมา ซึ่งมีพารามิเตอร์ 3 ตัวหนึ่งฟังก์ชันสองตัวแปรมาใช้งานผมมี add มี mul แต่ไม่อยากใช้ อยากได้ sub ซึ่งเป็นการลบแทนที่ ผมจะไปสร้าง sub แล้วก็นำเอาไปใช้ นี่ก็เป็นกรณีปกติ แต่ถ้าผมอยากจะสร้างมันทันทีในบรรทัดเดียว ผมใช้แลมด้าฟังก์ชัน ซึ่งก็คือฟังก์ชันที่สร้างแล้วใช้เลย ดังนี้ครับ

สังเกตว่าเราสร้างเวลาใช้งานเลย สร้างทีเดียวทิ้งและที่สำคัญ มันไม่มีชื่อครับ ไม่จำเป็นต้องเสียเวลาตั้งชื่อให้มัน เพราะไม่มีการอ้างอิงที่ใดอีก บางนี้เราก็เรียกมันว่าฟังก์ชันไร้นาม (anonymous function) แต่ในจักรวาลของ FP Alonzo Church ตั้งชื่อให้ว่าแลมด้าฟังก์ชัน (Lambda Function) แลมด้าในที่นี้เป็นอักษรกรีกตัวเล็กครับ หน้าตาเป็นดังรูปข้างล่างครับคงไม่ต้องบอกนะครับว่าแลมด้าจะมีบทบาทสำคัญต่อไปในอนาคตเชื่อว่าท่านดูละครหลังข่าวมามาก ท่านเดาได้ไม่น่าจะพลาดอยู่แล้ว

λ

มีข้อควรระวังครับ มือใหม่ที่หัดใช้ FP มักจะสับสนคิดว่าฟังก์ชันแลมด้าเป็นหัวใจของ FP นึกอะไรก็ออกมาเป็นฟังก์ชันแลมด้าทั้งหมด ทั้งนี้เพราะเข้าใจผิดในชื่อครับ แก่นของ FP อยู่ที่ Lambda Calculus เห็นคำว่าแลมด้าเหมือนกันเลยคิดว่านี่คือหัวใจ ไม่ใช่นะครับ ฟังก์ชันแลมด้าแท้ที่จริงแล้วเป็นสิ่งที่เรียกว่า “ผงชูรส” หรือ “Syntactic Sugar” มันเป็นสิ่งที่เสริมมาเพื่อเพิ่มความสะดวก อ่านง่าย ใช้คล่อง แต่ไม่มีมัน ไม่ถึงกับตายครับ จากตัวอย่างก็เห็นแล้วนะครับ ว่าแค่สร้างฟังก์ชัน sub เอาไว้ก่อน แล้วค่อยส่งมายัง process มันก็แค่นั้น แต่ถ้าภาษาไม่รองรับฟังก์ชันมีระดับ อันนี้สิจบเห่ของแท้ ไปต่อไม่เป็นเลย

ฟังก์ชันไม่สมบูรณ์

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

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

mul  และ double เป็นฟังก์ชันปกติ ดังที่กล่าวเอาไว้แล้ว ส่วน double n ก็รับพารามิเตอร์หนึ่งตัว ทำการคูณสองออกมาเป็นพลลัพธ์ ก็เป็นฟังก์ชันปกติครับ เอามาใช้งานบรรทัดที่ 3 ก็ได้ผลลัพธ์อย่างที่ควรเป็น  แต่ถ้าผมลองเปลี่ยนนิยามของ double เล่น ๆ ดู เป็น

ผมตัดพารามิเตอร์ n ทิ้งไป กลายเป็นฟังก์ชัน double ไม่ต้องการพารามิเตอร์ อันนี้ยังไม่เท่าไหร่ การสร้างฟังก์ชันไม่มีพารามิเตอร์เป็นเรื่องปกติ แต่ลองดูด้านขวาตอนใช้งานสิครับ  mul 2   เกิดอะไรขึ้นครับ mul ต้องการพารามิเตอร์สองตัว แต่นี่ส่งให้เพียงตัวเดียว แน่นอนครับ ถ้าเป็น Imperative จะทำอย่างนี้ไม่ได้ (คนละเรื่องกัน optional parameter นะครับ อันนี้ไม่มีก็ได้ แต่กรณีนี้ต้องมีครับ แต่ยังไม่มีตอนนี้เท่านั้น แต่มันต้องมี) mul 2 จึงกลายสภาพเป็นฟังก์ชันที่ไม่สมบูรณ์  ต้องการพารามิเตอร์สองตัวแต่เราให้เพียงแค่หนึ่งเท่านั้น แล้วใช้งานอย่างไร ท่านอาจถาม ก็ใช่เหมือนเดิมสิครับ ทำงานได้ด้วย

งงไหมครับ ฟังก์ชัน double ไม่ได้ต้องการพารามิเตอร์ แต่เราส่ง 5 เข้าไป  เดี๋ยวครับ เข้าใจผิดกันไปใหญ่แล้ว เมื่อ double ไม่ได้ต้องการค่าพารามิเตอร์ มันก็เลยไม่ได้อ่านไปครับ double มันสมบูรณ์ในตัวแล้ว จำกฎแทนที่ได้ไหมครับ กฎนั้นเลย การทำงานจริงๆ เป็นแบบนี้ครับ

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

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

บรรทัดแรกเรานิยามฟังก์ชัน process รับพารามิเตอร์สองตัว ตัวแรกเป็นฟังก์ชันที่รับพารามิเตอร์ตัวเดียว และพารามิเตอร์ตัวที่สองคือค่าที่จะส่งให้ฟังก์ชันนั้นทำ ดังนั้นในบรรทัดที่สองก็ตรงไปตรงมาใช้หลักการแทนที่ทำให้กลายเป็น sqrt 2  และหาค่าออกมาได้ในที่สุดบรรทัดที่สามนี่สิทีเด็ดใน Haskell  อย่างที่ทราบกันครับเครื่องหมายบวกเป็นฟังก์ชันที่ต้องการ พารามิเตอร์สองตัว ซ้ายและขวา ดังนั้นการที่จะส่งเครื่องหมาย + ให้แก่ process นั้นจะเป็นไปได้อย่างไร ในเมื่อ f นั้นต้องการพารามิเตอร์เพียงตัวเดียว แต่ + ต้องการพารามิเตอร์สองตัว ถ้าคุยกันภาษา Object-Oriented จะบอกว่ามันคือคนละ type กัน เอามาฟีเจอริ่งกันไม่ได้ แต่ในจักรวาลของ FP ทำได้ครับ โดยการแปลงเครื่องหมายบวก ที่ต้องการพารามิเตอร์สองตัว ทำให้เหลือตัวเดียวโดยการ “ถม” ไปตัวหนึ่ง ในที่นี่ (5+) เป็นการ “ถม” ซ้าย ทำให้มันกลายเป็นฟังก์ชันที่ขาดหรือต้องการพารามิเตอร์ตัวเดียว ซึ่งต้องกับนิยามของ f มันจึงมาจอยกันจอยกันได้ การทำเช่นนี้เป็นเรื่องปกติมากในโลกของ FP ครับ อย่างเช่นเคยครับ ถ้าถามหาการประยุกต์ก็ขอผัดผ่อนไปก่อนครับ

Currying : ผู้ชักใยเบื้องหลังฟังก์ชันไม่สมบูรณ์

ในหัวข้อนี้ ผมขอเล่าให้ฟังถึงเรื่องการถ่ายทำเบื้องหลังการทำงานของฟังก์ชันไม่สมบูรณ์ โดยกลไกที่ Haskell ใช้คือการแปรรูป (transform) โดยใช้ Currying ซึ่งผู้คิดค้นเป็นนักคณิตศาสตร์ชาวอเมริกัน (อีกแล้ว) ชื่อว่า Haskell Curry  ผู้อยู่ร่วมสมัยกับ Alonzo Church ว่าแต่ชื่อเขาคุ้น ๆ ไหมครับ ภาษาที่เราใช้งานเป็นหลักในบทนี้ (และบทต่อๆไป) ชื่อว่าภาษา Haskell และวิธีการที่กำลังเรียนรู้กันอยู่นี้ชื่อว่า Curry ทั้งสองเรื่องนี้ “คนอื่น”ตั้งชื่อให้เป็นเกียรติเป็นศรีนักคณิตศาสตร์ผู้นี้นั่นเอง ดังนั้นคงไม่แปลกใจนะครับถ้าผมบอกว่าภาษา Haskell นั้นใช้ Currying ได้อย่างเป็นธรรมชาติที่สุด

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

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

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

เพื่อความเข้าใจ ผมขอแยกแยะ Int ที่มีหลายตัวให้มันแตกต่างออกจากกันโดยการเติมตัวเลขเข้าไป เอาไปใช้กับ Haskell จริงไม่ได้นะครับ ทำเพื่อความง่ายในการทำความเข้าใจเท่านั้น ดังนี้

ถ้าเรา เขียนแค่  “process add”  มันจะกลายเป็นฟังก์ชัน ไม่สมบูรณ์ตาที่เรียนรู้มาในหัวข้อที่แล้ว  เมื่อเทียบกับ prototype แล้ว จะพบว่า “process add” ก็คือ  process  (Int1 -> Int2) นั่นเอง ซึ่งส่งกลับออกมาเป็นชนิดที่เหลือกคือ  (Int3 -> Int4)  นั่นก็คือส่งกลับออกมาเป็นฟังก์ชันที่รับตัวแปร Int3 หนึ่งตัว และส่งผลลัพธ์ออกมาเป็น Int4   มองเห็นอะไรไหมครับ ถ้าผมกำหนดให้

มันจะกลายเป็นว่า x นั้น กลายเป็นฟังก์ชันที่รับพารามิเตอร์ Int3 และส่งค่ากลับมาเป็น Int4  เห็นรึยังครับว่า เราสามารถแปลงฟังก์ชันที่ต้องการสองพารามิเตอร์ให้กลายเป็นฟังก์ชันที่ต้องการพารามิเตอร์เพียงตัวเดียวได้ โดยการส่งพารามิเตอร์ไปให้หนึ่งตัว ดังนั้น ถ้าเรามีฟังก์ชันที่ต้องการพารามิเตอร์ 10 ตัว แต่เราส่งไปให้เพียงตัวเดียว ผลลัพธ์ที่ได้คือ จะส่งกลับออกมาเป็นฟังก์ชันที่ต้องการค่าพารามิเตอร์ 9 ตัว นั่นเอง การส่งฟังก์ชันกลับออกมาในลักษณะนี้ก็คือ Currying นั่นเอง  ท่านสามารถมองเป็นการแทนที่แทนก็ไม่ผิดกติกาแต่อย่างใด

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

Laziness : ไฟไม่ลนก้นฉันจะไม่ทำ

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

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

พิจารณาเครื่องหมาย :  อ่านว่าคอนส์ (cons)  ในภาษา Haskell เป็นการสร้าง List นึกว่าเป็น Array ก็ได้   1 : 2 : 3   ก็จะได้ List หรือ Array ที่มีค่า 1, 2, 3  ในบทต่อไปครับจะมีรายละเอียดเรื่องนี้ แต่ตอนนี้รู้เท่านี้ก่อนครับจากบรรทัดแรกจะเห็นได้ว่าเป็นการสร้าง array ต่อหลังเพิ่มค่าทีละ 1 ไปเรื่อย ๆ ไม่มีที่สิ้นสุดครับถ้า n เป็น 5 ก็จะสร้าง 5, 6, 7, …  ไปเรื่อยๆไม่รู้จบถ้าเรียกใช้เมื่อใด หน่วยความจำเต็มแน่นอน

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

Haskell มีฟังก์ชันตัวหนึ่งครับชื่อว่า take ซึ่งรับพารามิเตอร์สองตัวตัวแรกเป็นตัวเลขบอกจำนวนตัวที่ต้องการและพารามิเตอร์ตัวที่สองคือ List  มันจะสร้าง List ย่อยที่มีสมาชิก n ตัวแรกจาก List ที่เราระบุถ้าเราสั่งงาน

คุณคิดว่าจะเกิดอะไรขึ้นถ้าเป็นภาษาในจักรวาล Imperative มันจะเข้าไปทำงานใน ints ก่อนจนเสร็จ จะได้ List ออกมาเพื่อส่งให้ take ทำต่อใช่ไหมครับ แต่ปัญหาก็คือ ints จะทำงานไม่รู้จบ ทำให้ take ไม่มีโอกาสได้ทำ ดังนั้นจึงแฮงค์ครับ แต่ถ้าเป็น Haskell ตอบออกมาเฉยเลยครับ  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ทำไมถึงทำได้ ก็เพราะความขี้เกียจไงครับ

take ทำงานก่อนครับเมื่อ take เริ่มทำงานมันจะขอตัวแรกของ ints ซึ่ง ints ก็จะให้ 1 ออกมาและ ints จะหยุดทำงานทันทีไม่ทำต่อ ในส่วนของการเรียกตัวเองเมื่อ take ขอตัวต่อไป ints จึงทำงานอีกครั้งได้ 2 แล้วจึงหยุดทำเช่นนี้ 10 ครั้ง take ก็เป็นไทแก่ตัวงานก็เสร็จสิ้นส่งผลลัพธ์เป็น List ออกมา นี่เองจึงเรียกว่าทำน้อยแต่ได้ผลงานมาก ความขี้เกียจนี้เอง ถือเป็นกลไกหลักของ FP ซึ่งมีประโยชน์มาก

ภาษาแบบ Interpreter อย่าง Python และ Ruby นั้นทำ Laziness ได้ยากมาก เท่าที่รู้คือปัจจุบันที่เขียนบทความนี้ยังทำไม่ได้ ภาษาแบบคอมไพล์มีไม่น้อยก็ไม่สามารถรองรับ Laziness ได้ ซึ่งต้องไปดูในรายละเอียดของแต่ละภาษาครับ

call by name

ในการเขียนโปรแกรมแบบ Imperative นั้นการเรียกใช้ฟังก์ชันจ ะมีการส่งพารามิเตอร์ไปยังฟังก์ชันที่เรียกว่าการ call โดยทั่วไปแล้วมีสองแบบคือ call by value และ call by reference ซึ่งเข้าใจดีอยู่แล้ว ภาษาบางภาษาเช่น Python เราสามารถเรียกใช้ฟังก์ชันโดยระบุชื่อตัวแปรได้เลยเช่น  mul(b=10, a=20)  เป็นต้น ข้อดีคือไม่ต้องจำลำดับการเรียงและที่สำคัญคืออ่าน Code โปรแกรมรู้เรื่องเลยไม่ต้องไปดูว่าพารามิเตอร์แต่ละตัวคืออะไร ซึ่งเรียกกันทั่วไป ว่า call by name ครับ

ที่พูดมาทั้งหมดนอกเรื่องทั้งนั้นครับผมอยากพูดถึง call by name ไม่ใช่อย่างที่กล่าวไว้ข้างบนนี้ครับ ผมกำลังคุยเรื่อง call by name ตามแบบของจักรวาล FP ไม่ใช่ตามแบบจักรวาล Imperative ชื่อมันซ้ำกัน แต่คนละความหมาย ก็เลยอธิบายถึงในมุมมอง Imperative เสียก่อน จะได้ไม่มาสับสนกับ FP  ในโลกของ FP นั้นก็ call by name ก็คือ Laziness ในระดับของการส่งพารามิเตอร์ จะเป็นอย่างไรลองดูตัวอย่างครับ

คำตอบได้ 18 แน่นอนครับ แต่การทำงานไม่เหมือนถ้าเป็นภาษา Imperative ทั่วไป ซึ่งจะเกิดการยุบ 1+2 กลายเป็น 3 เสียก่อนจะได้เป็น   sumsquare 3 4  แล้วยุบกลายเป็น (3*3) + (4*4)  แต่สำหรับ call by name แล้วหละก็ส่งเป็นภาพเข้าไปเลยครับ คือไม่มีการทำอะไรทั้งสิ้น ดังนี้ครับ

การทำงาน จะค่อยยุบทีเดียในขั้นตอนสุดท้าย ท่านอาจเห็นว่า call by name นั้นช้ากว่า call by value  ในกรณีข้างบนนี้ ถ้ารวบ (1+2) ไปทำ ก็จะได้ 3 ในทีเดียว แต่ถ้าขี้เกียจ ตอนท้ายต้องทำ (1+2) ถึงสองหน ในกรณี (1+3) ก็เช่นกัน  ซึ่งก็จริงครั บกรณีนี้เห็น ๆ เลยว่า call by value ทำงานได้เร็วกว่า แต่ถ้ามีพารามิเตอร์บางตัวไม่ได้ใช้งาน แบบนี้ call by name จะเร็วกว่าครับ  บางภาษาอย่าง Scala เลือกได้ว่าจะพารามิเตอร์แต่ละตัว call by value หรือ call by name ภาษาทางจักรวาลของ FP โดยทั่วไปจะ call by name เป็นหลัก  แต่สำหรับ Haskell แล้วมีการแอบปรับปรุงเล็กน้อยครับ คือแบบใส่หน่วยความจำของสิ่งที่ทำไปแล้ว ในกรณีนี้คือ 1+2 มันจึงทำครั้งเดียวครับ ครั้งต่อไปก็ดึงออกจากหน่วยความจำเลย ไม่ต้องคำนวณอีกแบบนี้เรียกว่า call by need ครับ ซึ่งทำงานเร็วขึ้นมาก

ทำไมภาษาแนว FP ต้องใช้ call by name น่าสงสัย ทั้งนี้เพราะความเป็น Laziness ไงครับ มันขี้เกียจทำมันเลยส่งเข้ามาตรง ๆ ไว้จวนตัวจริง ๆ แล้วค่อยทำในทีเดียวไงครับมาลองดูตัวอย่างขี้เกียจแล้วได้ดี อีกซักตัวอย่างดูครับ

จะเห็นว่าถ้าเรียกใช้ inf x แล้วหละก็ มันจะเกิดการเรียกตัวเองวนไปไม่รู้จบ จนหน่วยความจำเต็มกันเลยทีเดียว ลองดูบรรทัดที่ 3 ครับเรียกใช้ f  โดยส่ง 10 ไปยัง a และ inf 1 ไปยัง b  อันนี้ถ้าเป็นภาษาอื่นจะตายครับ แต่ในกรณีนี้เป็นแบบ call by name ครับ มันไม่มีการทำงาน มันยังไม่ทำงาน มันแค่ส่งลงมาในเมื่อตัวรับไม่มีการเรียกใช้มันก็แค่ข้ามไป  มีการใช้งานแค่ a มันจึงส่ง 10 กลับออกไป ปิดงานได้ อันนี้ก็นับได้ว่าเป็นข้อดีครับ

พบกันครั้งต่อไป

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

[Total: 3    Average: 2.7/5]

You may also like...

4 Responses

  1. Kai says:

    ขอบคุณครับ

  2. ศะรัณ says:

    ผมคิดว่า code ผิดเล็กน้อยนะครับ
    1: add a b = a + b
    2: mul a b = a * b
    3: add 1 2 — 3
    4: mul 2 3 — 6
    5: process f a b = f (a*a) (b*b)
    6: process mul 2 3 — 36
    7: process add 2 5 — 13

    บรรทัดที่ 7 ควรจะแก้ไขเป็น
    process add 2 3 — 13

  3. kulrapat says:

    ขอบคุณมากครับ

Leave a Reply

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