ไอเจ๋อกับหมู่บ้านแห่งหนึ่งในกทม.

Back to the basic กับ functional programing ว่ากันเรื่องของ Currying และ Composition

Elec
20Scoops CNX
Published in
5 min readSep 2, 2018

--

ในโลกที่แฟชั่นได้พัฒนา design ใหม่ๆมาอย่างต่อเนื่อง จนถึงจุดนึงก็ได้ย้อนกลับมาเอาของเก่ามาเล่าใหม่ ตอนแรกดูโบราณไม่ทันสมัย ก็กลับมากลายเป็นของฮิตไปเสียซะงั้น

ในโลกของ programing ก็เช่นกันเมื่ออยู่ดีๆ functional programing ที่เป็น programing paradigm สมัยเก่า (ก่อนที่ OOP จะมาแรงแซงทุกโค้ง) ก็กลับมามีคนสนอกสนใจ เอามาปัดฝุ่น ตบๆ ก็ดูดีขึ้นมาทีเดียว

อาจเป็นเพราะสมัยก่อน memory มีราคาแพงเหลือเกิน functional programing ที่เน้น immutable (ไม่สามารถเปลี่ยนค่าได้) ก็เลยไม่เป็นที่นิยม จะแก้ค่าทีก็สร้างตัวแปรใหม่ เปลือง memory พอตัว

หรืออาจเป็นเพราะ class ของ OOP ดูเหมือนจะเป็นทางเลือกที่แก้ปัญหาได้ดีกว่า functional programing เลยตกกระป๋องไป

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

ก่อนที่จะกลายเป็นบทความเล่าเรื่องเก่า กลับมาสิ่งที่ผมจะเล่าให้ฟังในวันนี้กันครับ นั้นก็คือ Composition และ Currying

เพราะสองตัวนี้สำหรับผมแล้วถือเป็นหัวใจของ functional programing เลยครับ เพราะ functional programing จุดเด่นของมันก็คือเรื่องของ Composition ครับ

Composition ก็คือการที่เราเขียนหน่วยย่อยที่สุด (function) แล้วนำมาประกอบกันจนกลายเป็นหน่วยใหญ่

ส่วน Currying จะทำให้การ Composition ทำได้ง่ายขึ้นครับ

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

Photo by Rick Mason on Unsplash

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

  • Pure function
  • Higher order function

Pure function คือการที่ function ที่เราเขียนไม่ยุ่งกับภายนอกของ function หรือที่เราเรียกกันว่าทำให้เกิด side effect เช่นการเปลี่ยนค่าของตัวแปรนอก function หรือการเปลี่ยนค่า input argument (Mutate)

ลองมาดูตัวอย่าง function ที่ไม่เป็น pure ก่อนนะครับ (impure function)

เห็นได้ว่า changeName และ changeAge ได้ทำการแก้ไขตัวแปรที่อยู่นอก function (ตัวแปร person)

ปัญหาที่จะตามมาในการเขียน impure function ก็คือ เราคาดเดาได้ยากครับว่า function ที่เราเรียกนั้นมันไปกระทบกับส่วนอื่นของโปรแกรมหรือเปล่า ตอนเขียนอาจจะจำได้ แต่นึกถึงตอนที่ผ่านไปซักพักและ line of code เยอะกว่านี้ เราก็จะงงๆว่า “เอ๊ะ ทำไม object person มันเปลี่ยนไปได้กันนะ ไปเปลี่ยนตรงไหนเนี่ย” ถ้าผ่านไปซักพักเราก็อาจจะสบถออกมาจนเพื่อนร่วมงานเอือมระอากันได้ หรือเพื่อนร่วมชะตากรรม(คนในทีม) มาเขียนด้วย ก็อาจจะมีการสาปแช่งกันเกิดขึ้น (ขนาดนั้นเลย 555)

นอกจากนี้ยังทำให้ function ถูก reuse ได้ยากครับ เพราะเกิด dependency กับโค้ดภายนอก (ในกรณีนี้คือตัวแปร person) ซึ่งทำให้นำไป resuse และประกอบกับ function อื่นๆได้ยากครับ

ทีนี้เรามาลองดู pure function กันครับ

สังเกตุได้ว่า changeName และ changeAge ไม่ได้เปลี่ยนแปลงค่าของ object person แต่อย่างใด แต่มันได้ทำการสร้าง object ใหม่ขึ้นมาพร้อมกับค่าที่เราเปลี่ยนแปลง

pure function ไม่ทำให้เกิด side effect และทำให้โค้ดอ่านได้ง่ายขึ้น

เพราะเราไม่ต้องมานั่งงงกันแล้วว่า object person มันเปลี่ยนค่าจากไหน เพราะมันไม่มีการเปลี่ยนค่าเลยแต่เป็นการสร้างใหม่ขึ้นมาแทน

นอกจากนี้ยังทำให้ changeName และ changeAge สามารถ reuse ได้ง่ายขึ้นด้วย จะเป็น person จะเป็น dog หรือ cat ก็แค่ใส่เข้าไปใน function ผ่าน argument

นอกจากนี้ testability ก็ดีขึ้น เพราะเราสามารถ test ได้ด้วยการใส่ค่าเข้าไปใน function แล้วตรวจสอบว่า output เป็นแบบที่ต้องการหรือไม่

เนื่องจาก pure function ไม่ได้ยุ่งเกี่ยวกับค่าภายนอกและไม่มี side effect ก็หมายความว่า run กี่ครั้งถ้า input ตัวเดิม output ก็ได้ค่าเดิม

Higher order function(HOF) ก็เป็นหัวใจสำคัญของการ composition เช่นกัน ถ้าไม่มี HOF ก็เหมือนกับตัวต่อที่ไม่มีตุ่มไว้ต่อกับตัวต่ออื่นๆ เลยทีเดียว

Higher order function พูดถึง function ต้องสามารถเป็น argument ของ function อื่นได้ และ function ต้องสามารถ return function ได้

function ต้องสามารถเป็น argument ของ function อื่นได้

คุ้นๆไหมครับ มันก็คือ .map ของ array ใน javascript นั่นเอง

อย่างในตัวอย่างนี้ mapFn เป็น argument ที่เป็น function

ซึ่งมันก็ไม่ใช่เรื่องใหม่ใน javascript แต่อย่างใดครับ มันก็คือ callback function ที่เราคุ้นเคยกันนั้นเอง โดยเฉพาะเวลาดึงข้อมูลจาก web service (ก่อนที่จะมี promise และ async await)

function return เป็น function ได้

Spoiler alert! มันคือ currying นั้นเอง

function add เป็น function ที่รับ argument ที่ชื่อว่า num1 โดยจะ return เป็น function ที่รับ argument ที่มีชื่อว่า num2 และทำการบวกค่า num1 และ num2

สังเกตุ function add10 ครับ เป็น function ที่เก็บค่า num1 ไว้ก่อนแล้ว (ค่า 10) เหลือแต่รอเรียกพร้อมกับใส่ค่า num2 ( console.log(add10(2)); )

for geek: ถ้าใครสนใจว่าทำไมค่า num1 ถึงไม่ถูกลบออกจาก memory ทั้งๆที่ function ได้ถูกเรียกไปแล้ว ให้ลองไปดูเรื่อง closure นะครับ

ซึ่งการที่ function จะเป็น HOF หรือไม่นั้น ขึ้นอยู่กับภาษา programing ครับ ซึ่ง javascript ก็เป็นหนึ่งในนั้น

พอเราเข้าใจเรื่อง Pure function และ HOF แล้ว เราก็จะมาพูดถึงเรื่องที่เขียนอยู่บนหัวข้อบทความกันครับ — Currying และ Composition

Currying

Currying คือการที่เราแตก function ที่รับหลายๆ arguments ให้เป็นหลายๆ function ที่รับเพียง argument เดียว

function ที่รับหลายๆ arguments

f: A x B -> Y  // function ที่รับ argument A และ B แล้ว return Y

ให้เป็นหลายๆ function ที่รับเพียง argument เดียว

f: A -> B -> Y // function ที่รับ argument A แล้ว return function ที่รับ argument B แล้ว return Y

ลองดูตัวอย่างโค้ด Javascript กันครับ

ผมยกตัวอย่าง function add ซึ่งเป็น function ปกติ และ curriedAdd ซึ่งเป็นการ currying ครับ

เนื่องจาก curriedAdd return function ที่ return function เลยต้อง invoke 2 ครั้ง เพื่อให้ function return ผลลัพธ์ ( curriedAdd(5)(10) )

แล้วการทำ Currying มีประโยชน์อย่างไรบ้าง

หนึ่งในนั้นก็คือ partially applied (ไม่ต้องระบุ argument ทั้งหมดตั้งแต่แรก สามารถระบุ argument เพียงบางส่วนก่อนแล้วค่อยเรียกใช้ทีหลังได้)

จากตัวอย่างข้างบน เราทำการเรีียก curriedAdd พร้อมกับค่า argument 10 และเก็บ returned function ไว้ที่ตัวแปร plus10 จากนั้นเราก็ทำการเรียก plus10 พร้อมกับ argument ที่เหลือ

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

จากตัวอย่างข้างบนเราเขียน function curriedMap โดยรับ argument mapFn ซึ่ง return function ที่รับ argument items แล้ว return ผลลัพธ์ออกไป (การทำงานเหมือนกับ .map ของ javascript นั่นเองครับ แต่วิธีใช้ต่างกัน)

สังเกตได้ว่าเราทำการเรียก curriedMap เพื่อสร้าง doubleItems และ tripleItems function เก็บไว้ก่อน แล้วหลังจากนั้นนำมา reuse ใช้กับ itemsOne และ itemsTwo

สรุปก็คือ มันทำให้เราสามารถ reuse function พร้อมกับค่า argument ได้ครับ และนี่ก็เป็นหนึ่งวิธีในการ share code ของ functional programing ครับผม

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

Composition

Composition คือ การนำผลลัพธ์ของการเรียก function หนึ่งไปเป็น argument ของอีก function หนึ่ง

ยกตัวอย่างเช่น เราต้องการทำ function ที่เปลี่ยนข้อความให้เป็นตัวอักษรใหญ่ทั้งหมดและ split แต่ล่ะตัวอักษรให้อยู่ใน array โดยที่ array จะเรียงลำดับจากหลังไปหน้า

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

ลองมาดูแบบ Composition กันครับ

หลักการ Composition ก็คือการที่เราเขียน function ย่อยๆ แล้วมาเรียกรวมกันเพื่อให้ได้ผลลัพธ์ที่เราต้องการ ซึ่งมันทำให้เรา reuse ได้ง่ายมากครับ

reverseArray(textToArray(toUpercaseCase(myName))) 🤮 ดูน่าเกลียดใช่ไหมครับ ตอนนี้ให้มองข้ามไปก่อน เดี๋ยวเราจะมีวิธีการเรียกที่สวยกว่านี้แน่นอนครับ

เช่น ถ้าเราต้องการทำอีก function หนึ่ง ที่ไว้แค่สำหรับเปลี่ยนข้อความให้เป็นตัวอักษรใหญ่และ split แต่ละตัวอักษรให้อยู่ใน array เราก็แค่เอา function reverseArray ออก

หรือ ถ้าเราต้องการลบตัวอักษร O ออกด้วย ก็แค่เพิ่ม function ที่ไว้ลบตัวอักษร O

เห็นไหมครับ เราสามารถนำ function ย่อยๆ ที่เราทำไว้มาประกอบกันเป็น function ใหม่ได้ ซึ่งมันเหมือนกับตัวต่อ ที่เราสามารถนำแต่ละชิ้นมาประกอบกันเป็นสิ่งใหม่ๆ ได้

สังเกตได้ว่า func1 , func2 และ func3 เป็น function ที่เกิดจากการประกอบ function อื่นๆ (composition)

ในความคิดของผม มันเป็นวิธีที่ flexible มาก, reuse ได้ง่าย และเคารพใน OCP

Ramda ตัวช่วยในการเขียน functional ใน javascript

Ramda เป็น library ที่ช่วยให้เราสามารถเขียน javascript ในรูปแบบ functional ได้ง่ายขึ้นและสวยขึ้นครับ

เรามาดูการทำ currying ด้วย Ramda กันครับ

เราแค่ใช้ curry ของ Ramda มาครอบ function ที่เราเขียนไว้ โดยที่เราไม่ต้องเขียน function ที่อยู่ในรูปแบบ function return function โดย curry จะแปลง function ของเราให้เป็น currying function ให้เอง

นอกจากนี้เราสามารถเรียก curriedAdd แบบปกติได้ด้วย curriedAdd(10, 40) ซึ่งถ้าเราไม่ใช้ curry ของ ramda เราต้องทำการเรียกด้วย curriedAdd(10)(40) เท่านั้น

ส่วน composition เราสามารถใช้ function compose ของ Ramda ในการช่วยทำ composition ซึ่งในความคิดของผมแล้วมันสวยกว่าที่เราเขียนเองมากๆ

ลองเปรียบเทียบกับตัวอย่างข้างบนของ composition ที่เราเขียนเองดูนะครับ

function compose จะรับ argument เป็น function ที่เราต้องการจะ compose แล้ว return function ออกมา

เมื่อเราทำการเรียก function ที่ return มาจาก compose พร้อมกับค่า argument เช่น func1('John') ค่า argument นั้น('John') จะกลายมาเป็น argument ของ function ตัวท้ายสุดที่อยู่ใน argument ของ compose ซึ่งผลลัพธ์ที่ได้ก็จะกลายมาเป็น argument ของ function ตัวก่อนหน้า แล้วทำแบบนี้ไปเรื่อยๆ จนกว่าจะครบทุก function ใน argument ของ compose ครับ

ยกตัวอย่างเช่น func2('John') argument John จะถูกให้กับ function toUpperCase ซึ่งจะได้ผลลัพธ์เป็น JOHN จากนั้นนำค่า JOHN ไปเป็น argument ของ function textToArray ซึ่งจะได้ผลลัพธ์เป็น ['J', 'O', 'H', 'N']

เห็นได้ว่า compose ของ Ramda ช่วยให้เราเขียน composition ได้สวยกว่าและอ่านง่ายกว่าครับ

และอีกปัญหาหนึ่งที่ Ramda ช่วยเข้ามาแก้ไขคือการที่เราต้องทำ function สำหรับ prototype method ของ value แต่ละแบบ เช่น Array ก็จะมี .reverse() ที่ไว้เรียงลำดับ element จากหลังไปหน้า หรือ String ก็จะมี .toUpperCase() ในการทำตัวอักษรทุกตัวเป็นตัวอักษรใหญ่ การที่เราจะใช้ prototype method จำเป็นต้องมี value ก่อน เช่น [1,2,3].reverse() ซึ่งทำให้เราไปใช้กับ compose ไม่ได้ เราจึงจำเป็นต้องทำ function ที่ไว้เรียก prototype method อีกทีเช่น

const reverseArray = text => text.reverse();

Ramda มี function utilty ต่างๆที่อยู่ในรูปแบบ function ปกติที่ไม่ใช่ prototype method ให้เราเลือกใช้มากมาย

ซึ่ง function utility ของ ramda ก็เป็น currying function ครับ (สังเกตุ function split และ replace

Ramda ยังมีอีกหลายๆ function ที่น่าสนใจ ลองไปศึกษาดูกันได้ครับ

Composition มีอยู่แล้วในคณิตศาสตร์

บางท่านอ่านมาแล้วอาจจะคุ้นๆ ที่จริงแล้วหลักการ composition function นั้นมีอยู่แล้วในคณิตศาสตร์ ซึ่งก็คือเรื่องของ Set นั้นเอง ซึ่งในเรื่อง Set ก็มีการพูดถึงเรื่อง function เช่นกันครับ

จากตัวอย่าง(ภาพบนสุด)ข้างบน Set A ก็เปรียบเสมือน argument (ที่เป็นไปได้) ของ function f เมื่อ argument ที่อยู่ใน Set A ถูกใช้กับ function f ก็จะได้ผลลัพธ์ตามที่ลูกศรชี้ซึ่งอยู่ใน Set B

เช่น f(1) = 1, f(2) = 3 , f(3) = 1, f(4) = 2, …

โดยที่ผลลัพธ์ของ function f และ สมาชิกของ Set A (input) นั้นจะเป็นสมาชิกของ Set B

จากนั้นเรานำสมาชิกซึ่งอยู่ใน Set B มาเข้า function g ต่อ แล้วเราจะได้ผลลัพธ์ที่เป็นสมาชิกของ Set C

ซึ่งเราสามารถประกอบ function f และ function g ได้ในทางคณิตศาสตร์โดยเราจะใช้สัญลักษณ์ º

f º g

เมื่อเรานำสมาชิก Set A มาเป็น argument ของ function fºg เราจะได้ผลลัพธ์เป็นสมาชิกที่อยู่ Set C เลย (ดูรูปด้านล่าง)

ซึ่งมันเหมือนกับการทำ composition ที่เราทำก่อนหน้านี้ครับ

const fog = compose(f, g);

และ function ในทางคณิตศาสตร์ก็มีกฏหนึ่งที่เหมือนกับ pure function ที่กล่าวไว้ข้างบนครับ

function has only one relationship for each input value

function ถ้าถูกเรียกด้วย input ตัวเดิม จะต้องได้ผลลัพธ์ค่าเดิมเสมอ

นั้นหมายความว่า Set ที่เป็น input ของ function จะไม่มีทางมีลูกศรชี้ไปยังสมาชิกหลายๆตัวของ Set ที่เป็นผลลัพธ์

ถ้าเป็นเช่นนั้นแล้ว คณิตศาสตร์ถือว่า นั้นไม่ใช่ function

การ composition function ใน programing กับ คณิตศาสตร์ เหมือนกันได้เป็นเพราะความบังเอิญหรือตั้งใจ ?

เรามาลองดูคำนิยามของ functional programing ใน wikipedia กันครับ

functional programming is a programming paradigm — a style of building the structure and elements of computer programs that treats computation as the evaluation of mathematical functions

keyword อยู่ที่ that treats computation as the evaluation of mathematical functions

ใช่แล้วครับ จริงๆแล้ว functional programing เป็นการเขียนโปรแกรมให้คล้ายคลึงกับคณิตศาสตร์นั้นเอง ไม่ว่าจะเป็น composition, pure function เป็นการเลียนแบบ function และ Set ในคณิตศาสตร์ครับ

สรุป

functional programing จะเน้นการเขียน function ให้เป็นหน่วยย่อย (do one thing and do it well — SRP) และสร้าง function ใหม่ๆ ที่เกิดจากการ compose function อื่นๆ

และ function ที่เขียนขึ้นมาควรจะเป็น pure function เพื่อง่ายต่อการ compose function

Currying มีส่วนช่วยให้การ compose function ง่ายขึ้น

functional programing จะมีวิธีการ share code หรือ reuse code โดยใช้ composition ของ function ซึ่งแตกต่างออกไปกับ OOP ที่จะใช้ inheritance ของ object

ก็หวังว่าบทความนี้จะช่วยให้หลายคนเข้าใจ functional programing มากยิ่งขึ้น และสามารถใช้ประโยชน์จาก compose และ curry ได้

functional programing ยังมีอีกหลายเรื่องมากครับ ไว้โอกาสหน้าจะมาเขียนเกี่ยวกับ functional programing อีกแน่นอนครับ

แล้วเจอกันใหม่ในบทความต่อไปครับผม

Thanks Jedsada Tiwongvorakul, Aekachai Boonruang, Wutti Tarn

--

--

Elec
20Scoops CNX

Front-end developer addicting in reading coding and caffeine. live in Chiang Mai, Thailand.