Song song với Procedural Programming
, và ở cùng cấp độ quan sát về việc kiến trúc phần mềm từ các sub-program
đó là mô hình Lập Trình Hàm Functional Programming
, cũng đã được đề cập trước đó trong một bài viết của Sub-Series JavaScript
. Ở thời điểm đó thì chúng ta đã điểm qua và so sánh song song một số nét đặc trưng của Procedural
và Functional
.
Tuy nhiên trong khuôn khổ của một bài giới thiệu ngắn thì vẫn còn rất nhiều những điểm chi tiết mà mình không thể mang vào để giới thiệu về các mô hình lập trình này. Và cụ thể là sau khi đi qua Sub-Series Ada
thì điều đó lại càng rõ ràng, những công cụ lập trình và các khái niệm liên quan, được cung cấp bởi một ngôn ngữ được thiết kế trọng tâm hướng đến Procedural
có nhiều phần không thể liệt kê được trong một bài viết.
Và bây giờ đã là thời điểm phù hợp để chúng ta tìm hiểu chi tiết về các công cụ và các khái niệm liên quan mà một ngôn ngữ được thiết kế trọng tâm Functional
cung cấp. Thực tế thì để tìm hiểu về Functional Programming
một cách nghiêm túc và sử dụng những lợi điểm của Functional
thì môi trường thuận lợi nhất hiện tại đang là ngôn ngữ Haskell
. Tuy nhiên để duy trì lượng cú pháp phải ghi nhớ tạm ở mức độ phù hợp thì mình đã chọn phương thức học tham chiếu từ Haskell
và triển khai trong Elm
với hết khả năng mà Elm
đang có.
Mặc dù cũng là một ngôn ngữ thuần Functional
nhưng so với Haskell
thì Elm
hỗ trợ ít cú pháp biểu thị code hơn và chỉ kèm theo vừa đủ các công cụ mang tính kiến trúc căn bản. Một số khái niệm về các công cụ tạo dạng thức triển khai code Functional
được Haskell
hỗ trợ sẵn như Type Class
, Functor
, Monad
, v.v... khi muốn sử dụng trong Elm
thì chúng ta sẽ phải tự định nghĩa mô phỏng lại. Tuy nhiên điều đó cũng sẽ đem lại điều kiện để chúng ta hiểu rõ hơn về các công cụ này và nắm bắt tốt hơn tư duy lập trình hàm.
Functional Aspects
Trong Sub-Series trước của Elm
, chúng ta đã nói về Declarative Programming
- một trong hai mô thức căn bản để viết một đoạn code mô tả logic cần máy tính thực hiện. Tuy nhiên Declarative
cũng giống như Imperative
, chỉ nói về việc biểu thị logic ở cấp độ chi tiết chứ không phải là ở cấp độ kiến trúc chương trình. Và sau đó, khi bắt đầu quan tâm tới việc thiết kế và tổ chức các sub-program
thì chúng ta mới có Procedural
và Functional
.
Điều đó có nghĩa là một ngôn ngữ Procedural
cũng có thể được thiết kế với cú pháp sử dụng ở dạng Declarative
, ví dụ như SQL
; Và một ngôn ngữ Functional
cũng có thể được thiết kế với cú pháp sử dụng ở dạng Imperative
, ví dụ như F-sharp
. Tuy nhiên sự thực thì đúng là các ngôn ngữ thuần Functional
đều sử dụng cú pháp Declarative
, và tuyệt đại đa số các ngôn ngữ Procedural
đều sử dụng cú pháp Imperative
.
Trong Sub-Series này thì chúng ta sẽ chỉ đề cập tới khía cạnh Functional
được biểu thị trong Elm
để tìm hiểu và có khả năng triển khai trong cả những ngôn ngữ Imperative
như JavaScript
nếu muốn.
first-class function & function composition
higher-order function & currying function
type variable & type class
functor & applicative
monoid & co-monad
Trên đây là danh sách liệt kê một số nét đặc trưng và khái niệm liên quan tới Functional Programming
mà chúng ta sẽ tìm hiểu trong Sub-Series này với sự tham chiếu từ Haskell
. Và trong bài viết mở đầu này thì chúng ta sẽ nói về các khái niệm First-Class Function
và Function Composition
.
First-Class Function
Đây là điểm đặc trưng căn bản nhất và quan trọng nhất để bất kỳ ngôn ngữ nào có thể được xem là có hỗ trợ Functional Programming
. Khái niệm First-Class Function
là cách nói ngắn gọn của câu Function is First-Class Citizen
- có thể hiểu nôm na là các Hàm Function
được xếp vào lớp các thành tố quan trọng nhất của phần mềm, tương đương với Kiểu Dữ Liệu Type
. Và biểu thị cần được hỗ trợ đó là một Hàm có thể được xem là một giá trị Value
, có thể được gán vào các biến, có thể được sử dụng làm các toán hạng của một biểu thức giữa các Hàm, có thể được truyền vào lời gọi Hàm khác, và có thể là kết quả trả về của một lời gọi Hàm nào đó.
Có thể gán một Hàm vào một biến để lưu trữ và sử dụng sau đó.
Chính vì lý do này nên mặc dù bạn có thể không chạm tới các ngôn ngữ thuần Functional
nhưng khi dạo quanh một số ngôn ngữ lập trình phổ thông để tham khảo tính năng và cú pháp, thì khả năng rất lớn là bạn sẽ gặp nhiều trường hợp định nghĩa hàm trong một số ngôn ngữ có hỗ trợ viết ở dạng biểu thức với phép gán =
vào một tên định danh nào đó. Kiểu cú pháp này chính là thứ xuất phát từ Functional Programming
.
module Main exposing (main)
import Html exposing (Html, text) main : Html message
main = text (greet "Elm") greet : String -> String
greet name = "Hello " ++ name ++ " again !"
Và trong JavaScript
như chúng ta đã biết từ Series lập trình web đầu tiên:
// -- greet : String -> String
const greet = (name) => "Hello" + name + "again !"
Hm... vị trí của phép gán =
dường như có phần hơi khác nhau. Trong code ví dụ của Elm
thì chúng ta đang có dạng mô tả hàm toán học f(x) = ... x ...
, và trong đó thì vị trí của f
chính là greet
và vị trí của x
là name
; Còn trong code ví dụ của JS
thì tính từ vị trí (name) => ...
có ý nghĩa tương đương với định nghĩa trong Elm
và tạo ra một hàm f
vô danh để gán vào hằng greet
.
Tuy nhiên chúng ta cũng có thể sửa lại code main
của Elm
để biểu thị rõ ràng hơn rằng - Hàm Function
cũng là một Giá Trị Value
như các kiểu dữ liệu khác. Hãy thử gán hàm greet
vào một biến trong hàm main
, sau đó gọi hàm thông qua tên biến này.
-- module ... main : Html message
main = let make = greet in text (make "Elm") -- greet : ...
Function Composition
Khái niệm Kết Hợp Hàm Function Composition
xuất hiện đồng thời với khái niệm First-Class Function
đầu tiên, bởi mục đích của việc nhìn nhận một Hàm Function
là một Giá Trị Value
và yêu cầu thiết kế ngôn ngữ hỗ trợ việc lưu trữ Hàm vào biến là để chúng ta có thể thực hiện các phép toán trên các hàm và sau đó thu được một Hàm tổng bộ all-in-one
và lưu vào một biến. Sau đó áp dụng hàm này trên dữ liệu đầu vào input
và thu về kết quả output
.
Ý tưởng này cũng được vay mượn từ Toán Học khi mà chúng ta biểu thức h(x) = f(g(x))
và khi một giá trị x
được đặt vào h(x)
thì đầu tiên chúng ta sẽ tính toán giá trị của biểu thức g(x)
rồi sau đó tiếp tục đặt giá trị này vào vị trí tham số của hàm f
để tính tiếp. Tuy nhiên, chúng ta cũng có thể viết h = f . g
để biểu thị rằng hàm h
là kết quả thu được khi kết hợp hàm f
và hàm g
để làm phẳng biểu thức h(x)
trước khi đặt x
vào và thực hiện tính toán trong một lượt.
main : Html message
main = let make = text << greet in make "Elm"
Ở đây chúng ta có code Hello, Elm !
được viết lại với main
được định nghĩa là kết quả thu được sau khi gọi Hàm make
với chuỗi "Elm"
, và hàm make
lúc này được định nghĩa là hàm kết hợp được tạo ra từ hàm text
và hàm greet
. Và dạng thức viết code này sẽ được sử dụng rất nhiều trong các Hàm chủ đạo điều khiển logic hoạt động chính của chương trình, tương đương với vai trò của các Thủ Tục trong môi trường Procedural
; Bởi như chúng ta đã biết thì trong môi trường Procedural
, các function
chỉ đóng vai trò sub-program
hỗ trợ cho các procedure
chủ đạo điều khiển logic hoạt động chính ngay dưới cấp của main
.
Về việc phân chia vai trò của các Hàm chủ đạo và các Hàm tiện ích hỗ trợ các thao tác tính toán thì trong môi trường Functional
không có quy ước nào. Tất cả hoàn toàn là lựa chọn của người viết code khi thiết kế các hàm và phải thực hiện ghi chú hay đưa ra quy ước đặt tên convention
để tự sắp xếp.
(chưa đăng tải) [Functional Programming + Elm] Bài 2 - Higher-Order Function & Currying Function