HomeLập trìnhJavaScriptCách sử dụng...

Cách sử dụng Currying và Composition trong JavaScript


Một cuộc trò chuyện thú vị mà tôi có vào tối nay đã khiến tôi suy nghĩ và xem xét lại một khái niệm mà tôi đã từng đùa giỡn trước đây – cà ri. Nhưng lần này, tôi muốn khám phá nó với tất cả các bạn!

Khái niệm cà ri không phải là một khái niệm mới, nhưng nó rất hữu ích. Nó cũng là nền tảng cho lập trình chức năng và là một loại cửa ngõ để suy nghĩ về các chức năng theo cách mô-đun hơn.

Và ý tưởng về thành phần, kết hợp các chức năng để tạo ra những chức năng lớn hơn, phức tạp hơn, hữu ích hơn có vẻ khá trực quan, nhưng cũng là một thành phần quan trọng trong lập trình chức năng.

Khi chúng ta bắt đầu kết hợp chúng, thì một số điều thú vị có thể xảy ra. Hãy xem làm thế nào điều này có thể làm việc.

Cà ri, có ai không?

Các hàm Curried đang hoạt động giống như bất kỳ hàm nào khác, nhưng cách bạn tiếp cận chúng hơi khác một chút.

Giả sử chúng ta muốn có một hàm có thể kiểm tra khoảng cách giữa hai điểm: {x1, y1}{x2, y2}, Ví dụ. Công thức cho điều đó hơi khó hiểu, nhưng không có gì chúng tôi không xử lý được:

công thức khoảng cách
Công thức tính khoảng cách giữa hai điểm, là một ứng dụng của định lý Pitago.

Thông thường, việc gọi hàm của chúng ta có thể giống như sau:

const distance = (start, end) => Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) );

console.log( distance( {x:2, y:2}, {x:11, y:8} );
// logs 10.816653826391969

Bây giờ, việc xử lý một hàm buộc nó phải nhận một tham số tại một thời điểm. Vì vậy, thay vì gọi nó như distance( start, end )chúng tôi sẽ gọi nó như thế này: distance(start)(end). Mỗi tham số được truyền riêng lẻ và mỗi lệnh gọi hàm trả về một hàm khác cho đến khi tất cả các tham số được cung cấp.

Nó có thể dễ hiển thị hơn là giải thích, vì vậy hãy xem xét hàm khoảng cách ở trên dưới dạng một hàm cong:

const distance = function(start){
  // we have a closed scope here, but we'll return a function that
  //  can access it - effectively creating a "closure".
  return function(end){
    // now, in this function, we have everything we need. we can do
    //  the calculation and return the result.
    return Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) );
  }
}

console.log( distance({x:2, y:2})({x:11, y:8});
// logs 10.816653826391969 again

Điều đó có vẻ giống như rất nhiều công việc để có được kết quả tương tự! chúng tôi có thể rút ngắn nó một chút, bằng cách sử dụng các chức năng mũi tên ES6:

const distancewithCurrying = 
        (start) => 
          (end) => Math.sqrt( Math.pow(end.x-start.x, 2) +
                              Math.pow(end.y-start.y, 2) );
Thụt lề được thêm vào để dễ đọc, không ảnh hưởng đến khả năng chạy

Nhưng một lần nữa, có vẻ như rất nhiều lời huyên náo mà không đạt được lợi ích thực sự nào, trừ khi chúng ta bắt đầu nghĩ về các chức năng của mình theo cách trừu tượng hơn.

Hãy nhớ rằng, các hàm chỉ có thể trả về một thứ. Mặc dù chúng tôi có thể cung cấp bất kỳ số lượng tham số nào, nhưng chúng tôi sẽ chỉ nhận lại một giá trị duy nhất, cho dù đó là số, mảng, đối tượng hay hàm. Chúng tôi chỉ nhận lại được một điều. Và bây giờ, với một hàm bị cong, chúng ta có một hàm chỉ có thể nhận một thứ. Có thể có một kết nối ở đó.

Khi nó xảy ra, sức mạnh của các chức năng bị xáo trộn có thể kết hợp và soạn, biên soạn họ.

Hãy xem xét công thức khoảng cách của chúng ta – điều gì sẽ xảy ra nếu chúng ta đang viết một trò chơi “bắt cờ” và có thể hữu ích khi tính toán khoảng cách của mỗi người chơi so với lá cờ một cách nhanh chóng và dễ dàng. Chúng tôi có thể có một mảng người chơi, mỗi người trong số họ chứa một {x, y} địa điểm. Với một mảng {x,y} các giá trị, một chức năng có thể sử dụng lại có thể khá hữu ích. Hãy chơi với ý tưởng đó trong một phút:

const players = [
  {
    name: 'Alice',
    color: 'aliceblue',
    position: { x: 3, y: 5}
  },{
    name: 'Benji',
    color: 'goldenrod',
    position: { x: -4, y: -4}
  },{
    name: 'Clarissa',
    color: 'firebrick',
    position: { x: -2, y: 8}
  }
];
const flag = { x:0, y:0};

Đây là thiết lập của chúng tôi: chúng tôi có một vị trí bắt đầu, flag, và chúng tôi có một loạt người chơi. Chúng tôi có hai chức năng khác nhau được xác định để tính toán sự khác biệt, hãy xem sự khác biệt:

// Given those, let's see if we can find a way to map 
//  out those distances! Let's do it first with the first
//  distance formula.
const distances = players.map( player => distance(flag, player.position) );
/***
 * distances == [
 *   5.830951894845301, 
 *   5.656854249492381, 
 *   8.246211251235321
 * ]
 ***/

// using a curried function, we can create a function that already
//  contains our starting point.
const distanceFromFlag = distanceWithCurrying(flag);
// Now, we can map over our players to extract their position, and
//  map again with that distance formula:
const curriedDistances = players.map(player=>player.position)
                                .map(distanceFromFlag)
/***
 * curriedDistances == [
 *   5.830951894845301, 
 *   5.656854249492381, 
 *   8.246211251235321
 * ]
 ***/

Vì vậy, ở đây, chúng tôi đã sử dụng distanceCurried chức năng để áp dụng một tham số, điểm bắt đầu. Điều đó trả về một hàm nhận một tham số khác, điểm kết thúc. Bằng cách ánh xạ qua các trình phát, chúng ta có thể tạo một mảng mới với chỉ cần dữ liệu chúng tôi cần, sau đó chuyển dữ liệu đó vào chức năng được xử lý của chúng tôi!

Đọc thêm  Cách xây dựng Menu thả xuống bằng JavaScript

Đó là một công cụ mạnh mẽ và một công cụ có thể mất một số thời gian để làm quen. Nhưng bằng cách tạo các hàm được sắp xếp và kết hợp chúng với các hàm khác, chúng ta có thể tạo một số hàm rất phức tạp từ các phần nhỏ hơn, đơn giản hơn.

Cách soạn các hàm Curried

Khả năng ánh xạ các chức năng đã được xử lý là rất hữu ích, nhưng bạn cũng sẽ tìm thấy những cách sử dụng tuyệt vời khác cho chúng. Đây là sự khởi đầu của “Lập trình hàm”: viết các hàm nhỏ, thuần túy hoạt động đúng như các bit nguyên tử này và sau đó kết hợp chúng như các khối xây dựng.

Hãy xem cách chúng ta có thể lấy các hàm bị xáo trộn và kết hợp chúng thành các hàm lớn hơn. Khám phá tiếp theo này sẽ đi vào chức năng bộ lọc.

Đầu tiên, một nền tảng nhỏ. Array.prototype.filter(), chức năng lọc ES6, cho phép chúng tôi xác định chức năng gọi lại, một chức năng lấy một hoặc nhiều giá trị đầu vào và trả về giá trị đúng hoặc sai dựa trên đó. Đây là một ví dụ:

// a source array,
const ages = [11, 14, 26, 9, 41, 24, 108];
// a filter function. Takes an input, and returns true/false from it.
function isEven(num){
  if(num%2===0){
    return true;
  } else {
    return false;
  }
}
// or, in ES6-style:
const isEven = (num) => num%2===0 ? true : false;
// and applied:
console.log( ages.filter(isEven) );
// [14, 26, 24, 108]
lọc một mảng số cho các giá trị chẵn

Bây giờ chức năng lọc đó, isEven, được viết theo một cách rất cụ thể: nó nhận một giá trị (hoặc các giá trị, nếu chúng ta muốn bao gồm chỉ mục của mảng chẳng hạn), thực hiện một số loại hoojinkery bên trong và trả về giá trị đúng hoặc sai. Mỗi lần.

Đây là bản chất của “hàm gọi lại bộ lọc”, mặc dù nó không dành riêng cho các bộ lọc – Array.prototype.everyArray.prototype.some sử dụng cùng một phong cách. Một cuộc gọi lại được kiểm tra đối với từng thành viên của một mảng và cuộc gọi lại nhận một số giá trị và trả về đúng hoặc sai.

Đọc thêm  Cách hủy cấu trúc đối tượng trong JavaScript

Hãy tạo thêm một vài chức năng lọc hữu ích, nhưng lần này nâng cao hơn một chút. Trong trường hợp này, chúng tôi có thể muốn “trừu tượng hóa” các chức năng của mình một chút, để chúng tôi có thể tái sử dụng chúng nhiều hơn.

Ví dụ, một số chức năng hữu ích có thể là isEqualTo hoặc isGreaterThan. Chúng tiên tiến hơn ở chỗ chúng yêu cầu hai giá trị: một để xác định là một thuật ngữ so sánh (gọi nó là một comparator) và một đến từ mảng hiện tại được so sánh (chúng tôi sẽ gọi nó là value). Đây là một chút mã hơn:

// we write a function that takes in a value...
function isEqualTo(comparator){
  // and that function *returns* a function that takes a second value
  return function(value){
    // and we simply compare those two values.
    return value === comparator;
  }
}
// again, in ES6:
const isEqualTo = (comparator) => (value) => value === comparator;

Từ thời điểm này, tôi sẽ gắn bó với phiên bản ES6, trừ khi có một lý do đặc biệt khó khăn để mở rộng mã ra phiên bản cổ điển. tiếp tục:

const isEqualTo = (comparator) => (value) => value === comparator;
const isGreaterThan = (comparator) => (value) => value > comparator;

// and in application:
const isSeven = isEqualTo(7);
const isOfLegalMajority = isGreaterThan(18);

Vì vậy, ở đó, hai chức năng đầu tiên là chức năng của chúng tôi. Họ mong đợi một tham số duy nhất và trả về một hàm cũng mong đợi một tham số duy nhất.

Dựa trên hai hàm tham số đơn đó, chúng tôi thực hiện một phép so sánh đơn giản. Hai thứ hai, isSevenisOfLegalMajoritychỉ đơn giản là các triển khai của hai chức năng đó.

Cho đến nay, chúng tôi vẫn chưa trở nên phức tạp hoặc tham gia và chúng tôi có thể duy trì quy mô nhỏ thêm một vài điều nữa:

// function to simply invert a value: true <=> false
const isNot = (value) => !value;

const isNotEqual = (comparator) => (value) => isNot( isEqual(comparator)(value) );
const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) );

Ở đây, chúng ta có một hàm tiện ích đơn giản là đảo ngược sự trung thực của một giá trị, isNot. Sử dụng điều đó, chúng tôi có thể bắt đầu sáng tác các phần lớn hơn: chúng tôi lấy bộ so sánh và giá trị của mình, chạy chúng qua isEqual chức năng, và sau đó chúng tôi isNot giá trị đó để nói isNotEqual.

Đây là bước khởi đầu của sáng tác, và công bằng mà nói – nó trông hoàn toàn ngớ ngẩn. Những gì có thể sử dụng sẽ có để viết tất cả những điều đó để có được điều này:

// all of the building blocks...
const isGreaterThan = (comparator) => (value) => value > comparator;
const isNot = (value) => !value;
const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) );

// simply to get this?
const isTooYoungToRetire = isLessThanOrEqualTo(65)

// and in implementation:
const ages = [31, 24, 86, 57, 67, 19, 93, 75, 63];
console.log(ages.filter(isTooYoungToRetire)

// is that any cleaner than:
console.log(ages.filter( num => num <= 65 ) )

“Kết quả cuối cùng khá giống nhau trong trường hợp này, vì vậy nó không thực sự giúp chúng tôi tiết kiệm được gì. Trên thực tế, với thiết lập trong ba chức năng đầu tiên đó, chúng tôi phải mất một khoảng thời gian nhiều nhiều thứ để xây dựng hơn là chỉ đơn giản là so sánh!”

Và đó là sự thật. Tôi sẽ không tranh luận điều đó. Nhưng nó chỉ nhìn thấy một mảnh nhỏ của một câu đố lớn hơn nhiều.

  • Đầu tiên, chúng tôi đang viết mã nhiều hơn thế tự làm tài liệu. Bằng cách sử dụng các tên hàm biểu cảm, chúng ta có thể thấy ngay rằng chúng ta đang lọc ages cho các giá trị isTooYoungToRetire. Chúng tôi không nhìn thấy toán học, chúng tôi đang nhìn thấy mô tả.
  • Thứ hai, bằng cách sử dụng các hàm nguyên tử rất nhỏ, chúng tôi có thể kiểm tra từng phần một cách riêng biệt, đảm bảo rằng nó luôn hoạt động chính xác như nhau. Sau này, khi chúng ta sử dụng lại các hàm nhỏ đó, chúng ta có thể tự tin rằng chúng sẽ hoạt động – giải phóng chúng ta khỏi việc kiểm tra từng phần nhỏ khi độ phức tạp của hàm tăng lên.
  • Thứ ba, bằng cách tạo các hàm trừu tượng, chúng ta có thể tìm thấy các ứng dụng cho chúng trong các dự án khác sau này. Xây dựng một thư viện các thành phần chức năng là một tài sản rất mạnh mẽ và tôi thực sự khuyên bạn nên trau dồi.
Đọc thêm  Cẩn thận với các phương thức mảng xâu chuỗi trong JavaScript

Với tất cả những gì đã nói, chúng ta cũng có thể lấy các chức năng nhỏ hơn đó và bắt đầu kết hợp chúng thành các phần lớn hơn và lớn hơn. Hãy thử điều đó ngay bây giờ: có cả hai isGreaterThanisLessThanchúng ta có thể viết một cách tốt đẹp isInRange chức năng!

const isInRange = (minComparator) 
                 => (maxComparator)
                   => (value) => isGreaterThan(minComparator)(value)
                              && isLessThan(maxComparator)(value)

const isTwentySomething = isInRange(19)(30);

Điều đó thật tuyệt – giờ đây chúng tôi có một phương tiện để kiểm tra nhiều điều kiện cùng một lúc. Nhưng nhìn vào đó, nó có vẻ không tự ghi chép cho lắm. Các && ở giữa không có gì khủng khiếp, nhưng chúng ta có thể làm tốt hơn.

Có lẽ nếu chúng ta viết nữa chức năng, một chức năng chúng ta có thể gọi and(). Các and hàm có thể nhận bất kỳ số lượng điều kiện nào và kiểm tra chúng dựa trên một giá trị nhất định. Điều đó sẽ hữu ích và có thể mở rộng.

const and = (conditions) = 
             (value) => conditions.every(condition => condition(value) )

const isInRange = (min)
                 =>(max) 
                  => and([isGreaterThan(min), isLessThan(max) ])

Nên and hàm nhận bất kỳ số lượng hàm lọc nào và chỉ trả về true nếu tất cả chúng đều đúng với một giá trị nhất định. Điều đó isInRange chức năng cuối cùng thực hiện chính xác điều tương tự như chức năng trước, nhưng nó có vẻ dễ đọc hơn nhiều và tự ghi lại.

Hơn nữa, nó sẽ cho phép chúng ta kết hợp bất kỳ hàm số nào: giả sử chúng ta muốn lấy các số chẵn trong khoảng từ 20 đến 40, chúng ta chỉ cần kết hợp isEven chức năng từ CÁCH lên trên cùng với chúng tôi isInRange một người sử dụng một andvà nó chỉ đơn giản là hoạt động.

Tóm tắt lại

Bằng cách sử dụng các chức năng bị xáo trộn, chúng tôi có thể kết hợp các chức năng với nhau một cách rõ ràng. Chúng ta có thể nối trực tiếp đầu ra của một hàm với đầu vào của hàm tiếp theo, vì cả hai giờ đây đều nhận một tham số duy nhất.

Bằng cách sử dụng thành phần, chúng ta có thể kết hợp các chức năng nhỏ hơn hoặc các chức năng được sắp xếp thành các cấu trúc lớn hơn và phức tạp hơn nhiều, với sự tự tin rằng các bộ phận nhỏ nhất đang hoạt động như mong đợi.

Đây là rất nhiều để tiêu hóa, và đó là một khái niệm sâu sắc. Nhưng nếu bạn dành thời gian và khám phá điều này nhiều hơn, tôi nghĩ bạn sẽ bắt đầu thấy những ứng dụng mà chúng tôi thậm chí còn chưa đụng tới, và bạn có thể viết bài tiếp theo như thế này thay cho tôi!



Zik.vn – Biên dịch & Biên soạn Lại

spot_img

Create a website from scratch

Just drag and drop elements in a page to get started with Newspaper Theme.

Buy Now ⟶

Bài viết liên quang

DMCA.com Protection Status