Minh bạch tham chiếu là gì?


38

Tôi đã thấy rằng trong các mô hình mệnh lệnh

f (x) + f (x)

có thể không giống như:

2 * f (x)

Nhưng trong một mô hình chức năng thì nó phải giống nhau. Tôi đã cố gắng thực hiện cả hai trường hợp trong Python và Scheme , nhưng đối với tôi chúng trông khá đơn giản giống nhau.

Điều gì sẽ là một ví dụ có thể chỉ ra sự khác biệt với hàm đã cho?


7
Bạn có thể, và thường làm, viết các hàm trong suốt tham chiếu trong python. Sự khác biệt là ngôn ngữ không thi hành nó.
Karl Bielefeldt

5
trong C và tương tự: f(x++)+f(x++)có thể không giống như 2*f(x++)(trong C, nó đặc biệt đáng yêu khi những thứ như thế được ẩn trong macro - tôi có bị gãy mũi không? Bạn đặt cược)
gnat

Theo hiểu biết của tôi, ví dụ của @ gnat là lý do tại sao các ngôn ngữ hướng chức năng như R sử dụng tham chiếu qua và tránh rõ ràng các chức năng sửa đổi đối số của chúng. Trong R, ít nhất, thực sự khó có thể vượt qua những hạn chế này (ít nhất là, theo cách di động, ổn định) mà không đào sâu vào hệ thống phức tạp của môi trường và không gian tên và đường dẫn tìm kiếm.
Shadowtalker

4
@ssdecontrol: Trên thực tế, khi bạn có tính minh bạch tham chiếu, thông qua giá trị và tham chiếu qua luôn mang lại kết quả chính xác như nhau, do đó, ngôn ngữ sử dụng ngôn ngữ nào không quan trọng. Các ngôn ngữ chức năng thường được chỉ định với một cái gì đó gần giống với giá trị của sự rõ ràng về ngữ nghĩa, nhưng việc triển khai chúng thường sử dụng tham chiếu qua để thực hiện (hoặc thậm chí cả hai, tùy thuộc vào ngôn ngữ nào nhanh hơn cho bối cảnh cụ thể).
Jörg W Mittag

4
@gnat: Đặc biệt, f(x++)+f(x++)có thể hoàn toàn là bất cứ điều gì, vì nó gọi hành vi không xác định. Nhưng điều đó không thực sự liên quan đến tính minh bạch tham chiếu - điều này sẽ không giúp ích cho cuộc gọi này, đó là 'không xác định' đối với các chức năng minh bạch tham chiếu như trong sin(x++)+sin(x++). Có thể là 42, có thể định dạng ổ cứng của bạn, có thể có quỷ bay ra khỏi mũi người dùng
Christopher Creutzig

Câu trả lời:


62

Độ trong suốt tham chiếu, được tham chiếu đến một hàm, chỉ ra rằng bạn có thể xác định kết quả của việc áp dụng hàm đó chỉ bằng cách xem xét các giá trị của các đối số của nó. Bạn có thể viết các hàm trong suốt tham chiếu trong bất kỳ ngôn ngữ lập trình nào, ví dụ Python, Scheme, Pascal, C.

Mặt khác, trong hầu hết các ngôn ngữ, bạn cũng có thể viết các hàm không tham chiếu trong suốt. Ví dụ: hàm Python này:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

không tham chiếu minh bạch, trong thực tế gọi

foo(x) + foo(x)

2 * foo(x)

sẽ tạo ra các giá trị khác nhau, cho bất kỳ đối số x. Lý do cho điều này là hàm sử dụng và sửa đổi một biến toàn cục, do đó kết quả của mỗi lần gọi phụ thuộc vào trạng thái thay đổi này, và không chỉ dựa vào đối số của hàm.

Haskell, một ngôn ngữ chức năng thuần túy, tách biệt nghiêm ngặt việc đánh giá biểu thức trong đó các hàm thuần túy được áp dụng và luôn minh bạch, từ thực thi hành động (xử lý các giá trị đặc biệt), không minh bạch về mặt tham chiếu, tức là thực hiện cùng một hành động có thể có mỗi lần kết quả khác nhau.

Vì vậy, đối với bất kỳ chức năng Haskell

f :: Int -> Int

và bất kỳ số nguyên nào x, nó luôn luôn đúng

2 * (f x) == (f x) + (f x)

Một ví dụ về một hành động là kết quả của chức năng thư viện getLine:

getLine :: IO String

Kết quả của việc đánh giá biểu thức, hàm này (thực sự là một hằng số) trước hết tạo ra một giá trị thuần của kiểu IO String. Các giá trị của loại này là các giá trị như bất kỳ giá trị nào khác: bạn có thể chuyển chúng xung quanh, đặt chúng vào cấu trúc dữ liệu, soạn chúng bằng các hàm đặc biệt, v.v. Ví dụ: bạn có thể tạo một danh sách các hành động như vậy:

[getLine, getLine] :: [IO String]

Các hành động đặc biệt ở chỗ bạn có thể yêu cầu thời gian chạy Haskell thực thi chúng bằng cách viết:

main = <some action>

Trong trường hợp này, khi chương trình Haskell của bạn được khởi động, bộ thực thi sẽ chuyển qua hành động bị ràng buộc mainthực thi nó, có thể tạo ra các hiệu ứng phụ. Do đó, thực thi hành động không minh bạch về mặt tham chiếu vì thực hiện cùng một hành động hai lần có thể tạo ra các kết quả khác nhau tùy thuộc vào thời gian chạy làm đầu vào.

Nhờ hệ thống loại của Haskell, một hành động không bao giờ có thể được sử dụng trong bối cảnh mà loại khác được mong đợi và ngược lại. Vì vậy, nếu bạn muốn tìm độ dài của chuỗi, bạn có thể sử dụng lengthhàm:

length "Hello"

sẽ trả về 5. Nhưng nếu bạn muốn tìm độ dài của chuỗi được đọc từ thiết bị đầu cuối, bạn không thể viết

length (getLine)

bởi vì bạn nhận được một lỗi loại: lengthmong đợi một đầu vào của danh sách loại (và thực tế, một chuỗi là một danh sách) nhưng getLinelà một giá trị của loại IO String(một hành động). Theo cách này, hệ thống loại đảm bảo rằng một giá trị hành động như getLine(thực hiện được thực hiện bên ngoài ngôn ngữ cốt lõi và có thể không trong suốt tham chiếu) không thể được ẩn bên trong giá trị loại không hành động Int.

CHỈNH SỬA

Để trả lời câu hỏi exizt, đây là một chương trình Haskell nhỏ đọc một dòng từ bảng điều khiển và in độ dài của nó.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

Hành động chính bao gồm hai giao dịch được thực hiện tuần tự:

  1. getlineloại IO String,
  2. cái thứ hai được xây dựng bằng cách đánh giá chức năng putStrLncủa kiểu String -> IO ()trên đối số của nó.

Chính xác hơn, hành động thứ hai được xây dựng bởi

  1. ràng buộc linevới giá trị được đọc bởi hành động đầu tiên,
  2. đánh giá các hàm thuần túy length(tính độ dài dưới dạng số nguyên) và sau đó show(biến số nguyên thành chuỗi),
  3. xây dựng hành động bằng cách áp dụng chức năng putStrLncho kết quả của show.

Tại thời điểm này, hành động thứ hai có thể được thực thi. Nếu bạn đã gõ "Xin chào", nó sẽ in "5".

Lưu ý rằng nếu bạn nhận được một giá trị từ một hành động bằng cách sử dụng <-ký hiệu, bạn chỉ có thể sử dụng giá trị đó bên trong một hành động khác, ví dụ: bạn không thể viết:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

bởi vì show (length line)có loại Stringtrong khi ký hiệu không yêu cầu một hành động ( getLineloại IO String) được theo sau bởi một hành động khác (ví dụ như putStrLn (show (length line))loại IO ()).

CHỈNH SỬA 2

Định nghĩa về tính minh bạch tham chiếu của Jörg W Mittag chung chung hơn của tôi (tôi đã nêu lên câu trả lời của anh ấy). Tôi đã sử dụng một định nghĩa hạn chế vì ví dụ trong câu hỏi tập trung vào giá trị trả về của các hàm và tôi muốn minh họa khía cạnh này. Tuy nhiên, RT nói chung đề cập đến ý nghĩa của toàn bộ chương trình, bao gồm các thay đổi về trạng thái toàn cầu và tương tác với môi trường (IO) gây ra bằng cách đánh giá một biểu thức. Vì vậy, để có một định nghĩa chung, chính xác, bạn nên tham khảo câu trả lời đó.


10
Downvoter có thể gợi ý làm thế nào tôi có thể cải thiện câu trả lời này không?
Giorgio

Vậy làm thế nào để có được độ dài của một chuỗi được đọc từ thiết bị đầu cuối trong Haskell?
sbichenko

2
Điều này cực kỳ mang tính mô phạm, nhưng để hoàn thiện, đây không phải là hệ thống kiểu của Haskell đảm bảo các hành động và chức năng thuần túy không trộn lẫn; thực tế là ngôn ngữ không cung cấp bất kỳ chức năng không tinh khiết nào mà bạn có thể gọi trực tiếp. Bạn thực sự có thể thực hiện IOloại của Haskell khá dễ dàng trong bất kỳ ngôn ngữ nào với lambdas và generic, nhưng vì bất kỳ ai cũng có thể gọi printlntrực tiếp, việc thực IOhiện không đảm bảo độ tinh khiết; nó chỉ đơn thuần là một quy ước.
Doval

Tôi có nghĩa là (1) tất cả các chức năng là thuần túy (tất nhiên, chúng là thuần túy vì ngôn ngữ không cung cấp bất kỳ chức năng không tinh khiết nào, mặc dù theo như tôi biết có một số cơ chế để bỏ qua điều đó) và (2) các chức năng thuần túy và hành động không tinh khiết có các loại khác nhau, vì vậy chúng không thể được trộn lẫn. BTW, bạn có ý gì khi gọi trực tiếp ?
Giorgio

6
Quan điểm của bạn về việc getLinekhông minh bạch tham chiếu là không chính xác. Bạn đang trình bày getLinenhư thể nó ước tính hoặc giảm xuống một số Chuỗi, Chuỗi cụ thể phụ thuộc vào đầu vào của người dùng. Điều này là không chính xác. IO Stringkhông chứa Chuỗi nào nhiều hơn Maybe String. IO Stringlà một công thức để có thể, có thể có được một Chuỗi và, như một biểu thức, nó thuần túy như bất kỳ chuỗi nào khác trong Haskell.
LuxuryMode

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

Tuy nhiên, đó không phải là ý nghĩa của tính minh bạch tham chiếu. RT có nghĩa là bạn có thể thay thế bất kỳ biểu thức nào trong chương trình bằng kết quả đánh giá biểu thức đó (hoặc ngược lại) mà không thay đổi ý nghĩa của chương trình.

Lấy ví dụ, chương trình sau:

def f(): return 2

print(f() + f())
print(2)

Chương trình này là minh bạch tham khảo. Tôi có thể thay thế một hoặc cả hai lần xuất hiện của f()với 2và nó vẫn sẽ làm việc như nhau:

def f(): return 2

print(2 + f())
print(2)

hoặc là

def f(): return 2

print(f() + 2)
print(2)

hoặc là

def f(): return 2

print(2 + 2)
print(f())

tất cả sẽ hành xử như nhau.

Vâng, thực sự, tôi đã lừa dối. Tôi sẽ có thể thay thế cuộc gọi printbằng giá trị trả về của nó (hoàn toàn không có giá trị) mà không thay đổi ý nghĩa của chương trình. Tuy nhiên, rõ ràng, nếu tôi chỉ loại bỏ hai printcâu lệnh, ý nghĩa của chương trình sẽ thay đổi: trước đó, nó đã in một cái gì đó lên màn hình, sau khi nó không. I / O không tham chiếu minh bạch.

Nguyên tắc đơn giản là: nếu bạn có thể thay thế bất kỳ biểu thức, biểu thức con hoặc lệnh gọi chương trình con nào bằng giá trị trả về của biểu thức đó, biểu thức con hoặc cuộc gọi chương trình con ở bất cứ đâu trong chương trình, mà không có chương trình thay đổi ý nghĩa của nó, thì bạn có tham chiếu minh bạch. Và điều này có nghĩa là, thực tế mà nói là bạn không thể có bất kỳ I / O nào, không thể có bất kỳ trạng thái đột biến nào, không thể có bất kỳ tác dụng phụ nào. Trong mọi biểu thức, giá trị của biểu thức phải phụ thuộc hoàn toàn vào các giá trị của các bộ phận cấu thành của biểu thức. Và trong mỗi lệnh gọi chương trình con, giá trị trả về phải phụ thuộc hoàn toàn vào các đối số.


4
"Không thể có bất kỳ trạng thái có thể thay đổi nào": Chà, bạn có thể có nó nếu nó bị ẩn và không ảnh hưởng đến hành vi có thể quan sát được của mã của bạn. Hãy suy nghĩ ví dụ về ghi nhớ.
Giorgio

4
@Giorgio: Điều này có lẽ chủ quan, nhưng tôi cho rằng kết quả được lưu trong bộ nhớ cache không thực sự là "trạng thái có thể thay đổi" nếu chúng bị ẩn và không có hiệu ứng có thể quan sát được. Tính không thay đổi luôn là một sự trừu tượng được triển khai trên phần cứng có thể thay đổi; thông thường nó được cung cấp bởi ngôn ngữ (cung cấp sự trừu tượng của "một giá trị" ngay cả khi giá trị có thể di chuyển giữa các thanh ghi và vị trí bộ nhớ trong khi thực thi và có thể biến mất một khi nó được biết sẽ không bao giờ được sử dụng nữa), nhưng nó không kém hiệu lực khi nó được cung cấp bởi một thư viện hoặc whatnot. (Tất nhiên là giả sử nó được triển khai chính xác.)
ruakh

1
+1 Tôi thực sự thích printví dụ này. Có lẽ một cách để thấy điều này, đó là những gì được in trên màn hình là một phần của "giá trị trả lại". Nếu bạn có thể thay thế printbằng giá trị trả về hàm của nó và ghi tương đương trên thiết bị đầu cuối, ví dụ này hoạt động.
Pierre Arlaud

1
@Giorgio Sử dụng không gian / thời gian không thể được coi là tác dụng phụ cho mục đích minh bạch tham chiếu. Điều đó sẽ làm 42 + 2không thể thay thế được vì chúng có thời gian chạy khác nhau và toàn bộ điểm minh bạch tham chiếu là bạn có thể thay thế một biểu thức bằng bất cứ thứ gì nó đánh giá. Việc xem xét quan trọng sẽ là an toàn chủ đề.
Doval

1
@overexchange: Tính minh bạch tham chiếu có nghĩa là bạn có thể thay thế mọi biểu hiện con bằng giá trị của nó mà không thay đổi ý nghĩa của chương trình. listOfSequence.append(n)lợi nhuận None, vì vậy bạn sẽ có thể thay thế tất cả các cuộc gọi đến listOfSequence.append(n)với Nonemà không thay đổi ý nghĩa của chương trình của bạn. Bạn có thể làm điều đó? Nếu không, thì nó không minh bạch.
Jörg W Mittag

1

Các phần của câu trả lời này được lấy trực tiếp từ một hướng dẫn chưa hoàn thành về lập trình chức năng , được lưu trữ trên tài khoản GitHub của tôi:

Một hàm được gọi là trong suốt tham chiếu nếu nó, được đưa ra cùng một tham số đầu vào, luôn tạo ra cùng một đầu ra (giá trị trả về). Nếu một người đang tìm kiếm một nhà tù cho lập trình chức năng thuần túy, tính minh bạch tham chiếu là một ứng cử viên tốt. Khi suy luận với các công thức về đại số, số học và logic, tính chất này - còn được gọi là tính thay thế của đẳng thức cho bằng - rất quan trọng về cơ bản mà nó thường được coi là ...

Hãy xem xét một ví dụ đơn giản:

x = 42

Trong một ngôn ngữ chức năng thuần túy, phía bên trái và bên phải của dấu bằng có thể thay thế cho nhau theo cả hai cách. Đó là, không giống như trong một ngôn ngữ như C, ký hiệu trên thực sự khẳng định sự bình đẳng. Một hậu quả của điều này là chúng ta có thể suy luận về mã chương trình giống như các phương trình toán học.

Từ wiki Haskell :

Các tính toán thuần túy mang lại giá trị như nhau mỗi lần chúng được gọi. Thuộc tính này được gọi là tính minh bạch tham chiếu và có thể tiến hành lập luận tương đương trên mã ...

Để tương phản điều này, loại hoạt động được thực hiện bởi các ngôn ngữ giống như C đôi khi được gọi là một nhiệm vụ phá hủy .

Thuật ngữ thuần túy thường được sử dụng để mô tả một thuộc tính của biểu thức, có liên quan đến cuộc thảo luận này. Đối với một chức năng được coi là thuần túy,

  • nó không được phép thể hiện bất kỳ tác dụng phụ nào, và
  • nó phải được minh bạch tham khảo.

Theo phép ẩn dụ hộp đen, được tìm thấy trong nhiều sách giáo khoa toán học, các hàm bên trong của một hàm hoàn toàn bị tách khỏi thế giới bên ngoài. Tác dụng phụ là khi một chức năng hoặc biểu thức vi phạm nguyên tắc này - nghĩa là, quy trình được phép giao tiếp theo cách nào đó với các đơn vị chương trình khác (ví dụ: để chia sẻ và trao đổi thông tin).

Tóm lại, tính minh bạch tham chiếu là điều bắt buộc để các hàm hoạt động như đúng , các hàm toán học cũng có trong ngữ nghĩa của các ngôn ngữ lập trình.


điều này dường như mở ra với bản sao từng chữ được lấy từ đây : "Một hàm được cho là minh bạch nếu nó, với cùng tham số đầu vào, luôn tạo ra cùng một đầu ra ..." Stack Exchange có quy tắc cho đạo văn , là Bạn biết gì về những điều này? "Đạo văn là hành động vô hồn khi sao chép các tác phẩm của người khác, tát tên của bạn vào đó và tự xưng là tác giả gốc ..."
gnat

3
Tôi đã viết trang đó.
yeraisisuser

nếu đây là trường hợp, hãy xem xét làm cho nó trông ít đạo văn hơn - bởi vì độc giả không có cách nào để nói. Bạn có biết làm thế nào để làm điều này tại SE? 1) Bạn tham khảo nguồn gốc, như "Như (tôi đã) viết [here](link to source)..." theo sau là 2) định dạng trích dẫn thích hợp (sử dụng dấu ngoặc kép hoặc tốt hơn là > ký hiệu cho điều đó). Sẽ không đau nếu ngoài việc đưa ra hướng dẫn chung, hãy trả lời các câu hỏi cụ thể được hỏi về, trong trường hợp này là về f(x)+f(x)/ 2*f(x), hãy xem Cách trả lời - nếu không, có vẻ như bạn chỉ đang quảng cáo trang của mình
gnat

1
Về mặt lý thuyết, tôi hiểu câu trả lời này. Nhưng, thực tế tuân theo các quy tắc này, tôi cần phải trả về danh sách chuỗi hailstone trong chương trình này . Làm thế nào để tôi làm điều này?
trao đổi quá mức
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.