Bạn gọi hàm nào trong đó cùng một đầu vào sẽ luôn trả về cùng một đầu ra, nhưng cũng có tác dụng phụ?


43

Nói rằng chúng ta có một chức năng thuần túy bình thường như

function add(a, b) {
  return a + b
}

Và sau đó chúng tôi thay đổi nó để nó có tác dụng phụ

function add(a, b) {
  writeToDatabase(Math.random())
  return a + b;
}

Nó không được coi là một chức năng thuần túy như tôi biết vì tôi thường nghe mọi người gọi các chức năng thuần túy là "các chức năng mà không có tác dụng phụ". Tuy nhiên, nó hoạt động giống như một hàm thuần túy cho đến khi nó sẽ trả về cùng một đầu ra cho cùng một đầu vào.

Có một tên khác cho loại chức năng này, nó không được đặt tên, hay nó vẫn thực sự thuần túy và tôi nhầm về định nghĩa của độ tinh khiết?


91
"Không phải là một chức năng thuần túy".
Ross Patterson

2
@RossPatterson đó cũng là những gì tôi nghĩ, nhưng bằng cách hỏi tôi đã học được về tính minh bạch tham chiếu nên tôi rất vui vì tôi đã không giữ nó cho riêng mình.
m0meni

9
Nếu writeToDatabasethất bại, nó có thể kích hoạt một ngoại lệ, do đó, addđôi khi chức năng thứ hai của bạn tạo ra một ngoại lệ ngay cả khi được gọi với cùng một đối số mà trước đó không có vấn đề gì ... hầu hết thời gian có các tác dụng phụ đều đưa ra loại điều kiện liên quan đến lỗi này. "Độ tinh khiết đầu vào-đầu ra".
Bakuriu

25
Một cái gì đó luôn cung cấp cùng một đầu ra cho một đầu vào nhất định được gọi là xác định .
njzk2

2
@ njzk2: Đúng, nhưng nó cũng không quốc tịch . Hàm xác định trạng thái có thể không cung cấp cùng một đầu ra cho mỗi đầu vào. Ví dụ: F(x)được định nghĩa để trả về truenếu nó được gọi với cùng đối số với cuộc gọi trước đó. Rõ ràng với trình tự {1,2,2} => {undefined, false, true}này là xác định, nhưng nó cung cấp đầu ra khác nhau cho F(2).
MSalters

Câu trả lời:


85

Tôi không chắc chắn về các định nghĩa phổ biến về độ tinh khiết, nhưng theo quan điểm của Haskell (một ngôn ngữ mà các lập trình viên có xu hướng quan tâm đến những thứ như độ tinh khiết và độ trong suốt tham chiếu), chỉ có chức năng đầu tiên của bạn là "thuần túy". Phiên bản thứ hai addkhông thuần túy . Vì vậy, để trả lời câu hỏi của bạn, tôi gọi nó là "không trong sạch";)

Theo định nghĩa này, một hàm thuần túy là một hàm:

  1. Chỉ phụ thuộc vào đầu vào của nó. Nghĩa là, với cùng một đầu vào, nó sẽ luôn trả về cùng một đầu ra.
  2. Được minh bạch tham chiếu: chức năng có thể được thay thế tự do bởi giá trị của nó và "hành vi" của chương trình sẽ không thay đổi.

Với định nghĩa này, rõ ràng chức năng thứ hai của bạn không thể được coi là thuần túy, vì nó phá vỡ quy tắc 2. Nghĩa là, hai chương trình sau KHÔNG tương đương:

function f(a, b) { 
    return add(a, b) + add(a, b);
}

function g(a, b) {
    c = add(a, b);
    return c + c;
}

Điều này là do mặc dù cả hai hàm sẽ trả về cùng một giá trị, hàm fsẽ ghi vào cơ sở dữ liệu hai lần nhưng gsẽ ghi một lần! Rất có khả năng ghi vào cơ sở dữ liệu là một phần trong hành vi có thể quan sát được của chương trình của bạn, trong trường hợp đó tôi đã cho thấy phiên bản thứ hai của bạn addkhông "thuần túy".

Nếu ghi vào cơ sở dữ liệu không phải là một phần có thể quan sát được trong hành vi của chương trình của bạn, thì cả hai phiên bản addcó thể được coi là tương đương và thuần túy. Nhưng tôi không thể nghĩ ra một kịch bản mà việc ghi vào cơ sở dữ liệu không thành vấn đề. Ngay cả vấn đề đăng nhập!


1
Không phải "chỉ phụ thuộc vào đầu vào của nó" có tính minh bạch tham chiếu được cung cấp sao? Điều này có nghĩa là RT đồng nghĩa với độ tinh khiết? (Tôi đang bối rối hơn về điều này khi tôi tìm kiếm càng nhiều nguồn)
Ixrec

Tôi đồng ý nó khó hiểu. Tôi chỉ có thể nghĩ về các ví dụ giả định. Nói f(x)không chỉ phụ thuộc vào x, mà còn phụ thuộc vào một số biến toàn cầu bên ngoài y. Sau đó, nếu fcó thuộc tính của RT, bạn có thể tự do trao đổi tất cả các lần xuất hiện của nó với giá trị trả về của nó miễn là bạn không chạm vào y . Vâng, ví dụ của tôi là đáng ngờ. Nhưng điều quan trọng là: nếu fghi vào cơ sở dữ liệu (hoặc ghi vào nhật ký) thì nó sẽ mất tài sản của RT: bây giờ việc bạn rời khỏi toàn cầu ykhông bị ảnh hưởng, bạn có biết ý nghĩa của chương trình thay đổi hay không tùy thuộc vào việc bạn có thực sự thay đổi hay không gọi fhoặc đơn giản là sử dụng giá trị trả về của nó.
Andres F.

Humph. Hãy để chúng tôi nói rằng chúng tôi có một chức năng hoàn toàn ngoại trừ các tác dụng phụ và cũng được đảm bảo có hành vi như vậy trong đó hai mẫu của bạn là tương đương. (Tôi đã gặp trường hợp này trong thực tế nên nó không phải là giả thuyết.) Tôi nghĩ chúng ta chưa hoàn thành.
Joshua

2
Tôi cho rằng chức năng thứ hai cũng có thể phá vỡ quy tắc số 1. Tùy thuộc vào ngôn ngữ và thực tiễn xử lý lỗi của API cơ sở dữ liệu đang sử dụng, chức năng có thể hoàn toàn không trả về bất cứ điều gì nếu cơ sở dữ liệu không có sẵn hoặc do lỗi ghi db vì một số lý do.
Zach Lipton

1
Vì Haskell đã được đề cập: Trong Haskell thêm một hiệu ứng phụ như thế đòi hỏi phải thay đổi chữ ký của hàm . (nghĩ về nó giống như đưa cơ sở dữ liệu gốc làm đầu vào bổ sung và nhận cơ sở dữ liệu đã sửa đổi làm giá trị trả về bổ sung của hàm). Thực tế có thể mô hình hóa các hiệu ứng phụ khá thanh lịch trong hệ thống loại đó, chỉ là các ngôn ngữ chính ngày nay không quan tâm đủ đến tác dụng phụ và độ tinh khiết để làm điều này.
ComicSansMS

19

Bạn gọi hàm [mà] cùng một đầu vào sẽ luôn trả về cùng một đầu ra, nhưng cũng có tác dụng phụ?

Hàm như vậy được gọi là

tính quyết định

Một thuật toán có hành vi có thể được dự đoán hoàn toàn từ đầu vào.

termwiki.com

Về nhà nước:

Tùy thuộc vào định nghĩa của một chức năng bạn sử dụng, một chức năng không có trạng thái. Nếu bạn đến từ thế giới hướng đối tượng, hãy nhớ rằng đó x.f(y)là một phương pháp. Là một chức năng, nó sẽ trông như thế f(x,y). Và nếu bạn đang ở trong trạng thái đóng với phạm vi từ vựng kèm theo, hãy nhớ rằng trạng thái bất biến cũng có thể là một phần của biểu thức hàm. Nó chỉ là trạng thái có thể thay đổi sẽ ảnh hưởng đến tính chất xác định chức năng. Vì vậy f (x) = x + 1 là xác định miễn là 1 không thay đổi. Không quan trọng nơi 1 được lưu trữ.

Chức năng của bạn là cả hai xác định. Đầu tiên của bạn cũng là một chức năng thuần túy. Thứ hai của bạn không thuần túy.

Chức năng tinh khiết

  1. Hàm luôn đánh giá cùng một giá trị kết quả được đưa ra cùng (các) giá trị đối số. Giá trị kết quả chức năng không thể phụ thuộc vào bất kỳ thông tin hoặc trạng thái ẩn nào có thể thay đổi trong khi tiến hành chương trình tiến hành hoặc giữa các lần thực hiện khác nhau của chương trình, cũng không thể phụ thuộc vào bất kỳ đầu vào bên ngoài nào từ các thiết bị I / O.

  2. Đánh giá kết quả không gây ra bất kỳ tác dụng phụ hoặc đầu ra có thể quan sát được về mặt ngữ nghĩa, chẳng hạn như đột biến của các đối tượng có thể thay đổi hoặc đầu ra cho các thiết bị I / O.

wikipedia.org

Chỉ 1 có nghĩa là xác định . Điểm 2 có nghĩa là minh bạch tham chiếu . Cùng nhau, chúng có nghĩa là một hàm thuần túy chỉ cho phép các đối số và giá trị trả về của nó thay đổi. Không có gì khác gây ra thay đổi. Không có gì khác được thay đổi.


-1. Viết vào cơ sở dữ liệu phụ thuộc vào trạng thái bên ngoài mà thường không thể xác định được khi nhìn vào đầu vào. Cơ sở dữ liệu có thể không có sẵn vì một số lý do và liệu hoạt động sẽ thành công là không thể dự đoán được. Đây không phải là hành vi xác định.
Frax

@Frax Bộ nhớ hệ thống có thể không khả dụng. CPU có thể không có sẵn. Là người quyết đoán không đảm bảo thành công. Nó đảm bảo rằng hành vi thành công là có thể dự đoán.
candied_orange

OOMing không cụ thể cho bất kỳ chức năng, nó là loại vấn đề khác nhau. Bây giờ, chúng ta hãy nhìn vào điểm 1. của định nghĩa "hàm thuần túy" của bạn (thực sự có nghĩa là "xác định"): "Giá trị kết quả của hàm không thể phụ thuộc vào bất kỳ thông tin hoặc trạng thái ẩn nào có thể thay đổi trong khi thực hiện chương trình tiến hành hoặc giữa các lần thực thi khác nhau của chương trình cũng không thể phụ thuộc vào bất kỳ đầu vào bên ngoài nào từ các thiết bị I / O ". Cơ sở dữ liệu là trạng thái tốt, vì vậy chức năng của OP rõ ràng không đáp ứng điều kiện này - nó không mang tính quyết định.
Frax

@candied_orange Tôi sẽ đồng ý nếu việc ghi vào DB chỉ phụ thuộc vào đầu vào. Nhưng đó là Math.random(). Vì vậy, trừ khi chúng ta giả sử PRNG (thay vì RNG vật lý) VÀ xem xét rằng PRNG là một phần của đầu vào (không phải là tham chiếu được mã hóa cứng), điều đó không mang tính quyết định.
marstato

1
@candied_orange trích dẫn trạng thái xác định của bạn "một thuật toán có hành vi có thể được dự đoán hoàn toàn từ đầu vào." Viết cho IO, với tôi, chắc chắn là hành vi, không phải là kết quả.
marstato

9

Nếu bạn không quan tâm đến tác dụng phụ, thì nó minh bạch về mặt tham chiếu. Tất nhiên có thể là bạn không quan tâm nhưng người khác thì không, vì vậy khả năng áp dụng thuật ngữ này phụ thuộc vào ngữ cảnh.

Tôi không biết một thuật ngữ chung cho chính xác các thuộc tính bạn mô tả, nhưng một tập hợp con quan trọng là những thuộc tính không có giá trị . Trong khoa học máy tính, hơi khác với toán học *, một hàm idempotent là một hàm có thể được lặp lại với cùng một hiệu ứng; điều đó có nghĩa là kết quả tác dụng phụ của việc thực hiện nhiều lần cũng giống như thực hiện một lần.

Vì vậy, nếu tác dụng phụ của bạn là cập nhật cơ sở dữ liệu với một giá trị nhất định trong một hàng nhất định hoặc để tạo một tệp có nội dung phù hợp chính xác, thì nó sẽ là idempotent , nhưng nếu nó được thêm vào cơ sở dữ liệu hoặc được thêm vào một tệp Sau đó, nó sẽ không.

Sự kết hợp của các chức năng idempotent có thể hoặc không thể là toàn bộ idempotent.

* Việc sử dụng idempotent khác nhau trong khoa học máy tính so với toán học dường như xuất phát từ việc sử dụng sai thuật ngữ toán học sau đó được thông qua vì khái niệm này rất hữu ích.


3
thuật ngữ "minh bạch tham chiếu" được định nghĩa chặt chẽ hơn so với việc "có ai quan tâm" hay không. thậm chí nếu chúng ta bỏ qua các vấn đề IO như vấn đề kết nối, mất tích chuỗi kết nối, timeout, vv, thì nó vẫn còn dễ dàng để chứng minh rằng một chương trình thay thế (f x, f x)với let y = f x in (y, y)sẽ chạy vào ra của đĩa không gian ngoại lệ nhanh gấp hai lần bạn có thể tranh luận rằng đây là những trường hợp cạnh bạn không quan tâm, nhưng với định nghĩa mờ nhạt như vậy, chúng tôi cũng có thể gọi là new Random().Next()minh bạch tham chiếu bởi vì quái gì, tôi không quan tâm số tôi nhận được bằng cách nào.
sara

@kai: Tùy thuộc vào ngữ cảnh, bạn có thể bỏ qua các tác dụng phụ. Mặt khác, giá trị trả về của hàm như ngẫu nhiên không phải là tác dụng phụ: đó là tác dụng chính của nó.
Giorgio

@Giorgio Random.Nexttrong .NET thực sự có tác dụng phụ. Rất nhiều như vậy. Nếu bạn có thể Next, gán nó cho một biến và sau đó gọi Nextlại và gán nó cho một biến khác, rất có thể chúng sẽ không bằng nhau. Tại sao? Bởi vì gọi Nextthay đổi một số trạng thái nội bộ ẩn trong Randomđối tượng. Đây là đối cực của sự minh bạch tham chiếu. Tôi không hiểu yêu cầu của bạn rằng "tác dụng chính" không thể là tác dụng phụ. Trong mã mệnh lệnh, nó phổ biến hơn không phải là hiệu ứng chính là tác dụng phụ, bởi vì các chương trình bắt buộc có bản chất là trạng thái.
sara

3

Tôi không biết các hàm như vậy được gọi như thế nào (hoặc thậm chí có một số tên hệ thống), nhưng tôi sẽ gọi hàm không thuần túy (như các câu trả lời khác được đưa ra) nhưng luôn trả về kết quả tương tự nếu được cung cấp cùng chức năng "của nó tham số "(so với chức năng của tham số và một số trạng thái khác). Tôi gọi nó chỉ là chức năng, nhưng thật không may khi chúng ta nói "chức năng" trong ngữ cảnh lập trình, chúng tôi muốn nói một cái gì đó hoàn toàn không phải là chức năng thực sự.


1
Đã đồng ý! Đó là (không chính thức) định nghĩa toán học của "hàm", nhưng như bạn nói, thật không may "hàm" có nghĩa là một cái gì đó khác nhau trong các ngôn ngữ lập trình, trong đó nó gần với "một thủ tục từng bước cần thiết để có được một giá trị".
Andres F.

2

Về cơ bản nó phụ thuộc vào việc bạn có quan tâm đến tạp chất hay không. Nếu ngữ nghĩa của bảng này là bạn không quan tâm có bao nhiêu mục, thì nó hoàn toàn. Khác, nó không tinh khiết.

Hoặc nói theo một cách khác, sẽ tốt miễn là tối ưu hóa dựa trên độ tinh khiết không phá vỡ ngữ nghĩa chương trình.

Một ví dụ thực tế hơn sẽ là nếu bạn đang cố gắng gỡ lỗi chức năng này và thêm các báo cáo ghi nhật ký. Về mặt kỹ thuật, việc đăng nhập là một tác dụng phụ. Các bản ghi làm cho nó không tinh khiết? Không.


Vâng, nó phụ thuộc. Có thể các bản ghi làm cho nó không trong sạch, ví dụ nếu bạn quan tâm đến số lần và vào thời gian nào, "INFO f () được gọi" xuất hiện trong nhật ký của bạn. Những gì bạn thường làm :)
Andres F.

8
-1 Nhật ký làm vấn đề. Trên hầu hết các nền tảng đầu ra của bất kỳ loại nào đều đồng bộ hóa luồng thực thi. Hành vi của chương trình của bạn trở nên phụ thuộc vào các luồng ghi khác, trên các trình ghi nhật ký bên ngoài, đôi khi vào các lần đọc nhật ký, ở trạng thái mô tả tệp. Nó là tinh khiết như xô của bụi bẩn.
Basilevs

@AresresF. Chà, có lẽ bạn không quan tâm đến số lần. Bạn có thể chỉ quan tâm rằng nó đã đăng nhập nhiều lần như chức năng được gọi.
DeadMG

@Basilevs Hành vi của chức năng hoàn toàn không phụ thuộc vào chúng. Nếu ghi nhật ký thất bại, bạn chỉ cần thực hiện ngay.
DeadMG

2
Vấn đề là bạn có chọn định nghĩa logger là một phần của môi trường thực thi hay không. Đối với một ví dụ khác, hàm thuần túy của tôi có còn thuần không nếu tôi đính kèm trình gỡ lỗi vào quy trình và đặt điểm dừng trên đó? Từ POV của người sử dụng trình gỡ lỗi, rõ ràng chức năng này có tác dụng phụ, nhưng thông thường chúng tôi phân tích một chương trình với quy ước rằng "điều này không được tính". Điều tương tự cũng có thể (nhưng không cần thiết) để ghi nhật ký được sử dụng để gỡ lỗi, mà tôi cho rằng đó là lý do tại sao dấu vết đang che giấu sự không tinh khiết của nó. Tất nhiên, ghi nhật ký quan trọng, ví dụ cho kiểm toán, tất nhiên một tác dụng phụ đáng kể.
Steve Jessop

1

Tôi muốn nói rằng điều tốt nhất để hỏi không phải là chúng ta sẽ gọi nó như thế nào, mà là cách chúng ta phân tích một đoạn mã như vậy. Và câu hỏi quan trọng đầu tiên của tôi trong một phân tích như vậy sẽ là:

  • Có tác dụng phụ phụ thuộc vào đối số cho chức năng, hoặc kết quả về tác dụng phụ?
    • Không: "Hàm hiệu quả" có thể được tái cấu trúc thành một hàm thuần túy, một hành động hiệu quả và một cơ chế để kết hợp chúng.
    • Có: "Hàm hiệu quả" là một hàm tạo ra kết quả đơn âm.

Điều này là đơn giản để minh họa trong Haskell (và câu này chỉ là một nửa trò đùa). Một ví dụ về trường hợp "không" sẽ giống như thế này:

double :: Num a => a -> IO a
double x = do
  putStrLn "I'm doubling some number"
  return (x*2)

Trong ví dụ này, hành động mà chúng ta thực hiện (in dòng "I'm doubling some number") không có tác động đến mối quan hệ giữa xvà kết quả. Điều này có nghĩa là chúng ta có thể cấu trúc lại nó theo cách này (sử dụng Applicativelớp và *>toán tử của nó ), điều này cho thấy hàm và hiệu ứng trên thực tế là trực giao:

double :: Num a => a -> IO a
double x = action *> pure (function x)
  where
    -- The pure function 
    function x = x*2  
    -- The side effect
    action = putStrLn "I'm doubling some number"

Vì vậy, trong trường hợp này, cá nhân tôi, sẽ nói rằng đó là trường hợp bạn có thể tạo ra một hàm thuần túy. Rất nhiều chương trình của Haskell nói về việc học cách này để tìm ra các phần thuần túy từ mã hiệu quả.

Một ví dụ về loại "có", trong đó các phần nguyên chất và hiệu quả không trực giao:

double :: Num a => a -> IO a
double x = do
  putStrLn ("I'm doubling the number " ++ show x)
  return (x*2)

Bây giờ, chuỗi mà bạn in phụ thuộc vào giá trị của x. Các chức năng phần (nhân xbởi hai), tuy nhiên, không phụ thuộc vào ảnh hưởng chút nào, vì vậy chúng tôi vẫn có thể yếu tố nó ra:

logged :: (a -> b) -> (a -> IO x) -> IO b
logged function logger a = do
  logger a
  return (function a)

double x = logged function logger
  where function = (*2) 
        logger x putStrLn ("I'm doubling the number " ++ show x)

Tôi có thể tiếp tục đánh vần các ví dụ khác, nhưng tôi hy vọng điều này đủ để minh họa điểm tôi đã bắt đầu: bạn không "gọi" nó một cái gì đó, bạn phân tích làm thế nào các phần tinh khiết và hiệu quả liên quan và đưa ra yếu tố khi nó là để lợi thế của bạn.

Đây là một trong những lý do Haskell sử dụng Monadlớp học của nó rất nhiều. Monads là (trong số những thứ khác) là một công cụ để thực hiện loại phân tích và tái cấu trúc này.


-2

Chức năng mà được dùng để gây ra tác dụng phụ thường được gọi là effectful . Ví dụ https://slpopejoy.github.io/posts/Effectful01.html


Chỉ có câu trả lời để đề cập đến thuật ngữ được công nhận rộng rãi có hiệu lực và nó được bỏ phiếu .... Tôi không biết là không biết gì. ..
Ben Hutchison

"có hiệu lực" là một từ mà tác giả của bài đăng đó đồng ý với nghĩa là "có tác dụng phụ". Anh ấy tự nói như vậy.
Robert Harvey

Chức năng hiệu quả của Google cho thấy nhiều bằng chứng là thuật ngữ được sử dụng rộng rãi. Bài đăng trên blog đã được đưa ra như một trong nhiều ví dụ, không phải là định nghĩa. Trong các vòng tròn lập trình chức năng nơi các hàm thuần túy là mặc định, cần có một thuật ngữ tích cực để mô tả các hàm có tác dụng phụ có chủ ý. Tức là nhiều hơn chỉ là sự vắng mặt của sự tinh khiết . Hiệu quả là thuật ngữ đó. Bây giờ, xem xét bản thân có giáo dục.
Ben Hutchison

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.