Hãy khoan chưa vội nói tới những khái niệm mới mà chúng ta đã dự định trong bài viết này. Phần mở đầu ở đây chúng ta sẽ nói tới một vài yếu tố liên quan tới các khái niệm ở bài trước. Đầu tiên chúng ta sẽ nói tới các biến được tạo ra trong các cú pháp gắn kèm binding
như let .. in
.
Mặc dù các ngôn ngữ Functional
đều được thiết kế với đặc trưng định kiểu dữ liệu chặt chẽ. Tuy nhiên các biến được tạo ra sẽ đều được ngầm định kiểu dữ liệu và trình biên dịch sẽ tự động tìm được thông tin từ giá trị được gán vào mỗi biến. Và sau đó để đảm bảo tính nhất quán và sự chặt chẽ trong logic code, các Biến trong môi trường thiết kế đặc trưng cho Functional Programming
mặc định đều sẽ không thay đổi.
Điều này có nghĩa là các Biến trong các môi trường Functional
như Elm
, Haskell
, PureScript
, có chức năng tương đương với các Hằng constant
trong các môi trường lập trình khác. Ngay cả ở thao tác định nghĩa các hàm thì các yếu tố ở phía bên trái ký hiệu =
, bao gồm biến đặt tên hàm và các tham số đầu vào cũng đều là các yếu tố bất dịch immutable
. Khái niệm bất biến immutable
và hiệu ứng biên side-effect
được sử dụng rất nhiều trong môi trường Functional
và chúng ta sẽ lưu ý từ đây.
elm repl
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> _
Higher-Order Function
Higher-Order Function được hiểu nôm na là các Hàm có vị trí quan sát cao hơn.
Ví dụ như khi chúng ta truyền một Hàm g
vào lời gọi Hàm f
và sau đó logic hoạt động bên trong Hàm f
sẽ thực hiện việc gọi Hàm g
để ủy thác một công việc nào đó. Lúc này Hàm f
được xem là Higher-Order Function
so với Hàm g
; Bởi vì f
đã biết thông tin về g
thông qua định kiểu tham số, còn g
thì không biết thông tin gì về f
.
> List.map not [True, False]
> [False,True] : List Bool
Trong ví dụ trên thì chúng ta đã truyền hàm not
vào lời gọi hàm map
của module List
, và ủy thác việc nghịch đảo các giá trị Bool
bên trong List
được truyền vào ở vị trí tham số tiếp theo. Như vậy List.map
ít nhất đã biết được rằng hàm được truyền vào vị trí ở tham số đầu tiên có dạng (a -> b)
để chuyển đổi giá trị của một phần tử trong List
. Và logic hoạt động của List.map
là thực hiện việc gọi hàm not
với lần lượt từng phần tử của List
để thu được các giá trị mới và tạo về một List
kết quả hoàn toàn mới.
Chúng ta cũng có thể sử dụng các biểu thức Hàm Vô Danh lambda
có cú pháp khá tương đồng với JavaScript
để truyền vào List.map
. Trong trường hợp này, Elm
sẽ ngầm định kiểu dữ liệu của tham số đầu vào cho Hàm Vô Danh là kiểu của dữ liệu trong List
, còn kết quả trả về thì sẽ tùy thuộc vào logic bên trong lambda
.
> List.map (\n -> n * 9) (List.range 0 9)
[0,9,18,27,36,45,54,63,72,81] : List Int > (\n -> n * 9)
<function> : number -> number
Và đây là cách mà JavaScript
đã triển khai sẵn HOD
hỗ trợ cho người viết code sử dụng với phương thức map
của các mảng Array
.
var origin = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var nile = origin.map ((n) => n * 9) console.log (nile)
Khái niệm Higher-Order Function
cũng có thể được biểu thị trong trường hợp khác, khi Hàm f
trả về một giá trị là một Hàm g
. Lúc này f
cũng được xem là Hàm có góc quan sát cao hơn so với g
. Tuy nhiên để làm ví dụ mô tả thì chúng ta sẽ chuyển sang khái niệm liên quan tiếp theo là Currying Function
.
Currying Function
Trong cửa sổ REPL
của Elm
, chúng ta hãy thử kiểm tra xem thông tin định kiểu của List.map
, bởi chúng ta đã biết tới Higher-Order Function
và biết đâu sẽ có lúc muốn tự định nghĩa một Hàm HOD
như vậy.
> List.map
<function> : (a -> b) -> List a -> List b
Như vậy là chúng ta có List.map
là một hàm <function>
, sẽ nhận vào tham số đầu tiên là một hàm (a -> b)
, và tham số tiếp theo là một danh sách List a
, và trả về kết quả là một danh sách mới List b
. Tuy nhiên chúng ta cũng có thể đọc thông tin định kiểu của List.map
với tham số sau và kết quả trả về được nhóm lại bằng ngoặc đơn ()
như thế này:
<function> : (a -> b) -> (List a -> List b)
Đó có nghĩa là List.map
là một hàm <function>
, sẽ nhận vào tham số là một hàm (a -> b)
và trả về một hàm mới (List a -> List b)
. Như vậy chúng ta có thể hiểu List.map
còn là Higher-Order Function
của hàm (List a -> List b)
nữa.
Lúc này đứng từ góc độ sử dụng hàm List.map
, chúng ta sẽ có thể tạo ra hàm mới (List a -> List b)
rồi sau đó mới sử dụng hàm này cho các List
khác nhau.
> kyudo = List.map (\n -> n * 9)
<function> : List number -> List number > kyudo (List.range 0 9)
[0,9,18,27,36,45,54,63,72,81] : List Int > kyudo [0,10,20,30,40,50,60,70,80,90]
[0,90,180,270,360,450,540,630,720,810] : List number
Như vậy List.map
đã được sử dụng bằng cách áp dụng các tham số từng phần partial application
, thay vì các tham số được truyền vào cùng một lượt trong một lời gọi hàm. Và thao tác định nghĩa hàm với các tham số được xếp lớp như vậy để sau đó chúng ta có thể áp dụng từng phần được gọi là Currying Function
.
Trong nhiều ngôn ngữ lập trình hàm bao gồm Elm
và Haskell
, PureScript
đã kể ở trên thì cú pháp định nghĩa hàm đã tự động hóa Currying
. Ở các ngôn ngữ khác nếu không được hỗ trợ về mặt cú pháp thì chúng ta sẽ phải viết hơi dài dòng hơn ví dụ như JavaScript
trước khi có cú pháp lambda
. Còn đối với các ngôn ngữ có hỗ trợ cú pháp lambda
thì chúng ta chỉ cần viết nối tiếp các lambda
có tham số đơn.
// -- map : (a -> b) -> (Array a -> Array b)
function map (f) { return function ($array) { /* ... */ }
} // -- map : (a -> b) -> (Array a -> Array b)
const map => (f) => ($array) => { /* ... */ }
Tuy nhiên nếu tự định nghĩa một hàm Array.map
như thế này trong JavaScript
, hiển nhiên chúng ta sẽ cần phải sử dụng tới các yếu tố Imperative
như tạo mảng kết quả rỗng và cập nhật qua các vòng lặp. Điểm quan trọng mà chúng ta cần lưu ý là mảng ban đầu $array
không nên bị thay đổi về mặt nội dung sau bất kỳ thao tác nào.
(chưa đăng tải) [Functional Programming + Elm] Bài 3 - Type Variable & Type Class