Hàm Bậc Cao & Hàm Currying

Tram Ho

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.

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.

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.

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.

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.

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:

Đó 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.

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 ElmHaskell, 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.

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.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo