+3

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 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 😄. Tất nhiên không phải cứ code ngắn hơn là tốt.

4. Dễ dàng mở rộng

Do DP vốn dễ đọc hơn, đơn giản và an toàn hơn, hỗ trợ đa luồng tốt hơn. DP cũng rất thích hợp khi triển khai với các design pattern mới nên nó sẽ dễ dàng để mở rộng, thực hiện việc maintain hơn. Tại sao Netflix lại sẵn sàng bỏ thời gian, công sức, tiền bạc để xây dựng lại hệ thống với DP. Ngay như khách hàng hiện tại của chúng tôi cũng đang nghiêm túc xem xét việc ứng dụng ReactiveCocoa trong việc maintain 1 dự án khá lớn: team hiện tại có gần 3 tháng để nghiên cứu code!!!

Đến đây nếu bạn nào có hứng thú tìm hiểu về DP thì hãy đứng dậy, đi 1 vòng, làm vài động tác cho giãn gân cốt sau đó quay lại đọc tiếp phần cuối của bài này. Tối sẽ gợi ý giúp bạn hướng tiếp cận DP sao cho "hợp thời".

alt

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

Từ khóa ở đây là gì: Functional Reactive Programming(FRP).

FRP ở đây không phải là FRP hay được dùng trong lập trình thời gian thực mà anh em làm phần cứng nhúng hay nhắc tới (Embedded) đâu ạ. Mọi người có thể xem thêm ở đây: Why I cannot say FRP but I just did

Functional Reactive Programming thực ra là sự kết hợp của Functional Programming + Reactive Programming: 2 thành phần nhỏ thuộc Declarative.

  • Reactive Programming: cái này thì đơn giản thôi ạ, nếu ai làm .NET thì sẽ biết Data Binding, chính nó đấy ạ. Hiểu đơn giản thì: a = b + c. Khi b/c thay đổi thì giá trị của a cũng thay đổi theo, tự động, ngay tức thời.
  • Functional Programming (FP): là phương thức giải quyết vấn đề bằng cách chia nhỏ vấn đề thành cácfunction. Mỗi function lại có input là 1 function khác và output lại là 1 function. Trong quá trình kết nối các function, chúng hạn chế việc thay đổi các state cũng như không quy định rõ thứ tự của các function. Functional Programming coi chương trình như là 1 bài toán (math problem) chứ không phải là 1 chuỗi các thao tác (series operations).

Các đặc điểm của FP

  • First-Class Functions
  • High-Order Functions
  • Pure Functions
  • Closures
  • Immutable State

=> Không cần quan tâm đến những cái tên là gì, bạn chỉ cần hiểu chúng là gì thôi!

  • First-Class Functions: bạn có thể lưu 1 function như 1 biến.
  • High-Order Functions: Function có thể nhận input là 1 function và trả ra ouput cũng là 1 function.
  • Pure Functions: Function không thay đổi bất kì biến nào. Nó chỉ đơn giản là nhận vào input và đưa rao utput. Không thay đổi state, không tạo ra side-effects. Map, Reduce ở 2 ví dụ đầu tiên là pure function.
  • Closures: Trong Objective-C nó là Block, Java là Lambda, Delegates trong C#… và dĩ nhiên là Closures trong rất nhiều ngôn ngữ khác 😄.

What_did_I_read-2.gif

4. Nhân hoà???

Hãy nghiên cứu thêm về cái gọi là FRP, tôi cam đoan bạn sẽ thấy thích thú. Ngôn ngữ bạn đang dùng không cần là ngôn ngữ thuần Functional Programming như Haskell, Erlang, Scala… nhưng trên github có sẵn các thư viện giúp bạn thực hiện điều đó. Cái tên RxExtension – ReactiveX của MS đáng được chú ý đầu tiên. Còn nếu bạn vẫn muốn có thêm bằng chứng thuyết phục, hãy đợi bài tiếp theo của tôi demo FRP trên Objective-C/Swift sử dụng ReactiveCocoa/RxSwift.

Phải đọc cả một đống chữ, với hàng loạt các khái niệm khó hiểu cùng cách trình bày yếu kém của tôi có thể làm bạn cảm thấy rối. Vạn sự khởi đầu nan, nhất là khi bạn phải bỏ lại đằng sau tư duy code đã ăn sâu trong người bao lâu nay.

Việc học FRP không giống như học 1 ngôn ngữ thông thường chúng ra đã từng học. Nó giống như chúng ta học code từ đầu, học theo 1 hướng khác, code theo 1 cách khác hoàn toàn! Hãy tự thử thách chính mình, bỏ lại thói quen dùng HOW và làm quen với cách “cao cấp” hơn: WHAT

Hãy cho DeclarativeFunctional Reactive Programming 1 cơ hội, còn sau đó quyết định như thế nào là ở bạn. Riêng tôi đã quyết định con đường của mình từ đây: trở thành công dân của “Tân Thế Giới”.

43.gif43.gif43.giftho-bay-mau-42-2.png25.gif 25.gif 25.gif

PS: Bài này tôi viết đã khá lâu, khi đang rất hào hứng những bước đầu tìm hiểu về Delcarative Programming và FRP nên sẽ có không ít sai sót, ý kiến chủ quan thiếu chính xác. Hiện tôi cũng đang làm sử dụng FRP vào công việc hàng ngày của mình, cũng bổ sung được những kiến thức nhất định nhưng nếu update vào bài viết này sợ là sẽ quá dài, 2 là sẽ phức tạp cho những người mới bắt đầu. Tôi chỉ chỉnh sửa lại 1 chút với mong muốn chia sẻ những kiến thức sơ khai mà mình có được khi mới bắt đầu, muốn thổi lên những ngọn lửa nhỏ trong cộng đồng lập trình viên, muốn có những đồng nghiệp cùng tìm hiểu, chia sẻ những kiến thức, những vấn đề gặp phải với FRP để cùng nhau đi lên.

Tháng 6 này nếu có thời gian, có thể tôi sẽ viết thêm 2 bài chi tiết hơn về Reactive và Functional Programming, 2 phần mà trong bài này còn quá sơ sài và còn rât rất nhiều cái hay để bàn.

Link tham khảo:

CHARACTERISTICS OF DECLARATIVE PROGRAMMING LANGUAGES

The introduction to Reactive Programming you’ve been missing

Funtional programming in large-scale project development

Enemy of the state! or why OOP is not suited to large scale software.

Functional Programming in Swift

Swift Is Not Functional

Imperative vs. Declarative Programming – Pros and Cons

Imperative versus declarative

Functional Programming should be your #1 priority for 2015

Hoặc tìm trên Youtube với rất nhiều video của các diễn giả nổi tiếng: Uncle Bob, Erik Meijer…

Nếu bạn thật sự muốn tìm hiểu sâu hơn nữa, hãy đọc quyển sách này, in ra thì sẽ dễ đọc hơn. Hãy đọc lần lượt, ít nhất qua trang 216. Tôi đang đọc, mong rằng bạn cũng sẽ đọc: "The Structure and Interpretation of Computer Programs

Rất vui nếu bạn ghé tăm blog của tôi: https://dangthaison91.wordpress.com


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí