it-swarm-vi.com

Tại sao tác dụng phụ được coi là xấu trong lập trình chức năng?

Tôi cảm thấy rằng tác dụng phụ là một hiện tượng tự nhiên. Nhưng nó là một cái gì đó giống như điều cấm kỵ trong các ngôn ngữ chức năng. Những lý do là gì?

Câu hỏi của tôi là cụ thể cho phong cách lập trình chức năng. Không phải tất cả các ngôn ngữ lập trình/mô hình.

70
Gulshan

Viết các hàm/phương thức của bạn mà không có tác dụng phụ - vì vậy chúng các hàm thuần túy - giúp dễ dàng lý luận về tính chính xác của chương trình của bạn.

Nó cũng giúp bạn dễ dàng soạn các hàm đó để tạo hành vi mới.

Nó cũng làm cho các tối ưu hóa nhất định có thể, trong đó trình biên dịch có thể ghi nhớ các kết quả của các hàm hoặc sử dụng Loại bỏ Subexpression chung.

Chỉnh sửa: theo yêu cầu của Stewol: Bởi vì rất nhiều trạng thái của bạn được lưu trữ trong ngăn xếp (luồng dữ liệu, không phải luồng điều khiển, như Jonas đã gọi nó ở đây ), bạn có thể song song hoặc sắp xếp lại việc thực hiện các phần đó tính toán của bạn độc lập với nhau. Bạn có thể dễ dàng tìm thấy những phần độc lập đó vì một phần không cung cấp đầu vào cho phần kia.

Trong các môi trường có trình gỡ lỗi cho phép bạn khôi phục ngăn xếp và tiếp tục tính toán (như Smalltalk), có các hàm thuần có nghĩa là bạn có thể dễ dàng thấy giá trị thay đổi như thế nào, bởi vì các trạng thái trước đó có sẵn để kiểm tra. Trong một phép tính nặng đột biến, trừ khi bạn thêm rõ ràng các hành động do/hoàn tác vào cấu trúc hoặc thuật toán của mình, bạn không thể xem lịch sử tính toán. (Điều này liên kết trở lại đoạn đầu tiên: viết các hàm thuần túy giúp dễ dàng hơn kiểm tra tính chính xác của chương trình của bạn.)

73
Frank Shearar

Bạn đã hiểu sai, lập trình chức năng thúc đẩy hạn chế tác dụng phụ để làm cho chương trình dễ hiểu và tối ưu hóa. Thậm chí Haskell cho phép bạn ghi vào tập tin.

Về cơ bản điều tôi đang nói là các lập trình viên chức năng không nghĩ tác dụng phụ là xấu, họ chỉ nghĩ đơn giản là hạn chế sử dụng tác dụng phụ là tốt. Tôi biết nó có vẻ như là một sự phân biệt đơn giản như vậy nhưng nó làm cho tất cả sự khác biệt.

24
ChaosPandion

Từ một bài viết về Lập trình hàm :

Trong thực tế, các ứng dụng cần phải có một số tác dụng phụ. Simon Peyton-Jones, người đóng góp chính cho ngôn ngữ lập trình chức năng Haskell, nói như sau: "Cuối cùng, bất kỳ chương trình nào cũng phải thao túng trạng thái. Một chương trình không có tác dụng phụ nào là một loại hộp đen. Tất cả những gì bạn có thể nói là cái hộp trở nên nóng hơn. " ( http://oscon.blip.tv/file/324976 ) Điều quan trọng là hạn chế tác dụng phụ, xác định rõ chúng và tránh phân tán chúng trong toàn bộ mã.

23
Peter Stuifzand

Một vài lưu ý:

  • Các chức năng không có tác dụng phụ có thể được thực hiện song song, trong khi các chức năng có tác dụng phụ thường yêu cầu một số loại đồng bộ hóa.

  • Các chức năng không có tác dụng phụ cho phép tối ưu hóa mạnh mẽ hơn (ví dụ bằng cách sử dụng bộ đệm kết quả một cách minh bạch), vì miễn là chúng tôi nhận được kết quả đúng, thậm chí không quan trọng liệu chức năng đó có thực sự Thực thi

13
user281377

Bây giờ tôi chủ yếu làm việc trong mã chức năng, và từ quan điểm đó, nó dường như rõ ràng. Tác dụng phụ tạo ra một rất lớn gánh nặng tinh thần đối với các lập trình viên cố gắng đọc và hiểu mã. Bạn không nhận thấy gánh nặng đó cho đến khi bạn thoát khỏi nó trong một thời gian, sau đó đột nhiên phải đọc lại mã với các tác dụng phụ.

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

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

Trong một ngôn ngữ chức năng, tôi biết rằng foo vẫn là 42. Tôi thậm chí không phải nhìn vào mã ở giữa, ít hiểu nó hơn hoặc nhìn vào việc triển khai các hàm mà nó gọi.

Tất cả những thứ về đồng thời và song song hóa và tối ưu hóa đều tốt đẹp, nhưng đó là những gì các nhà khoa học máy tính đưa vào tập tài liệu. Không phải tự hỏi ai đang biến đổi biến của bạn và khi nào là điều tôi thực sự thích trong thực hành hàng ngày.

11
Karl Bielefeldt

Rất ít ngôn ngữ làm cho nó không thể gây ra tác dụng phụ. Các ngôn ngữ hoàn toàn không có tác dụng phụ sẽ rất khó sử dụng (gần như không thể), ngoại trừ trong một khả năng rất hạn chế.

Tại sao tác dụng phụ được coi là xấu xa?

Bởi vì họ làm cho nó khó khăn hơn nhiều để suy luận về chính xác những gì một chương trình làm, và để chứng minh rằng nó làm những gì bạn mong đợi nó làm.

Ở mức rất cao, hãy tưởng tượng thử nghiệm toàn bộ trang web 3 tầng chỉ với thử nghiệm hộp đen. Chắc chắn, nó có thể thực hiện được, tùy thuộc vào quy mô. Nhưng chắc chắn có rất nhiều sự trùng lặp đang diễn ra. Và nếu có is một lỗi (có liên quan đến tác dụng phụ), thì bạn có thể phá vỡ toàn bộ hệ thống để kiểm tra thêm, cho đến khi lỗi được chẩn đoán và sửa chữa, và sửa lỗi là Triển khai vào môi trường thử nghiệm.

Lợi ích

Bây giờ, quy mô xuống. Nếu bạn khá giỏi trong việc viết mã miễn phí hiệu ứng phụ, bạn sẽ nhanh hơn bao nhiêu để suy luận về những gì mã hiện có đã làm? Bạn có thể viết bài kiểm tra đơn vị nhanh hơn bao nhiêu? Bạn tự tin đến mức nào mà mã không có tác dụng phụ được đảm bảo không có lỗi và người dùng có thể hạn chế tiếp xúc với bất kỳ lỗi nào mà nó đã có?

Nếu mã không có tác dụng phụ, trình biên dịch cũng có thể có các tối ưu hóa bổ sung mà nó có thể thực hiện. Nó có thể dễ dàng hơn nhiều để thực hiện những tối ưu hóa. Có thể dễ dàng hơn nhiều để thậm chí khái niệm hóa một tối ưu hóa cho mã miễn phí hiệu ứng phụ, điều đó có nghĩa là nhà cung cấp trình biên dịch của bạn có thể thực hiện các tối ưu hóa khó có thể xảy ra trong mã với các tác dụng phụ.

Đồng thời cũng đơn giản hơn để thực hiện, để tự động tạo và tối ưu hóa khi mã không có tác dụng phụ. Điều này là do tất cả các mảnh có thể được đánh giá một cách an toàn theo bất kỳ thứ tự nào. Cho phép các lập trình viên viết mã đồng thời cao được coi là thách thức lớn tiếp theo mà Khoa học Máy tính cần phải giải quyết, và một trong số ít các rào cản còn lại chống lại Định luật Moore .

6
Merlyn Morgan-Graham

Các tác dụng phụ giống như "rò rỉ" trong mã của bạn sẽ cần được xử lý sau, bởi bạn hoặc một số đồng nghiệp không nghi ngờ.

Các ngôn ngữ chức năng tránh các biến trạng thái và dữ liệu có thể thay đổi như một cách làm cho mã ít phụ thuộc vào ngữ cảnh và nhiều mô đun hơn. Modularity đảm bảo rằng công việc của một nhà phát triển sẽ không ảnh hưởng/làm suy yếu công việc của người khác.

Tỷ lệ phát triển tỷ lệ với quy mô nhóm, là "chén thánh" của phát triển phần mềm ngày nay. Khi làm việc với các lập trình viên khác, một vài thứ quan trọng như mô đun. Ngay cả những tác dụng phụ đơn giản nhất của logic cũng khiến việc cộng tác trở nên vô cùng khó khăn.

4
Ami

Chà, IMHO, điều này khá đạo đức giả. Không ai thích tác dụng phụ, nhưng mọi người đều cần chúng.

Điều nguy hiểm về tác dụng phụ là nếu bạn gọi một chức năng, thì điều này có thể có ảnh hưởng không chỉ đến cách hành xử của chức năng khi nó được gọi lần sau, mà có thể nó có tác dụng này đối với các chức năng khác. Do đó, tác dụng phụ giới thiệu hành vi không thể đoán trước và phụ thuộc không cần thiết.

Các mô hình lập trình như OO và cả chức năng đều giải quyết vấn đề này. OO giảm vấn đề bằng cách áp đặt một mối quan tâm. Điều này có nghĩa là trạng thái ứng dụng, bao gồm Rất nhiều dữ liệu có thể thay đổi, được gói gọn vào các đối tượng, mỗi đối tượng chỉ chịu trách nhiệm duy trì trạng thái của chính nó. Bằng cách này, nguy cơ phụ thuộc được giảm xuống và các vấn đề bị cô lập hơn và dễ theo dõi hơn.

Lập trình hàm có một cách tiếp cận triệt để hơn nhiều, trong đó trạng thái ứng dụng đơn giản là bất biến theo quan điểm của lập trình viên. Đây là một ý tưởng hay, nhưng tự nó làm cho ngôn ngữ trở nên vô dụng. Tại sao? Bởi vì BẤT K I hoạt động I/O nào cũng có tác dụng phụ. Ngay khi bạn đọc từ bất kỳ luồng đầu vào nào, trạng thái ứng dụng của bạn có thể sẽ thay đổi, vì lần sau khi bạn gọi cùng chức năng, kết quả có thể sẽ khác. Bạn có thể đang đọc dữ liệu khác nhau, hoặc - cũng có thể - hoạt động có thể thất bại. Điều này cũng đúng với đầu ra. Ngay cả đầu ra là một hoạt động với các tác dụng phụ. Điều này không có gì bạn nhận ra thường xuyên hiện nay, nhưng hãy tưởng tượng bạn chỉ có 20K cho đầu ra của mình và nếu bạn xuất ra nữa, ứng dụng của bạn gặp sự cố vì bạn hết dung lượng đĩa hoặc bất cứ điều gì.

Vì vậy, có, tác dụng phụ là khó chịu và nguy hiểm từ quan điểm của một lập trình viên. Hầu hết các lỗi đến từ cách một số phần nhất định của trạng thái ứng dụng được lồng vào nhau theo cách gần như tối nghĩa, thông qua các tác dụng phụ không cần thiết và thường xuyên. Từ quan điểm của người dùng, tác dụng phụ là điểm sử dụng máy tính. Họ không quan tâm đến những gì xảy ra bên trong hoặc cách thức tổ chức. Họ làm một cái gì đó và mong đợi máy tính THAY ĐỔI tương ứng.

4
back2dos

Bất kỳ tác dụng phụ nào cũng đưa ra các tham số đầu vào/đầu ra bổ sung phải được tính đến khi kiểm tra.

Điều này làm cho việc xác thực mã phức tạp hơn nhiều vì môi trường không thể chỉ giới hạn ở mã được xác thực, mà phải mang lại một số hoặc tất cả môi trường xung quanh (toàn cầu được cập nhật trong mã đó, do đó phụ thuộc vào điều đó mã, lần lượt phụ thuộc vào việc sống bên trong một máy chủ đầy đủ Java EE ....)

Bằng cách cố gắng tránh các tác dụng phụ, bạn hạn chế số lượng ngoại ứng cần thiết để chạy mã.

2
user1249

Theo kinh nghiệm của tôi, thiết kế tốt trong lập trình hướng đối tượng bắt buộc sử dụng các hàm có tác dụng phụ.

Ví dụ, lấy một ứng dụng UI cơ bản. Tôi có thể có một chương trình đang chạy với một đồ thị đối tượng đại diện cho trạng thái hiện tại của mô hình miền của chương trình của tôi. Các thông báo đến các đối tượng trong biểu đồ đó (ví dụ, thông qua các cuộc gọi phương thức được gọi từ bộ điều khiển lớp UI). Biểu đồ đối tượng (mô hình miền) trên heap được sửa đổi để đáp ứng với các thông báo. Người quan sát mô hình được thông báo về bất kỳ thay đổi nào, UI và có thể các tài nguyên khác được sửa đổi.

Khác xa với sự sắp xếp chính xác của các hiệu ứng phụ sửa đổi heap và sửa đổi màn hình này là cốt lõi của OO (trong trường hợp này là mẫu MVC).

Tất nhiên, điều đó không có nghĩa là các phương pháp của bạn nên có tác dụng phụ độc lập. Và các chức năng miễn phí có tác dụng phụ có một vị trí trong việc cải thiện khả năng đọc và đôi khi hiệu suất của mã của bạn.

1
flamingpenguin

Như các câu hỏi ở trên đã chỉ ra, các ngôn ngữ chức năng không quá nhiều ngăn mã không có tác dụng phụ như cung cấp cho chúng tôi các công cụ để quản lý tác dụng phụ nào có thể xảy ra trong một đoạn mã nhất định và khi nào.

Điều này hóa ra có những hậu quả rất thú vị. Đầu tiên, và rõ ràng nhất, có rất nhiều điều bạn có thể làm với mã miễn phí có hiệu lực phụ, đã được mô tả. Nhưng cũng có những thứ khác chúng ta có thể làm, ngay cả khi làm việc với mã có tác dụng phụ:

  • Trong mã có trạng thái có thể thay đổi, chúng ta có thể quản lý phạm vi của trạng thái theo cách để đảm bảo tĩnh rằng nó không thể rò rỉ bên ngoài một chức năng nhất định, điều này cho phép chúng ta thu thập rác mà không cần sơ đồ kiểu đếm tham chiếu hoặc quét và quét , nhưng vẫn chắc chắn rằng không có tài liệu tham khảo tồn tại. Các đảm bảo tương tự cũng hữu ích cho việc duy trì thông tin nhạy cảm về quyền riêng tư, v.v. (Điều này có thể đạt được bằng cách sử dụng đơn vị ST trong haskell)
  • Khi sửa đổi trạng thái chia sẻ trong nhiều luồng, chúng ta có thể tránh sự cần thiết của khóa bằng cách theo dõi các thay đổi và thực hiện cập nhật nguyên tử khi kết thúc giao dịch hoặc quay lại giao dịch và lặp lại nếu một luồng khác thực hiện sửa đổi xung đột. Điều này chỉ có thể đạt được bởi vì chúng tôi có thể đảm bảo rằng mã không có hiệu ứng nào ngoài các sửa đổi trạng thái (mà chúng tôi có thể vui vẻ từ bỏ). Điều này được thực hiện bởi đơn vị STM (Bộ nhớ giao dịch phần mềm) trong Haskell.
  • chúng ta có thể theo dõi các tác động của mã và hộp cát tầm thường, lọc mọi hiệu ứng có thể cần thực hiện để đảm bảo an toàn, do đó cho phép (ví dụ) người dùng nhập mã được thực thi an toàn trên trang web
0
Jules

Cái ác là một chút trên đầu trang .. tất cả phụ thuộc vào bối cảnh sử dụng ngôn ngữ.

Một xem xét khác cho những người đã được đề cập là nó làm cho bằng chứng về tính chính xác của một chương trình đơn giản hơn nhiều nếu không có tác dụng phụ chức năng.

0
Ilan

Trong các cơ sở mã phức tạp, các tương tác phức tạp của các tác dụng phụ là điều khó khăn nhất mà tôi tìm thấy để giải thích. Tôi chỉ có thể nói cá nhân theo cách mà bộ não của tôi hoạt động. Tác dụng phụ và trạng thái dai dẳng và đột biến đầu vào, v.v khiến tôi phải suy nghĩ về "khi nào" và "nơi" xảy ra lý do về tính chính xác, không chỉ là "những gì" đang xảy ra trong từng chức năng riêng lẻ.

Tôi không thể chỉ tập trung vào "cái gì". Tôi không thể kết luận sau khi kiểm tra kỹ lưỡng một chức năng gây ra tác dụng phụ rằng nó sẽ lan truyền không khí đáng tin cậy trong toàn bộ mã sử dụng nó, vì người gọi vẫn có thể sử dụng sai nó bằng cách gọi sai thời điểm, từ sai chủ đề, sai đặt hàng. Trong khi đó, một chức năng không gây ra tác dụng phụ và chỉ trả về một đầu ra mới được cung cấp một đầu vào (không chạm vào đầu vào) là không thể sử dụng sai theo cách này.

Nhưng tôi là một loại người thực dụng, tôi nghĩ, hoặc ít nhất là cố gắng, và tôi không nghĩ rằng chúng ta nhất thiết phải dập tắt tất cả các tác dụng phụ đến mức tối thiểu nhất để suy luận về tính chính xác của mã của chúng tôi (ít nhất là Tôi sẽ thấy điều này rất khó thực hiện trong các ngôn ngữ như C). Nơi tôi thấy rất khó để suy luận về tính đúng đắn là khi chúng ta có sự kết hợp giữa các luồng kiểm soát phức tạp và các tác dụng phụ.

Các luồng điều khiển phức tạp đối với tôi là những luồng giống như biểu đồ, thường là đệ quy hoặc giống đệ quy (hàng đợi sự kiện, ví dụ, không gọi trực tiếp các sự kiện theo cách đệ quy nhưng về bản chất là "giống như đệ quy"), có thể thực hiện mọi việc trong quá trình duyệt qua cấu trúc biểu đồ được liên kết thực tế hoặc xử lý hàng đợi sự kiện không đồng nhất có chứa hỗn hợp các sự kiện chiết trung để xử lý chúng ta đến tất cả các loại phần khác nhau của codebase và tất cả đều gây ra các tác dụng phụ khác nhau. Nếu bạn đã cố gắng rút ra tất cả các địa điểm cuối cùng bạn sẽ có mã, nó sẽ giống như một biểu đồ phức tạp và có khả năng với các nút trong biểu đồ mà bạn không bao giờ mong đợi sẽ ở đó trong thời điểm đó và cho rằng tất cả chúng đều ở đó gây ra tác dụng phụ, điều đó có nghĩa là bạn có thể không chỉ ngạc nhiên về những chức năng được gọi mà còn những tác dụng phụ nào đang xảy ra trong thời gian đó và thứ tự xảy ra.

Các ngôn ngữ chức năng có thể có các luồng điều khiển cực kỳ phức tạp và đệ quy, nhưng kết quả rất dễ hiểu về mặt chính xác vì không có tất cả các loại tác dụng phụ chiết trung đang diễn ra trong quá trình. Chỉ khi các luồng điều khiển phức tạp đáp ứng các tác dụng phụ chiết trung mà tôi cảm thấy đau đầu khi cố gắng hiểu toàn bộ những gì đang diễn ra và liệu nó sẽ luôn luôn làm đúng.

Vì vậy, khi tôi gặp những trường hợp đó, tôi thường cảm thấy rất khó khăn, nếu không nói là không thể, cảm thấy rất tự tin về tính chính xác của mã đó, hãy để một mình rất tự tin rằng tôi có thể thay đổi mã như vậy mà không vấp phải điều gì đó bất ngờ. Vì vậy, giải pháp cho tôi là đơn giản hóa luồng điều khiển hoặc giảm thiểu/thống nhất các tác dụng phụ (bằng cách thống nhất, ý tôi là chỉ gây ra một loại tác dụng phụ cho nhiều thứ trong một giai đoạn cụ thể trong hệ thống, chứ không phải hai hoặc ba hoặc một tá). Tôi cần một trong hai điều đó xảy ra để cho phép bộ não đơn giản của tôi cảm thấy tự tin về tính chính xác của mã tồn tại và tính chính xác của những thay đổi tôi giới thiệu. Khá dễ dàng để tự tin về tính chính xác của mã giới thiệu các tác dụng phụ nếu các tác dụng phụ là thống nhất và đơn giản cùng với luồng điều khiển, như vậy:

for each pixel in an image:
    make it red

Thật dễ dàng để lý giải về tính chính xác của mã như vậy, nhưng chủ yếu là do các tác dụng phụ quá đồng đều và luồng điều khiển quá đơn giản. Nhưng hãy nói rằng chúng tôi đã có mã như thế này:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove Edge
         remove vertex

Sau đó, đây là mã giả quá đơn giản, thường sẽ liên quan đến nhiều chức năng hơn và các vòng lặp lồng nhau và nhiều thứ khác sẽ phải tiếp tục (cập nhật nhiều bản đồ kết cấu, trọng lượng xương, trạng thái lựa chọn, v.v.), nhưng ngay cả mã giả cũng gây khó khăn cho Lý do về tính chính xác vì sự tương tác của dòng điều khiển phức tạp giống như đồ thị và các tác dụng phụ đang diễn ra. Vì vậy, một chiến lược để đơn giản hóa đó là trì hoãn việc xử lý và chỉ tập trung vào một loại tác dụng phụ tại một thời điểm:

for each vertex to remove:
     mark connected edges
for each marked Edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked Edge:
     remove Edge
for each vertex to remove:
     remove vertex

... Một cái gì đó cho hiệu ứng này như là một bước lặp của đơn giản hóa. Điều đó có nghĩa là chúng tôi chuyển dữ liệu nhiều lần, điều này chắc chắn phát sinh chi phí tính toán, nhưng chúng tôi thường thấy chúng tôi có thể đa luồng mã kết quả như vậy dễ dàng hơn, giờ đây, các tác dụng phụ và luồng kiểm soát đã mang tính chất đồng nhất và đơn giản hơn này. Hơn nữa, mỗi vòng lặp có thể được làm cho thân thiện với bộ đệm hơn so với di chuyển qua biểu đồ được kết nối và gây ra các hiệu ứng phụ khi chúng ta đi (ví dụ: sử dụng một bit song song để đánh dấu những gì cần phải đi qua để chúng ta có thể thực hiện các lần chuyển tiếp bị trì hoãn theo thứ tự tuần tự được sắp xếp sử dụng bitmasks và FFS). Nhưng quan trọng nhất, tôi thấy phiên bản thứ hai dễ dàng hơn nhiều để suy luận về tính chính xác cũng như thay đổi mà không gây ra lỗi. Vì vậy, đó là cách tôi tiếp cận nó bằng mọi cách và tôi áp dụng cùng một kiểu tư duy để đơn giản hóa việc xử lý lưới ở trên như đơn giản hóa việc xử lý sự kiện và vv - các vòng lặp đồng nhất hơn với các luồng điều khiển đơn giản gây ra các tác dụng phụ thống nhất.

Và sau tất cả, chúng ta cần các tác dụng phụ xảy ra tại một số điểm, nếu không, chúng ta sẽ chỉ có các chức năng xuất dữ liệu mà không có nơi nào để đi. Thông thường chúng ta cần ghi lại một cái gì đó vào một tập tin, hiển thị một cái gì đó ra màn hình, gửi dữ liệu qua một ổ cắm, một cái gì đó thuộc loại này và tất cả những thứ này là tác dụng phụ. Nhưng chúng tôi chắc chắn có thể giảm số lượng tác dụng phụ không cần thiết, và cũng giảm số lượng tác dụng phụ xảy ra khi các luồng điều khiển rất phức tạp và tôi nghĩ sẽ dễ dàng hơn để tránh lỗi nếu chúng tôi làm.

0
user204677