Giới thiệu về lập trình chức năng — Phần 3
Giới thiệu về lập trình chức năng
Bài viết dựa trên bài viết
Đây là phần ba trong 4 phần giới thiệu về lập trình ‘chức năng’ trong Javascript. Ở bài viết trước, chúng ta đã tìm hiểu về cách lập trình chức năng trên các mảng và danh sách. Ở bài viết này, chúng ta sẽ nghiên cứu về 1 loại hàm có tên là higher-order.
+ Phần 1: Các chất liệu và động lực
+ Phần 2: Làm việc với mảng và danh sách
+ Phần 4: Làm việc với phong cách (sắp được dịch)
I. Hàm tạo hàm
Nhắc lại bài viết trước, chúng ta thực hiện tách một số câu lệnh sang các hàm đơn giản. Tiếp đó ta thay thế vòng for thông thường vào trong hàm map và reduce. Bài hôm nay sẽ bàn về các ‘hàm tạo hàm’, chúng là những công cụ giúp chúng ta viết code ngắn gọn, xúc tích hơn.
Hàm tạo hàm đôi khi còn được biết tới với tên gọi hàm higher-order. Để hiểu được chúng, chúng ta cần ‘ghé thăm’ vài tính năng của Javascript cho phép nó tạo ra loại hàm này. Thảm khảo về
II. Các chất liệu tiếp theo trong lập trình hàm
* Bài viết đầu tiên có nói tới 2 chất liệu chính là biến và hàm.
1. Hàm Closure và phạm vi của biến
Một trong những phần khó nhằn trong Javascript là chỗ các biến mà một hàm có thể ‘thấy’. Giả sử ta khai báo biến x bên trong một hàm, biến x không thể được thấy bên ngoài hàm đó. Ví dụ:
Tuy nhiên, nếu ta định nghĩa một hàm ở trong một hàm khác, hàm bên trong có thể truy xuất tới biến ở hàm bên ngoài:
Có điểm bạn sẽ thấy là sau khi thực thi xong hàm outer(), biến outerVar không bị mất đi. Nên khi gọi f(), chương trình log ra kết quả ‘Hatter’. Đây là điểm đặc biệt của Javascript so với các ngôn ngữ khác. Bạn sẽ mất chút thời gian để làm quen với điều này. Nếu gặp khó khăn trong việc xác định các biến mà hàm có thể thấy. Đừng vội lo lắng, tới nơi hàm được khai báo, ta hoàn toàn phát hiện ra các biến mà hàm đó thấy được.
2. Biến ‘arguments’
Khi bạn tạo một hàm trong Javascript, nó sẽ tạo ra một biến đặc biệt tên là arguments. Cấu trúc của nó khá giống mảng. VD:
Quan sát kết quả, ta có nhận xét sau:
▪ arguments là một đối tượng có tên khóa ứng với các chỉ mục của mảng như 0, 1,…
▪ Điểm nữa là arguments chứa tất cả các đối số được sử dụng để gọi hàm, nó không bắt buộc phải ứng với tham số được quy định khi định nghĩa hàm. Bên dưới là ví dụ mà số lượng tham số và đối số là khác nhau:
▪ Biến arguments cũng có thuộc tính ‘length’ giống mảng:
3. Call và Apply
Như ta biết trước đó, các mảng có sẵn các phương thức chả hạn như map và reduce. Ở chiều tương tự, các hàm cũng được cung cấp cho một vài phương thức.
Thông thường chúng ta gọi hàm bằng cách sử dụng một cặp ngoặc tròn và truyền vào các đối số:
Một cách khác để gọi hàm là sử dụng phương thức call:
Đối số đầu tiên thể hiện giá trị của `this` mà ta có thể sử dụng ở phần thân hàm, hiện tại ta không cần quan tâm. Các đối số thứ 2 trở đi sẽ được truyền lần lượt tới hàm.
Phương thức apply có chức năng khá tương đồng với call. Khác nhau cơ bản là call nhận một danh sách các đối số, trong khi apply nhận một mảng các đối số ở tham số thứ hai:
Ví dụ:
Cả hai phương thức này đều được sử dụng khi chúng ta xây dựng lên các ‘hàm tạo hàm’.
4. Hàm nặc danh
Một hàm nặc danh (cũng được biết tới với tên gọi biểu thức lambda) là một hàm được định nghĩa nhưng không có tên. Các hàm nặc danh thường được sử dụng với map và reduce:
III. Áp dụng một phần
▪ Trong khoa học máy tính áp dụng một phần (tên khác là ‘hàm áp dụng một phần’ hay ‘hàm cục bộ’) là nói tới việc cố định một số lượng đối số cho một hàm, tạo ra một hàm mới với số lượng đối số nhỏ hơn. Cho hàm f, Ta có thể cố định fix / bind đối số thứ nhất, tạo ra hàm cục bộ:
▪ Ví dụ, ta tạo ra hàm addClass() nhận vào một tên class và một thẻ DOM:
Chúng ta muốn sử dụng hàm này với map để thêm class vào các thẻ DOM.
Kết hợp map và addClass:
▪ Tuy nhiên, chương trình trên sẽ không thực thi thành công bởi một vấn đề: bên trong map, ta truyền từng phần tử DOM tới tham số thứ nhất của callback, dẫn tới gọi callback sẽ không khớp với addClass. Cho hình dưới để bạn tiện so sánh:
Giải pháp là tạo một hàm mới, trong đó gọi tới addClass, đồng thời ‘fix-cứng’ tên class chúng ta muốn:
Giờ chúng ta có một hàm chỉ nhận 1 tham số. Nó phù hợp để có thể truyền tới map:
▪ Nhưng nếu muốn thêm tên một class khác, đòi hỏi phải tạo thêm một hàm nữa:
🤔 có gì đó đang lặp lại!! — Lúc nãy sẽ thật tuyệt nếu ta có một hàm chuyên để cố định tham số đầu tiên:
Chú ý câu lệnh return đầu tiên. Chúng ta đang tạo một hàm mà trả về một hàm khác
▪ Có vẻ partialFirstOfTwo hoạt động khá ổn với hàm nhận chính xác 2 tham số. Nhưng làm thế nào để ‘áp dụng một phần’ với hàm nhận 3 tham số? Hay có thể là 4 hoặc hơn? Tóm lại là một hàm giúp fix-cứng cho nhiều hơn một tham số? Với yêu cầu này, chúng ta sẽ sử dụng phương thức slice và apply như sau:
Hiện tại, chúng ta chưa cần nắm rõ chi tiết hàm partial hoạt động. Chỉ nên biết rằng, hàm này cho phép chúng ta ‘áp dụng một phần/fix-cứng’ một số lượng đối số bất kì của hàm.
Javascript cung cấp sẵn cho các hàm một phương thức hoạt động tương tự partial là bind. Vấn đề là nó mong chờ tham số đầu tiên phải là một đối tượng biến this tham chiếu tới. Ví dụ, nếu muốn ‘áp dụng một phần’ tới document.getElementById, bạn phải truyền document ở tham số đầu tiên:
Dẫu vậy, đa phần ta không phải sử dụng tới biến this (đặc biệt viết code theo cách lập trình chức năng), vì vậy ta chỉ cần đưa null vào tham số thứ nhất. VD:
Xem thêm về
1. tổng hợp hàm — composition
Như các bạn đã biết ở bài viết trước, lập trình chức năng là về lấy ra các hàm nhỏ, đơn giản, đặt chúng lại với nhau để giải quyết những vấn đề phức tạp hơn. Viết code theo kiểu này bao gồm nhiều kĩ thuật, như ‘Áp dụng một phần’ được nói bên trên. Một kĩ thuật khác có thể được sử dụng là ‘Tổng hợp hàm’, nó giúp kết hợp các hàm lại với nhau.
▪ Dạng đơn giản nhất của tổng hợp hàm gồm 2 hàm a và b, cả hai đều chỉ nhận 1 tham số. Tổng hợp chúng cho ra hàm c. Kết quả thu được từ b sẽ là đối số khi gọi a. Giá trị thu được từ a là kết quả của c: c(x) = a(b(x)). Ví dụ:
Hàm composeTwo giúp kết hợp 2 hàm funA và funB ra funC. Tuy nhiên, chúng ta có thể muốn kết hợp nhiều hơn 2 hàm với nhau. Điều này đòi hỏi một hàm tổng hợp có tính khái quát hơn:
Một lần nữa, gạt phần body của hàm compose sang một bên, hãy chú ý tới những gì mà nó làm được. Khi áp dụng vào ví dụ trên:
Lợi ích của hàm tổng hợp sẽ rõ hơn khi chúng ta tìm hiểu về hàm currying nói tới ở phần sau.
▪ Với một vài hàm tiện ích nhỏ, chúng ta có thể sử dụng compose để làm cho code được rõ ràng, ngắn gọn. Ví dụ, ta có một bài thơ sau:
* brillig: Trong từ điển nghĩa là 4h chiều, là lúc mà bạn bắt đầu nướng (broiling) gì đó cho bữa tối.
Với định dạng như này, bài thơ sẽ không được hiển thị tốt lắm trên trình duyệt. Nó nên được định dạng theo chuẩn HTML
Chú ý nếu bạn đọc các đối sổ ở compose từ trái sang phải, chúng sẽ được gọi theo thứ tự ngược lại. Các bạn có thể thấy một chút bối rối, bởi vậy, một số thư viện lập trình chức năng cung cấp các hàm thực thi theo chiều thuận tên là pipe hay flow.
Sử dụng pipe, chúng ta có thể viết modifyPoem như sau:
2. Currying
Giới hạn của ‘compose’ là nó coi các hàm truyền vào đều chỉ nhận 1 tham số. Nó không phải vấn đề gì lớn vì ta đã có ‘partial’, nó có thể chuyển đổi hàm nhiều tham số tới hàm một tham số. Tuy nhiên, partial vẫn chưa đủ. Hàm curry có thể là một giải pháp, có thể coi là là phiên bản ‘kích thích của partial’.
Chi tiết của currying có chút phức tạp. Trước tiên hãy xem một ví dụ. Chúng ta có hàm formatName dùng để đặt tên người vào trong dấu chú thích. Nó có 3 tham số. Phiên bản được curry-hóa (để dễ gọi tôi sẽ viết loại hàm này với tên là hàm cà-ri) của formatName sẽ có ít hơn 3 tham số, nó có vài tham số được ‘áp dụng một phần’:
Có vài thứ cần chú ý về hàm ‘cà-ri’:
Đây là code để bạn tiện so sánh giữa currying và partial.
Nó vẫn chưa thể hiện lợi ích gì hơn so với ‘partial’. Nhưng currying sẽ trở nên vô cùng hữu dụng khi đi cùng hàm tổng hợp.
Quay lại ví dụ bài thơ phía trên, sẽ thế nào nếu ta muốn đưa câu ‘four o’clock in the afternoon’ vào trong thẻ <em>?
Để ý, ta sử dụng hàm pipe thay vì compose. Cũng không có hàm trung gian nữa, sử dụng hàm nặc danh (🎭). Khi gọi pipe ta sẽ truyền vào các hàm ‘cà-ri’. Chúng cũng dễ đọc đấy chứ!
Đây là code để bạn tiện so sánh giữa
Bên dưới định nghĩa hàm curry được điều chỉnh từ cuốn
3. Tại sao?
Vậy là ta đã đi tìm hiểu về những đặc điểm và ứng dụng của partial, compose, pipe và curry. Chúng là những công cụ giúp ích trong việc lắp ghép các hàm nhỏ, đơn giản để giải quyết các vấn đề lớn hơn. Nhưng chúng có thực sự hữu ích? Chúng làm được điều gì khác so với trước đây? Quả thật, nó mở ra một phong cách mới cho việc viết code. Nó đưa ta nghĩ theo một con đường khác, có thể đơn giản hơn trong giải quyết các vấn đề. Nó cũng giúp giảm bug, dễ kiểm tra code được viết ra (Hãy thử và đánh giá nhé). Nếu bạn còn thấy hứng thú, xin mời đón đọc bài viết tới.
Hãy tiếp tục ủng hộ và giữ kết nối với Vnknowledge các bạn nhé:
- Vnknowledge Page
- Vnknowledge Youtube
- Vnknowledge Patreon
Xin cảm ơn các bạn!