Functional Programming ใน C#
คุณจะเขียน Program ยังไงไม่ให้มีการใช้ variable ครับ? ปกติเวลาเราเขียน C# กันก่อนหน้านี้ เราก็รู้ว่าคำสั่งต่าง ๆ ใน C# Run ทีละบรรทัด 1,2,3,4 เรียงกันไป ดังนั้นเราก็ต้องพึ่งพา variable ในการเก็บข้อมูลเพื่อทำงานในคำสั่งถัดไป เช่น ถ้าเราต้องการค่า 10! (10*9*8*...*1) คุณจะทำอย่างไรครับ เขียนอย่างนี้ใช่ไหมครับ
int n = 1; for (int m = 10; m > 1; m--) n *= m; Console.WriteLine(n); //3628800
เห็นไหมครับคุณประกาศตัวแปรตั้ง 2 ตัว m กับ n แล้วก็ Code อีก 3-4 บรรทัด ซึ่งอ่านทั้งหมด ต้องเอามาคิดต่อว่านี่คือการทำ 10! และไอ้ค่า m,n พวกนี้แหละครับที่คุณต้องคอยตาม Debug เวลา Program ทำงานผิดพลาด
Functional Programming (FP) จะคิดต่างกัน FP จะไม่มองการทำงานเป็นชุดคำสั่ง FP จะมองการทำงานเป็น Expression (นิยาม) จากตัวอย่างข้างบนจะมองว่า 10! คือ Factorial ของ 10 นิยามของ Factorial คือ
ถ้าเป็นภาษา Haskell ซึ่งถือว่าเป็น Pure functional programming เขียนอย่างนี้ครับ
fac 0 = 1 fac n = n * fac (n-1)
เวลาจะใช้ Function ก็
fac 10
เห็นไหมครับไม่มี variable ที่ประกาศใช้เองใน Function และไม่มีการ assign ค่า มีแต่ส่งผ่านค่าไป Function ถัดไป แถม Code ก็เข้าใจง่าย เพราะเป็นนิยามของ Function นั้น ๆ
เนื่องจาก FP นี้เน้นการทำงานกับ Function มาก จึงเกิด Concept ในการใช้งานขึ้นอย่างมากมาย พื้นฐานการทำงาน โดยทั่วไปของ FP มีดังนี้ครับ
Recursion หรือ การที่ Function เรียก Function ตัวเอง เช่น Function Factorial ตามตัวอย่าง ตัว C# Support การทำ Recursion ในระดับ member ของ Class ครับ แต่ไม่ Support ในระดับ anonymous methods แต่ก็สามารถแก้โดย assign ค่าเป็น null ก่อนได้ครับ
Func<int, int> fac = null; fac = n => (n == 0) ? 1 : n * fac(n - 1);
อย่างไรก็ดีการทำแบบนี้มีจุดอ่อนคือ ค่า fac นี้ ถ้าไปเปลี่ยนค่ามัน การทำงานจะผิดทันทีครับ ดูตัวอย่างและวิธีการแก้ไข ที่นี่
Pattern Matching หรือ การเปรียบเทียบ Input เพื่อเลือกการทำงานของ Function เช่น ในตัวอย่าง fac 0 คืนค่าเป็น 1 แต่ถ้าเป็น fac n คืนค่าเป็น n * fac (n-1) C# ไม่รองรับการทำ Pattern Matching ครับ แต่ไม่เป็นไรครับผมมี Code มาแก้ให้พอถูไถไปได้
public static Func<T, TResult> When<T, TResult>(this Func<T, TResult> func, Func<T, bool> predicate, Func<T, TResult> alternative) { return arg => predicate(arg) ? alternative(arg) : func(arg); } public static Func<T1, T2, TResult> When<T1, T2, TResult>(this Func<T1, T2, TResult> func, Func<T1, T2, bool> predicate, Func<T1, T2, TResult> alternative) { return (arg1, arg2) => predicate(arg1, arg2) ? alternative(arg1, arg2) : func(arg1, arg2); }
เอา Code ด้านบนไปใส่ใน Class สำหรับทำ Extension Methods นะครับ Code นี้ ยอมรับ 1-2 parameters นะครับ เวลาใช้งาน เช่น จะเขียน Function Factorial
Func<int, int> fac = null; fac = n => n * fac(n - 1); fac = fac.When(n => n == 0, n => 1);
เวลาประกาศ Function ให้ทำส่วนที่เป็น default ก่อน เช่นใน Code ด้านบนก็ประกาศ Function ของ factorial ก่อน แล้วค่อยกำหนดว่า ถ้า n เป็น 0 แล้วค่อยส่ง 1 กลับไป
Higher-order function คือ Function ที่มี Input เป็น Function ไม่ก็ Output เป็น Function หรือทั้ง 2 อย่าง C# รองรับครับ แต่ตอนประกาศ method จะดูแล้วซับซ้อนนิดนึง เช่น จะทำ Function สำหรับทำ Derivative มี Input เป็น Function คณิตศาสตร์นึง และ Output เป็นอีก Function นึงที่เกิดจากการ Diff
Func<Func<double, double>, Func<double, double>> diff = f => x => (f(x + 0.000001) - f(x)) / 0.000001;
var cos = diff(Math.Sin); Console.WriteLine(cos(Math.PI)); //-1 //diff ของ sin ได้ cos, cos(pi) ได้ -1
var inverse = diff(Math.Log); Console.WriteLine(inverse(2)); //0.5 //diff ของ ln ได้ inverse, inverse(2) ได้ 0.5
ดูบรรทัดแรกสิครับงงไหมครับ มันเป็นการประกาศว่านี่คือ Function ที่ Input เป็น Function นึง และ Output เป็นอีก Function นึง ซึ่ง C# ประกาศทียาวมาก ซึ่งถ้าไปเทียบกับ Haskell แล้วเวลาประกาศ แค่นี้เองครับ
diff :: (Double -> Double) -> (Double -> Double)
นี่ขนาด Code ของ Haskell ไม่ได้ลงสีนะครับ ยังอ่านง่ายเลย
สุดท้ายครับเรื่องของ Currying กับ Partial Function Application Currying คือการทำให้ Function ที่รับหลาย ๆ Parameters แตกออกเป็น Function ที่รับแค่ Parameter เดียว หลาย ๆ Function ซ้อนกันไป ดูตัวอย่าง
Func(a1,a2) => Func(a1)(a2)
จะเห็นว่า a1 กับ a2 แยกออกจากกันแล้ว ทีนี้เราสามารถ assign ค่า a1 ก่อน เราก็จะได้ Func(a2) มา การ assign ค่าบางส่วนเพื่อให้ได้ Function ใหม่นี่แหละครับ เรียกว่า Partial Function Application การทำ Curry และ Partial ใน C# เพิ่ม Code ตามนี้ครับ
public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult< func) { return arg1 => arg2 => func(arg1, arg2); } public static Func<T1, T2, TResult> Uncurry<T1, T2, TResult>(this Func<T1, Func<T2, TResult>> func) { return (arg1, arg2) => func(arg1)(arg2); } public static Func<T2, TResult> Partial<T1, T2, TResult>(this Func<T1, T2, TResult> func, T1 arg1) { return arg2 => func(arg1, arg2); }
เอา Code ด้านบนไปใส่ใน Class สำหรับ Extension Methods นะครับ ทีนี้มาลองดูตัวอย่างกัน
Func<Func<double, double>, Func<double, double>> diff = f => x => (f(x + 0.000001) - f(x)) / 0.000001;
var diffAndDo = diff.Uncurry(); Console.WriteLine(diffAndDo(Math.Sin, Math.PI)); //-1
var anotherDiff = diffAndDo.Curry(); var cos = anotherDiff(Math.Sin); Console.WriteLine(cos(Math.PI)); //-1
var inverse = diffAndDo.Partial(Math.Log); Console.WriteLine(inverse(2)); //0.5
Method ชื่อ Uncurry ทำให้ Func(a1)(a2) เป็น Func(a1,a2) โดยปกติ diff เราต้อง assign function 2 ครั้ง แต่ Uncurry ทำให้เราสามารถ assign Math.Sin กับ Math.PI พร้อมกันใน Function เดียวได้
Curry ก็คือการแตกออกทำให้ Func(a1,a2) เป็น Func(a1)(a2) อีกครั้ง ทีนี้เราสามารถทำ Partial Apply ได้ ซึ่งทำให้เกิดอีก Function ชื่อ cos
Partial เป็น Method รวบรัด เราไม่ต้องทำ Curry ก่อน แล้วมาทำ Partial Apply 2 ต่อ Method Partial จะให้โยน parameter แรกเข้าไปเลย ซึ่งทำให้เกิด Function ใหม่ทันที
จบแล้วครับพื้นฐานของ FP Post หน้าจะเป็นเรื่องของ FP ในการใช้งานจริงครับ
* อ่านจบแล้ว กรุณา Comment ด้วยครับ เพื่อเป็น Feedback และเป็นกำลังใจ * บทความนี้คัดลอกได้ แต่ต้องมี Link หรือ Url กลับมาที่หน้านี้ และเมื่อคัดลอกแล้วกรุณา Comment แจ้งด้วยครับ
Create Date : 29 กรกฎาคม 2551 |
Last Update : 9 สิงหาคม 2551 15:22:16 น. |
|
8 comments
|
Counter : 3121 Pageviews. |
|
|