12/09/2018, 13:46

Tìm hiểu về Linq trong C#(phần 1 – Functional programming)

Nếu bạn là một lập trình viên .NET (hoặc Mono), và bạn đã từng học C# thì nhiều khả năng là đã dùng qua Linq rồi (hoặc đã dùng rồi mà chưa biết tên gọi là Linq). Linq cho phép bạn viết những biểu thức như var bar = foo.Where(f => f.foo < 3).OrderBy(f => ...

Nếu bạn là một lập trình viên .NET (hoặc Mono), và bạn đã từng học C# thì nhiều khả năng là đã dùng qua Linq rồi (hoặc đã dùng rồi mà chưa biết tên gọi là Linq). Linq cho phép bạn viết những biểu thức như

hoặc

Đây là những đoạn code đẹp và dễ hiểu mà hiếm có ở một ngôn ngữ nào khác ngoài C# (hay VB). Điều đáng buồn là hiện nay, hầu hết các tài liệu nói về Linq bằng tiếng Việt đều chỉ tập trung nói về việc dùng cú pháp Linq để truy cập Database, hoặc thực hiện một vài thao tác đơn giản đối với các mảng dữ liệu, mà thiếu đi những tài liệu nói rõ về bản chất của Linq, vốn mạnh mẽ hơn rất nhiều so với những ứng dụng nhỏ nêu trên.

Linq được thiết kế nhằm giúp lập trình viên thao tác với các nguồn dữ liệu bằng một cú pháp mang tính hàm hóa (functional) cao (điều này sẽ được giải thích kĩ hơn sau). Linq là một trong những điểm tạo nên vẻ đẹp của C# mà ít ngôn ngữ nào có được. Vậy nên việc hiểu rõ bản chất của Linq sẽ giúp các bạn nắm bắt được bất cứ ứng dụng nào, từ việc xử lý mảng, đến truy xuất dữ liệu thông qua Linq to Sql. Việc hiểu rõ Linq cũng giúp các bạn nắm bắt được những khái niệm về lập trình hàm (Functional programming), nhằm tận dụng được hết sức mạnh mà C# và .NET mang lại.

Series bài viết này sẽ viết về các vấn đề mang tính học thuật và bản chất của Linq. Bạn sẽ không học được cách dùng Linq để truy xuất Database ở đây – những ứng dụng đó sẽ chỉ được điểm qua để hỗ trợ cho bài viết mà thôi. Vấn đề chính là chúng ta nắm bắt được Linq là gì, và làm thế nào Linq lại cho phép người dùng viết những đoạn code gọn vào đẹp như vậy.

Linq trong .NET được dùng trên 2 interface chủ yếu: IEnumerable<T> vàIObservable<T> . Các bài viết trong series này sẽ chủ yếu viết về IEnumerable<T>. IObservable<T> sẽ được đề cập đến sau dành cho những ai thích tìm hiểu thêm về thư viện Reactive Extensions.

Thế nào là lập trình hàm (Functional programming) ?

Để hiểu rõ về Linq, trước hết chúng ta cần biết sơ qua về lập trình hàm (Functional programming, từ giờ sẽ gọi tắt là FP). Tại sao? Vì Linq được thiết kế để sử dụng theo phong cách FP.

Cách tốt nhất để hiểu về khái niệm FP là tìm hiểu tại sao người ta lại nghĩ ra khái niệm này, và nó nhằm giải quyết vấn đề gì. Hãy hồi tưởng lại về ngày đầu tiên bạn đi học lập trình, bạn nhận được một bài tập lập trình dạng “Hello World” đơn giản mà thầy giáo cho, và sung sướng khi thực hiện được nó. Rồi càng ngày, các bài tập mà bạn phải thực hiện càng phức tạp, càng khiến bạn phải viết nhiều code và thao tác với nhiều cấu trúc dữ liệu khác nhau. Điều này làm nảy sinh 3 vấn đề cơ bản. Thứ nhất, bạn muốn sử dụng một đoạn logic nhiều lần, với nhiều đối tượng dữ liệu khác nhau. Để làm được điều này, bạn phân tách chương trình của mình thành những tiến trình (procedure) khác nhau. Nếu như những tiến trình này (hãy gọi chúng là tiến trình A, B chẳng hạn) cùng hoạt động trên một cấu trúc dữ liệu nào đó và cùng thay đổi cấu trúc đó, điều đó có nghĩa là trong suốt quá trình hoạt động, A và B luôn luôn phải biết xem tiến trình còn lại hoạt động thế nào. Hãy thử lấy một ví dụ đơn giản nhất: hiện lên 1 thông báo khi phần mềm gặp lỗi:

Thoạt nhìn đoạn code trên có vẻ đơn giản và hoạt động chính xác. Tuy nhiên hãy thử nghĩ kĩ xem: bạn luôn phải nhớ rõ rằng chỉ được phép gọi HandleError() sau khi đã gọi ShowErrorMessage(), nếu không method HandleError() sẽ hoạt động sai. Khi làm việc với một project lớn, bạn sẽ không thể nào nhớ hết được các tiểu tiết như thế này, mà bắt buộc phải đọc đi đọc lại cùng một đoạn code, và kể cả như vậy thì lỗi vẫn có thể xảy ra. Tệ hại hơn nữa, khi bạn làm việc chung với những người khác, họ sẽ khó có thể đoán được rằng method ShowErrorMessage() làm những gì, mà bắt buộc phải đi sâu vào đọc code của bạn để hiểu.

Vấn đề thứ hai, đó là việc các tiến trình A, B thay đổi cấu trúc dữ liệu chung  khiến code của bạn trở nên khó hiểu, ngay cả với chính bản thân người viết. Trong ví dụ trên, không ai nghĩ rằng method Calculate() ngoài việc tính toán, còn có thể thay đổi một vài thuộc tính của class. Sự thay đổi này được gọi là tác dụng phụ (side-effects) trong hàm

Vấn đề thứ 3, đó là khi các tiến trình kể trên hoạt động không độc lập, sẽ rất khó để bạn có thể Test và Debug. Việc test method Calculate() kể trên sẽ rất khó khăn, do nhiều thuộc tính cần phải được thiết lập đúng trước khi Calculate() được gọi.

Tất nhiên, đoạn code trên cố tính được tạo ra nhằm mục đích ví dụ (mặc dù trên thực tế, vẫn có những người đoạn code dạng như vậy được đưa vào sử dụng). Nhưng nếu bạn đã từng lập trình, ắt hẳn đôi khi lâm vào những tình huống tương tự.

Câu hỏi là: Thế thì FP giúp gì được tôi để giải quyết những tình huống như vậy ?

Câu trả lời: FP đưa ra khái niệm hàm “thuần” (pure function). Một hàm “thuần” là một hàm toán học thuần túy, có nghĩa là nó nhận các giá trị đầu vào, trả về giá trị đầu ra, không có trạng thái ẩn và không thay đổi bất cứ cấu trúc dữ liệu nào. Cụm từ “không có trạng thái ẩn” có nghĩa rằng hàm của bạn chỉ được sử dụng đầu vào, cộng với các phép tin học cố định ( cộng trừ nhân chia, vòng lặp…) để tạo ra đầu ra. Điều này có nghĩa rằng với cùng một tập hợp đầu vào, bạn có thực hiện hàm đó bao nhiêu lần thì giá trị trả về chỉ là duy nhất.

Chúng ta hãy thử xem xem, bằng những khái niệm trên, đoạn code phía trên có thể  được cải thiện ra sao:

Giờ thì đoạn code của chúng ta đã khá hơn nhiều: hàm Calculate trở thành 1 hàm void, hay nói cách khác là 1 Action, hàm ý rằng đây là một tiến trình nào đó. Các hàm CalculateResult, ShowErrorMessage, HandleError chỉ đơn thuần nhận và trả giá trị mà không thay đổi bất cứ thuộc tính gì. Đoạn code trên còn có một lợi ích ngầm: Khi bạn muốn thay đổi logic tính toán, nhằm mục đích tối ưu hóa hoặc thay đổi về logic chương trình, bạn chỉ cần chỉnh sửa logic đó trong hàm CalculateResult. Điều này khiến cho hàm của bạn có tính tái sử dụng cao(hay còn gọi là được module hóa)

Những phần mềm ngày nay có độ phức tạp ngày càng cao, do đó mà FP càng ngày càng trở thành một xu thế. Có những ngôn ngữ được thiết kế để lập trình dưới 100% dưới dạng FP (Haskell, F#…) C# là ngôn ngữ hướng đối tượng, nhưng cung cấp rất nhiều những tính năng giúp lập trình hàm. Ví dụ như, trong C#, có một kiểu dữ liệu để miêu tả 1 hàm: Func<>. Một Func<int, bool> có nghĩa là một hàm nhận argument dạng int, và trả về boolean. Đối với những tiến trình không có giá trị trả về (hay còn gọi là hàm void), C# hiển thị dưới dạng Action<>.

Chúng ta hãy thử xem có thể thay đổi method Calculate kể trên bằng những kiểu dữ liệu này như thế nào

Mức độ module hóa của đoạn code được nâng lên một “tầm cao mới”. Giờ đây method Calculate không còn bắt buộc sử dụng hàm CalculateResult để tính toán nữa, mà có thể dùng bất cứ hàm nào được cung cấp khi được gọi. Kĩ thuật này được gọi là phân tách (Decoupling). Nó cho phép method Calculate() được sử dụng rộng rãi hơn.

Tóm lại: Vậy thì những điều này liên quan gì đến Linq?

Nếu bạn chưa biết gì về FP trước khi đọc bài viết này thì xin chúc mừng. Bạn đã có thêm nhiều kiến thức bổ ích. Tuy nhiên chủ đề chính của chúng ta vẫn là Linq.

Sở dĩ mình dành hẳn một bài dài giới thiệu về FP vì nó là cơ sở để các kĩ sư của Microsoft thiết kế nên Linq. Linq là một thư viện nhằm giúp thao tác với các tập hợp, hay nói rộng hơn, là các nhóm dữ liệu. Do đó một trong những tiêu chí đầu tiên của Linq là việc bảo toàn tính nguyên vẹn của data: Tất cả các hàm trong Linq không biến đổi cấu trúc dữ liệu hiện có, mà chỉ trả về cấu trúc mới. Nếu bạn đã nghiên cứu đủ sâu, bạn sẽ thấy rằng việc các chức năng phần mềm đều có thể quy về việc thao tác và sử dụng những tập hợp dữ liệu. Do đó, những hàm trong Linq có thể nói là những hàm mạnh mẽ nhất mà bạn có thể học được, và điều này không thể thực hiện được nếu không có những khái niệm của Functional Programming.

Techtalk via goatysite

0