Thứ Tư, ngày 30 tháng 12 năm 2009

// // Leave a Comment

EX3 - Lập trình FP trong C# 3.0

Người lập trình đã dễ dàng tiếp cận với lập trình FP do nhiều kỹ thuật trong C# 3.0 có điểm chung với FP. Đối với người lập trình OOP, cảm nhận đầu tiên khi làm việc với C# 3.0 là sự thay đổi về cú pháp lập trình với các kỹ thuật như Anonymous type, Lambda Expression hay Lazy evaluation. Kết hợp các kỹ thuật này lại với nhau sẽ giảm thiểu số lượng công việc và áp lực công việc cho người lập trình, đồng thời làm tăng tính hiệu quả của sản phẩm do code nhỏ, gọn,

Type Inference – Anonymous type:

Type Inference: Được dùng để khai báo biến với từ khóa “var” mà không cần định nghĩa kiểu dữ liệu. Trình biên dịch sẽ tự động suy luận kiểu bằng cách tham chiếu đến giá trị được gán cho biến. Tuy nhiên kiểu khai báo này chỉ được sử dụng trong phạm vi local.

var Name =”An”;

var Age = “1/1/1980”;

var Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

Anonymous type: được xây dựng trên khái niệm “Tuple” - tập một dãy dữ liệu. Thí dụ dưới đây có 3 Tuple.

Name:An

salary:300

City: Ho Chi Minh

Anonymous type dùng để định nghĩa một kiểu dữ liệu mới nhưng không cần chỉ rõ tên và cấu trúc dữ liệu.

var myInfo = new {Name =”An”, Age=”1/1/1980”, Salary=300};

var mySalary = myInfo.Salary * 2;

Biến myInfo được khởi tạo với một kiểu dữ liệu hoàn toàn mới nhưng không định nghĩa cấu trúc rõ ràng. Phép toán “var mySalary = myInfo.Salary*2” không gây ra lỗi bởi trình biên dịch hiểu rõ cấu trúc kiểu dữ liệu của myInfo. Một trong những ưu điểm của Anonymous type là kết hợp với Type Inference trong việc tham chiếu dữ liệu.

Ứng dụng Type Inference – Anonymous type trong việc tham chiếu dữ liệu: các dự án ít hay nhiều đều liên quan đến tương tác với dữ liệu và người lập trình phải viết những đoạn mã để lọc một tập dữ liệu mới từ tập dữ liệu ban đầu. Tập dữ liệu mới có thể ít field hơn hay có thể kết hợp với tập dữ liệu khác nhiều field hơn. Công việc này được gọi là tham chiếu hay “Projection”. Trước đây công việc này thường tốn khá nhiều thời gian do phải định nghĩa cấu trúc dữ liệu, viết mã lưu dữ liệu vào cấu trúc mới ... mà đôi khi cấu trúc dữ liệu mới chỉ sử dụng trong một hàm. Amonumous type trở nên rất hiệu quả trong những yêu cầu như thế. Với tập dữ liệu ở thí dụ 4, yêu cầu tạo một danh sách những người tên “Huy” và danh sách mới này với chỉ lấy hai field Name và Salary và thêm một field mới Allowance có thể xử lý như sau:

Thí dụ 8:
var searchList = from r in people
where r.Name == “Huy”
select new { r.Name, r.Salary, Allowance = r.Salary * 0.7 };

Kiểu dữ liệu mới “searchList” với 3 field “Name, Salary, Allowance” được tạo mà không cần đòi hỏi quá nhiều thời gian cho việc định nghĩa cấu trúc, viết code hay phát hiện lỗi. Tuy nhiên “Anonymous type” chỉ sử dụng trong pham vi local.

Lambda Expression: Anonymous method hỗ trợ cách viết inline code nhưng chưa có sự thay đổi đáng kể về cú pháp câu lệnh. Lambda Expression là sự phát triển của Anonymous method, giúp giải quyết gánh nặng của người lập trình trong việc viết các đoạn mã. Có thể hiểu Lambda Expression như một hàm mang các biến số. Cú pháp “x => x+1” giống như hàm có một biến “x” và giá trị trả về của hàm là “x+1”. Lambda Expression được xây dựng trên nền của lý thuyết “Lambda calculus” được phát minh trong thập niên 1930. Kí tự Lambda (?) của người Hy Lạp được dùng đặt phía trước các biến số của hàm. Thí dụ dưới đây biểu diễn hàm một biến và hàm hai biến bằng Lambda Expression và cú pháp trong ngôn ngữ C# 3.0.

f(x) = x f(x,y) = x + y

Lambda Expression x ? x (x,y) ? x + y

C# 3.0 x => x (x,y) => x + y

Nhờ vào cú pháp đơn giản, nên dòng lệnh cũng đơn giản.

Thí du 9:
Func Add = (x, y) => x + y;
Lambda Expression có thể không có một tham số (parameter) nào hoặc có nhiều parameter và các parameter này cũng có thể được khai báo kiểu hoặc không cần khai báo kiểu.
n => (n-1)*(n-2)
x => (x % 2) == 0
(int x) => (x % 2) == 0
p => p.Name == “Huy” && p.Age > 25

Các kỹ thuật Closure hay Currying đều có thể sử dụng trong Lambda Expression.

Closure trong Lambda Expression là khả năng hiểu và kiểm soát biến không nằm trong phạm vi của biểu thức.

Thí dụ 10:
int x = 99;
Func add = y => y + x;
int firstResult = add(1); // 99 + 1 = 100
int secondResult = add(2); // 99 + 2 = 101

Tuy “y” không là biến local và nó cũng không là parameter của method nhưng chương trình không báo lỗi và nó được xem như là một biến “tự do”. Kết quả của biểu thức firstResult là 100 và trong trường hợp này Closure có vai trò lưu trạng thái giá trị một biến để có thể sử dụng lại sau.

Currying trong Lambda Expression cho hàm f(x,y):

static Func f(int x)

{ Func add = (y, z) => y + z;

return y => add(x, y);

}

var Currying = f(1);

int three = Currying(2); // = 1 + 2

int four = Currying(3); // = 1 + 3

Thật khó để thấy ứng dụng của Currying vào thực tế. Tuy nhiên nó có thể thích hợp trong khoa học máy tính cho việc chia nhỏ các hàm toán học có n biến chứa các phép toán phức tạp thành n-1 hàm đơn giản hơn.

Query Expression:
Đây là sự hỗ trợ ngôn ngữ LINQ cho C# 3.0 trong việc xây dựng những câu truy vấn inline. Ngoài những câu lệnh trông giống như ngôn ngữ SQL “Select”, “From”, “Where” (thí dụ 8) thì việc ứng dụng “Lambda Expression” vào trong “Query Expression” sẽ làm đơn giản đi công việc truy vấn dữ liệu. Các câu lệnh truy vấn trở nên ngắn gọn và dễ hiểu.

Thí dụ 8 được viết lại bằng “Lambda Expression”:

Thí dụ 11:

var searchList = people
.Where(p => p.Name == “Huy”)
.Select(p => new { p.Name, p.Salary, allowance = p.Salary * 0.7 });

Một thí dụ tìm tuổi lớn nhất trong danh sách, nếu trước đây người lập trình phải viết một stored procedure hay một đoạn mã so sánh các phần tử trong danh sách thì với C# 3.0 chỉ cần một lệnh:

int maxAge = people.Max(p => p.Age);

Một kỹ thuật rất hữu ích trong Query Expression là Extension Method. Trước đây, đối với các class thuộc hãng thứ ba hay những class vì lý do nào đó được khai báo đóng kín, người lập trình thường gặp nhiều khó khăn do không được phép thay đổi hay thêm những method cho phù hợp với yêu cầu mới. Thí dụ class Integer của C#, người lập trình không thể thêm method Square như lệnh gọi dưới đây:

Thí dụ 12:

int myValue = 2;
myValue.Square();

Class Integer của C# hoàn toàn không có method Square. Nhờ kỹ thuật Extention Method, người lập trình có thể làm việc với những yêu cầu như thế. Giả sử class ListPerson chỉ cung cấp duy nhất hàm tìm kiếm theo tên và không thể thừa kế hay thêm mới vào class này. Nếu có một yêu cầu tìm kiếm theo tuổi thì người lập trình không thể tự giải quyết vấn đề. Dùng kỹ thuật Extention Method định nghĩa một class mới dạng static và bên trong class này định nghĩa một static method với từ khóa this cho biến số đầu tiên của method. Method Square được định nghĩa:

public static int Square(this int myNumber)

{ return myNumber * myNumber; }

Trở lại yêu cầu tìm kiếm theo tuổi trong danh sách, tạo một method mới tên MyQuery:

namespace MyExtentionMethod

{ delegate R Func(T t);

static class PersonExtensions

{ public static IEnumerable MyQuery(this IEnumerable

sequence, Func predicate)

{ foreach (T item in sequence)

if (predicate(item))

yield return item; }

}

}

Tìm những nhân viên có tuổi lớn hơn 29 hay có lương lớn hơn 400:

ListPerson person = new ListPerson(people);

var myListAge = person.MyQuery(p => p.Age > 29);

var myListSalary = person.MyQuery(p => p.Salary > 400 );

Trông có vẻ như method MyQuery thuộc vào class ListPerson (person.MyQuery) hơn là class mới định nghĩa PersonExtensions.

Lazy evaluation: Đây là kỹ thuật rất được các ngôn ngữ lập trình quan tâm, nhưng Lazy evaluation trong C# 3.0 tự nhiên và đơn giản hơn. Giả sử f(x,y) = x+y chỉ xảy ra khi thỏa điều kiện x >= 0 và y >= 0:

Thí dụ 13:

static Func Add = (x, y) => x + y;
static int MyLazyEvaluation(int x, int y, int function)
{ if (x <= 0 || y <= 0) return 0; else return function;}
int value = MyLazyEvaluation(-1, 2, Add(-1, 2));

Hàm MyLazyEvaluation đuợc truyền giá trị “-1” cho tham số x, cho nên chương trình sẽ không thi hành hàm Add(-1,2).

Từ khóa yield trong vòng lặp cũng là Lazy evaluation. Có thể kiểm tra tính Lazy evaluation của từ khóa yield bằng đoạn mã trong danh sách dưới đây:

static IEnumerable Print(this IEnumerable person)

{ foreach (Person p in person)

{ Console.WriteLine(index.ToString()+”: Person: {0}”, p.Name);

index += 1;

p.Name = p.Name.ToUpper();

yield return p; }

}

Đoạn mã kiểm tra tính Lazy valuation của thí dụ trên:

Console.WriteLine(“Before using ToUpper()”);

var printPerson = people.Print();

Console.WriteLine(“After using ToUpper()”);

foreach (Person pp in printPerson)

Console.WriteLine(“-- After Upper: {0}”, pp.Name);

Kết quả trên màn hình:

Before using ToUpper()

After using ToUpper()

1: Customer: An

After Upper: AN

2: Customer: Dung

After Upper: DUNG
......

Thí dụ trên cho thấy vòng lặp “foreach (Person p in person)” trong hàm Print hoàn toàn không thi hành liên tục dòng lệnh ToUpper() với từ khoá yield. Lệnh ToUpper() một person tiếp theo chỉ được gọi khi nó thật sự được yêu cầu. Trong trường hợp này là dòng lệnh “foreach (Person pp in printPerson)” của đoạn mã kiểm tra kết quả Lazy valuation.

Higher Order Function: Tuy là một kỹ thuật rất cơ bản trong lập trình FP nhưng nó chỉ mới xuất hiện từ C# 2.0. Higher Order Function xem hàm cũng là dạng dữ liệu cho nên parameter của hàm cũng là một hàm. Trong các thí dụ trên, có một số ứng dụng Higher Order Function như thí dụ 4, parameter của method FindAll là hàm SearchName (people.FindAll(SearchName)). Thí dụ 10, parameter “function” trong hàm MyLazyEvaluation(int x, int y, int function) có kiểu là integer, nhưng nó lại được truyền vào là một hàm Add(-1, 2) (“MyLazyEvaluation(-1, 2, Add(-1, 2))”) .

“Filter”, “Map” và “Reduce” được xem là ba Higher Order Function rất hữu ích cho các phép toán trong dãy (List, Aray, IEnumerable). Tuy nhiên Higher Order Function trong C# 2.0 chỉ hỗ trợ cho kiểu dữ liệu List và Array.

Filter: Các method FindAll hay RemoveAll được xem là các Filter và parameter của các hàm này là một hàm điều kiện mà một khi các phần tử trong dãy thỏa điều kiện của hàm thì các phần tử này sẽ được chọn (Find) hay bị xóa (Remove) khỏi danh sách. Thí dụ tìm kiếm danh sách những người tên “An”:

people.FindAll(delegate(Person p) { return p.Name.Equals(“An”); })

Tuy nhiên, nếu muốn tìm một danh sách những người tên “Huy”, người lập trình lại phải viết thêm một Anonymous Delegate tương tự như thế. Để tránh trường hợp này, sử dụng Higher Order Function bằng cách định nghĩa hàm SearchName:

public static Predicate SearchName(string name)

{ return delegate(Person p) { return p.Name.Equals(name); }; }
......

var AnList = people.FindAll(SearchName(“An”));

var HuyList = people.FindAll(SearchName(“Huy”));

Cách viết trong C# 3.0 ngắn gọn hơn:

var AnList = people.FindAll(p => p.Name.Equals(“An”));

hay

var person = people.Where(p=>p.Name.Equals(“An”));

Như vậy “Filter” trong C# 2.0 tương đồng với “Where” trong C# 3.0.

Map: Dùng để tạo ra một dãy mới từ một dãy ban đầu theo một phép toán tương ứng. Hàm ConvertAll() là một “Map”. Parameter của hàm này cũng là một hàm sẽ ảnh hưởng lên từng phần tử của dãy. Câu lệnh tạo một danh sách mới với mức lương nhân với hệ số “0.7”:

var allowanceSalary = people.ConvertAll(delegate(Person p)

{ return p.Salary *= 0.7; });

viết lại với C# 3.0:

var allowanceSalary = people.ConvertAll(p => p.Salary * 0.7);

hay

var allowanceSalary = people.Select(p => p.Salary * 0.7);

Như vậy “Map” trong C# 2.0 tương đồng với “Select” trong C# 3.0.

Reduce: Còn được hiểu như là “Fold” trong lập trình FP. Đây là khả năng tính toán giá trị của dãy bằng cách lướt qua tất cả các phần tử trong dãy. Tính tổng tiền lương trong danh sách:

int total = people.Aggregate(0,

delegate(int currentSum, Person p)

{ return currentSum + p.Salary; });

C# 3.0:

var total = people.Select(p => p.Salary).Aggregate(0, (x, y) => x + y);

.NET Framework 3.5 cung cấp một số hàm tính toán cho dãy như Sum, Average, Count, Min, Max. Do đó có thể thấy rằng Filter hay Map trong C# 2.0 không còn thích hợp cho C# 3.0 bởi vì Lambda Expression đã hoàn toàn có thể thay thế và LINQ đang ngày càng phổ biến. Thí dụ tính tổng số lương của những người có tuổi lớn hơn 29:

var total = people.Where(p => p.Age>29).Select(p => p.Salary).Sum();

hay

var total = (from p in people where (p.Age > 29) select p.Salary).Sum();

KẾT LUẬN

Lập trình FP trong .Net tuy còn hạn chế, nhưng tiếp cận với kỹ thuật này sẽ giúp người lập trình tiết kiệm rất nhiều thời gian trong việc viết và quản lý code cũng như phát hiện lỗi, đồng nghĩa với tiết kiệm chi phí và nhân sự cho dự án. Và hơn nữa, FP cho phép khai thác sức mạnh của các bộ xử lý đa nhân.

Xem bài trước các ví dụ về lập trình hàm

0 comments: