Tại sao dễ dàng hơn để lý luận về các ngôn ngữ lập trình và chương trình không có tác dụng phụ?


8

Tôi đọc "Tại sao của Y" từ Richard P. Gabriel . Đây là một bài viết dễ đọc về tổ hợp Y, khá hiếm. Bài viết bắt đầu với định nghĩa đệ quy của hàm giai thừa:

(letrec ((f (lambda (n)
              (if (< n 2) 1 (* n (f (- n 1)))))))
  (f 10))

Và giải thích rằng letreccó thể được xác định với một tác dụng phụ:

(let ((f #f))
  (set! f (lambda (n)
            (if (< n 2) 1 (* n (f (- n 1))))))
  (f 10))

Và phần còn lại của bài viết mô tả, cũng có thể định nghĩa letrecvới bộ kết hợp Y:

(define (Y f)
  (let ((g (lambda (h)
             (lambda (x)
               ((f (h h)) x)))))
    (g g)))

(let ((f (Y (lambda (fact)
              (lambda (n)
                (if (< n 2) 1 (* n (fact (- n 1)))))))))
  (f 10))

Rõ ràng điều này phức tạp hơn nhiều so với phiên bản có tác dụng phụ. Lý do tại sao có lợi khi thích kết hợp Y hơn tác dụng phụ được đưa ra chỉ bằng tuyên bố:

Lý do dễ dàng hơn về các ngôn ngữ lập trình và chương trình không có tác dụng phụ.

Điều này không được giải thích thêm. Tôi cố gắng tìm một lời giải thích.


Dòng "dễ lý luận hơn" đó là tuyên truyền thuần túy. Nó luôn được đưa ra như một bài viết về đức tin - không cần bằng chứng cũng không được cung cấp - và khi được phân tích nghiêm túc, nó thậm chí không vượt qua bài kiểm tra gây cười. Như bạn đã lưu ý, rõ ràng là phiên bản Y Combinator phức tạp hơn gấp đôi, và do đó khó hiểu và lý do hơn!
Mason Wheeler

5
@MasonWheeler Việc truyền một đối tượng có thể thay đổi sang một số phương thức khiến cho việc xác định nơi nào nó được sử dụng hoàn toàn làm đầu vào và nơi bị biến đổi tại chỗ. Sự thay thế chức năng - trả về một bản sao mới của đối tượng - làm cho nó rõ ràng. Tôi sẽ không nói rằng thuần túy luôn tốt hơn, nhưng thật khó để khẳng định rằng các đồ thị lớn của các đối tượng có thể thay đổi rất dễ lý do. Có quá nhiều bối cảnh vô hình liên quan.
Doval

@Doval Làm thế nào là "rõ ràng" khi bạn có nhiều bản sao của các đối tượng của bạn chạy xung quanh, một số trong đó đã lỗi thời, một số khác là kinh điển, và bây giờ bạn phải giữ thẳng điều đó? Nghe còn khó hiểu hơn nữa! (Hoặc cách khác, bạn phải đảm bảo rằng có không có tham chiếu đến bất kỳ bản sao thứ hai, đó là một nhiệm vụ chính xác tương đương với quản lý bộ nhớ sử dụng, mà FP tìm thấy soooooo khó hiểu và khó có thể lý do về điều đó nó đã phát minh thu gom rác thải để tránh sự cần thiết để làm như vậy!)
Mason Wheeler

2
@MasonWheeler Ngay cả khi dữ liệu được cho là thay đổi, bạn muốn kiểm soát việc ai sẽ thay đổi nó. Bạn muốn truyền nó cho một phương thức không được phép làm thay đổi nó, nhưng ai đó có thể làm hỏng và đưa ra một lỗi kết thúc bằng cách biến đổi dữ liệu. Sau đó, bạn kết thúc việc tạo ra "bản sao phòng thủ" (thực sự là một khuyến nghị trong sách Java hiệu quả!) Và thực hiện nhiều công việc hơn / tạo ra nhiều rác hơn là sử dụng cấu trúc dữ liệu bất biến ngay từ đầu. Thực tế là dữ liệu sẽ thay đổi không bao giờ cản trở bất cứ ai sử dụng các kiểu chuỗi hoặc số bất biến.
Doval

2
@MasonWheeler Các ngôn ngữ FP không tạo ra nhiều rác, nếu không chúng sẽ vô dụng. Đó không phải là cách họ làm việc "đằng sau hậu trường". "Dễ dàng lý luận về" thường đề cập đến lý luận tương đương, đó không phải là vấn đề gây cười. Lý luận tương đương có thể được thực hiện trong nhiều mô hình, với sự thành công khác nhau, nhưng trong các ngôn ngữ FP thường dễ dàng hơn và đó là một chiến thắng lớn (mặc dù phải trả giá bằng những thứ khác; mọi thứ đều là sự đánh đổi trong cuộc sống).
Andres F.

Câu trả lời:


13

Rõ ràng, bạn có thể tìm thấy các ví dụ về các hàm thuần túy cực kỳ khó đọc, thực hiện các phép tính giống như các hàm có tác dụng phụ dễ đọc hơn nhiều. Đặc biệt là khi bạn sử dụng một phép biến đổi cơ học như tổ hợp Y để đi đến một giải pháp. Đó không phải là những gì có nghĩa là "dễ dàng hơn để lý do."

Lý do dễ dàng hơn để lý giải về các chức năng mà không có tác dụng phụ là bạn chỉ phải quan tâm đến đầu vào và đầu ra. Với các tác dụng phụ, bạn cũng phải lo lắng về số lần các hàm được gọi, chúng được gọi theo thứ tự nào, dữ liệu nào được tạo trong hàm, dữ liệu nào được chia sẻ và dữ liệu nào được sao chép. tất cả thông tin đó cho bất kỳ chức năng nào có thể được gọi là nội bộ cho chức năng bạn đang gọi và đệ quy nội bộ cho các chức năng đó, v.v.

Hiệu ứng này dễ thấy hơn rất nhiều trong mã sản xuất với nhiều lớp so với các hàm ví dụ đồ chơi. Chủ yếu là nó có nghĩa là bạn có thể dựa nhiều hơn vào chữ ký loại của hàm. Bạn thực sự nhận thấy gánh nặng của các tác dụng phụ nếu bạn lập trình chức năng thuần túy trong một thời gian sau đó quay lại với nó.


10

Một tính chất thú vị của các ngôn ngữ không có tác dụng phụ là việc giới thiệu song song, đồng thời hoặc không đồng bộ có thể thay đổi ý nghĩa của chương trình. Nó có thể làm cho nó nhanh hơn. Hoặc nó có thể làm cho nó chậm hơn. Nhưng nó không thể làm cho nó sai.

Điều này làm cho nó tầm thường để tự động song song hóa các chương trình. Thật là tầm thường, trên thực tế, bạn thường kết thúc với quá nhiều song song! Nhóm GHC đã thử nghiệm song song hóa tự động. Họ thấy rằng ngay cả các chương trình đơn giản cũng có thể bị phân tách thành hàng trăm, thậm chí hàng ngàn luồng. Chi phí hoạt động của tất cả các luồng đó sẽ áp đảo bất kỳ khả năng tăng tốc tiềm năng nào theo một số bậc độ lớn.

Vì vậy, để tự động song song hóa các chương trình chức năng, vấn đề trở thành "làm thế nào để bạn nhóm các hoạt động nguyên tử nhỏ lại với nhau thành các kích thước hữu ích của các mảnh song song", trái ngược với các chương trình không tinh khiết, trong đó vấn đề là "làm thế nào để bạn chia nhỏ các hoạt động nguyên khối lớn thành hữu ích kích thước của các mảnh song song ". Điều tốt đẹp về điều này là cái trước có thể được thực hiện theo phương pháp heurist (hãy nhớ rằng: nếu bạn hiểu sai, điều tồi tệ nhất có thể xảy ra là chương trình chạy chậm hơn một chút so với trước đây), trong khi cái sau tương đương với việc giải quyết Dừng Sự cố (trong trường hợp chung) và nếu bạn gặp sự cố, chương trình của bạn sẽ bị sập (nếu bạn may mắn!) Hoặc trả lại kết quả sai một cách tinh tế (trong trường hợp xấu nhất).


nó cũng có thể bế tắc hoặc livelock, không phải là tốt hơn ...
Dedsplator

6

Các ngôn ngữ có tác dụng phụ sử dụng phân tích răng cưa để xem liệu vị trí bộ nhớ có thể cần được tải lại sau khi gọi hàm hay không. Làm thế nào bảo thủ phân tích này là phụ thuộc vào ngôn ngữ.

Đối với C, điều này phải khá bảo thủ, vì ngôn ngữ không phải là loại an toàn.

Đối với Java và C #, chúng không phải bảo thủ vì tính an toàn của loại tăng.

Quá bảo thủ ngăn chặn tối ưu hóa tải.

Phân tích như vậy sẽ không cần thiết (hoặc tầm thường tùy thuộc vào cách bạn nhìn vào nó) trong một ngôn ngữ mà không có tác dụng phụ.


Lưu ý rằng răng cưa chỉ có thể với cả các biến và tham chiếu có thể thay đổi. Một ngôn ngữ chỉ có một hoặc ngôn ngữ khác không có vấn đề này
vườn

4

Luôn luôn tối ưu hóa để tận dụng bất kỳ giả định nào bạn đưa ra. Sắp xếp lại các hoạt động đến với tâm trí.

Một ví dụ thú vị xuất hiện trong tâm trí thực sự xuất hiện trong một số ngôn ngữ lắp ráp cũ. Cụ thể MIPS có một quy tắc rằng lệnh sau khi nhảy có điều kiện được thực thi, bất kể nhánh nào được thực hiện. Nếu bạn không muốn điều này, bạn đặt NOP sau khi nhảy. Điều này đã được thực hiện do cách thức cấu trúc đường ống MIPS. Có một gian hàng 1 chu kỳ tự nhiên được tích hợp trong thực hiện bước nhảy có điều kiện, vì vậy bạn cũng có thể làm điều gì đó hữu ích với chu trình đó!

Trình biên dịch thường sẽ tìm kiếm một hoạt động cần được thực hiện trên cả hai nhánh và trượt nó vào khe đó, để đạt hiệu suất cao hơn một chút. Tuy nhiên, nếu trình biên dịch không thể làm điều đó, nhưng có thể cho thấy rằng không có tác dụng phụ nào đối với hoạt động, trình biên dịch có thể dính cơ hội vào vị trí đó. Do đó, trên một đường dẫn, mã sẽ thực thi một lệnh nhanh hơn. Trên con đường khác, không có tác hại.


1

"letrec có thể được định nghĩa với một tác dụng phụ ..." Tôi thấy không có tác dụng phụ trong định nghĩa của bạn. Đúng, nó sử dụng set!một cách điển hình để tạo ra các tác dụng phụ trong Lược đồ, nhưng trong trường hợp này không có tác dụng phụ - vì fhoàn toàn cục bộ, nó không thể được tham chiếu bởi bất kỳ chức năng nào ngoài địa phương. Do đó, nó không phải là một tác dụng phụ như nhìn thấy từ bất kỳ phạm vi bên ngoài. Những gì mã này làm là hack xung quanh một giới hạn trong phạm vi của Đề án mà theo mặc định không cho phép định nghĩa lambda đề cập đến chính nó.

Một số ngôn ngữ khác có khai báo trong đó một biểu thức được sử dụng để tạo giá trị cho hằng số có thể tham chiếu đến chính hằng đó. Trong một ngôn ngữ như vậy, định nghĩa tương đương chính xác có thể được sử dụng, nhưng rõ ràng điều này không tạo ra hiệu ứng phụ, vì chỉ một hằng số được sử dụng. Xem, ví dụ, chương trình Haskell tương đương này:

let f = \ n -> if n < 2 
                 then 1 
                 else n*(f (n-1)) 
        in (f 5)

(đánh giá tới 120).

Điều này rõ ràng không có tác dụng phụ (vì để một hàm trong Haskell có tác dụng phụ, nó phải trả về kết quả của nó được bọc trong Monad, nhưng kiểu được trả về ở đây là kiểu số đơn giản), nhưng có mã giống hệt về cấu trúc với mã bạn trích dẫn.


Nói chung nó là một tác dụng phụ, bởi vì letcó thể trả về chức năng cục bộ.
ceving

2
@ceving - ngay cả khi đó, nó không phải là một tác dụng phụ, bởi vì việc sửa đổi vị trí lưu trữ bị giới hạn trong thời gian nó có thể xảy ra trong một thời gian trước khi bất kỳ mã nào khác có thể đọc được nó . Để một tác dụng phụ là có thật, một số tác nhân bên ngoài có thể nhận thấy rằng nó đã xảy ra; trong trường hợp này, không có cách nào có thể xảy ra.
Periata Breatta

0

Điều này không được giải thích thêm. Tôi cố gắng tìm một lời giải thích.

Đó là điều gì đó vốn có đối với nhiều người trong chúng ta, những người đã gỡ lỗi các cơ sở mã lớn nhưng bạn phải đối phó với quy mô đủ lớn ở cấp độ giám sát trong một thời gian đủ dài để đánh giá cao nó. Nó giống như hiểu được tầm quan trọng của vị trí trong Poker. Ban đầu, nó dường như không phải là một lợi thế hữu ích để tồn tại vào cuối mỗi lượt chơi cho đến khi bạn ghi lại lịch sử một triệu bàn tay và nhận ra rằng bạn đã giành được nhiều tiền hơn ở vị trí nhiều hơn là ra ngoài.

Điều đó nói rằng, tôi không đồng ý với ý kiến ​​cho rằng thay đổi đối với biến cục bộ là tác dụng phụ. Theo quan điểm của tôi, một chức năng không gây ra tác dụng phụ nếu nó không sửa đổi bất cứ điều gì ngoài phạm vi của nó, rằng bất cứ điều gì nó chạm vào và ngăn chặn sẽ không ảnh hưởng đến bất cứ điều gì bên dưới ngăn xếp cuộc gọi hoặc bất kỳ bộ nhớ hoặc tài nguyên nào mà chức năng không có được .

Nói chung, điều khó nhất để giải thích trong một cơ sở mã phức tạp, quy mô lớn không có quy trình kiểm tra hoàn hảo nhất có thể tưởng tượng là quản lý trạng thái bền bỉ, giống như tất cả các thay đổi đối với các đối tượng dạng hạt trong thế giới trò chơi video khi bạn lội qua hàng chục hàng ngàn chức năng trong khi cố gắng thu hẹp giữa một danh sách vô số nghi phạm mà một người thực sự gây ra bất biến trên toàn hệ thống bị vi phạm ("điều này không bao giờ nên xảy ra, ai đã làm điều đó?"). Nếu không có gì được thay đổi bên ngoài chức năng, thì nó có thể không thể gây ra sự cố trung tâm.

Tất nhiên điều này là không thể làm trong mọi trường hợp. Bất kỳ ứng dụng nào cập nhật cơ sở dữ liệu được lưu trữ trên một máy khác, về bản chất, được thiết kế để gây ra tác dụng phụ, cũng như bất kỳ ứng dụng nào được thiết kế để tải và ghi tệp. Nhưng có rất nhiều thứ chúng ta có thể làm mà không có tác dụng phụ trong nhiều chức năng và nhiều chương trình, ví dụ, nếu chúng ta có một thư viện cấu trúc dữ liệu bất biến và tiếp tục mang tư duy này.

Thật thú vị, John Carmack đã nhảy vào LISP và băng nhóm bất biến mặc dù bắt đầu trong thời kỳ mã hóa C được điều chỉnh vi mô nhất. Tôi đã thấy mình làm một điều tương tự, mặc dù tôi vẫn sử dụng C rất nhiều. Đó là bản chất của những người thực dụng, tôi nghĩ, những người đã dành nhiều năm để gỡ lỗi và cố gắng lý luận về các hệ thống phức tạp, quy mô lớn nói chung từ cấp độ giám sát. Ngay cả những lỗi mạnh mẽ đáng kinh ngạc và không có nhiều lỗi vẫn có thể khiến bạn cảm thấy khó chịu rằng có gì đó không ổn đang ẩn nấp nếu có rất nhiều trạng thái phức tạp liên tục được sửa đổi trong số các biểu đồ chức năng liên kết phức tạp nhất giữa hàng triệu dòng mã. Ngay cả khi mọi giao diện đơn lẻ được kiểm tra với một bài kiểm tra đơn vị và tất cả đều vượt qua, '

Trong thực tế tôi thường thấy lập trình hàm làm cho việc hiểu một hàm khó hơn. Nó vẫn xoay não tôi thành những khúc quanh và nút thắt, đặc biệt là với logic đệ quy phức tạp. Nhưng tất cả các chi phí trí tuệ liên quan đến việc tìm ra một vài chức năng được viết bằng ngôn ngữ chức năng bị lấn át bởi một hệ thống phức tạp với các trạng thái liên tục bị thay đổi qua hàng chục ngàn chức năng, trong đó mỗi chức năng gây ra tác dụng phụ cộng lại sự phức tạp của lý luận về tính đúng đắn của toàn bộ hệ thống.

Lưu ý rằng bạn không cần một ngôn ngữ chức năng thuần túy để làm cho các chức năng tránh các tác dụng phụ. Các trạng thái cục bộ đã thay đổi trong một hàm không được tính là một hiệu ứng phụ, giống như một forbiến đếm vòng lặp cục bộ thành một hàm không được tính là một tác dụng phụ. Tôi thậm chí còn viết mã C ngày nay với mục đích tránh các tác dụng phụ khi có thể và đã tự tạo cho mình một thư viện cấu trúc dữ liệu an toàn, bất biến, có thể được sửa đổi một phần trong khi phần còn lại của dữ liệu bị sao chép và nó đã giúp tôi rất nhiều lý do về tính đúng đắn của hệ thống của tôi. Tôi hoàn toàn không đồng ý với tác giả theo nghĩa đó. Ít nhất là trong C và C ++ trong phần mềm quan trọng, một chức năng có thể được ghi nhận là không có tác dụng phụ nếu nó không chạm vào bất cứ thứ gì có thể ảnh hưởng đến thế giới bên ngoài chức năng.

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.