Tác dụng phụ Phá vỡ tính minh bạch tham chiếu


11

Lập trình chức năng trong Scala giải thích tác động của tác dụng phụ trong việc phá vỡ tính minh bạch của tham chiếu:

tác dụng phụ, trong đó ngụ ý một số vi phạm minh bạch tham chiếu.

Tôi đã đọc một phần của SICP , trong đó thảo luận về việc sử dụng mô hình thay thế của LĐNH để đánh giá một chương trình.

Vì tôi gần như hiểu mô hình thay thế với độ trong suốt tham chiếu (RT), bạn có thể hủy cấu trúc một hàm thành các phần đơn giản nhất của nó. Nếu biểu thức là RT, thì bạn có thể hủy biểu thức và luôn nhận được kết quả tương tự.

Tuy nhiên, như các trích dẫn ở trên, sử dụng các tác dụng phụ có thể / sẽ phá vỡ mô hình thay thế.

Thí dụ:

val x = foo(50) + bar(10)

Nếu foobar không có tác dụng phụ, thì việc thực thi một trong hai chức năng sẽ luôn trả lại kết quả tương tự x. Nhưng, nếu chúng có tác dụng phụ, chúng sẽ thay đổi một biến làm gián đoạn / ném cờ lê vào mô hình thay thế.

Tôi cảm thấy thoải mái với lời giải thích này, nhưng tôi không hoàn toàn mò mẫm nó.

Vui lòng sửa cho tôi và điền vào bất kỳ lỗ hổng nào liên quan đến các tác dụng phụ phá vỡ RT, thảo luận về các hiệu ứng trên mô hình thay thế là tốt.

Câu trả lời:


20

Hãy bắt đầu với một định nghĩa cho tính minh bạch tham chiếu :

Một biểu thức được cho là minh bạch tham chiếu nếu nó có thể được thay thế bằng giá trị của nó mà không thay đổi hành vi của một chương trình (nói cách khác, mang lại một chương trình có cùng hiệu ứng và đầu ra trên cùng một đầu vào).

Điều đó có nghĩa là (ví dụ) bạn có thể thay thế 2 + 5 bằng 7 trong bất kỳ phần nào của chương trình và chương trình vẫn hoạt động. Quá trình này được gọi là sự thay thế. Thay thế là hợp lệ nếu và chỉ khi, 2 + 5 có thể được thay thế bằng 7 mà không ảnh hưởng đến bất kỳ phần nào khác của chương trình .

Hãy nói rằng tôi có một lớp được gọi Baz, với các hàm FooBartrong đó. Để đơn giản, chúng tôi sẽ chỉ nói điều đó FooBarcả hai trả về giá trị được truyền vào. Vì vậy Foo(2) + Bar(5) == 7, như bạn mong đợi. Tính minh bạch tham chiếu đảm bảo rằng bạn có thể thay thế biểu thức Foo(2) + Bar(5)bằng biểu thức 7ở bất kỳ đâu trong chương trình của bạn và chương trình sẽ vẫn hoạt động giống hệt nhau.

Nhưng điều gì sẽ xảy ra nếu Footrả về giá trị được truyền vào, nhưng Bartrả lại giá trị được truyền vào, cộng với giá trị cuối cùng được cung cấp cho Foo? Điều đó đủ dễ để làm nếu bạn lưu trữ giá trị của Foomột biến cục bộ trong Bazlớp. Chà, nếu giá trị ban đầu của biến cục bộ đó là 0, biểu thức Foo(2) + Bar(5)sẽ trả về giá trị mong đợi của 7lần đầu tiên bạn gọi nó, nhưng nó sẽ trả về 9lần thứ hai bạn gọi nó.

Điều này vi phạm minh bạch tham chiếu hai cách. Đầu tiên, Bar không thể được tính để trả về cùng một biểu thức mỗi lần nó được gọi. Thứ hai, một hiệu ứng phụ đã xảy ra, cụ thể là việc gọi Foo ảnh hưởng đến giá trị trả về của Bar. Vì bạn không còn có thể đảm bảo Foo(2) + Bar(5)sẽ bằng 7, nên bạn không thể thay thế được nữa.

Đây là ý nghĩa của tính minh bạch tham chiếu trong thực tế; một hàm trong suốt tham chiếu chấp nhận một số giá trị và trả về một số giá trị tương ứng, mà không ảnh hưởng đến mã khác ở nơi khác trong chương trình và luôn trả về cùng một đầu ra cho cùng một đầu vào.


5
Vì vậy, phá vỡ RTkhông cho phép bạn sử dụng substitution model.Vấn đề lớn với việc không thể sử dụng substitution modellà sức mạnh của việc sử dụng nó để lý do về một chương trình?
Kevin Meredith

Điều đó hoàn toàn chính xác.
Robert Harvey

1
+1 câu trả lời tuyệt vời rõ ràng và dễ hiểu. Cảm ơn bạn.
Racheet

2
Ngoài ra, nếu các chức năng đó trong suốt hoặc "thuần túy" thì thứ tự chúng thực sự chạy không quan trọng, chúng tôi không quan tâm nếu foo () hoặc bar () chạy trước và trong một số trường hợp chúng có thể không bao giờ đánh giá nếu không cần thiết
Zachary K

1
Tuy nhiên, một ưu điểm khác của RT là các biểu thức minh bạch tham chiếu đắt tiền có thể được lưu trong bộ nhớ cache (vì việc đánh giá chúng một hoặc hai lần sẽ tạo ra kết quả chính xác như nhau).
dcastro

3

Hãy tưởng tượng rằng bạn đang cố gắng xây dựng một bức tường và bạn đã được cung cấp một loại hộp với kích thước và hình dạng khác nhau. Bạn cần lấp đầy một lỗ hình chữ L cụ thể trên tường; Bạn nên tìm một hộp hình chữ L hoặc bạn có thể thay thế hai hộp thẳng có kích thước phù hợp?

Trong thế giới chức năng, câu trả lời là một trong hai giải pháp sẽ hoạt động. Khi xây dựng thế giới chức năng của bạn, bạn không bao giờ phải mở các hộp để xem những gì bên trong.

Trong thế giới bắt buộc, thật nguy hiểm khi xây dựng bức tường của bạn mà không kiểm tra nội dung của mỗi hộp so sánh chúng với nội dung của mọi hộp khác:

  • Một số có chứa nam châm mạnh và sẽ đẩy các hộp từ khác ra khỏi tường nếu căn chỉnh không đúng cách.
  • Một số rất nóng hoặc lạnh và sẽ phản ứng xấu nếu được đặt trong không gian liền kề.

Tôi nghĩ rằng tôi sẽ dừng lại trước khi tôi lãng phí thời gian của bạn với những ẩn dụ khó xảy ra hơn, nhưng tôi hy vọng vấn đề được đưa ra; gạch chức năng không chứa những bất ngờ tiềm ẩn và hoàn toàn có thể dự đoán được. Bởi vì bạn luôn có thể sử dụng các khối nhỏ hơn có kích thước và hình dạng phù hợp để thay thế cho một khối lớn hơn và không có sự khác biệt giữa hai hộp có cùng kích thước và hình dạng, bạn có độ trong suốt tham chiếu. Với những viên gạch bắt buộc, không đủ để có một cái gì đó có kích thước và hình dạng phù hợp - bạn phải biết gạch được xây dựng như thế nào. Không tham chiếu minh bạch.

Trong một ngôn ngữ chức năng thuần túy, tất cả những gì bạn cần thấy là chữ ký của hàm để biết nó làm gì. Tất nhiên, bạn có thể muốn nhìn vào bên trong để xem nó hoạt động tốt như thế nào, nhưng bạn không cần phải nhìn.

Trong một ngôn ngữ bắt buộc, bạn không bao giờ biết những gì bất ngờ có thể ẩn giấu bên trong.


"Trong một ngôn ngữ chức năng thuần túy, tất cả những gì bạn cần thấy là chữ ký của hàm để biết nó làm gì." - Điều đó thường không đúng. Đúng, theo giả định của đa hình tham số, chúng ta có thể kết luận rằng một hàm kiểu (a, b) -> achỉ có thể là fsthàm và hàm của kiểu a -> achỉ có thể là identityhàm, nhưng bạn không nhất thiết có thể nói bất cứ điều gì về hàm kiểu (a, a) -> a, chẳng hạn.
Jörg W Mittag

2

Khi tôi gần như hiểu mô hình thay thế (với độ trong suốt tham chiếu (RT)), bạn có thể hủy cấu trúc một hàm thành các phần đơn giản nhất của nó. Nếu biểu thức là RT, thì bạn có thể hủy biểu thức và luôn nhận được kết quả tương tự.

Vâng, trực giác hoàn toàn đúng. Dưới đây là một vài gợi ý để có được chính xác hơn:

Giống như bạn đã nói, bất kỳ biểu thức RT nào cũng phải có single"kết quả". Đó là, được đưa ra một factorial(5)biểu thức trong chương trình, nó sẽ luôn mang lại cùng một "kết quả". Vì vậy, nếu một số nhất định factorial(5)có trong chương trình và nó mang lại 120, thì nó sẽ luôn mang lại 120 bất kể "thứ tự bước" nào được mở rộng / tính toán - bất kể thời gian .

Ví dụ: factorialhàm.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Có một vài cân nhắc với lời giải thích này.

Trước hết, hãy ghi nhớ các mô hình đánh giá khác nhau (xem thứ tự áp dụng so với bình thường) có thể mang lại "kết quả" khác nhau cho cùng một biểu thức RT.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

Trong đoạn mã trên, firstsecondđược minh bạch tham chiếu, tuy nhiên, biểu thức ở cuối mang lại "kết quả" khác nhau nếu được đánh giá theo thứ tự thông thường và thứ tự áp dụng (theo biểu thức sau, biểu thức không dừng lại).

.... dẫn đến việc sử dụng "kết quả" trong dấu ngoặc kép. Vì nó không bắt buộc phải có biểu thức để dừng lại, nên nó có thể không tạo ra giá trị. Vì vậy, sử dụng "kết quả" là loại mờ. Người ta có thể nói một biểu thức RT luôn mang lại kết quả tương tự computationstheo mô hình đánh giá.

Thứ ba, có thể phải thấy hai người foo(50)xuất hiện trong chương trình ở các địa điểm khác nhau dưới dạng các biểu thức khác nhau - mỗi người cho kết quả riêng có thể khác nhau. Chẳng hạn, nếu ngôn ngữ cho phép phạm vi động, cả hai biểu thức, mặc dù giống nhau về mặt từ vựng, đều khác nhau. Trong perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

Phạm vi động đánh lừa bởi vì nó giúp người ta dễ dàng nghĩ xlà đầu vào duy nhất foo, trong thực tế, nó là xy. Một cách để thấy sự khác biệt là chuyển đổi chương trình thành một chương trình tương đương không có phạm vi động - nghĩa là truyền các tham số một cách rõ ràng, vì vậy thay vì xác định foo(x), chúng tôi xác định foo(x, y)và chuyển ymột cách rõ ràng trong các trình gọi.

Vấn đề là, chúng ta luôn ở trong một functiontư duy: đưa ra một đầu vào nhất định cho một biểu thức, chúng ta được cung cấp một "kết quả" tương ứng. Nếu chúng ta đưa ra cùng một đầu vào, chúng ta sẽ luôn mong đợi cùng một "kết quả".

Bây giờ, những gì về mã sau đây?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

Các foothủ tục phá vỡ RT vì có định nghĩa lại. Nghĩa là, chúng ta định nghĩa ytrong một thời điểm, và sau trên, xác định lại rằng cùng y . Trong ví dụ perl ở trên, ys là các ràng buộc khác nhau mặc dù chúng có cùng tên chữ "y". Ở đây các ys thực sự giống nhau. Đó là lý do tại sao chúng tôi nói (tái) gán là một hoạt động meta : trên thực tế bạn đang thay đổi định nghĩa về chương trình của bạn.

Một cách thô bạo, mọi người thường mô tả sự khác biệt như sau: trong cài đặt miễn phí hiệu ứng phụ, bạn có một ánh xạ từ input -> output. Trong cài đặt "bắt buộc", bạn có input -> ouputbối cảnh statecó thể thay đổi theo thời gian.

Bây giờ, thay vì chỉ thay thế các biểu thức cho các giá trị tương ứng của chúng, người ta cũng phải áp dụng các phép biến đổi cho statemỗi thao tác yêu cầu nó (và tất nhiên, các biểu thức có thể tham khảo ý kiến ​​tương tự stateđể thực hiện tính toán).

Vì vậy, nếu trong một chương trình miễn phí có hiệu lực phụ, tất cả những gì chúng ta cần biết để tính toán một biểu thức là đầu vào riêng lẻ của nó, thì trong một chương trình bắt buộc, chúng ta cần biết các đầu vào và toàn bộ trạng thái, cho từng bước tính toán. Lý luận là người đầu tiên chịu một cú đánh lớn (bây giờ, để gỡ lỗi một thủ tục có vấn đề, bạn cần đầu vào kết xuất lõi). Một số thủ thuật được đưa ra không thực tế, như ghi nhớ. Nhưng đồng thời, sự đồng thời và song song trở nên thách thức hơn nhiều.


1
Rất vui khi bạn đề cập đến việc ghi nhớ. Điều này có thể được sử dụng như một ví dụ về trạng thái bên trong không thể nhìn thấy bên ngoài: một chức năng sử dụng ghi nhớ vẫn trong suốt về mặt tham chiếu mặc dù bên trong nó sử dụng trạng thái và đột biến.
Giorgio
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.