การเขียนโปรแกรม: เขียนโปรแกรมด้วยสิ่งที่น้อยกว่าความว่างเปล่า
(joshmoody.org)- บทความนี้เป็นเรื่องเล่าเชิงเสียดสีในสถานการณ์สัมภาษณ์งาน ที่ผู้สมัครพยายามแก้โจทย์ FizzBuzz แบบง่าย ๆ ด้วยคอมบิเนเตอร์ฟังก์ชันบริสุทธิ์ที่อิงแคลคูลัสแลมบ์ดา
- ตัวเอกเริ่มต้นด้วย JavaScript แต่ค่อย ๆ นิยามคอมบิเนเตอร์ S, K, I และสร้างโครงสร้างพื้นฐานของภาษาใหม่ขึ้นมาด้วยตนเอง
- จากนั้นยังแทนค่าบูลีน ตัวเลข ลิสต์ และสตริงทั้งหมดด้วยคอมบิเนเตอร์ล้วน ๆ และใช้Y combinator เพื่อทำรีเคอร์ชัน
- ท้ายที่สุดเขาสร้าง FizzBuzz แบบ point-free ได้สำเร็จอย่างสมบูรณ์ แต่โค้ดก็ขยายตัวจนอยู่ในระดับที่มนุษย์แทบไม่อาจเข้าใจได้
- บทความนี้สำรวจแก่นแท้ของภาษาโปรแกรมและความสุดขั้วของการนามธรรมอย่างมีอารมณ์ขัน พร้อมสะท้อนการเสียดสีตัวเองในวัฒนธรรมนักพัฒนา
การเริ่มต้นของการสัมภาษณ์: FizzBuzz และคอมบิเนเตอร์
- เรื่องเริ่มจากฉากที่ผู้สัมภาษณ์ Dana ขอให้ผู้สมัครแก้โจทย์ FizzBuzz
- แต่แทนที่จะใช้ลูปทั่วไป ผู้สมัครกลับเริ่มจากการนิยามคอมบิเนเตอร์ S และ K เพื่อแก้ปัญหา
- Dana รู้สึกงุนงง แต่ผู้สมัครก็พูดว่า “ขออีกนิดเดียว” แล้วเดินหน้าต่อ
- ผู้สมัครนิยามคอมบิเนเตอร์หลากหลายตัว เช่น I, B, C, W, T และใช้สิ่งเหล่านี้เป็นองค์ประกอบพื้นฐานของภาษาใหม่
- คอมบิเนเตอร์แต่ละตัวทำหน้าที่แทนแนวคิดหลักของแคลคูลัสแลมบ์ดา เช่น การประกอบฟังก์ชัน การสลับอาร์กิวเมนต์ และการประยุกต์ใช้กับตัวเอง
- ในคอมเมนต์ของโค้ดยังมีชื่อเล่นของคอมบิเนเตอร์อย่าง “Bluebird, Cardinal, Warbler, Thrush” ปรากฏอยู่
การนิยามบูลีนและตัวเลข
- ผู้สมัครนิยาม TRUE, FALSE, NOT ด้วยคอมบิเนเตอร์ล้วน ๆ เพื่อสร้างตรรกะบูลีนขึ้นมา
- Dana ถามว่า “นี่แคลคูลัสแลมบ์ดาหรือเปล่า” แต่ผู้สมัครตอบว่า “แคลคูลัสแลมบ์ดามันเทอะทะเกินไป”
- จากนั้นจึงนิยาม ZERO, SUCC, PRED, IS_ZERO เพื่อประกอบเป็นระบบตัวเลข (Church numeral)
SUCCคือฟังก์ชันตัวถัดไป,PREDคือฟังก์ชันตัวก่อนหน้า และDECREMENTคือการลดค่าที่จะไม่ลดต่ำกว่า 0- มีการนิยามตัวเลขตั้งแต่
ONEถึงTENไล่ตามลำดับ
รีเคอร์ชันและ Y combinator
- ผู้สมัครนิยามฟังก์ชัน ADD แล้วค่อย ๆ แปลงมันให้อยู่ในรูปแบบ point-free
- เขานิยาม
ADD_MAKERแล้วห่อมันด้วยY combinator เพื่อให้เกิดรีเคอร์ชันได้ - Dana ทักว่า “ใน JavaScript ก็ทำรีเคอร์ชันได้อยู่แล้ว” แต่ผู้สมัครตอบว่า “อีกเดี๋ยวมันก็จะไม่ใช่ JavaScript แล้ว”
- เขานิยาม
- เมื่อเกิด stack overflow จากการประเมินผลแบบ eager ของ JavaScript ผู้สมัครจึงนำโค้ดไปรันบนอินเทอร์พรีเตอร์ JS แบบ “lazy” ชื่อ Skoobert
- ผลลัพธ์คือสามารถพิมพ์
3ออกมาได้สำเร็จ
- ผลลัพธ์คือสามารถพิมพ์
เลขคณิต ลิสต์ และสตริง
- ผู้สมัครอิมพลีเมนต์การคำนวณอย่าง SUBTRACT, MULTIPLY, MOD, DIVIDE ทั้งหมดบนฐานคอมบิเนเตอร์
- แต่ละการดำเนินการถูกประกอบขึ้นจากการเชื่อม
IS_ZERO,PRED,SUCCและส่วนอื่น ๆ แบบรีเคอร์ซีฟ
- แต่ละการดำเนินการถูกประกอบขึ้นจากการเชื่อม
- ต่อมาเขานิยาม CONS, FIRST, REST, EMPTY เพื่อสร้างโครงสร้างลิสต์
- รวมถึงอิมพลีเมนต์โอเปอเรชันเชิงฟังก์ชันระดับสูงอย่าง
MAP,FOLD,RANGE,CONCATทั้งหมดในรูปคอมบิเนเตอร์
- รวมถึงอิมพลีเมนต์โอเปอเรชันเชิงฟังก์ชันระดับสูงอย่าง
- เพื่อแสดงลิสต์ออกมาเป็นสตริง เขาเขียนฟังก์ชัน
renderListและshowLines- ส่วน Dana ดูเหมือนจะยอมแพ้ไปแล้วและไม่ตอบสนองอะไรอีก
การประกอบอักขระและสตริง
- ผู้สมัครนิยามรหัสอักขระตั้งแต่ 1 ถึง 9 และต่อยอดเป็นหลักสิบ โดยใช้คอมบิเนเตอร์
- ตั้งแต่
CHAR_AถึงCHAR_ZและCHAR_0ถึงCHAR_9ล้วนถูกแทนด้วยการประกอบเชิงตัวเลข
- ตั้งแต่
- ผ่านฟังก์ชัน
ARRAYเขาแปลงสตริงเป็นลิสต์ แล้วสร้างFIZZ,BUZZ,FIZZBUZZ- จากนั้นใช้ฟังก์ชัน
extractStringเพื่อแปลงลิสต์กลับเป็นสตริงจริง - ผลลัพธ์ที่แสดงบนคอนโซลคือ
"fizzbuzz"
- จากนั้นใช้ฟังก์ชัน
การแปลงจากตัวเลขเป็นสตริง
- มีการนิยาม
NUMBER_TO_DIGIT_LISTสำหรับแปลงตัวเลขเป็นลิสต์ของหลัก และNUMBER_TO_STRINGสำหรับแปลงลิสต์นั้นเป็นสตริง- ใช้
DIVIDE,MOD,TENและองค์ประกอบอื่น ๆ เพื่อแยกแต่ละหลักออกมา - จากนั้นแมปอักขระตัวเลขผ่านลิสต์
DIGITS_NUMERAL
- ใช้
- กระบวนการนี้ทำให้การแปลง ตัวเลข → อักขระ → สตริง เป็นไปได้อย่างสมบูรณ์ภายในระบบคอมบิเนเตอร์เดียวกัน
การทำ FizzBuzz ให้สมบูรณ์
- มีการนิยาม
FIFTEENและONE_HUNDREDแล้วใช้MAPกับRANGEเพื่อสร้างลิสต์ตั้งแต่ 1 ถึง 100- สำหรับแต่ละตัวเลข จะตรวจสอบว่าเป็นพหุคูณของ 3, 5 หรือ 15 เพื่อพิมพ์
"Fizz","Buzz","FizzBuzz"หรือสตริงของตัวเลขนั้น - จากนั้นแสดงผลลัพธ์สุดท้ายด้วย
showLines(extractString)(FIZZBUZZ_RESULT)
- สำหรับแต่ละตัวเลข จะตรวจสอบว่าเป็นพหุคูณของ 3, 5 หรือ 15 เพื่อพิมพ์
- Dana ถามว่า “ตอนนี้พอใจหรือยัง” แต่ผู้สมัครกลับลบนิยามตัวแปรทั้งหมดออก แล้วเขียนโค้ดใหม่ด้วยคอมบิเนเตอร์บริสุทธิ์เพียงอย่างเดียว
- ผลที่ได้คือนิพจน์ขนาดมหึมาที่ประกอบด้วย S และ K เท่านั้น ยาวหลายพันบรรทัด
บทสรุปและความหมาย
- บทความนี้สำรวจทั้งองค์ประกอบพื้นฐานที่สุดของภาษาโปรแกรม และแนวโน้มของนักพัฒนาที่ทำปัญหาง่าย ๆ ให้ซับซ้อนเกินเหตุ ในเชิงเสียดสี
- ชื่อเรื่องที่ว่า “เขียนโปรแกรมด้วยสิ่งที่น้อยกว่าความว่างเปล่า” สื่อถึงความพยายามสร้างโลกขึ้นใหม่จากหน่วยที่เล็กที่สุดของภาษา
- ผลงานชิ้นนี้คือวรรณกรรมเชิงเทคนิคที่ถ่ายทอดความเข้าใจลึกซึ้งเกี่ยวกับการเขียนโปรแกรมเชิงฟังก์ชัน แคลคูลัสแลมบ์ดา และตรรกะคอมบิเนเตอร์ ผ่านอารมณ์ขันและการเล่าเรื่อง
- ขณะเดียวกันก็เสียดสี “จิตวิญญาณนักพัฒนาแบบที่ต่อให้เป็น FizzBuzz ก็ยังเปลี่ยนให้กลายเป็นการทดลองเชิงปรัชญาได้”
1 ความคิดเห็น
ความเห็นจาก Hacker News
ดูเหมือนหลายคนจะคิดว่า “นี่มันอะไรกันแน่?” เลยขอลองอธิบายใจความของ combinator
combinator คือฟังก์ชันที่ไม่เปลี่ยนแปลงสถานะส่วนกลางและไม่ capture ตัวแปรภายนอก ถ้าให้พารามิเตอร์เดิมก็จะได้ผลลัพธ์เดิมเสมอ และไม่ส่งผลใด ๆ กับส่วนอื่นของระบบ
เมื่อนำฟังก์ชันบริสุทธิ์แบบนี้มาประกอบกัน ก็จะได้ combinator ตัวใหม่ กล่าวคือ การ compose ฟังก์ชันบริสุทธิ์ก็ยังคงเป็นฟังก์ชันบริสุทธิ์อยู่
ฟังก์ชันพวกนี้เขียนด้วย assembly หรือ C ก็ได้ไม่ยาก ตัวอย่างเช่น ถ้า implement S และ K ตรง ๆ บน x64 แล้วคอมไพล์ Common Lisp แบบอิง combinator ลงไป ข้างล่างสุดก็เท่ากับว่า Lisp รันอยู่บนฟังก์ชัน assembly แค่สองตัว
ประสิทธิภาพอาจไม่ดีนัก แต่ถ้าสร้าง inline combinator สำหรับแพตเทิร์นที่ใช้บ่อยสักไม่กี่ร้อยแบบแล้วตั้งชื่อให้ มันก็จะกลายเป็น “เครื่องจักร” ที่ใช้งานได้จริงพอสมควร
โครงสร้างแบบนี้ยังคล้ายกับ Forth ที่หวาดกลัว mutation, bytecode VM, หรือ CPU ที่เรียก combinator ว่า “คำสั่ง”
ท้ายที่สุด combinator อาจเรียกได้ว่าเป็น การแสดงออกของวิทยาการคอมพิวเตอร์ประยุกต์ ที่พยายามไม่สนใจรายละเอียดการ implement ให้มากที่สุด เพราะอย่างนั้นจึงอาจบอกได้ว่ามันเรียบง่ายกว่า lambda calculus
ถ้า implement lambda calculus ด้วย combinator ไม่กี่ตัว แล้ววาง Lisp ไว้ข้างบน งานที่ผูกกับเครื่องซึ่งต้องทำอยู่ก้นสแตกก็จะลดลงอย่างมาก
แค่เอาสิ่งนี้ไปประกอบกันไม่กี่ครั้งก็พอ
“ฉันไม่มีวันใช้ lambda calculus หรอก มันเป็น ภาษาที่พองเกินเหตุ”
แต่จริง ๆ แล้ว combinatory logic พองกว่าอีก ต้องใช้บิตมากกว่าเพื่อแทนโปรแกรมเดียวกัน
ตรงกันข้าม lambda calculus กลับเป็นหนึ่งใน ภาษาที่กระชับที่สุด
แม้แต่ในภาษา eager อย่าง JavaScript ก็ทำให้เป็น lazy ได้ด้วยการห่ออาร์กิวเมนต์ด้วย lambda หลอก ๆ ดูตัวอย่างได้ที่ JS BLC interpreter นี้
งานวิจัยที่เกี่ยวข้องดูได้ใน PDF นี้ และ ตัวอย่าง IOCCC
“รู้จัก FizzBuzz ไหม?”
“แน่นอนสิ เอาแบบ code golf 10 ตัวอักษร, แบบ 12 บรรทัดที่จูเนียร์ก็เข้าใจได้, หรือแบบเพี้ยนสุด ๆ เอาไว้ โชว์ความรู้ประหลาด ดีล่ะ?”
ลิงก์ผลลัพธ์
คำอธิบาย combinator logic นี่เท่มาก สไตล์การเขียนทำให้นึกถึงซีรีส์นี้
ทำให้นึกถึงบทความ “FizzBuzz in TensorFlow” ที่เคยอ่านมาก่อน
ลิงก์
กรอบแบบ การสัมภาษณ์เขียนโปรแกรม นี้ดูจะผิด ๆ ไปหน่อย
จุดประสงค์ของการสัมภาษณ์คือ (1) ตรวจสอบความรู้ด้านการเขียนโปรแกรมของผู้สมัคร และ (2) ดูความเข้ากันได้กับ วัฒนธรรมการเขียนโปรแกรมของบริษัท
ตัวอย่างเช่น ถ้ากำลังรับตำแหน่ง JavaScript แต่ผู้สมัครหมกมุ่นกับ combinatory logic อย่างลึกมาก ก็มีโอกาสสูงว่าจะไม่เข้ากับวัฒนธรรม ดังนั้นจะไม่ผ่านก็คงไม่แปลก
ถึงเวลาต้องจดจำ Moses Schönfinkel แล้ว
เขาเป็นคนคิดค้น combinatory logic ตอนเป็นศิษย์ของ Hilbert ในปี 1920 หลังจากกลับรัสเซียเขาป่วยทางจิต และเสียชีวิตอย่างยากจนในมอสโก ตาม Wikipedia บอกว่าต้นฉบับงานวิจัยของเขาถูกเพื่อนบ้านเอาไปเผาเป็นเชื้อเพลิงทำความร้อน
โค้ดบล็อกสุดท้ายใหญ่จนต้องมีสกรอลล์ (166KB)
S กับ K เป็น ฟังก์ชันแบบเคอร์รี ที่รับอาร์กิวเมนต์ครั้งละหนึ่งตัว
รายละเอียดดูได้ในคำตอบบน StackOverflow นี้
ชอบชื่อแบบนกอย่าง “Bluebird, cardinal, warbler, thrush” มาก อยากเอามาใช้เป็น ธรรมเนียมการตั้งชื่อ ใหม่
“Barendregt ไง Church มันแมสเกินไปแล้ว”
“เดี๋ยวมันก็จะไม่ใช่ JavaScript อีกต่อไป”
ให้ความรู้สึกเหมือน Douglas Adams มาสอน SICP
“นั่นคือ... Y combinator เหรอ?”
“ใช่แล้ว ต้องใช้ถ้าจะทำ recursion”
เกร็ดน่าสนุก: fixed-point combinator มีอยู่ไม่จำกัดจำนวน กล่าวคือ คุณสามารถทำ recursion ได้อีกนับไม่ถ้วนโดยไม่ต้องใช้ Y combinator