Chức năng thuần túy: “Không có tác dụng phụ” có ngụ ý “Đầu ra luôn giống nhau, đầu vào giống nhau” không?


84

Hai điều kiện xác định một hàm purenhư sau:

  1. Không có tác dụng phụ (tức là chỉ cho phép thay đổi phạm vi cục bộ)
  2. Luôn trả về cùng một đầu ra, với cùng một đầu vào

Nếu điều kiện đầu tiên luôn đúng, có khi nào điều kiện thứ hai không đúng không?

Tức là nó chỉ thực sự cần thiết với điều kiện đầu tiên?


3
Cơ sở của bạn không được chỉ định rõ. "Đầu vào" quá rộng. Các hàm có thể được coi là hai loại đầu vào. Lập luận của họ và "môi trường" / "theo ngữ cảnh". Một hàm trả về thời gian hệ thống có thể được coi là thuần túy (mặc dù nó không phải là obv) nếu bạn không phân biệt được hai loại đầu vào này.
Alexander - Phục hồi Monica vào

4
@Alexander: Trong ngữ cảnh của "pure function", "input" thường được hiểu là các tham số / đối số được truyền một cách rõ ràng (theo bất kỳ cơ chế nào mà ngôn ngữ lập trình sử dụng). Đó là một phần của định nghĩa về "chức năng thuần túy". Nhưng bạn nói đúng, điều quan trọng là phải quan tâm đến định nghĩa.
sleske

3
Ví dụ phản đối tầm thường: trả về giá trị của một biến toàn cục. Không có tác dụng phụ (toàn cầu chỉ được đọc!), Nhưng vẫn có khả năng kết quả khác nhau mỗi lần. (Nếu bạn không thích hình cầu, hãy trả về địa chỉ của một biến cục bộ phụ thuộc vào ngăn xếp cuộc gọi tại thời điểm chạy).
Peter - Phục hồi Monica

2
Bạn cần mở rộng định nghĩa của mình về "tác dụng phụ"; bạn nói rằng một phương pháp tinh khiết không tạo ra tác dụng phụ, nhưng bạn cần phải cũng lưu ý rằng một phương pháp tinh khiết không tiêu thụ tác dụng phụ được sản xuất ở nơi khác.
Eric Lippert

2
@sleske Có lẽ thường được hiểu, nhưng việc thiếu sự phân biệt đó là nguyên nhân chính xác khiến OP nhầm lẫn.
Alexander - Phục hồi Monica

Câu trả lời:


114

Dưới đây là một số ví dụ phản chứng không thay đổi phạm vi bên ngoài nhưng vẫn bị coi là không tinh khiết:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (thừa nhận là có thay đổi PRNG, nhưng không được coi là có thể quan sát được)

Truy cập các biến không cục bộ không phải hằng số là đủ để có thể vi phạm điều kiện thứ hai.

Tôi luôn nghĩ về hai điều kiện để có được sự tinh khiết là bổ sung cho nhau:

  • đánh giá kết quả không được có ảnh hưởng đến trạng thái phụ
  • kết quả đánh giá không được ảnh hưởng bởi trạng thái bên

Thuật ngữ hiệu ứng phụ chỉ đề cập đến đầu tiên, chức năng sửa đổi trạng thái không cục bộ. Tuy nhiên, đôi khi các thao tác đọc cũng được coi là tác dụng phụ: khi chúng là các thao tác và cũng liên quan đến việc ghi, ngay cả khi mục đích chính của chúng là truy cập một giá trị. Ví dụ cho việc đó đang tạo một số giả ngẫu nhiên để sửa đổi trạng thái bên trong của bộ tạo, đọc từ luồng đầu vào để nâng cao vị trí đọc hoặc đọc từ cảm biến bên ngoài có liên quan đến lệnh "thực hiện phép đo".


1
Cảm ơn Bergi. Vì lý do nào đó, tôi nghĩ rằng các tác dụng phụ bao gồm việc đọc các biến bên ngoài phạm vi cục bộ, nhưng tôi đoán nó chỉ là một tác dụng phụ nếu nó ghi các biến bên ngoài như vậy.
Magnus

17
Nếu prompt("you choose")không có tác dụng phụ, chúng ta nên lùi lại một bước và làm rõ ý nghĩa của tác dụng phụ.
Holger

1
@Magnus Vâng, chính xác đó là ý nghĩa của hiệu ứng . Tôi cũng sẽ cố gắng làm rõ trong câu trả lời của mình, tôi không mong đợi sự chú ý lớn như vậy và muốn đưa ra câu trả lời xứng đáng với hàng chục phiếu bầu :-)
Bergi

2
Đối với tất cả những gì bạn biết Math.random () trả về một diode nhiệt. Nó không thực sự được chỉ định để sử dụng RNG xấu.
Joshua

1
Trong hai điều kiện, tôi đã nghe điều kiện đầu tiên được gọi là "hiệu ứng" trong khi điều kiện sau được gọi là "hệ số". Cả hai đều là "tác dụng phụ" và không tinh khiết. f (hệ số, đầu vào) -> ảnh hưởng, đầu ra Hệ số ảnh hưởng là đầu vào đến từ những thay đổi trong môi trường rộng hơn, các hiệu ứng là đầu ra thay đổi môi trường rộng hơn. Ví dụ: Elm và Clojurescrips re-frame làm việc với mô hình này.

30

Cách "bình thường" để diễn đạt hàm thuần túy là gì, là về tính minh bạch tham chiếu . Một hàm là thuần túy nếu nó trong suốt về mặt tham chiếu .

Tính trong suốt tham chiếu , đại khái, có nghĩa là bạn có thể thay thế lời gọi hàm bằng giá trị trả về của nó hoặc ngược lại tại bất kỳ điểm nào trong chương trình mà không làm thay đổi ý nghĩa của chương trình.

Vì vậy, ví dụ, nếu C printflà trong suốt về mặt tham chiếu, hai chương trình này phải có cùng ý nghĩa:

printf("Hello");

5;

và tất cả các chương trình sau phải có cùng ý nghĩa:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

Bởi vì printftrả về số ký tự được viết, trong trường hợp này là 5.

Nó thậm chí còn rõ ràng hơn với các voidchức năng. Nếu tôi có một chức năng void foo, thì

foo(bar, baz, quux);

nên giống như

;

Tức là vì fookhông trả về gì, nên tôi có thể thay thế nó bằng không mà không làm thay đổi ý nghĩa của chương trình.

Vì vậy, rõ ràng là cả hai đều printfkhông foominh bạch về mặt quy chiếu, và do đó cả hai đều không trong sáng. Trên thực tế, một voidhàm không bao giờ có thể minh bạch về mặt tham chiếu, trừ khi nó không phải là lựa chọn.

Tôi thấy định nghĩa này dễ xử lý hơn nhiều so với định nghĩa bạn đã đưa ra. Nó cũng cho phép bạn áp dụng nó ở bất kỳ mức độ chi tiết nào bạn muốn: bạn có thể áp dụng nó cho các biểu thức riêng lẻ, cho các hàm, cho toàn bộ chương trình. Ví dụ, nó cho phép bạn nói về một chức năng như thế này:

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

Chúng ta có thể phân tích các biểu thức tạo nên hàm và dễ dàng kết luận rằng chúng không minh bạch về mặt tham chiếu và do đó không thuần túy, vì chúng sử dụng cấu trúc dữ liệu có thể thay đổi, cụ thể là memomảng. Tuy nhiên, chúng ta cũng có thể nhìn vào các chức năng và có thể thấy rằng nó referentially minh bạch và do đó tinh khiết. Điều này đôi khi được gọi là sự thuần khiết bên ngoài , tức là một chức năng có vẻ thuần khiết với thế giới bên ngoài, nhưng được thực hiện bên trong không tinh khiết.

Các chức năng như vậy vẫn hữu ích, bởi vì trong khi tạp chất lây nhiễm sang mọi thứ xung quanh nó, giao diện tinh khiết bên ngoài xây dựng một loại "rào cản độ tinh khiết", nơi tạp chất chỉ lây nhiễm ba dòng của hàm, nhưng không rò rỉ ra phần còn lại của chương trình. . Ba dòng này dễ phân tích tính đúng đắn hơn nhiều so với toàn bộ chương trình.


2
Tạp chất đó ảnh hưởng đến toàn bộ chương trình khi bạn có đồng thời.
R .. GitHub NGỪNG TRỢ GIÚP ICE

@R .. Bạn có thể nghĩ ra cách đồng thời có thể làm cho hàm Fibonacci được mô tả bên ngoài không trong sạch không? Tôi không thể. Việc ghi vào memo[n]là không quan trọng, và việc không thể đọc từ nó chỉ làm lãng phí chu kỳ CPU.
Brilliand

Tôi đồng ý với cả hai bạn. Tạp chất có thể dẫn đến các vấn đề đồng thời, nhưng không phải trong trường hợp cụ thể này.
Jörg W Mittag

@R .. Không khó để tưởng tượng một phiên bản nhận biết đồng thời.
user253751 Ngày

1
@Brilliand Ví dụ: memo[n] = ...đầu tiên có thể tạo một mục từ điển, sau đó lưu giá trị vào đó. Điều này để lại một cửa sổ trong đó một luồng khác có thể nhìn thấy một mục chưa được khởi tạo.
user253751

12

Đối với tôi, dường như điều kiện thứ hai mà bạn đã mô tả là một hạn chế yếu hơn điều kiện đầu tiên.

Hãy để tôi cho bạn một ví dụ, giả sử bạn có một chức năng để thêm một chức năng cũng ghi nhật ký vào bảng điều khiển:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

Điều kiện thứ hai bạn cung cấp được thỏa mãn: hàm này luôn trả về cùng một đầu ra khi được cung cấp cùng một đầu vào. Tuy nhiên, nó không phải là một chức năng thuần túy vì nó bao gồm tác dụng phụ của việc đăng nhập vào bảng điều khiển.

Nói đúng ra, một hàm thuần túy là một hàm thỏa mãn tính chất minh bạch tham chiếu . Đó là thuộc tính mà chúng ta có thể thay thế một ứng dụng hàm bằng giá trị mà nó tạo ra mà không làm thay đổi hành vi của chương trình.

Giả sử chúng ta có một hàm chỉ cần thêm:

function addOne(x) {
  return x + 1;
}

Chúng tôi có thể thay thế addOne(5)bằng 6bất kỳ đâu trong chương trình của mình và sẽ không có gì thay đổi.

Ngược lại, chúng ta không thể thay thế addOneAndLog(x)bằng giá trị 6ở bất kỳ đâu trong chương trình của mình mà không thay đổi hành vi vì biểu thức đầu tiên dẫn đến một thứ được ghi vào bảng điều khiển trong khi biểu thức thứ hai thì không.

Chúng tôi coi bất kỳ hành vi bổ sung nào này addOneAndLog(x)thực hiện bên cạnh việc trả về kết quả đầu ra là một tác dụng phụ .


"Đối với tôi, dường như điều kiện thứ hai mà bạn đã mô tả là một hạn chế yếu hơn điều kiện đầu tiên." Không, hai điều kiện độc lập về mặt logic.
sleske

@sleske bạn nhầm rồi. Tôi đã cung cấp các định nghĩa rõ ràng cho các thuật ngữ thuần túy và tác dụng phụ. Trong những ràng buộc này, không có gì là một hàm không có tác dụng phụ ngoài việc trả về cùng một đầu ra cho một đầu vào nhất định. Tuy nhiên, tôi đã cung cấp các ví dụ trong đó điều kiện thứ hai có thể được thỏa mãn mà không cần điều kiện đầu tiên. Khái niệm fundamnetal để hiểu khái niệm về sự tinh khiết là tính minh bạch có thể tham chiếu.
TheInnerLight

Lỗi chính tả nhỏ: Không có gì mà một hàm không có tác dụng phụ có thể làm ngoài việc trả về cùng một đầu ra cho một đầu vào nhất định.
TheInnerLight

Còn những thứ như quay lại thời gian hiện tại thì sao? Điều đó không có tác dụng phụ, nhưng nó trả về một đầu ra khác cho cùng một đầu vào. Hay nói một cách tổng quát hơn, bất kỳ hàm nào mà kết quả không chỉ phụ thuộc vào các tham số đầu vào mà còn phụ thuộc vào một biến toàn cục (có thể thay đổi).
sleske

2
Có vẻ như bạn đang sử dụng một định nghĩa khác về "tác dụng phụ" với những gì thường được sử dụng. Một tác dụng phụ thường được định nghĩa là "hiệu ứng có thể quan sát được bên cạnh việc trả về một giá trị" hoặc "thay đổi trạng thái có thể quan sát được" - xem ví dụ: Wikipedia , bài đăng này trên softwareengineering.SE . Bạn hoàn toàn đúng rằng Date.now()nó không thuần túy / minh bạch về mặt tham chiếu, nhưng không phải vì nó có tác dụng phụ, mà bởi vì kết quả của nó phụ thuộc vào nhiều hơn là chỉ đầu vào của nó.
sleske

7

Có thể có một nguồn ngẫu nhiên từ bên ngoài hệ thống. Giả sử rằng một phần tính toán của bạn bao gồm nhiệt độ phòng. Sau đó, việc thực hiện chức năng sẽ mang lại kết quả khác nhau mỗi lần tùy thuộc vào yếu tố bên ngoài ngẫu nhiên của nhiệt độ phòng. Trạng thái không bị thay đổi khi thực hiện chương trình.

Tất cả những gì tôi có thể nghĩ ra, dù sao.


3
Theo tôi, những "ngẫu nhiên từ bên ngoài hệ thống" là một dạng của hiệu ứng phụ. Chức năng với những hành vi này không phải là "pures".
Joseph M. Dion

2

Vấn đề với các định nghĩa FP là chúng rất giả tạo. Mỗi đánh giá / tính toán đều có tác dụng phụ đối với người đánh giá. Về mặt lý thuyết, nó đúng. Việc phủ nhận điều này chỉ cho thấy rằng những người biện hộ cho FP bỏ qua triết học và logic: "đánh giá" có nghĩa là thay đổi trạng thái của một môi trường thông minh nào đó (máy móc, bộ não, v.v.). Đây là bản chất của quá trình đánh giá. Không có thay đổi - không có "tích". Tác động có thể rất rõ ràng: làm nóng CPU hoặc hỏng hóc, tắt bo mạch chủ trong trường hợp quá nóng, v.v.

Khi bạn nói về tính minh bạch tham chiếu, bạn nên hiểu rằng thông tin về tính minh bạch đó có sẵn cho con người với tư cách là người tạo ra toàn bộ hệ thống và người nắm giữ thông tin ngữ nghĩa và có thể không có sẵn cho trình biên dịch. Ví dụ: một hàm có thể đọc một số tài nguyên bên ngoài và nó sẽ có đơn nguyên IO trong chữ ký của nó nhưng nó sẽ trả về cùng một giá trị mọi lúc (ví dụ: kết quả của current_year > 0). Trình biên dịch không biết rằng hàm sẽ luôn trả về cùng một kết quả, vì vậy hàm là không tinh khiết nhưng có thuộc tính trong suốt tham chiếu và có thể được thay thế bằng Truehằng số.

Vì vậy, để tránh sự thiếu chính xác đó, chúng ta nên phân biệt các hàm toán học và "hàm" trong ngôn ngữ lập trình. Các hàm trong Haskell luôn không tinh khiết và định nghĩa về độ tinh khiết liên quan đến chúng luôn rất có điều kiện: chúng đang chạy trên phần cứng thực với các tác dụng phụ và đặc tính vật lý thực, điều này sai đối với các hàm toán học. Điều này có nghĩa là ví dụ với hàm "printf" là hoàn toàn không chính xác.

Nhưng không phải tất cả các hàm toán học đều thuần túy: mỗi hàm có t(thời gian) làm tham số có thể không tinh khiết: tchứa tất cả các hiệu ứng và bản chất ngẫu nhiên của hàm: trong trường hợp phổ biến, bạn có tín hiệu đầu vào và không biết về giá trị thực, nó có thể thậm chí là một tiếng ồn.


2

Nếu điều kiện đầu tiên luôn đúng, có khi nào điều kiện thứ hai không đúng không?

Đúng

Hãy xem xét đoạn mã đơn giản bên dưới

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

Mã này sẽ trả về đầu ra ngẫu nhiên cho cùng một bộ đầu vào nhất định - tuy nhiên nó không có bất kỳ tác dụng phụ nào.

Hiệu ứng tổng thể của cả hai điểm # 1 & # 2 mà bạn đã đề cập khi kết hợp với nhau có nghĩa là: Tại bất kỳ thời điểm nào nếu hàm Sumcó cùng i / p được thay thế bằng kết quả của nó trong một chương trình, ý nghĩa tổng thể của chương trình không thay đổi . Đây là không có gì khác ngoài sự minh bạch tham chiếu .


Nhưng trong trường hợp này, điều kiện đầu tiên không được xác minh: việc ghi vào bảng điều khiển được coi là một tác dụng phụ, vì nó tự thay đổi trạng thái của máy.
Chân phải

@Rightleg thx vì đã chỉ ra nó. Bằng cách nào đó tôi đã hiểu sai OP hoàn toàn theo cách khác. câu trả lời đã sửa.
rahulaga_dev

2
Nó không thay đổi trạng thái của bộ tạo ngẫu nhiên?
Eric Duminil

1
Tạo ra một số ngẫu nhiên là chính nó một tác dụng phụ, trừ khi trạng thái của bộ tạo số ngẫu nhiên được cung cấp một cách rõ ràng mà sẽ làm cho chức năng thỏa mãn điều kiện 2.
TheInnerLight

1
rndkhông thoát khỏi hàm, vì vậy thực tế là trạng thái của nó thay đổi không quan trọng đối với độ thuần khiết của hàm, nhưng thực tế là hàm Randomtạo sử dụng thời gian hiện tại làm giá trị gốc có nghĩa là có các "đầu vào" khác ab.
Sneftel
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.