Ưu điểm của lập trình không trạng thái?


131

Gần đây tôi đã học về lập trình chức năng (cụ thể là Haskell, nhưng tôi cũng đã trải qua các hướng dẫn về Lisp và Erlang). Mặc dù tôi thấy các khái niệm rất khai sáng, tôi vẫn không thấy khía cạnh thực tế của khái niệm "không có tác dụng phụ". Những lợi thế thực tế của nó là gì? Tôi đang cố suy nghĩ theo lối tư duy chức năng, nhưng có một số tình huống dường như quá phức tạp mà không có khả năng lưu trạng thái một cách dễ dàng (tôi không coi các đơn vị của Haskell là 'dễ dàng').

Có đáng để tiếp tục học chuyên sâu Haskell (hoặc một ngôn ngữ chức năng thuần túy khác) không? Là lập trình chức năng hoặc không trạng thái thực sự năng suất hơn so với thủ tục? Có khả năng tôi sẽ tiếp tục sử dụng Haskell hoặc ngôn ngữ chức năng khác sau này không, hay tôi chỉ nên học nó để hiểu?

Tôi ít quan tâm đến hiệu suất hơn năng suất. Vì vậy, tôi chủ yếu hỏi liệu tôi sẽ làm việc hiệu quả hơn bằng ngôn ngữ chức năng hơn là hướng thủ tục / hướng đối tượng / bất cứ điều gì.

Câu trả lời:


167

Đọc lập trình chức năng trong một Nutshell .

Có rất nhiều lợi thế để lập trình không quốc tịch, không ít trong số đó là đáng kể đa luồng và mã đồng thời. Nói một cách thẳng thắn, trạng thái có thể thay đổi là kẻ thù của mã đa luồng. Nếu các giá trị là bất biến theo mặc định, các lập trình viên không cần phải lo lắng về một luồng làm thay đổi giá trị của trạng thái chia sẻ giữa hai luồng, vì vậy nó sẽ loại bỏ cả một lớp lỗi đa luồng liên quan đến điều kiện cuộc đua. Vì không có điều kiện chủng tộc, nên cũng không có lý do để sử dụng khóa, vì vậy tính bất biến cũng loại bỏ cả một lớp lỗi khác liên quan đến bế tắc.

Đó là lý do lớn tại sao lập trình chức năng quan trọng, và có lẽ là lý do tốt nhất để nhảy lên tàu lập trình chức năng. Ngoài ra còn có rất nhiều lợi ích khác, bao gồm gỡ lỗi đơn giản hóa (nghĩa là các hàm là thuần túy và không biến đổi trạng thái trong các phần khác của ứng dụng), mã ngắn gọn và biểu cảm hơn, mã nồi hơi ít hơn so với các ngôn ngữ phụ thuộc nhiều vào các mẫu thiết kế và trình biên dịch có thể tích cực hơn để tối ưu hóa mã của bạn.


5
Tôi thứ hai này! Tôi tin rằng lập trình chức năng sẽ được sử dụng rộng rãi hơn nhiều trong tương lai vì sự phù hợp của nó với lập trình song song.
Ray Hidayat

@Ray: Tôi cũng sẽ thêm lập trình phân tán!
Anton Tykhyy

Hầu hết điều đó là đúng, ngoại trừ việc gỡ lỗi. Nói chung, khó khăn hơn trong haskell, bởi vì bạn không có ngăn xếp cuộc gọi thực sự, chỉ là một ngăn xếp khớp mẫu. Và thật khó để dự đoán mã của bạn kết thúc như thế nào.
hasufell

3
Ngoài ra, lập trình chức năng không thực sự về "không trạng thái". Đệ quy là trạng thái ngầm (cục bộ) và là điều chính chúng ta làm trong haskell. Điều đó trở nên rõ ràng khi bạn thực hiện một vài thuật toán không tầm thường trong haskell thành ngữ (ví dụ: công cụ hình học tính toán) và vui vẻ gỡ lỗi chúng.
hasufell

2
Không thích đánh đồng trạng thái với FP. Nhiều chương trình FP chứa đầy trạng thái, nó chỉ đơn giản tồn tại trong một bao đóng chứ không phải là một đối tượng.
mikemaccana

46

Càng nhiều phần của chương trình của bạn là không trạng thái, càng có nhiều cách để đặt các phần lại với nhau mà không có bất cứ điều gì bị phá vỡ . Sức mạnh của mô hình không trạng thái không nằm ở trạng thái không trạng thái (hoặc độ tinh khiết) mỗi se , mà khả năng nó mang lại cho bạn để viết các chức năng mạnh mẽ, có thể tái sử dụng và kết hợp chúng.

Bạn có thể tìm thấy một hướng dẫn tốt với rất nhiều ví dụ trong bài viết Tại sao các vấn đề lập trình chức năng (PDF) của John Hughes .

Bạn sẽ có gobs năng suất cao hơn, đặc biệt nếu bạn chọn một ngôn ngữ chức năng mà còn có các kiểu dữ liệu đại số và khớp mẫu (CAML, SML, Haskell).


Mixins cũng không cung cấp mã có thể tái sử dụng theo cách tương tự với OOP? Không ủng hộ OOP chỉ cố gắng tự hiểu mọi thứ.
mikemaccana

20

Nhiều câu trả lời khác đã tập trung vào khía cạnh hiệu suất (song song) của lập trình chức năng, mà tôi tin là rất quan trọng. Tuy nhiên, bạn đã hỏi cụ thể về năng suất, như trong, bạn có thể lập trình điều tương tự nhanh hơn trong một mô hình chức năng hơn là trong một mô hình bắt buộc.

Tôi thực sự thấy (từ kinh nghiệm cá nhân) rằng lập trình trong F # phù hợp với cách tôi nghĩ tốt hơn, và vì vậy nó dễ dàng hơn. Tôi nghĩ đó là sự khác biệt lớn nhất. Tôi đã lập trình ở cả F # và C #, và có rất ít "chiến đấu với ngôn ngữ" trong F #, mà tôi yêu thích. Bạn không cần phải suy nghĩ về các chi tiết trong F #. Dưới đây là một vài ví dụ về những gì tôi thấy tôi thực sự thích.

Ví dụ: mặc dù F # được nhập tĩnh (tất cả các loại được giải quyết tại thời điểm biên dịch), kiểu suy luận chỉ ra loại bạn có, vì vậy bạn không cần phải nói. Và nếu không thể tìm ra nó, nó sẽ tự động làm cho hàm / lớp / bất cứ thứ gì chung chung của bạn. Vì vậy, bạn không bao giờ phải viết bất kỳ cái gì chung chung, tất cả đều tự động. Tôi thấy điều đó có nghĩa là tôi đang dành nhiều thời gian hơn để suy nghĩ về vấn đề và giảm cách thực hiện nó. Trên thực tế, bất cứ khi nào tôi quay lại C #, tôi thấy tôi thực sự nhớ loại suy luận này, bạn không bao giờ nhận ra nó gây mất tập trung như thế nào cho đến khi bạn không cần phải làm điều đó nữa.

Cũng trong F #, thay vì viết các vòng lặp, bạn gọi các hàm. Đó là một thay đổi tinh tế, nhưng có ý nghĩa, bởi vì bạn không phải suy nghĩ về cấu trúc vòng lặp nữa. Ví dụ: đây là một đoạn mã sẽ đi qua và khớp với thứ gì đó (tôi không thể nhớ cái gì, đó là từ câu đố Euler của dự án):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Tôi nhận ra rằng thực hiện bộ lọc sau đó bản đồ (đó là chuyển đổi từng yếu tố) trong C # sẽ khá đơn giản, nhưng bạn phải suy nghĩ ở cấp độ thấp hơn. Đặc biệt, bạn phải tự viết vòng lặp và có câu lệnh if rõ ràng của riêng bạn và các loại điều đó. Từ khi học F #, tôi nhận ra rằng tôi thấy việc viết mã theo cách chức năng dễ dàng hơn, nếu bạn muốn lọc, bạn viết "bộ lọc" và nếu bạn muốn lập bản đồ, bạn viết "bản đồ", thay vì thực hiện từng chi tiết

Tôi cũng thích toán tử |> mà tôi nghĩ tách F # khỏi ocaml và có thể các ngôn ngữ chức năng khác. Đó là toán tử đường ống, nó cho phép bạn "dẫn" đầu ra của một biểu thức vào đầu vào của một biểu thức khác. Nó làm cho mã theo cách tôi nghĩ nhiều hơn. Giống như trong đoạn mã trên, có nghĩa là, "lấy chuỗi các yếu tố, lọc nó, sau đó ánh xạ nó." Đó là một mức độ suy nghĩ rất cao, mà bạn không có được bằng một ngôn ngữ lập trình bắt buộc bởi vì bạn quá bận rộn để viết vòng lặp và câu lệnh if. Đó là điều tôi nhớ nhất mỗi khi tôi đi vào một ngôn ngữ khác.

Vì vậy, nói chung, mặc dù tôi có thể lập trình ở cả C # và F #, tôi thấy việc sử dụng F # dễ dàng hơn vì bạn có thể nghĩ ở cấp cao hơn. Tôi sẽ lập luận rằng vì các chi tiết nhỏ hơn được loại bỏ khỏi lập trình chức năng (ít nhất là trong F #), nên tôi làm việc hiệu quả hơn.

Chỉnh sửa : Tôi thấy trong một trong những ý kiến ​​mà bạn đã hỏi ví dụ về "trạng thái" trong ngôn ngữ lập trình chức năng. F # có thể được viết một cách bắt buộc, vì vậy đây là một ví dụ trực tiếp về cách bạn có thể có trạng thái có thể thay đổi trong F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i

1
Tôi đồng ý với bài viết của bạn nói chung, nhưng |> không liên quan gì đến lập trình chức năng. Thật ra, a |> b (p1, p2)chỉ là cú pháp đường cho b (a, p1, p2). Kết hợp điều này với tính kết hợp đúng và bạn đã có nó.
Anton Tykhyy

2
Đúng, tôi nên thừa nhận rằng có lẽ rất nhiều trải nghiệm tích cực của tôi với F # có liên quan nhiều đến F # hơn là với lập trình chức năng. Tuy nhiên, vẫn có một mối tương quan mạnh mẽ giữa hai loại này, và mặc dù những thứ như suy luận kiểu và |> không phải là lập trình chức năng mỗi se, chắc chắn tôi sẽ tuyên bố chúng "đi với lãnh thổ". Ít nhất là nói chung.
Ray Hidayat

|> chỉ là một hàm infix bậc cao hơn, trong trường hợp này là toán tử ứng dụng hàm. Xác định các toán tử infix bậc cao hơn của riêng bạn chắc chắn là một phần của lập trình chức năng (trừ khi bạn là một SchTable). Haskell có $ của nó giống nhau ngoại trừ thông tin trong đường ống chảy từ phải sang trái.
Norman Ramsey

15

Xem xét tất cả các lỗi khó khăn mà bạn đã dành một thời gian dài để gỡ lỗi.

Bây giờ, có bao nhiêu lỗi trong số đó là do "tương tác ngoài ý muốn" giữa hai thành phần riêng biệt của một chương trình? (Gần như tất cả lỗi luồng có dạng như sau: chủng tộc liên quan đến ghi dữ liệu chia sẻ, bế tắc, ... Thêm vào đó, người ta thường tìm thấy thư viện có một số hiệu ứng bất ngờ về tình trạng toàn cầu, hoặc đọc / viết registry / môi trường, vv) tôi sẽ khẳng định rằng ít nhất 1 trong 3 "lỗi cứng" rơi vào loại này.

Bây giờ nếu bạn chuyển sang lập trình không trạng thái / bất biến / thuần túy, tất cả các lỗi đó sẽ biến mất. Bạn được trình bày với một số thách thức mới để thay thế (ví dụ như khi bạn làm muốn module khác nhau để tương tác với môi trường), nhưng trong một ngôn ngữ như Haskell, những tương tác được một cách rõ ràng reified vào hệ thống loại, có nghĩa là bạn chỉ có thể xem xét các loại một chức năng và lý do về loại tương tác mà nó có thể có với phần còn lại của chương trình.

Đó là chiến thắng lớn từ IMO 'bất biến'. Trong một thế giới lý tưởng, tất cả chúng ta đều thiết kế các API tuyệt vời và ngay cả khi mọi thứ có thể thay đổi, các hiệu ứng sẽ là cục bộ và được ghi chép rõ ràng và các tương tác 'bất ngờ' sẽ được giữ ở mức tối thiểu. Trong thế giới thực, có rất nhiều API tương tác với trạng thái toàn cầu theo vô số cách và đây là nguồn gốc của các lỗi nguy hiểm nhất. Mong muốn không quốc tịch là khao khát được loại bỏ các tương tác ngoài ý muốn / ngầm / đằng sau hậu trường giữa các thành phần.


6
Ai đó đã từng nói rằng ghi đè một giá trị có thể thay đổi có nghĩa là bạn rõ ràng đang thu gom rác / giải phóng giá trị trước đó. Trong một số trường hợp, các phần khác của chương trình không được thực hiện bằng cách sử dụng giá trị đó. Khi các giá trị không thể bị đột biến, lớp lỗi này cũng biến mất.
shapr

8

Một lợi thế của các hàm không trạng thái là chúng cho phép tính toán trước hoặc lưu vào bộ đệm các giá trị trả về của hàm. Thậm chí một số trình biên dịch C cho phép bạn đánh dấu rõ ràng các hàm là không trạng thái để cải thiện khả năng tối ưu của chúng. Như nhiều người khác đã lưu ý, các hàm không trạng thái dễ song song hơn nhiều.

Nhưng hiệu quả không phải là mối quan tâm duy nhất. Một hàm thuần túy dễ kiểm tra và gỡ lỗi hơn vì mọi thứ ảnh hưởng đến nó đều được nêu rõ ràng. Và khi lập trình bằng ngôn ngữ chức năng, người ta có thói quen tạo ra càng ít chức năng "bẩn" (với I / O, v.v.) càng tốt. Phân tách các công cụ trạng thái theo cách này là một cách tốt để thiết kế chương trình, ngay cả trong các ngôn ngữ không có chức năng.

Các ngôn ngữ chức năng có thể mất một lúc để "hiểu" và thật khó để giải thích với người chưa trải qua quá trình đó. Nhưng hầu hết những người kiên trì đủ lâu cuối cùng cũng nhận ra rằng sự ồn ào đó đáng giá, ngay cả khi họ không sử dụng nhiều ngôn ngữ chức năng.


Phần đầu tiên đó là một điểm thực sự thú vị, tôi chưa bao giờ nghĩ về điều đó trước đây. Cảm ơn!
Sasha Chedygov

Giả sử bạn có sin(PI/3)trong mã của mình, trong đó PI là hằng số, trình biên dịch có thể đánh giá hàm này tại thời gian biên dịch và nhúng kết quả vào mã được tạo.
Artelius

6

Không có trạng thái, rất dễ dàng tự động song song mã của bạn (vì CPU được tạo ra với càng nhiều lõi nên điều này rất quan trọng).


Vâng, tôi chắc chắn đã xem xét điều đó. Mô hình đồng thời của Erlang nói riêng là rất hấp dẫn. Tuy nhiên, tại thời điểm này tôi không thực sự quan tâm đến đồng thời nhiều như năng suất. Có một phần thưởng năng suất từ ​​lập trình mà không có nhà nước?
Sasha Chedygov

2
@musicfreak, không có phần thưởng năng suất. Nhưng như một lưu ý, các ngôn ngữ FP hiện đại vẫn cho phép bạn sử dụng trạng thái nếu bạn thực sự cần nó.
Không biết

Có thật không? Bạn có thể đưa ra một ví dụ về trạng thái trong một ngôn ngữ chức năng, để tôi có thể thấy nó được thực hiện như thế nào không?
Sasha Chedygov

Kiểm tra các đơn nguyên nhà nước trong Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
rampion

4
@Un Unknown: Tôi không đồng ý. Lập trình không có trạng thái làm giảm sự xuất hiện của các lỗi do các tương tác không lường trước / không lường trước của các thành phần khác nhau. Nó cũng khuyến khích thiết kế tốt hơn (khả năng sử dụng lại nhiều hơn, tách biệt cơ chế và chính sách và loại công cụ đó). Nó không phải lúc nào cũng thích hợp cho nhiệm vụ trong tay nhưng trong một số trường hợp thực sự tỏa sáng.
Artelius

6

Các ứng dụng web không trạng thái là rất cần thiết khi bạn bắt đầu có lưu lượng truy cập cao hơn.

Có thể có nhiều dữ liệu người dùng mà bạn không muốn lưu trữ ở phía máy khách vì lý do bảo mật chẳng hạn. Trong trường hợp này, bạn cần lưu trữ phía máy chủ. Bạn có thể sử dụng phiên mặc định của ứng dụng web nhưng nếu bạn có nhiều phiên bản của ứng dụng, bạn sẽ cần đảm bảo rằng mỗi người dùng luôn được hướng đến cùng một thể hiện.

Bộ cân bằng tải thường có khả năng có 'phiên dính' trong đó bộ cân bằng tải một số cách biết máy chủ nào sẽ gửi yêu cầu của người dùng. Tuy nhiên, điều này không lý tưởng, ví dụ, điều đó có nghĩa là mỗi khi bạn khởi động lại ứng dụng web của mình, tất cả người dùng được kết nối sẽ mất phiên.

Một cách tiếp cận tốt hơn là lưu trữ phiên phía sau các máy chủ web trong một số loại lưu trữ dữ liệu, ngày nay có rất nhiều sản phẩm nosql tuyệt vời có sẵn cho việc này (redis, mongo, elaticsearch, memcached). Bằng cách này, các máy chủ web không trạng thái nhưng bạn vẫn có phía máy chủ trạng thái và tính khả dụng của trạng thái này có thể được quản lý bằng cách chọn thiết lập kho dữ liệu phù hợp. Các cửa hàng dữ liệu này thường có sự dư thừa lớn, do đó hầu như luôn luôn có thể thực hiện các thay đổi cho ứng dụng web của bạn và thậm chí là lưu trữ dữ liệu mà không ảnh hưởng đến người dù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.