12/08/2018, 13:27

THỜI TRANG LẬP TRÌNH – SỰ TRỖI DẬY CỦA DECLARATIVE PROGRAMMING!

Kể từ sau loạt bài về Apple Watch + CI , tôi muốn chuyển sang các chủ đề khác nói về các món ăn chơi nhảy múa mà không phải là về code, công việc. Nhưng nói thật là các món ăn chơi thì nhiều, cũng lắm sự kỳ công đòi hỏi người chơi phải có niềm yêu thích thực sự, thời gian tìm hiểu nhất định. Thêm ...

Kể từ sau loạt bài về Apple Watch + CI, tôi muốn chuyển sang các chủ đề khác nói về các món ăn chơi nhảy múa mà không phải là về code, công việc. Nhưng nói thật là các món ăn chơi thì nhiều, cũng lắm sự kỳ công đòi hỏi người chơi phải có niềm yêu thích thực sự, thời gian tìm hiểu nhất định. Thêm nữa là khi viết những bài như vậy cần phải có kiến thức rộng, bao quát cũng như khá khó để tạo được cảm hứng cho người đọc. Thế nên thôi, lại quay về với cái máng lợn là: CODE…

Chắc hẳn trong công việc, mọi người đều có những mục tiêu riêng, đích đến nhất định qua các quãng thời gian dài ngắn khác nhau. Là 1 một lập trình viên iOS đơn thuần, mục tiêu trong năm nay của tôi là học 1 ngôn ngữ lập trình mới, vốn đang rất hot trong cộng đồng cũng như nội bộ công ty: Swift của Apple. Nhưng lần mò, vâng vẫn là cái trò lần mò, tôi được biết tới Functional Programming. Nhưng nghe lạ tai quá, tìm hiểu mãi thì mới đi đến ngọn nguồn của vấn đề: Declarative Programming.

1425999742-1425306960-paradigm-cover.jpg

Mới đầu đọc thì tôi chỉ biết đến nó như là 1 lĩnh vực nhỏ trong thế giới Computer Science bao la và cao siêu khó lường. Nhưng các kết quả tìm kiếm thường có cụm từ: Declarative Programming vs Imperative Programming. Imperative Programming (IP) thường gắn liền với lập trình hướng đối tượng OOP mà lâu nay tôi và các bạn(xin lỗi nếu có vơ đũa cả nắm) vẫn nghĩ là tối ưu, là phương pháp hay nhất khi phát triển phần mềm. Imperative Programming là cách mà bao lâu nay tôi vẫn lập trình, từ thời học cấp 3, Đại học và cả khi đi làm. Và nó có 1 thế giới đối nghịch Declarative Programming (DP). Vậy là ngoài kia, ngoài cái thế giới mà tôi đang sống có 1 thế giới khác mà bao lâu nay tôi không hề hay biết. Trên đường đi khám phá thế giới mới tôi gặp rất nhiều lời ca ngợi về nó, càng thôi thúc sự tò mò, khát khao khám phá của bản thân.

1. Giới thiệu

Hai mô hình trên vốn khá rộng, nhưng để định nghĩa thì có thể gói gọn 1 cách tương phản rõ ràng như sau:

  • Imperative programming: telling the “machine” how to do something, and as a result what you want to happen will happen.
  • Declarative programming: telling the “machine” what you would like to happen, and let the computer figure out how to do it.

7ac3bddd67684ad7992e0f02e02f4f0cbfdbc809513e7049b69483d4d3c358fa.jpg

Tôi chọn để nguyên văn tiếng Anh vì nó vốn quá súc tích, ngắn gọn và rất dễ để nhận ra sự khác bọt ở đây. Tuy nhiên hẳn các bạn cũng sẽ thấy mơ hồ với từ khóa “what”, “how” được dùng ở đây. Vậy hãy để tôi lấy ví dụ cho dễ hiểu.

Ví dụ 1: Nhân đôi các phần tử có trong mảng cho sẵn.

  • Imperative: Xời, đơn giản! Đây là cách bạn sẽ nghĩ ra ngay trong đầu:

2015-09-19_00-29-45.png .

=> Cách thức giải quyết bài toán:

  1. Khai báo 1 mảng doubled mới để lưu dữ liệu
  2. Duyệt qua các phần tử của mảng.
  3. Thực hiện phép nhân đôi các phần tử trong mảng
  4. Lần lượt lưu kết quả vào mảng doubled
  • Declarative:

2015-09-19_00-30-05.png

Ở đây sẽ khác hơn 1 chút, chúng ta không thấy việc duyệt mảng mà chỉ thấy câu lệnh map cùng phép tính nhân với 2 (x2).

Lệnh map ở đây thực hiện việc tạo 1 mảng doubled mới từ mảng đã cho với các phần tử đã được x2 qua function (n) {return n*2)} (ở đây function (n) được khai báo trực tiếp thay vì được tách ra ngoài).

2015-09-19_01-33-03-1120x432.png

“Dùng hàm có sẵn, thư viện bên ngoài thì nói làm gì. Hư cấu!”. Nhưng xin các bạn hãy nhìn lại. Cùng 1 bài toán sẽ có nhiều cách giải, về nguyên lý cơ bản là giống nhau nhưng cú pháp khác nhau, điều đó giải thích tại sao lại có cách giải này hay hơn cách giải bt khác. Xin nhấn mạnh sự khác biệt ở đây là khả năng đọc hiểu code(readability). Dùng mô hình Imperative chúng ta sẽ cần chỉ ra các bước tuần tự cần thực hiện trong khi Declarative sẽ cho thấy ngay cái ta cần xảy ra mà không bận tâm phải duyệt mảng, lưu kết quả vào mảng như thế nào. Bài toán yêu cầu nhân đôi các giá trị và code của bạn tập trung vào việc nhân đôi x2.

function(n) ở trên được gọi là pure function: nó không hề thay đổi bất kì giá trị nào (side effects) mà chỉ nhận vào input, trả ra output sau khi thực hiện tính toán.

––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

Ví dụ 2: Tính tổng tất các phần tử trong mảng.

  • Imperative: Tạo 1 biến mới, duyệt qua các phần tử và thực hiện cộng dồn vào biến đã tạo (How to do)

2015-09-19_01-48-04-2.png

  • Declarative:

2015-09-19_01-48-19-2.png

Hàm reduce thực hiện gộp các phần tử trong mảng thành 1 giá trị duy nhất theo function(sum, n) truyền vào. Hãy nhìn vào code, cái bạn sẽ thấy ngay được mapreduce giúp chúng ta xử lý với mảng, tất cả chúng ta cần làm ở đây là chỉ ra chúng ta muốn gì (what we you want to do).

2015-09-19_01-32-50-2.png

Tôi đoán rằng đến đây các bạn và cả tôi ngày xưa cũng vẫn thấy lạ lẫm, không thể hiểu rõ sự khác nhau giữa 2 mô hình trên. Hẳn bạn sẽ tự bảo việc gì phải làm theo những cách kỳ quái, khó hiểu một cách trừu tượng thế này? Làm theo cách bao lâu nay bạn vẫn làm, cách bạn được học trong nhà trường, cách mà hàng triệu lập trình viên trên thế giới vẫn làm: bảo cho máy tính các bước cần thực hiện để giải quyết vấn đề. Như thế không tốt hơn sao?

Hãy hình dùng các mô hình trên như các thể loại nhạc, người thích thể loại nhạc này, người thì lại không và cho nó là tẻ nhạt, thậm chí dị hợm. Hồi xưa tôi nghĩ là mình không thích Metal, Black Metal (mấy thể loại nhạc nhức đầu, chẳng hát mà toàn gào thét…) nhưng bây giờ thì sao, tôi có thể trả lời ngay là tôi chỉ thích nó…

Hồi bé các bạn có ghét khi bố mẹ mở nhạc vàng trong khi muốn xem Đan Trường, Lam Trường… Nhưng sau này lớn lên, trưởng thành hơn thì ít nhiều bạn lại thích nghe nhạc vàng? Đã bao giờ bạn luôn miệng nói rằng mình ghét Sơn Tùng, chả quan tâm MTP là đứa nào. Nhưng khi tình cờ nghe “Con cua ngang qua, Nắng ấm xa dần, Nem của ngày hôm qua”… bạn thấy cũng lạ lạ, hay hay nếu không biết nó là của Sơn Tùng MTP? Hãy để tôi chỉ cho bạn 1 ngôn ngữ mà ai cũng từng học, từng dùng mà không hề biết nó là Declarative Programming:SQL (Structured Query Language)!!!

Hãy xem câu query sau:

2015-09-19_02-06-43-2.png

Hình dung cách bạn sẽ viết code để giải bài toán này theo cách Imperative?

2015-09-19_02-07-48-2.png

Nhìn xem làm theo cách vốn có của SQL sẽ đơn giản, ngắn gọn và dễ hiểu hơn biết nhường nào. Bạn chỉ cần quan tâm: lấy gì, ở đâu, điều kiện là gì? Việc còn lại là để DBMS làm công việc còn lại cho bạn. Giống như cách bạn nhờ ai đó lấy cho lon pepsi trong tủ lạnh:

  • Imperative: đứng dậy, đi đến tủ lạnh, mở cửa ra, lấy 1 lon pepsi, đem đến đây cho tôi!
  • Declarative: Em yêu ơi, anh ước có 1 lon pepsi lạnh. => Thấy không, bạn đơn giản chỉ cần nói ra điều ước của mình, còn làm sao thì đó là việc của ông bụt, bà tiên. Điều kì diệu đó cũng được thực hiện bởi sự hỗ trợ của ngôn ngữ bạn dùng khi bạn code.

pepsi_halloween-724x1024.jpg

Bây giờ trở lại với vấn đề lý thuyết sẽ dễ hiểu hơn và phần nào trả lời câu hỏi to tướng từ đầu đến giờ vẫn chả nhỏ đi được là bao!

**2. Thiên thời **

Tại sao tôi lại đặt tiêu đề là sự trỗi dậy của Declarative Programming? Nó không phải mới được đề xướng trong những năm gần đây, mà nó đã có từ rất lâu rồi, từ những năm 50 – thời sơ khai của máy tính hiện đại, đầu tiên với ngôn ngữ lập trình Lisp. Bao lâu nay nó vẫn tồn tại song song với Imperative mà chúng ta không hề hay biết. Vậy tại sao sau 60 năm nó mới lại nổi lên?

Thời kì đầu, máy tính còn khá chậm chạp, cồng kềnh, tốc độ xử lý không nhanh như bây giờ. Thời đó có 2 tư tưởng chính khi phát triển các ngôn ngữ lập trình:

  • Rất quen thuộc, đó là dựa theo nguyên lý Von Neumann.
  • Đi từ nguyên lý cơ bản trong toán học. Sự chậm chạp của máy tính thời đó không đủ điều kiện để Lisp phát triển và thế là Imperative lên ngôi, đặc biệt với sự ra đời của C. Đến thập niên 90, có thể coi là thời kỳ thịnh vượng của lập trình hướng đối tượng (OOP) với sự kết hợp mô hình Imperative Programming. Chúng ta trừu tượng hóa các vấn đề thành các đối tượng, đưa ra các phương thức mà đối tượng đó có rồi kết nối chúng lại để tạo ra một chuỗi các bước để đối tượng thực hiện giải quyết vấn đề (how). Hàng loạt các ngôn ngữ mới ra đời đi theo con đường multi-paradigm (OOP + Imperative): Java, C#, C++, Smalltalk, Python, Ruby, PHP…

Nhưng bây giờ, máy tính phát triển chóng mặt, tốc độ, kích thước cải thiện rất rất nhiều. Theo định luật Moore thì cứ sau 18 tháng, số lượng bóng bán dẫn(transistor) lại tăng lên 2 lần(vài năm trước thì có thể hiểu là nhanh hơn gấp đôi). Và cơ hội thứ hai lại đến với Declarative Programming.

rcb4j-2.jpg

Sau hơn 50 năm đã quá quen thuộc với Imperative thì Declarative thực sự là làn gió mới với dân lập trình (giống như hiện tượng Swift của Apple sau hơn 30 năm thống trị của Objective-C). Mọi người khi nghe tên đều thấy tò mò, thú vị với nó. Và Declarative đã không làm người dùng thất vọng. Có không ít lập trình viên tỏ ra đặc biệt thích thú, có người thì coi việc học các ngôn ngữ Declarative là một mục tiêu quan trọng, có người còn chuyển hẳn sang sử dụng nó như là mô hình chính trong việc phát triển phần mềm:

  • Microsoft ra mắt ngôn ngữ F#, phát triển bộ công cụ RxExtension cho .NET, Java, JS, C++, Python, Ruby… bây giờ là cả Swift nữa.
  • Netflix sử dụng RxJava để viết lại JVM.
  • Github sử dụng ReactiveCocoa để viết ứng dụng Github trên Mac OS X.
  • MS, AnguarJS… sử dụng Declarative trong việc thiết kế giao diện (UI) cho ứng dụng: XAML
  • NewYork Times, Soundcloud, FourSquare… rất nhiều công ty công nghệ sử dụng Declarative. ** => Vậy lợi ích của Declarative Programming là gì?**

3. Địa lợi.

**1. Hạn chế sự thay đổi **

Các đối tượng, dữ liệu trong chương trình sẽ rất ít khi bị thay đổi, xuyên suốt trong quá trình thực hiện. Bạn sẽ ít phải bận tâm hơn khi dữ liệu có bị thay đổi ở những hàm nào, luồng (thread) nào tác động đế nó…? Làm việc với các giá trị bất biến (constant) sẽ dễ dàng, ít lỗi và dễ kiểm soát hơn rất nhiều. Đó là lý do tại sao Apple khuyến khích dùng Value Types và Immutable Value trong Swift.

2. Giảm thiểu state side-effects

Đến nay vẫn chưa có định nghĩa cụ thể side-effects là gì? Nhưng hãy tưởng tượng bạn debug 1 lỗi được tạo khi sử dụng function của người động nghiệp viết ra. Bạn tìm ra nguyên nhân là do biến x. Bạn thấy x được thay đổi trong 10 hàm khác nhau. Và bạn phải lặn ngụp trong 10 hàm đấy để tìm ra nguyên nhân cuối cùng… Boom!

1425999735-1425293914-windows8-bsod-2.jpg

Declarative không khuyến khích thay đổi giá trị của các biến (change state), ouput tạm thời của chương trình, thay vào đó các kỹ thuật pipelines, Higher-order function được sử dụng(Google để biết thêm chi tiết). Điều đó hạn chế được các sự thay đổi ngoài ý muốn của state.

Và nếu bạn lập trình đa luồng multi-thread) hoặc hơn nữa là lập trình tính toán song song sử dụng Multi-core(CPU) thì sẽ thấy việc kiểm soát sự thay đổi data giữa các luồng/CPU core sẽ rất nhức đầu: Race condition, Deadlock, Thread safe, Context Switch....

Bạn nên xem thêm ở đây: [CHARACTERISTICS OF DECLARATIVE PROGRAMMING LANGUAGESơ(http://cgi.csc.liv.ac.uk/~frans/OldLectures/2CS24/declarative.html#detail)

3. Code ngắn hơn, dễ hiểu hơn.

Ở 3 ví dụ đầu bài tôi đã cho các bạn thấy khi sử dụng Declarative, code của bạn sẽ ngắn hơn và dễ đọc hơn rất nhiều. Nó tập trung vào input, bạn muốn làm gì với input để tạo ra ouput. Hãy hình dung bạn phải đọc hàng loạt các vòng lặp lồng nhau rồi vắt óc xâu chuỗi để biết người tiền nhiệm muốn gì khi viết code này.

Hoặc thêm 1 ví dụ nữa.

Ví dụ 3:

  • Haskell:

2015-09-19_05-33-11-2.png

  • Javascript :

2015-09-19_05-33-26-2.png

Nhìn xem, Haskell chỉ mất 2 dòng code, nhìn khá dị nhưng cũng rất dễ hiểu: input = 7 thì message là “Much 7 very wow.” còn không sẽ là “Ooops, try again.”. If-else, for-loops viết theo 1 cách khác, code rất ngắn và nếu có thời gian làm quen bạn sẽ đọc hiểu logic rất nhanh và dễ dàng. Tin tôi đi             </div>
            
            <div class=

0