/ 365วันแห่งโปรแกรม

[365 วันแห่งโปรแกรม #day12] Anonymous function, Lambda expression, Higher-order function, Delegate (C#)

วันที่สิบสองของ ‪#‎365วันแห่งโปรแกรม‬ วันนี้เราจะมีคุยกันเรื่อง Anonymous function, Lambda expression, Higher-order function, Delegate (C#)


Anonymous function

Anonymous function คือฟังก์ชันที่ไม่ระบุชื่อ มีแต่ส่วนของ input parameters และ body ในเมื่อฟังก์ชัน Anonymous ไม่มีชื่อ จึงไม่สามารถเรียกใช้งานได้โดยตรง Anonymous functions มักถูกใช้เพื่อ ส่งเข้าไปยง Higher-order function หรือเป็นผลลัพธ์ของ Higher-order function

เมื่อไหร่ที่ควรสร้าง Anonymous function? เมื่อแน่ใจว่าฟังก์ชันนั้นจะถูกเรียกใช้ครั้งเดียวหรือไม่บ่อย หรือใช้จาก caller เดียวเท่านั้น Anonymous function ถูกใช้อย่างแพร่หลายในภาษาที่เป็น functional หรือภาษาที่มีฟังก์ชันเป็นประชากรชั้นหนึ่ง เพราะเราสามารถ assign ฟังก์ชันให้เป็นค่าของตัวแปรได้โดยตรง เช่นใน javascript เราสามารถ ประกาศ

var factorial = function(n){/*do something*/} 

ได้ทันที

Lambda Expressions

Lambda Expressions คือรูปแบบหนึ่งของ Anonymous function มีพื้นฐานอิงมาจาก Lambda Expressions ใน Lambda calculus ซึ่งเป็นจุดกำเนิดของ universal model of computation (Turing machines ถูกสร้างขึ้นมาตามคอนเซ็ปต์นี้) Lambda Expressions ในโปรแกรมมิ่งมักอยู่ในรูปแบบของ

input => statement

โดย statement ทางด้านขวา อาจจะเป็น expression หรือเป็นกลุ่มของ statement และจะคืนค่าหรือไม่ก็ได้

Higher-order function

Higher-order function คือฟังก์ชันที่รับพารามิเตอร์เป็น function หรือคืนค่าเป็น function ตัวอย่างที่เห็นชัดๆ น่าจะเป็น map

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

function map(list, strategy){
	list.forEach(function(item){
		yield strategy(item);
	});
}

map ถือเป็น Higher-order function ขึ้นมาทันที เพราะรับ strategy ซึ่งเป็นฟังก์ชันเข้าไป

ตัวอย่างของฟังก์ชันที่คืนค่าเป็นฟังก์ชัน

function greaterThan(n) {
	return function(m) { return m > n; };
}

ฟังก์ชัน greaterThan จะรับค่า n แล้วนำไปสร้างเป็นฟังก์ชันใหม่แล้วคืนกลับมา

isGreaterThan5 = greaterThan(5); // function(m) { return m > 5; }
isGreaterThan5(4); //false
isGreaterThan5(6); //true

Anonymous function ใน OO

ในปัจจุบันแล้ว เมื่อ OO เริ่มไม่ตอบโจทย์งานใหม่ๆ ภาษาที่เป็น OO ก็เริ่มปรับตัว เริ่มนำความสามารถของ functional มาใส่ และหนึ่งในนั้นก็คือ function as First-class citizen

ต่อไปผมจะขอยกตัวอย่างใน C# ซึ่งเป็นภาษาที่มีการใส่ความสามารถของ functional มานานแล้ว

ใน C# มี delegate ซึ่งทำหน้าที่เหมือน function pointer ใน C/C++ เพื่อทำให้สามารถ assign ฟังก์ชันให้ตัวแปรได้ และสามารถส่งฟังก์ชันเข้าไปในฟังก์ชันอื่นได้

การใช้ delegate ทำได้โดย ประกาศ delegate โดยระบุพารามิเตอร์และชนิดของ return พารามิเตอร์และชนิดของ returnที่ระบุนี้จะเป็นชุดเดียวกับพารามิเตอร์และชนิดของ returnของฟังก์ชันที่ delegate นี้รับได้

delegate void TestDelegate(string s); //delegate นี้ใช้ได้กับฟังก์ชันที่มีพารามิเตอร์เป็น string ตัวเดียว และไม่คืนค่า

หลังจากนั้นเราลองสร้างฟังก์ชันที่เข้ากันได้กับ delegate นี้

static void M(string s)
{
	Console.WriteLine(s);
}

หลังจากนั้นเราจะสามารถสร้างตัวแปรในชนิดของ delegate นั้นขึ้นมาได้

TestDelegate testDelA = new TestDelegate(M);

ต่อไปลองสั่งให้ testDelA ทำงาน

testDelA("Hello. My name is M and I write lines.");

หลังจากรันโค้ดนี้ จะเห็นข้อความบน console เป็น

Hello. My name is M and I write lines.

ตัวอย่างข้างบนเป็นการใช้ delegate กับ named function แต่ใน C# 2.0 เราสามารถเขียน
Anonymous method ได้แล้ว โดยใช้ keyword delegate ตามด้วยพารามิเตอร์และ body ของฟังก์ชัน

TestDelegate testDelB = delegate(string s) 
	{
    	Console.WriteLine(s); 
    };
testDelB ("Hello. My name is M and I write lines.");

เมื่อรันโค้ดนี้ จะได้ผลแบบเดียวกับตัวอย่างที่แล้ว ต่อมาใน C# 3.0 เราสามารถเขียน Lambda Expressions ได้ ทำให้เราสามารถเขียนโค้ดด้านบนใหม่ได้เป็น

TestDelegate testDelC = (x) => { Console.WriteLine(x); };
testDelC ("Hello. My name is M and I write lines.");

ทั้งสามตัวอย่างนั้นได้ผลลัพธ์เหมือนกัน แต่จะเห็นว่าโค้ดที่ออกมานั้นดูง่ายขึ้นเรื่อยๆ

ใน C# มีการสร้าง predefined delegate (delegate ที่ใช้ได้โดยไม่ต้องประกาศ) มาให้จำนวนหนึ่ง เช่น

  • Action<T> สำหรับใช้แทน void delegate (T) เพื่อใช้กับ delegate ที่ไม่คืนค่า

  • Func<T,TResult> สำหรับใช้แทน TResult delegate (T) เพื่อใช้กับ delegate ที่คืนค่า

  • EventHandler<T> สำหรับใช้แทน void delegate (T extends EventArgs) เพื่อใช้ใน Event Handler ของ UI

ใน LINQ เราสามารถเขียน query ในแบบ method-based ได้ โดยในรูปแบบนี้ทุกเมธอดจะรับพารามิเตอร์เป็น Func ที่มี return type ต่างกัน

IEnumerable<T>
.Where(Func<T,bool>) //ใช้ฟังก์ชันที่คืนค่าเป็น boolean ถ้าเป็นจริงก็จะเลือก
.OrderBy(Func<T,object>) //ใช้ฟังก์ชันที่คืนค่าเป็น object แล้วเรียงด้วย object นั้น
.Select(Func<T,object>); //อยากได้อะไรจาก query นี้ ก็ทำตรงนี้

เช่น

people
.Where(p => p.Role == "Admin")
.OrderBy(p => p.Id)
.Select(p => p.Name);

หมายความว่า จาก people (IEnumerable) ให้เลือกมาเฉาะคนที่มี Role เป็น Admin แล้วเรียงตาม Id แล้วเอามาแต่ Name อย่างเดียวนะไม่เอาอย่างอื่น

เราใช้เรื่องพวกนี้ทำอะไรกันบ้าง?

  • Event handler หรือ Callback เมื่อเกิด event ขึ้น เราจะสามารถเรียกฟังก์ชันขึ้นมาทำงาน เช่นเมื่อคลิกปุ่ม แล้วให้โชว์ dialog เราก็แค่โยนฟังก์ชันที่ใช้แสดง dialog เข้าไปใน function click ของปุ่ม

  • Observation บางครั้งเราก็ต้องการดักจับเหตุการณ์อะไรบางอย่างที่เกิดขึ้น เช่น state บางอย่างถูกเปลี่ยนแล้วไปให้ไปคำนวณค่าบางอย่างใหม่ โดยใช้วิธีสร้าง subscriber list เล้วเมื่อเกิด event ขึ้น ก็รันฟังก์ชันทั้งหมดใน list นั้น

  • Higher-order function อันนี้ก็ตามที่บอกไปแล้ว

ก่อนหน้าที่จะมีพวกนี้เราใช้ชีวิตยังไง? ใช้ Interface/class ที่มีฟังก์ชันที่ต้องการ แล้วก็โยน object ของมันทั้งก้อนเข้าไป เช่น ในจาวาหากเราต้องการ เรียงลำดับ object ใน list โดยใช้วิธีการเปรียบเทียบแบบอื่นนอกจากวิธีหลัก (ที่ได้จาก interface Comparable) เราจำเป็นต้องใช้ object ของ Comparator ในการ compare

class Foo{
	public int bar;
}

List<Foo> foos = //list of foo
Comparator<Foo> comparator = new Comparator<Foo>(){
	@Override
	public int compare(Foo foo1, Foo foo2 {
		return foo1.bar - foo2.bar;
	}
}

class Foo{
	public int bar;
}

Collections.sort(foos, comparator);

แต่ถ้าเป็น functional เราสามารถใช้การโยนฟังก์ชันเข้าไปได้เลย

Collections.sort(foos, (foo1, foo2) => foo1.bar - foo2.bar);

จะเห็นได้ว่าเมื่อเราสามารถใช้ฟังก์ชันเป็นพารามิเตอร์ได้ overhead ก็ลดลง อะไรๆ ก็ดูดีขึ้น

‪#‎day12‬ #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...