Thứ năm, 12/12/2019 | 00:00 GMT+7

Giải thích về lập trình chức năng JavaScript: Ứng dụng một phần và làm xoăn

Với việc áp dụng thư viện Redux JavaScript, chuỗi công cụ và tiện ích mở rộng cú pháp Lý do , và khung JavaScript chu kỳ , lập trình chức năng với JavaScript ngày càng trở nên phù hợp. Hai ý tưởng quan trọng có nguồn root từ suy nghĩ hàm là currying , biến một hàm của nhiều đối số thành một chuỗi các lệnh gọi hàm và ứng dụng từng phần , cố định giá trị của một số đối số của hàm mà không đánh giá đầy đủ hàm. Trong bài viết này, ta sẽ khám phá một số ví dụ về những ý tưởng này đang hoạt động, cũng như xác định một vài địa điểm mà chúng hiển thị có thể khiến bạn ngạc nhiên.

Sau khi đọc cái này, bạn có thể :

  • Xác định ứng dụng từng phần và cách nấu cà ri và giải thích sự khác biệt giữa hai phương pháp này.
  • Sử dụng ứng dụng từng phần để sửa các đối số cho một hàm.
  • Chức năng Curry để tạo điều kiện cho ứng dụng một phần
  • Thiết kế các chức năng tạo điều kiện cho ứng dụng từng phần.

Ví dụ không có ứng dụng một phần

Như với nhiều mẫu, ứng dụng một phần dễ hiểu hơn với ngữ cảnh.

Hãy xem xét hàm buildUri này:

function buildUri (scheme, domain, path) {   return `${scheme}://${domain}/${path}` } 

Ta gọi như thế này:

buildUri('https', 'twitter.com', 'favicon.ico') 

Điều này tạo ra chuỗi https://twitter.com/favicon.ico .

Đây là một chức năng hữu ích nếu bạn đang tạo nhiều URL. Tuy nhiên, nếu bạn làm việc chủ yếu trên web, bạn sẽ hiếm khi sử dụng scheme ngoài http hoặc https :

const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico') const googleHome = buildUri('https', 'google.com', '') 

Lưu ý sự tương đồng giữa hai dòng này: Cả hai đều truyền https làm đối số ban đầu. Ta muốn loại bỏ sự lặp lại và viết một cái gì đó giống như:

const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico') 

Có một vài cách để làm điều này. Hãy xem làm thế nào để đạt được nó với ứng dụng một phần.

Ứng dụng một phần: Sửa đối số

Ta đã đồng ý rằng, thay vì:

const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico') 

Ta muốn viết:

const twitterFavicon =  buildHttpsUri('twitter.com', 'favicon.ico') 

Về mặt lý thuyết, buildHttpsUri thực hiện chính xác những điều tương tự như buildUri , nhưng với một giá trị cố định cho mình scheme tranh luận.

Ta có thể triển khai buildHttpsUri trực tiếp như vậy:

function buildHttpsUri (domain, path) {   return `https://${domain}/${path}` } 

Điều này sẽ làm những gì ta muốn, nhưng vẫn chưa giải quyết được vấn đề của ta hoàn toàn. Ta đang sao chép buildUri , nhưng mã hóa cứng https làm đối số scheme của nó.

Ứng dụng một phần cho phép ta làm điều này, nhưng bằng cách tận dụng mã ta đã có trong buildUri . Đầu tiên, ta sẽ xem cách thực hiện việc này bằng cách sử dụng một thư viện tiện ích chức năng có tên là Ramda . Sau đó, ta sẽ thử làm điều đó bằng tay.

Sử dụng Ramda

Sử dụng Ramda, một phần ứng dụng trông giống như sau:

// Assuming we're in a node environment const R = require('ramda')  // R.partial returns a new function (!) const buildHttpsUri = R.partial(buildUri, ['https']) 

Sau đó, ta có thể làm:

const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico') 

Hãy phân tích những gì đã xảy ra ở đây:

  • Ta đã gọi hàm partial của Ramda và truyền hai đối số: Thứ nhất, một hàm, được gọi là buildUri và thứ hai, một mảng chứa một giá trị "https" .
  • Sau đó, Ramda trả về một hàm mới, hoạt động giống như buildUri , nhưng với "https" là đối số đầu tiên của nó.

Việc chuyển nhiều giá trị hơn trong mảng sẽ sửa các đối số khác:

// Bind `https` as first arg to `buildUri`, and `twitter.com` as second const twitterPath = R.partial(buildUri, ['https', 'twitter.com'])  // Outputs: `https://twitter.com/favicon.ico` const twitterFavicon = twitterPath('favicon.ico') 

Điều này cho phép ta sử dụng lại mã chung mà ta đã viết ở nơi khác bằng cách cấu hình nó cho các trường hợp đặc biệt.

Ứng dụng một phần thủ công

Trong thực tế, bạn sẽ sử dụng các tiện ích như partial khi nào bạn cần sử dụng ứng dụng một phần. Tuy nhiên, để minh họa, ta hãy cố gắng tự làm điều này.

Hãy xem đoạn mã trước, sau đó mổ xẻ.

// Line 0 function fixUriScheme (scheme) {   console.log(scheme)   return function buildUriWithProvidedScheme (domain, path) {     return buildUri(scheme, domain, path)   } }  // Line 1 const buildHttpsUri = fixUriScheme('https')  // Outputs: `https://twitter.com/favicon.ico` const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico') 

Hãy chia nhỏ những gì đã xảy ra.

  • Trên Dòng 0, ta xác định một hàm có tên là fixUriScheme . Hàm này chấp nhận một scheme và trả về một hàm khác.
  • Trên Dòng 1, ta lưu kết quả của việc gọi fixUriScheme('https') vào một biến có tên là buildHttpsUri , hoạt động giống hệt như version ta đã tạo bằng Ramda.

Hàm fixUriScheme của ta chấp nhận một giá trị và trả về một hàm. Nhớ lại rằng điều này làm cho nó trở thành một hàm bậc cao hơn , hoặc HOF. Hàm trả về này chỉ chấp nhận hai đối số: domainpath .

Lưu ý , khi ta gọi hàm trả về này, ta chỉ truyền domainpath một cách rõ ràng, nhưng nó nhớ scheme mà ta đã truyền trên Dòng 1. Điều này là do hàm bên trong, buildUriWithProvidedScheme , có quyền truy cập vào tất cả các giá trị trong phạm vi hàm mẹ của nó , ngay cả sau khi hàm cha đã trả về. Đây là những gì ta gọi là đóng cửa .

Điều này khái quát. Bất kỳ khi nào một hàm trả về một hàm khác, hàm được trả về có quyền truy cập vào bất kỳ biến nào được khởi tạo trong phạm vi của hàm mẹ. Đây là một ví dụ điển hình về việc sử dụng bao đóng để đóng gói trạng thái.

Ta có thể làm điều gì đó tương tự bằng cách sử dụng một đối tượng với các phương thức:

class UriBuilder {    constructor (scheme) {     this.scheme = scheme   }    buildUri (domain, path) {     return `${this.scheme}://${domain}/${path}`   } }  const httpsUriBuilder = new UriBuilder('https')  const twitterFavicon = httpsUriBuilder.buildUri('twitter.com', 'favicon.ico') 

Trong ví dụ này, ta cấu hình từng cá thể của lớp UriBuilder với một scheme cụ thể. Sau đó, ta có thể gọi phương thức buildUri , phương thức này kết hợp domainpath mong muốn của user với scheme cấu hình trước của ta để tạo ra URL mong muốn.

Khái quát hóa

Nhớ lại ví dụ mà ta đã bắt đầu với:

const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')  const googleHome = buildUri('https', 'google.com', '') 

Hãy thực hiện một thay đổi nhỏ:

const twitterHome = buildUri('https', 'twitter.com', '')  const googleHome = buildUri('https', 'google.com', '') 

Lần này có hai điểm chung: Lược đồ, "https" , trong cả hai trường hợp và đường dẫn, ở đây là chuỗi trống.

Hàm partial mà ta đã thấy trước đó áp dụng một phần từ bên trái. Ramda cũng cung cấp partialRight , cho phép ta một phần áp dụng từ phải sang trái.

const buildHomeUrl = R.partialRight(buildUri, [''])  const twitterHome = buildHomeUrl('https', 'twitter.com') const googleHome = buildHomeUrl('https', 'google.com') 

Ta có thể làm điều này xa hơn:

const buildHttpsHomeUrl = R.partial(buildHomeUrl, ['https'])  const twitterHome = buildHttpsHomeUrl('twitter.com') const googleHome = buildHttpsHomeUrl('google.com') 

Cân nhắc thiết kế

Để sửa cả đối số schemepath cho buildUrl , trước tiên ta phải sử dụng partialRight , sau đó sử dụng partial trên kết quả.

Điều này không lý tưởng. Sẽ tốt hơn nếu ta có thể sử dụng partial (hoặc partialRight ), thay vì cả hai theo thứ tự.

Hãy xem nếu ta có thể sửa chữa điều này. Nếu ta xác định lại buildUrl :

function buildUrl (scheme, path, domain) {   return `${scheme}://${domain}/${path}` } 

Phiên bản mới này vượt qua các giá trị mà ta có thể biết trước. Đối số cuối cùng, domain , là đối số mà ta có nhiều khả năng muốn thay đổi. Sắp xếp các đối số theo thứ tự này là một nguyên tắc chung.

Ta cũng có thể chỉ sử dụng partial :

const buildHttpsHomeUrl = R.partial(buildUrl, ['https', '']) 

Điều này dẫn đến quan điểm rằng thứ tự đối số quan trọng. Một số đơn đặt hàng thuận tiện hơn cho việc áp dụng một phần so với những đơn đặt hàng khác. Hãy dành thời gian suy nghĩ về thứ tự đối số nếu bạn định sử dụng các hàm của bạn với ứng dụng một phần.

Ứng dụng một phần cà ri và tiện lợi

Bây giờ ta đã xác định lại buildUrl với một thứ tự đối số khác:

 function buildUrl (scheme, path, domain) {    return `${scheme}://${domain}/${path}`  }  

Lưu ý :

  • Các đối số mà ta có nhiều khả năng muốn sửa nhất xuất hiện ở bên trái. Một trong những ta muốn thay đổi là tất cả các cách ở bên phải.
  • buildUri là một hàm gồm ba đối số. Nói cách khác, ta cần vượt qua ba điều để nó chạy.

Có một chiến lược ta có thể sử dụng để tận dụng lợi thế này:

const curriedBuildUrl = R.curry(buildUrl)  // We can fix the first argument... const buildHttpsUrl = curriedBuildUrl('https') const twitterFavicon = buildHttpsUrl('twitter.com', 'favicon.ico')  // ...Or fix both the first and second arguments... const buildHomeHttpsUrl = curriedBuildUrl('https', '') const twitterHome = buildHomeHttpsUrl('twitter.com')  // ...Or, pass everything all at once, if we have it const httpTwitterFavicon = curriedBuildUrl('http', 'favicon.ico', 'twitter.com') 

Các món cà ri chức năng phải mất một chức năng, món cà ri nó, và trả về một chức năng mới, không giống như partial .

Currying là quá trình chuyển đổi một hàm mà ta gọi cùng một lúc với nhiều biến, như buildUrl , thành một chuỗi các lệnh gọi hàm, trong đó ta chuyển từng biến một.

  • curry không sửa chữa các đối số ngay lập tức. Hàm trả về nhận nhiều đối số như hàm root .
  • Nếu bạn chuyển tất cả các đối số cần thiết cho hàm curried, nó sẽ hoạt động giống như buildUri .
  • Nếu bạn truyền ít đối số hơn so với hàm ban đầu đã nhận, hàm curried sẽ tự động trả về cùng thứ mà bạn nhận được bằng cách gọi partial .

Currying mang đến cho ta những gì tốt nhất của cả hai thế giới: Ứng dụng tự động từng phần và khả năng sử dụng các chức năng ban đầu của ta .

Lưu ý currying giúp tạo các version ứng dụng một phần của các hàm của ta dễ dàng hơn. Điều này là do các hàm curry thuận tiện để áp dụng một phần, miễn là ta đã cẩn thận về thứ tự đối số của bạn .

Ta có thể gọi curriedbuildUrl giống như cách ta gọi buildUri :

const curriedBuildUrl = R.curry(buildUrl)  // Outputs: `https://twitter.com/favicon.ico` curriedBuildUrl('https', 'favicon.ico', 'twitter.com') 

Ta cũng có thể gọi nó như thế này:

curriedBuildUrl('https')('favicon.ico')('twitter.com') 

Lưu ý curriedBuildUrl('https') trả về một hàm, hoạt động giống như buildUrl , nhưng với schemas của nó được cố định thành "https" .

Sau đó, ta ngay lập tức gọi hàm này bằng "favicon.ico" . Điều này trả về một hàm khác, hoạt động giống như buildUrl , nhưng với schemas của nó được cố định thành "https" và đường dẫn của nó được cố định thành chuỗi trống.

Cuối cùng, ta gọi hàm này với "twitter.com" . Vì đây là đối số cuối cùng nên hàm phân giải thành giá trị cuối cùng là: http://twitter.com/favicon.ico .

Bài học quan trọng là: curriedBuldUrl có thể được gọi như một chuỗi các lệnh gọi hàm, trong đó ta chỉ chuyển một đối số cho mỗi lệnh gọi. Đó là quá trình chuyển đổi một hàm của nhiều biến được truyền “tất cả cùng một lúc” thành một chuỗi “lệnh gọi một đối số” mà ta gọi là currying.

Kết luận

Hãy tóm tắt lại những điều chính:

  • Ứng dụng một phần cho phép ta sửa các đối số của một hàm. Điều này cho phép ta lấy các chức năng mới, với hành vi cụ thể, từ các chức năng khác, tổng quát hơn.
  • Currying chuyển đổi một hàm chấp nhận nhiều đối số “tất cả cùng một lúc” thành một chuỗi các lệnh gọi hàm, mỗi lệnh chỉ bao gồm một đối số tại một thời điểm. Các hàm có cấu trúc với thứ tự đối số được thiết kế tốt sẽ thuận tiện để áp dụng một phần.
  • Ramda cung cấp các tiện ích partial , partial partialRightcurry . Các thư viện phổ biến tương tự bao gồm UnderscoreLodash .

Tags:

Các tin liên quan

Cách gói một gói JavaScript Vanilla để sử dụng trong React
2019-12-12
Cách phát triển một trình tải lên tệp tương tác với JavaScript và Canvas
2019-12-12
Cách sử dụng map (), filter () và Reduce () trong JavaScript
2019-12-12
Giới thiệu về Closures và Currying trong JavaScript
2019-12-12
Bắt đầu với các hàm mũi tên ES6 trong JavaScript
2019-12-12
Cách đếm số nguyên âm trong một chuỗi văn bản bằng thuật toán JavaScript
2019-12-12
Cách sử dụng phép gán cấu trúc hủy trong JavaScript
2019-12-12
Giải thích về lập trình chức năng JavaScript: Kết hợp & truyền tải
2019-12-12
Cách sử dụng .every () và .some () để điều khiển mảng JavaScript
2019-12-12
Chuyển đổi Mảng sang Chuỗi trong JavaScript
2019-12-05