Đây có phải là một chức năng thuần túy?


117

Hầu hết các nguồn định nghĩa một hàm thuần túy có hai thuộc tính sau:

  1. Giá trị trả về của nó là giống nhau cho cùng một đối số.
  2. Đánh giá của nó không có tác dụng phụ.

Đó là điều kiện đầu tiên liên quan đến tôi. Trong hầu hết các trường hợp, thật dễ dàng để đánh giá. Hãy xem xét các hàm JavaScript sau (như được hiển thị trong bài viết này )

Nguyên chất:

const add = (x, y) => x + y;

add(2, 4); // 6

Không tinh khiết:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

Thật dễ dàng để thấy rằng chức năng thứ 2 sẽ cung cấp các đầu ra khác nhau cho các cuộc gọi tiếp theo, do đó vi phạm điều kiện đầu tiên. Và do đó, nó không trong sạch.

Phần này tôi nhận được.


Bây giờ, đối với câu hỏi của tôi, hãy xem xét chức năng này chuyển đổi một số tiền nhất định bằng đô la sang euro:

(EDIT - Sử dụng consttrong dòng đầu tiên. letVô tình được sử dụng trước đó.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Giả sử chúng tôi lấy tỷ giá hối đoái từ một db và nó thay đổi mỗi ngày.

Bây giờ, bất kể tôi gọi hàm này bao nhiêu lần hôm nay , nó sẽ cho tôi cùng một đầu ra cho đầu vào 100. Tuy nhiên, nó có thể cho tôi một đầu ra khác vào ngày mai. Tôi không chắc điều này có vi phạm điều kiện đầu tiên hay không.

IOW, bản thân hàm không chứa bất kỳ logic nào để thay đổi đầu vào, nhưng nó phụ thuộc vào hằng số bên ngoài có thể thay đổi trong tương lai. Trong trường hợp này, chắc chắn nó sẽ thay đổi hàng ngày. Trong các trường hợp khác, nó có thể xảy ra; nó có thể không

Chúng ta có thể gọi các chức năng như vậy chức năng thuần túy. Nếu câu trả lời là KHÔNG, làm thế nào chúng ta có thể cấu trúc lại nó thành một?


6
Độ tinh khiết của một ngôn ngữ năng động như JS là một chủ đề rất phức tạp:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms

29
Độ tinh khiết có nghĩa là bạn có thể thay thế lệnh gọi hàm bằng giá trị kết quả của nó ở cấp mã mà không thay đổi hành vi của chương trình.
bob

1
Để biết thêm một chút về những gì tạo nên tác dụng phụ, và với thuật ngữ lý thuyết nhiều hơn, hãy xem cs.stackexchange.com/questions/116377/ Lỗi
Gilles 'SO- ngừng trở thành ác quỷ'

3
Ngày nay, chức năng là (x) => {return x * 0.9;}. Ngày mai, bạn sẽ có một chức năng khác cũng có thể là thuần túy (x) => {return x * 0.89;}. Lưu ý rằng mỗi khi bạn chạy, (x) => {return x * exchangeRate;}nó sẽ tạo ra một chức năng mới và chức năng đó là thuần túy vì exchangeRatekhông thể thay đổi.
dùng253751

2
Đây là một hàm không tinh khiết, Nếu bạn muốn làm cho nó thuần túy, bạn có thể sử dụng const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; cho một hàm thuần túy, Its return value is the same for the same arguments.nên giữ luôn, 1 giây, 1 thập kỷ .. sau này không có vấn đề gì
Vikash Tiwari

Câu trả lời:


133

Các dollarToEurogiá trị trả về 's phụ thuộc vào một biến bên ngoài đó không phải là một cuộc tranh cãi; do đó, chức năng là không tinh khiết.

Trong câu trả lời là KHÔNG, làm thế nào chúng ta có thể cấu trúc lại hàm thành thuần túy?

Một lựa chọn là vượt qua exchangeRate. Bằng cách này, mỗi lần đối số là (something, somethingElse), đầu ra được đảm bảosomething * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Lưu ý rằng đối với lập trình chức năng, bạn nên tránh let- luôn luôn sử dụng constđể tránh phân công lại.


6
Không có các biến miễn phí không phải là một yêu cầu cho một hàm thuần túy: const add = x => y => x + y; const one = add(42);Ở đây cả hai addoneđều là các hàm thuần túy.
zerkms

7
const foo = 42; const add42 = x => x + foo;<- đây là một hàm thuần túy khác, một lần nữa sử dụng các biến miễn phí.
zerkms

8
@zerkms - Tôi rất muốn thấy câu trả lời của bạn cho câu hỏi này (ngay cả khi nó chỉ viết lại của SurePerformance để sử dụng các thuật ngữ khác nhau). Tôi không nghĩ rằng nó sẽ được nhân đôi, và nó sẽ được chiếu sáng, đặc biệt là khi được trích dẫn (lý tưởng là có nguồn tốt hơn bài viết Wikipedia ở trên, nhưng nếu đó là tất cả những gì chúng ta nhận được, vẫn là một chiến thắng). (Thật dễ dàng để đọc bình luận này trong một số loại ánh sáng tiêu cực. Hãy tin tôi rằng tôi là người thật, tôi nghĩ rằng một câu trả lời như vậy sẽ rất tuyệt và muốn đọc nó.)
TJ Crowder

17
Tôi nghĩ rằng cả bạn và @zerkms đều sai. Bạn dường như nghĩ rằng dollarToEurohàm trong ví dụ trong câu trả lời của bạn là không tinh khiết vì nó phụ thuộc vào biến miễn phí exchangeRate. Điều đó thật vô lý. Như zerkms đã chỉ ra, độ tinh khiết của hàm không liên quan gì đến việc nó có biến tự do hay không. Tuy nhiên, zerkms cũng sai vì ông tin rằng dollarToEurohàm này không tinh khiết vì nó phụ thuộc vào exchangeRatecơ sở dữ liệu. Ông nói rằng nó không trong sạch vì "nó phụ thuộc vào IO một cách quá mức."
Aadit M Shah

9
(tiếp) Một lần nữa, điều đó thật vô lý vì nó cho thấy điều đó dollarToEurokhông trong sạch vì exchangeRatelà một biến tự do. Nó gợi ý rằng nếu exchangeRatekhông phải là một biến tự do, tức là nếu đó là một đối số, thì dollarToEuronó sẽ là thuần túy. Do đó, nó cho thấy đó dollarToEuro(100)là không tinh khiết nhưng dollarToEuro(100, exchangeRate)là tinh khiết. Điều đó rõ ràng vô lý bởi vì trong cả hai trường hợp, bạn phụ thuộc vào exchangeRatecơ sở dữ liệu. Sự khác biệt duy nhất là có hay không exchangeRatelà một biến miễn phí trong dollarToEurohàm.
Aadit M Shah

76

Về mặt kỹ thuật, bất kỳ chương trình nào bạn thực hiện trên máy tính đều không tinh khiết vì cuối cùng nó sẽ biên dịch theo các hướng dẫn như điều di chuyển giá trị này vào thành phố eaxvà thêm giá trị này vào nội dung của eaxTrực, không tinh khiết. Điều đó không hữu ích lắm.

Thay vào đó, chúng tôi nghĩ về độ tinh khiết bằng cách sử dụng hộp đen . Nếu một số mã luôn tạo ra cùng một đầu ra khi được cung cấp cùng một đầu vào thì nó được coi là thuần túy. Theo định nghĩa này, chức năng sau đây cũng hoàn toàn mặc dù bên trong nó sử dụng bảng ghi nhớ không tinh khiết.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

Chúng tôi không quan tâm đến nội bộ vì chúng tôi đang sử dụng phương pháp hộp đen để kiểm tra độ tinh khiết. Tương tự, chúng tôi không quan tâm rằng tất cả mã cuối cùng được chuyển đổi thành hướng dẫn máy không tinh khiết vì chúng tôi đang nghĩ về độ tinh khiết bằng phương pháp hộp đen. Nội bộ không quan trọng.

Bây giờ, hãy xem xét các chức năng sau đây.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

greetchức năng tinh khiết hay không tinh khiết? Theo phương pháp hộp đen của chúng tôi, nếu chúng tôi cung cấp cho nó cùng một đầu vào (ví dụ World) thì nó luôn in cùng một đầu ra ra màn hình (tức là Hello World!). Theo nghĩa đó, nó không phải là tinh khiết? Không, không phải vậy. Lý do nó không thuần túy là vì chúng tôi coi việc in một cái gì đó lên màn hình là một tác dụng phụ. Nếu hộp đen của chúng tôi tạo ra tác dụng phụ thì nó không thuần túy.

Một tác dụng phụ là gì? Đây là nơi mà khái niệm về tính minh bạch tham chiếu là hữu ích. Nếu một chức năng được minh bạch tham chiếu thì chúng ta luôn có thể thay thế các ứng dụng của chức năng đó bằng kết quả của chúng. Lưu ý rằng điều này không giống như chức năng nội tuyến .

Trong chức năng nội tuyến, chúng tôi thay thế các ứng dụng của một chức năng bằng phần thân của hàm mà không làm thay đổi ngữ nghĩa của chương trình. Tuy nhiên, một hàm trong suốt tham chiếu có thể luôn được thay thế bằng giá trị trả về của nó mà không làm thay đổi ngữ nghĩa của chương trình. Hãy xem xét ví dụ sau.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Ở đây, chúng tôi đã nêu ra định nghĩa greetvà nó không thay đổi ngữ nghĩa của chương trình.

Bây giờ, hãy xem xét các chương trình sau đây.

undefined;
undefined;

Ở đây, chúng tôi đã thay thế các ứng dụng của greethàm bằng các giá trị trả về của chúng và nó đã thay đổi ngữ nghĩa của chương trình. Chúng tôi không còn in lời chào lên màn hình. Đó là lý do tại sao in ấn được coi là một tác dụng phụ, và đó là lý do tại sao greetchức năng này không tinh khiết. Nó không tham chiếu minh bạch.

Bây giờ, hãy xem xét một ví dụ khác. Hãy xem xét chương trình sau đây.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Rõ ràng, mainchức năng là không tinh khiết. Tuy nhiên, timeDiffchức năng là tinh khiết hay không tinh khiết? Mặc dù nó phụ thuộc vào serverTimeviệc xuất phát từ một cuộc gọi mạng không tinh khiết, nhưng nó vẫn trong suốt về mặt tham chiếu vì nó trả về cùng một đầu ra cho cùng một đầu vào và vì nó không có bất kỳ tác dụng phụ nào.

zerkms có thể sẽ không đồng ý với tôi về điểm này. Trong câu trả lời của mình , ông nói rằng dollarToEurohàm trong ví dụ sau là không tinh khiết bởi vì nó phụ thuộc vào IO một cách liên tục.

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Tôi phải không đồng ý với anh ta vì thực tế là việc exchangeRatexuất phát từ cơ sở dữ liệu là không liên quan. Đó là một chi tiết nội bộ và phương pháp hộp đen của chúng tôi để xác định độ tinh khiết của hàm không quan tâm đến các chi tiết bên trong.

Trong các ngôn ngữ chức năng thuần túy như Haskell, chúng ta có một lối thoát để thực hiện các hiệu ứng IO tùy ý. Nó được gọi unsafePerformIOvà như tên ngụ ý nếu bạn không sử dụng đúng cách thì không an toàn vì nó có thể phá vỡ tính minh bạch tham chiếu. Tuy nhiên, nếu bạn biết bạn đang làm gì thì nó hoàn toàn an toàn để sử dụng.

Nó thường được sử dụng để tải dữ liệu từ các tệp cấu hình gần đầu chương trình. Tải dữ liệu từ các tập tin cấu hình là một hoạt động IO không tinh khiết. Tuy nhiên, chúng tôi không muốn bị gánh nặng bằng cách chuyển dữ liệu làm đầu vào cho mọi chức năng. Do đó, nếu chúng ta sử dụng unsafePerformIOthì chúng ta có thể tải dữ liệu ở cấp cao nhất và tất cả các chức năng thuần túy của chúng ta có thể phụ thuộc vào dữ liệu cấu hình toàn cầu bất biến.

Lưu ý rằng chỉ vì một chức năng phụ thuộc vào một số dữ liệu được tải từ tệp cấu hình, cơ sở dữ liệu hoặc cuộc gọi mạng, không có nghĩa là chức năng đó không trong sạch.

Tuy nhiên, hãy xem xét ví dụ ban đầu của bạn có ngữ nghĩa khác nhau.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Ở đây, tôi giả sử rằng vì exchangeRatekhông được xác định là const, nó sẽ được sửa đổi trong khi chương trình đang chạy. Nếu đó là trường hợp dollarToEurochắc chắn là một chức năng không tinh khiết bởi vì khi exchangeRateđược sửa đổi, nó sẽ phá vỡ tính minh bạch tham chiếu.

Tuy nhiên, nếu exchangeRatebiến không được sửa đổi và sẽ không bao giờ được sửa đổi trong tương lai (nghĩa là nếu đó là giá trị không đổi), thì ngay cả khi nó được định nghĩa là let, nó sẽ không phá vỡ tính minh bạch tham chiếu. Trong trường hợp đó, dollarToEurothực sự là một chức năng thuần túy.

Lưu ý rằng giá trị của exchangeRatecó thể thay đổi mỗi khi bạn chạy lại chương trình và nó sẽ không phá vỡ tính minh bạch tham chiếu. Nó chỉ phá vỡ tính minh bạch tham chiếu nếu nó thay đổi trong khi chương trình đang chạy.

Ví dụ: nếu bạn chạy timeDiffví dụ của tôi nhiều lần thì bạn sẽ nhận được các giá trị khác nhau serverTimevà do đó kết quả khác nhau. Tuy nhiên, vì giá trị serverTimekhông bao giờ thay đổi trong khi chương trình đang chạy, nên timeDiffhàm này hoàn toàn.


3
Điều này rất nhiều thông tin. Cảm ơn. Và tôi đã có nghĩa là sử dụng consttrong ví dụ của tôi.
tuyết

3
Nếu bạn đã có nghĩa là để sử dụng constthì dollarToEurochức năng thực sự là tinh khiết. Cách duy nhất giá trị exchangeRatesẽ thay đổi là nếu bạn chạy lại chương trình. Trong trường hợp đó, quy trình cũ và quy trình mới là khác nhau. Do đó, nó không phá vỡ tính minh bạch tham chiếu. Nó giống như gọi một hàm hai lần với các đối số khác nhau. Các đối số có thể khác nhau nhưng trong hàm, giá trị của các đối số không đổi.
Aadit M Shah

3
Điều này nghe có vẻ như một lý thuyết nhỏ về thuyết tương đối: các hằng số chỉ tương đối ổn định, không hoàn toàn, cụ thể là liên quan đến quá trình chạy. Rõ ràng câu trả lời đúng duy nhất ở đây. +1.
bob

5
Tôi không đồng ý với "là không tinh khiết vì cuối cùng nó sẽ biên dịch theo các hướng dẫn như cách di chuyển giá trị này vào eax, và thêm giá trị này vào nội dung của eax . Nếu eaxbị xóa - thông qua tải hoặc xóa - mã vẫn xác định bất kể những gì khác đang xảy ra và do đó là thuần túy. Nếu không, câu trả lời rất toàn diện.
3Dave

3
@Bergi: Thật ra, trong một ngôn ngữ thuần túy với các giá trị bất biến, danh tính là không liên quan. Cho dù hai tham chiếu đánh giá đến cùng một giá trị là hai tham chiếu đến cùng một đối tượng hoặc các đối tượng khác nhau chỉ có thể được quan sát bằng cách thay đổi đối tượng thông qua một trong các tham chiếu và quan sát xem giá trị cũng thay đổi khi được truy xuất thông qua tham chiếu khác. Không có đột biến, danh tính trở nên không liên quan. (Như Rich Hickey sẽ nói: Danh tính là một chuỗi các quốc gia theo thời gian.)
Jörg W Mittag

23

Câu trả lời của một người theo chủ nghĩa thuần túy (trong đó "tôi" theo nghĩa đen là tôi, vì tôi nghĩ câu hỏi này không có câu trả lời "đúng" chính thức nào ):

Trong một ngôn ngữ động như JS có rất nhiều khả năng đối với các loại cơ sở khỉ, hoặc tạo ra các loại tùy chỉnh bằng cách sử dụng các tính năng như Object.prototype.valueOfkhông thể biết liệu một hàm có thuần túy hay không chỉ bằng cách nhìn vào nó, vì nó có phụ thuộc vào người gọi hay không để tạo ra tác dụng phụ.

Bản demo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Một câu trả lời của tôi - người thực dụng:

Từ định nghĩa từ wikipedia

Trong lập trình máy tính, một hàm thuần túy là một hàm có các thuộc tính sau:

  1. Giá trị trả về của nó là giống nhau cho cùng một đối số (không có biến thể với biến tĩnh cục bộ, biến không cục bộ, đối số tham chiếu có thể thay đổi hoặc luồng đầu vào từ thiết bị I / O).
  2. Đánh giá của nó không có tác dụng phụ (không có đột biến biến tĩnh cục bộ, biến không cục bộ, đối số tham chiếu có thể thay đổi hoặc luồng I / O).

Nói cách khác, nó chỉ quan trọng cách một chức năng hoạt động, chứ không phải cách nó được thực hiện. Và miễn là một hàm cụ thể giữ 2 thuộc tính này - nó hoàn toàn bất kể nó được thực hiện chính xác như thế nào.

Bây giờ đến chức năng của bạn:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Điều đó không chắc chắn bởi vì nó không đủ điều kiện yêu cầu 2: nó phụ thuộc vào IO liên tục.

Tôi đồng ý tuyên bố trên là sai, xem câu trả lời khác để biết chi tiết: https://stackoverflow.com/a/58749249/251311

Các tài nguyên liên quan khác:


4
@TJCrowder melà zerkms người cung cấp câu trả lời.
zerkms

2
Vâng, với Javascript, tất cả là về sự tự tin, không phải là sự đảm bảo
bob

4
@bob ... hoặc đó là một cuộc gọi chặn.
zerkms

1
@zerkms - Cảm ơn. Chỉ để tôi chắc chắn 100%, sự khác biệt chính giữa bạn add42và tôi addXhoàn toàn là tôi xcó thể bị thay đổi và bạn ftkhông thể thay đổi (và do đó, add42giá trị trả về của bạn không thay đổi dựa trên ft)?
TJ Crowder

5
Tôi không đồng ý rằng dollarToEurohàm trong ví dụ của bạn không trong sạch. Tôi giải thích tại sao tôi không đồng ý trong câu trả lời của tôi. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

Giống như những câu trả lời khác đã nói, cách bạn đã thực hiện dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

thực sự là thuần túy, vì tỷ giá hối đoái không được cập nhật trong khi chương trình đang chạy. Tuy nhiên, về mặt khái niệm, dollarToEurocó vẻ như nó phải là một hàm không tinh khiết, ở chỗ nó sử dụng bất cứ tỷ giá hối đoái cập nhật nhất nào. Cách đơn giản nhất để giải thích sự khác biệt này là bạn chưa thực hiện dollarToEuronhưng dollarToEuroAtInstantOfProgramStart.

Chìa khóa ở đây là có một số tham số được yêu cầu để tính toán chuyển đổi tiền tệ và rằng một phiên bản thực sự thuần túy của tướng dollarToEurosẽ cung cấp cho tất cả chúng. Các thông số trực tiếp nhất là lượng USD cần chuyển đổi và tỷ giá hối đoái. Tuy nhiên, vì bạn muốn nhận tỷ giá hối đoái của mình từ thông tin được công bố, giờ đây bạn có ba tham số để cung cấp:

  • Số tiền để trao đổi
  • Một cơ quan lịch sử để tư vấn cho tỷ giá hối đoái
  • Ngày mà giao dịch diễn ra (để lập chỉ mục cho cơ quan lịch sử)

Cơ quan lịch sử ở đây là cơ sở dữ liệu của bạn và giả sử rằng cơ sở dữ liệu không bị xâm phạm, sẽ luôn trả về kết quả tương tự cho tỷ giá hối đoái vào một ngày cụ thể. Do đó, với sự kết hợp của ba tham số này, bạn có thể viết một phiên bản hoàn toàn tự cung cấp, hoàn toàn tự nhiên dollarToEuro, có thể trông giống như thế này:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Việc triển khai của bạn nắm bắt các giá trị không đổi cho cả cơ quan lịch sử và ngày giao dịch tại thời điểm chức năng được tạo - cơ quan lịch sử là cơ sở dữ liệu của bạn và ngày bị bắt là ngày bạn bắt đầu chương trình - tất cả số tiền còn lại là số tiền , mà người gọi cung cấp. Phiên bản không tinh khiết của dollarToEurogiá trị luôn nhận được giá trị cập nhật nhất về cơ bản lấy tham số ngày một cách ngầm định, đặt nó thành tức thời mà hàm được gọi, không đơn giản chỉ vì bạn không bao giờ có thể gọi hàm với cùng tham số hai lần.

Nếu bạn muốn có một phiên bản thuần túy của dollarToEuro mà vẫn có thể nhận được giá trị cập nhật nhất, bạn vẫn có thể ràng buộc thẩm quyền lịch sử, nhưng không để tham số ngày không bị ràng buộc và yêu cầu ngày từ người gọi làm đối số, kết thúc với một cái gì đó như thế này:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@Snowman Bạn được chào đón! Tôi đã cập nhật câu trả lời một chút để thêm nhiều ví dụ mã.
TheHansinator

8

Tôi muốn rút lại một chút từ các chi tiết cụ thể của JS và sự trừu tượng của các định nghĩa chính thức và nói về những điều kiện cần phải giữ để cho phép tối ưu hóa cụ thể. Đó thường là điều chính chúng ta quan tâm khi viết mã (mặc dù nó cũng giúp chứng minh tính đúng đắn). Lập trình chức năng không phải là một hướng dẫn cho thời trang mới nhất cũng không phải là một lời thề tự từ chối. Nó là một công cụ để giải quyết vấn đề.

Khi bạn có mã như thế này:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Nếu exchangeRatekhông bao giờ có thể được sửa đổi ở giữa hai cuộc gọi đến dollarToEuro(100), có thể ghi nhớ kết quả của cuộc gọi đầu tiên dollarToEuro(100)và tối ưu hóa cuộc gọi thứ hai. Kết quả sẽ giống nhau, vì vậy chúng ta chỉ cần nhớ giá trị từ trước đó.

exchangeRatethể được đặt một lần, trước khi gọi bất kỳ chức năng nào tìm kiếm và không bao giờ sửa đổi. Ít hạn chế hơn, bạn có thể có mã tìm kiếmexchangeRate một lần cho một chức năng hoặc khối mã cụ thể và sử dụng cùng một tỷ giá hối đoái trong phạm vi đó. Hoặc, nếu chỉ chủ đề này có thể sửa đổi cơ sở dữ liệu, bạn sẽ có quyền cho rằng, nếu bạn không cập nhật tỷ giá hối đoái, không ai khác thay đổi nó trên bạn.

Nếu fetchFromDatabase()bản thân nó là một hàm thuần túy đánh giá một hằng số và exchangeRatelà bất biến, chúng ta có thể gấp hằng số này trong suốt quá trình tính toán. Một trình biên dịch biết trường hợp này có thể đưa ra suy luận giống như bạn đã làm trong nhận xét, dollarToEuro(100)đánh giá là 90.0 và thay thế toàn bộ biểu thức bằng hằng số 90.0.

Tuy nhiên, nếu fetchFromDatabase()không thực hiện I / O, được coi là tác dụng phụ, tên của nó vi phạm Nguyên tắc tối thiểu.


8

Hàm này không thuần túy, nó phụ thuộc vào một biến bên ngoài, gần như chắc chắn sẽ thay đổi.

Do đó, hàm thất bại điểm đầu tiên bạn thực hiện, nó không trả về cùng một giá trị khi cho cùng một đối số.

Để làm cho hàm này "thuần túy", chuyển exchangeRatevào làm đối số.

Điều này sau đó sẽ đáp ứng cả hai điều kiện.

  1. Nó sẽ luôn trả về cùng một giá trị khi chuyển cùng giá trị và tỷ giá hối đoái.
  2. Nó cũng sẽ không có tác dụng phụ.

Mã ví dụ:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
"Điều gần như chắc chắn sẽ thay đổi" --- không phải vậy const.
zerkms

7

Để mở rộng các điểm mà người khác đã thực hiện về tính minh bạch tham chiếu: chúng ta có thể định nghĩa độ tinh khiết chỉ đơn giản là độ trong suốt tham chiếu của các lệnh gọi hàm (tức là mọi lệnh gọi đến hàm có thể được thay thế bằng giá trị trả về mà không thay đổi ngữ nghĩa của chương trình).

Hai thuộc tính bạn đưa ra là cả hậu quả của tính minh bạch tham chiếu. Ví dụ: hàm sau f1không tinh khiết, vì nó không cho kết quả giống nhau mỗi lần (thuộc tính bạn đã đánh số 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Tại sao điều quan trọng là có được kết quả tương tự mỗi lần? Bởi vì nhận được các kết quả khác nhau là một cách để một lệnh gọi hàm có ngữ nghĩa khác nhau từ một giá trị và do đó phá vỡ tính minh bạch tham chiếu.

Giả sử chúng ta viết mã f1("hello", "world"), chúng ta chạy nó và nhận giá trị trả về "hello". Nếu chúng tôi thực hiện tìm / thay thế mọi cuộc gọi f1("hello", "world")và thay thế chúng bằng "hello"chúng tôi sẽ thay đổi ngữ nghĩa của chương trình (tất cả các cuộc gọi sẽ được thay thế bằng "hello", nhưng ban đầu khoảng một nửa trong số chúng sẽ được đánh giá "world"). Do đó các cuộc gọi đến f1không được minh bạch tham chiếu, do đó f1không trong sạch.

Một cách khác mà một lệnh gọi hàm có thể có các ngữ nghĩa khác nhau với một giá trị là bằng cách thực hiện các câu lệnh. Ví dụ:

function f2(x) {
  console.log("foo");
  return x;
}

Giá trị trả về f2("bar")sẽ luôn luôn "bar", nhưng ngữ nghĩa của giá trị "bar"khác với cuộc gọi f2("bar")vì sau này cũng sẽ đăng nhập vào bàn điều khiển. Thay thế cái này bằng cái kia sẽ thay đổi ngữ nghĩa của chương trình, vì vậy nó không minh bạch về mặt tham chiếu và do đó f2không trong sạch.

dollarToEuroChức năng của bạn có trong suốt tham chiếu (và do đó thuần túy) hay không phụ thuộc vào hai điều:

  • 'Phạm vi' của những gì chúng tôi coi là minh bạch tham chiếu
  • Liệu ý exchangeRatechí có bao giờ thay đổi trong 'phạm vi' đó không

Không có phạm vi "tốt nhất" để sử dụng; thông thường chúng ta sẽ nghĩ về một lần chạy chương trình, hoặc thời gian tồn tại của dự án. Tương tự như vậy, hãy tưởng tượng rằng các giá trị trả về của mọi hàm được lưu vào bộ nhớ cache (như bảng ghi nhớ trong ví dụ được đưa ra bởi @ aadit-m-shah): khi nào chúng ta cần xóa bộ đệm, để đảm bảo rằng các giá trị cũ sẽ không can thiệp vào ngữ nghĩa?

Nếu exchangeRateđang sử dụng varthì nó có thể thay đổi giữa mỗi cuộc gọi đến dollarToEuro; chúng tôi sẽ cần xóa mọi kết quả được lưu trong bộ nhớ cache giữa mỗi cuộc gọi, do đó sẽ không có tính minh bạch tham chiếu nào để nói đến.

Bằng cách sử dụng, constchúng tôi sẽ mở rộng 'phạm vi' để chạy chương trình: sẽ an toàn khi lưu các giá trị trả về của bộ đệm dollarToEurocho đến khi chương trình kết thúc. Chúng ta có thể tưởng tượng sử dụng một macro (trong một ngôn ngữ như Lisp) để thay thế các lệnh gọi hàm bằng các giá trị trả về của chúng. Lượng tinh khiết này là phổ biến cho những thứ như giá trị cấu hình, tùy chọn dòng lệnh hoặc ID duy nhất. Nếu chúng ta hạn chế suy nghĩ về một lần chạy chương trình thì chúng ta sẽ nhận được hầu hết các lợi ích của độ tinh khiết, nhưng chúng ta phải cẩn thận trong các lần chạy (ví dụ: lưu dữ liệu vào một tệp, sau đó tải nó trong một lần chạy khác). Tôi sẽ không gọi các chức năng đó là "thuần túy" theo nghĩa trừu tượng (ví dụ nếu tôi đang viết một định nghĩa từ điển), nhưng không có vấn đề gì với việc coi chúng là thuần túy trong ngữ cảnh .

Nếu chúng ta coi thời gian tồn tại của dự án là 'phạm vi' thì chúng ta sẽ "minh bạch nhất" và do đó "thuần khiết nhất", thậm chí theo nghĩa trừu tượng. Chúng tôi sẽ không bao giờ cần phải xóa bộ nhớ cache giả định của chúng tôi. Chúng tôi thậm chí có thể thực hiện việc "lưu trữ" này bằng cách viết lại trực tiếp mã nguồn trên đĩa, để thay thế các cuộc gọi bằng các giá trị trả về của chúng. Điều này thậm chí sẽ hoạt động trên các dự án, ví dụ chúng ta có thể tưởng tượng một cơ sở dữ liệu trực tuyến về các hàm và giá trị trả về của chúng, trong đó bất kỳ ai cũng có thể tìm kiếm một lệnh gọi hàm và (nếu trong DB) sử dụng giá trị trả về được cung cấp bởi ai đó ở phía bên kia của thế giới đã sử dụng một chức năng giống hệt nhau nhiều năm trước trong một dự án khác nhau.


4

Như đã viết, nó là một chức năng thuần túy. Nó không tạo ra tác dụng phụ. Hàm này có một tham số chính thức, nhưng nó có hai đầu vào và sẽ luôn xuất cùng một giá trị cho bất kỳ hai đầu vào nào.


2

Chúng ta có thể gọi các chức năng như vậy chức năng thuần túy. Nếu câu trả lời là KHÔNG, làm thế nào chúng ta có thể cấu trúc lại nó thành một?

Như bạn đã lưu ý, "nó có thể mang lại cho tôi một đầu ra khác vào ngày mai" . Nếu đó là trường hợp, câu trả lời sẽ là "không" vang dội . Điều này đặc biệt như vậy nếu hành vi dự định của bạn dollarToEurođã được giải thích chính xác là:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Tuy nhiên, một cách giải thích khác tồn tại, nơi nó sẽ được coi là thuần túy:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro trực tiếp ở trên là tinh khiết.


Từ góc độ công nghệ phần mềm, việc tuyên bố sự phụ thuộc của dollarToEurochức năng là điều cần thiết fetchFromDatabase. Do đó, cấu trúc lại định nghĩa dollarToEuronhư sau:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Với kết quả này, với tiền đề là các fetchFromDatabasechức năng thỏa đáng, sau đó chúng ta có thể kết luận rằng hình chiếu fetchFromDatabasetrên dollarToEurophải thỏa đáng. Hoặc câu lệnh " fetchFromDatabaselà thuần khiết" ngụ ý dollarToEurolà thuần túy (vì fetchFromDatabasecơ sở cho dollarToEuroyếu tố vô hướng của x.

Từ bài viết gốc, tôi có thể hiểu đó fetchFromDatabaselà một thời gian chức năng. Chúng ta hãy cải thiện nỗ lực tái cấu trúc để làm cho sự hiểu biết đó trở nên minh bạch, do đó rõ ràng đủ điều kiện fetchFromDatabaselà một chức năng thuần túy:

fetchFromDatabase = (dấu thời gian) => {/ * ở đây thực hiện triển khai * /};

Cuối cùng, tôi sẽ cấu trúc lại tính năng như sau:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Do đó, dollarToEurocó thể được kiểm tra đơn vị bằng cách đơn giản chứng minh rằng nó gọi chính xác fetchFromDatabase(hoặc đạo hàm của nó exchangeRate).


1
Điều này rất sáng sủa. +1. Cảm ơn.
tuyết

Trong khi tôi tìm thấy câu trả lời của bạn nhiều thông tin hơn và có lẽ việc tái cấu trúc tốt hơn cho trường hợp sử dụng cụ thể của dollarToEuro ; Tôi đã đề cập trong OP rằng có thể có các trường hợp sử dụng khác. Tôi đã chọn DollarToEuro vì nó gợi lên ngay lập tức những gì tôi đang cố gắng thực hiện, nhưng có thể có một cái gì đó kém tinh tế phụ thuộc vào một biến tự do có thể thay đổi, nhưng không nhất thiết là một chức năng của thời gian. Với ý nghĩ đó, tôi thấy công cụ tái cấu trúc được xếp hạng là công cụ dễ truy cập hơn và là công cụ có thể giúp đỡ những người khác có trường hợp sử dụng tương tự. Cảm ơn sự giúp đỡ của bạn bất kể.
tuyết

-1

Tôi là một người song ngữ Haskell / JS và Haskell là một trong những ngôn ngữ tạo ra sự quan tâm lớn về độ tinh khiết của chức năng, vì vậy tôi nghĩ rằng tôi sẽ cung cấp cho bạn viễn cảnh từ cách Haskell nhìn thấy nó.

Như những người khác đã nói, trong Haskell, đọc một biến có thể thay đổi thường được coi là không trong sạch. Có một sự khác biệt giữa các biếnđịnh nghĩa trong đó các biến có thể thay đổi sau đó, các định nghĩa là giống nhau mãi mãi. Vì vậy, nếu bạn đã khai báo nó const(giả sử nó chỉ là mộtnumber và không có cấu trúc bên trong có thể thay đổi), đọc từ đó sẽ sử dụng một định nghĩa, đó là thuần túy. Nhưng bạn muốn mô hình tỷ giá hối đoái thay đổi theo thời gian, và điều đó đòi hỏi một số khả năng biến đổi và sau đó bạn rơi vào tình trạng không tinh khiết.

Để mô tả những thứ không trong sạch đó (chúng ta có thể gọi chúng là hiệu ứng của Google, và cách sử dụng của chúng có hiệu lực, thay vì sử dụng tinh khiết) trong Haskell, chúng tôi làm những gì bạn có thể gọi là siêu lập trình . Ngày nay, siêu lập trình thường đề cập đến các macro không phải là ý tôi, mà chỉ là ý tưởng viết một chương trình để viết một chương trình khác nói chung.

Trong trường hợp này, trong Haskell, chúng tôi viết một phép tính thuần túy để tính toán một chương trình hiệu quả mà sau đó sẽ làm những gì chúng tôi muốn. Vì vậy, toàn bộ điểm của tệp nguồn Haskell (ít nhất, một tệp mô tả chương trình, không phải thư viện) là để mô tả một tính toán thuần túy cho một chương trình hiệu quả mà nó tạo ra main. Sau đó, công việc của trình biên dịch Haskell là lấy tệp nguồn này, thực hiện tính toán thuần túy đó và đặt chương trình hiệu quả đó dưới dạng thực thi nhị phân ở đâu đó trên ổ cứng của bạn để chạy sau khi rảnh rỗi. Nói cách khác, có một khoảng cách, giữa thời điểm khi tính toán thuần túy chạy (trong khi trình biên dịch làm cho việc thực thi) và thời gian khi chương trình hiệu quả chạy (bất cứ khi nào bạn chạy thực thi).

Vì vậy, đối với chúng tôi, các chương trình hiệu quả thực sự là một cấu trúc dữ liệu và về bản chất chúng không làm bất cứ điều gì chỉ bằng cách được đề cập (chúng không có hiệu ứng * bên cạnh giá trị trả về; giá trị trả về của chúng chứa hiệu ứng của chúng). Đối với một ví dụ rất nhẹ về lớp TypeScript mô tả các chương trình bất biến và một số thứ bạn có thể làm với chúng,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

Điều quan trọng là nếu bạn có Program<x>thì không có tác dụng phụ nào xảy ra và đây là những thực thể hoàn toàn có chức năng. Ánh xạ một chức năng qua một chương trình không có bất kỳ tác dụng phụ nào trừ khi chức năng đó không phải là một chức năng thuần túy; giải trình tự hai chương trình không có bất kỳ tác dụng phụ nào; Vân vân.

Vì vậy, ví dụ về cách áp dụng điều này trong trường hợp của bạn, bạn có thể viết một số hàm thuần trả lại các chương trình để lấy người dùng bằng ID và thay đổi cơ sở dữ liệu và tìm nạp dữ liệu JSON, như

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

và sau đó bạn có thể mô tả một công việc định kỳ để cuộn URL và tra cứu một số nhân viên và thông báo cho người giám sát của họ theo cách hoàn toàn có chức năng như

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Vấn đề là mọi chức năng đơn lẻ ở đây là một chức năng hoàn toàn thuần túy; không có gì thực sự xảy ra cho đến khi tôi thực sự action.run()thiết lập nó thành chuyển động. Ngoài ra, tôi có thể viết các chức năng như,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

và nếu JS đã hủy bỏ lời hứa, chúng tôi có thể có hai chương trình đua nhau và lấy kết quả đầu tiên và hủy lần thứ hai. (Ý tôi là chúng ta vẫn có thể, nhưng nó trở nên ít rõ ràng hơn để làm gì.)

Tương tự trong trường hợp của bạn, chúng tôi có thể mô tả thay đổi tỷ giá hối đoái với

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

exchangeRatecó thể là một chương trình nhìn vào một giá trị có thể thay đổi,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

nhưng ngay cả như vậy, chức năng này dollarsToEuros này bây giờ là một hàm thuần túy từ một số đến một chương trình tạo ra một số và bạn có thể suy luận về nó theo cách cân bằng xác định mà bạn có thể suy luận về bất kỳ chương trình nào không có tác dụng phụ.

Tất nhiên, chi phí là cuối cùng bạn phải gọi nó .run() ở đâu đó , và điều đó sẽ không trong sạch. Nhưng toàn bộ cấu trúc tính toán của bạn có thể được mô tả bằng một tính toán thuần túy và bạn có thể đẩy tạp chất đến lề của mã.


Tôi tò mò tại sao điều này tiếp tục bị hạ thấp nhưng ý tôi là tôi vẫn đứng đó (thực tế, đó là cách bạn thao túng các chương trình trong Haskell, nơi mọi thứ hoàn toàn mặc định) và sẽ sẵn sàng điều chỉnh các downvote. Tuy nhiên, nếu những người downvot muốn để lại bình luận giải thích những gì họ không thích về nó, tôi có thể cố gắng cải thiện nó.
CR Drost

Vâng, tôi đã tự hỏi rằng tại sao có rất nhiều downvote nhưng không một bình luận, bên cạnh tất nhiên là tác giả.
Buda Örs
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.