Một đơn nguyên là gì?


1415

Nhìn thoáng qua Haskell gần đây, điều gì sẽ là một lời giải thích ngắn gọn, súc tích, thực tế về bản chất của một đơn nguyên là gì?

Tôi đã tìm thấy hầu hết các giải thích mà tôi đã gặp là khá khó tiếp cận và thiếu chi tiết thực tế.


12
Eric Lippert đã viết một câu trả lời cho câu hỏi này ( stackoverflow.com/questions/2704652/ế ), đó là do một số vấn đề tồn tại trong một trang riêng.
P Shved

70
Đây là một giới thiệu mới bằng cách sử dụng javascript - Tôi thấy nó rất dễ đọc.
Stewol



2
Một đơn nguyên là một mảng các chức năng với các hoạt động của người trợ giúp. Xem câu trả lời này
cibercitizen1

Câu trả lời:


1059

Thứ nhất: Thuật ngữ đơn nguyên hơi trống nếu bạn không phải là nhà toán học. Một thuật ngữ thay thế là trình xây dựng tính toán mô tả thêm một chút về những gì chúng thực sự hữu ích cho.

Bạn hỏi ví dụ thực tế:

Ví dụ 1: Danh sách hiểu :

[x*2 | x<-[1..10], odd x]

Biểu thức này trả về số nhân của tất cả các số lẻ trong phạm vi từ 1 đến 10. Rất hữu ích!

Hóa ra đây thực sự chỉ là đường cú pháp cho một số hoạt động trong danh sách đơn nguyên. Việc hiểu danh sách tương tự có thể được viết là:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

Hoặc thậm chí:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

Ví dụ 2: Đầu vào / Đầu ra :

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

Cả hai ví dụ đều sử dụng các đơn nguyên, các nhà xây dựng tính toán AKA. Chủ đề chung là các chuỗi đơn nguyên hoạt động theo một số cách cụ thể, hữu ích. Trong phần hiểu danh sách, các hoạt động được kết nối sao cho nếu một hoạt động trả về một danh sách, thì các hoạt động sau được thực hiện trên mỗi mục trong danh sách. Mặt khác, đơn vị IO thực hiện các hoạt động một cách tuần tự, nhưng lại chuyển một "biến ẩn", đại diện cho "trạng thái của thế giới", cho phép chúng ta viết mã I / O theo cách chức năng thuần túy.

Hóa ra mô hình hoạt động xích khá hữu ích và được sử dụng cho nhiều thứ khác nhau trong Haskell.

Một ví dụ khác là các trường hợp ngoại lệ: Sử dụng Errorđơn nguyên, các thao tác được xâu chuỗi sao cho chúng được thực hiện tuần tự, trừ khi có lỗi được ném, trong trường hợp đó, phần còn lại của chuỗi bị bỏ.

Cả cú pháp hiểu danh sách và ký hiệu là đường cú pháp cho các hoạt động chuỗi sử dụng >>=toán tử. Một đơn nguyên về cơ bản chỉ là một loại hỗ trợ người >>=vận hành.

Ví dụ 3: Trình phân tích cú pháp

Đây là một trình phân tích cú pháp rất đơn giản để phân tích một chuỗi được trích dẫn hoặc một số:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

Các hoạt động char, digitvv là khá đơn giản. Họ phù hợp hoặc không phù hợp. Phép thuật là đơn nguyên quản lý luồng điều khiển: Các thao tác được thực hiện tuần tự cho đến khi trận đấu thất bại, trong trường hợp đó, đơn vị quay lại bản mới nhất <|>và thử tùy chọn tiếp theo. Một lần nữa, một cách hoạt động chuỗi với một số ngữ nghĩa bổ sung, hữu ích.

Ví dụ 4: Lập trình không đồng bộ

Các ví dụ trên có trong Haskell, nhưng hóa ra F # cũng hỗ trợ các đơn nguyên. Ví dụ này bị đánh cắp từ Don Syme :

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

Phương pháp này lấy một trang web. Dòng punch là việc sử dụng GetResponseAsync- nó thực sự chờ phản hồi trên một luồng riêng biệt, trong khi luồng chính trả về từ hàm. Ba dòng cuối cùng được thực hiện trên luồng sinh sản khi nhận được phản hồi.

Trong hầu hết các ngôn ngữ khác, bạn sẽ phải tạo một hàm riêng biệt cho các dòng xử lý phản hồi. Các asyncđơn vị có thể tự "tách" khối và hoãn việc thực hiện nửa sau. ( async {}Cú pháp chỉ ra rằng luồng điều khiển trong khối được xác định bởi asyncđơn nguyên.)

Chúng hoạt động như thế nào

Vì vậy, làm thế nào một đơn nguyên có thể làm tất cả những điều kiểm soát dòng chảy ưa thích này? Điều thực sự xảy ra trong một khối do (hoặc một biểu thức tính toán như chúng được gọi trong F #), là mọi hoạt động (về cơ bản là mọi dòng) được gói trong một hàm ẩn danh riêng biệt. Các chức năng này sau đó được kết hợp bằng cách sử dụng bindtoán tử (đánh vần là >>=Haskell). Vì bindhoạt động kết hợp các chức năng, nó có thể thực thi chúng khi nó thấy phù hợp: tuần tự, nhiều lần, ngược lại, loại bỏ một số, thực thi một số trên một luồng riêng biệt khi cảm thấy như vậy và cứ thế.

Ví dụ, đây là phiên bản mở rộng của mã IO từ ví dụ 2:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

Điều này là xấu hơn, nhưng nó cũng rõ ràng hơn những gì đang thực sự xảy ra. Các >>=nhà điều hành là thành phần kỳ diệu: Phải mất một giá trị (ở phía bên trái) và máy liên hợp nó với một chức năng (ở bên phải), để tạo ra một giá trị mới. Giá trị mới này sau đó được lấy bởi >>=toán tử tiếp theo và kết hợp lại với một hàm để tạo ra một giá trị mới. >>=có thể được xem như một người đánh giá nhỏ.

Lưu ý rằng >>=quá tải cho các loại khác nhau, vì vậy mỗi đơn nguyên có cách thực hiện riêng >>=. (Mặc dù vậy, tất cả các hoạt động trong chuỗi phải thuộc loại đơn vị giống nhau, nếu không thì >>=toán tử sẽ không hoạt động.)

Việc thực hiện đơn giản nhất có thể >>=chỉ là lấy giá trị ở bên trái và áp dụng nó cho hàm bên phải và trả về kết quả, nhưng như đã nói trước đây, điều làm cho toàn bộ mô hình trở nên hữu ích là khi có một điều gì đó xảy ra trong quá trình thực hiện của đơn nguyên >>=.

Có một số thông minh bổ sung trong cách các giá trị được truyền từ thao tác này sang thao tác tiếp theo, nhưng điều này đòi hỏi một sự giải thích sâu hơn về hệ thống loại Haskell.

Tổng hợp

Trong thuật ngữ Haskell, một đơn vị là một loại được tham số hóa, là một thể hiện của lớp loại Monad, được định nghĩa >>=cùng với một vài toán tử khác. Theo thuật ngữ của giáo dân, một đơn vị chỉ là một loại mà >>=hoạt động được xác định.

Bản thân nó >>=chỉ là một cách thức rườm rà của các chức năng xâu chuỗi, nhưng với sự hiện diện của ký hiệu ẩn giấu "hệ thống ống nước", các hoạt động đơn điệu hóa ra là một sự trừu tượng rất hay và hữu ích, nhiều nơi hữu ích trong ngôn ngữ và hữu ích để tạo ngôn ngữ nhỏ của riêng bạn trong ngôn ngữ.

Tại sao các đơn nguyên khó?

Đối với nhiều người học Haskell, các đơn vị là một chướng ngại vật họ gặp phải như một bức tường gạch. Bản thân các đơn nguyên không phức tạp, nhưng việc triển khai dựa trên nhiều tính năng Haskell tiên tiến khác như các loại tham số, các lớp loại, v.v. Vấn đề là I / O của Haskell dựa trên các đơn nguyên và I / O có lẽ là một trong những điều đầu tiên bạn muốn hiểu khi học một ngôn ngữ mới - sau tất cả, thật không vui khi tạo ra các chương trình không tạo ra bất kỳ chương trình nào đầu ra. Tôi không có giải pháp tức thời cho vấn đề trứng gà này, ngoại trừ đối xử với I / O như "phép thuật xảy ra ở đây" cho đến khi bạn có đủ kinh nghiệm với các phần khác của ngôn ngữ. Lấy làm tiếc.

Blog tuyệt vời về các đơn vị: http://adit.io/posts/2013-04-17-functor,_applicative,_and_monads_in_pictures.html


66
Là một người đã có rất nhiều vấn đề trong việc hiểu các đơn nguyên, tôi có thể nói rằng câu trả lời này đã giúp .. một chút. Tuy nhiên, vẫn còn một số điều mà tôi không hiểu. Theo cách nào thì danh sách hiểu được một đơn nguyên? Có một hình thức mở rộng của ví dụ đó? Một điều khác thực sự làm phiền tôi về hầu hết các giải thích đơn nguyên, bao gồm cả điều này - Có phải họ cứ tiếp tục trộn lẫn "một đơn nguyên là gì?" với "một đơn nguyên tốt cho cái gì?" và "Làm thế nào một đơn nguyên được thực hiện?". bạn đã nhảy con cá mập đó khi bạn viết "Một đơn nguyên về cơ bản chỉ là một loại hỗ trợ toán tử >> =." Mà chỉ có tôi ...
Breton

83
Ngoài ra tôi không đồng ý với kết luận của bạn về lý do tại sao các đơn vị khó khăn. Nếu bản thân các đơn vị không phức tạp, thì bạn sẽ có thể giải thích chúng là gì mà không có một đống hành lý. Tôi không muốn biết về việc thực hiện khi tôi đặt câu hỏi "Đơn vị là gì", tôi muốn biết ý nghĩa của việc gãi là gì. Cho đến nay có vẻ như câu trả lời là "Bởi vì các tác giả của haskell là những người buồn bã và quyết định rằng bạn nên làm điều gì đó phức tạp một cách ngu ngốc để thực hiện những điều đơn giản, vì vậy bạn phải học các đơn vị để sử dụng haskell, không phải vì chúng hữu ích trong mọi cách chính họ "...
Breton

70
Nhưng .. điều đó không thể đúng, phải không? Tôi nghĩ rằng các đơn vị rất khó vì dường như không ai có thể tìm ra cách giải thích chúng mà không bị cuốn vào các chi tiết triển khai khó hiểu. Ý tôi là .. xe buýt trường học là gì? Đó là một nền tảng kim loại với một thiết bị ở phía trước tiêu thụ một sản phẩm dầu mỏ tinh chế để lái trong một chu kỳ một số pít-tông kim loại, lần lượt xoay một trục khuỷu được gắn vào một số bánh răng dẫn động một số bánh xe. Các bánh xe đã phồng túi cao su xung quanh chúng có giao diện với bề mặt nhựa đường để tạo ra một bộ ghế để di chuyển về phía trước. Các ghế di chuyển về phía trước vì ...
Breton

130
Tôi đã đọc tất cả những điều này và vẫn không biết một đơn vị là gì, ngoài thực tế rằng đó là điều mà các lập trình viên Haskell không hiểu đủ để giải thích. Các ví dụ không giúp được gì nhiều, vì đây là tất cả những điều người ta có thể làm mà không cần đơn nguyên, và câu trả lời này không làm rõ cách thức các đơn vị làm cho chúng dễ dàng hơn, chỉ khó hiểu hơn. Một phần của câu trả lời gần như hữu ích này là nơi đường cú pháp của ví dụ # 2 đã bị xóa. Tôi nói đã đến gần bởi vì, ngoài dòng đầu tiên, bản mở rộng không có bất kỳ sự tương đồng thực sự nào với bản gốc.
Laurence Gonsalves

81
Một vấn đề khác dường như là đặc hữu đối với lời giải thích của các đơn vị là nó được viết bằng Haskell. Tôi không nói Haskell là một ngôn ngữ xấu - Tôi đang nói đó là một ngôn ngữ tồi để giải thích các đơn nguyên. Nếu tôi biết Haskell tôi đã hiểu các đơn nguyên, vì vậy nếu bạn muốn giải thích các đơn nguyên, hãy bắt đầu bằng cách sử dụng ngôn ngữ mà những người không biết các đơn vị có nhiều khả năng hiểu. Nếu bạn phải sử dụng Haskell, đừng sử dụng đường cú pháp - hãy sử dụng tập hợp con nhỏ nhất, đơn giản nhất của ngôn ngữ bạn có thể và đừng hiểu về Haskell IO.
Laurence Gonsalves

712

Giải thích "đơn vị là gì" giống như nói "số là gì?" Chúng tôi sử dụng số tất cả các thời gian. Nhưng hãy tưởng tượng bạn đã gặp một người không biết gì về những con số. Làm thế quái nào bạn sẽ giải thích những con số là gì? Và làm thế nào bạn thậm chí sẽ bắt đầu mô tả tại sao điều đó có thể hữu ích?

Một đơn nguyên là gì? Câu trả lời ngắn gọn: Đó là một cách cụ thể của các hoạt động xích cùng nhau.

Về bản chất, bạn đang viết các bước thực hiện và liên kết chúng với nhau bằng "hàm liên kết". (Trong Haskell, nó được đặt tên >>=.) Bạn có thể tự viết các cuộc gọi cho toán tử liên kết hoặc bạn có thể sử dụng cú pháp đường khiến trình biên dịch chèn các lệnh gọi hàm đó cho bạn. Nhưng dù bằng cách nào, mỗi bước được phân tách bằng một lệnh gọi đến hàm liên kết này.

Vì vậy, chức năng liên kết giống như một dấu chấm phẩy; nó phân tách các bước trong một quy trình. Công việc của hàm liên kết là lấy đầu ra từ bước trước và đưa nó vào bước tiếp theo.

Điều đó không có vẻ quá khó, phải không? Nhưng có nhiều hơn một loại đơn nguyên. Tại sao? Làm sao?

Chà, hàm liên kết chỉ có thể lấy kết quả từ một bước và đưa nó sang bước tiếp theo. Nhưng nếu đó là "tất cả" thì đơn nguyên làm ... điều đó thực sự không hữu ích. Và đó là điều quan trọng để hiểu: Mỗi đơn nguyên hữu ích còn làm một việc khác ngoài việc chỉ là một đơn nguyên. Mỗi đơn nguyên hữu ích có một "sức mạnh đặc biệt", làm cho nó trở nên độc nhất.

(Một đơn nguyên mà không có gì đặc biệt được gọi là "bản sắc đơn nguyên". Thay vào đó như chức năng nhận diện, điều này giống như một điều hoàn toàn vô nghĩa âm thanh, nhưng hóa ra không phải là ... Nhưng đó là một câu chuyện ™.)

Về cơ bản, mỗi đơn nguyên có cách thực hiện riêng của hàm liên kết. Và bạn có thể viết một hàm liên kết sao cho nó thực hiện mọi thứ giữa các bước thực hiện. Ví dụ:

  • Nếu mỗi bước trả về một chỉ báo thành công / thất bại, bạn có thể có ràng buộc thực hiện bước tiếp theo chỉ khi bước trước đó thành công. Theo cách này, một bước thất bại sẽ hủy bỏ toàn bộ chuỗi "tự động" mà không có bất kỳ thử nghiệm có điều kiện nào từ bạn. (Đơn vị thất bại .)

  • Mở rộng ý tưởng này, bạn có thể thực hiện "ngoại lệ". ( Monad Error hoặc Exception Monad .) Vì bạn tự xác định chúng chứ không phải là một tính năng ngôn ngữ, bạn có thể xác định cách chúng hoạt động. (Ví dụ: có thể bạn muốn bỏ qua hai ngoại lệ đầu tiên và chỉ hủy bỏ khi ném ngoại lệ thứ ba .)

  • Bạn có thể làm cho mỗi bước trả về nhiều kết quả và có vòng lặp chức năng liên kết trên chúng, đưa từng bước vào bước tiếp theo cho bạn. Theo cách này, bạn không phải viết các vòng lặp ở mọi nơi khi xử lý nhiều kết quả. Hàm liên kết "tự động" thực hiện tất cả điều đó cho bạn. ( Danh sách đơn nguyên .)

  • Cũng như chuyển "kết quả" từ bước này sang bước khác, bạn cũng có thể có chức năng liên kết truyền dữ liệu bổ sung xung quanh. Dữ liệu này hiện không hiển thị trong mã nguồn của bạn, nhưng bạn vẫn có thể truy cập nó từ bất cứ đâu mà không cần phải truyền thủ công cho mọi chức năng. ( Độc giả Monad .)

  • Bạn có thể làm cho nó để "dữ liệu bổ sung" có thể được thay thế. Điều này cho phép bạn mô phỏng các cập nhật phá hoại , mà không thực sự thực hiện các cập nhật phá hoại. ( Bang Monad và anh em họ của nó là Nhà văn Monad .)

  • Bởi vì bạn chỉ mô phỏng các cập nhật phá hoại, bạn có thể làm những việc không thể với các cập nhật phá hoại thực sự . Ví dụ: bạn có thể hoàn tác bản cập nhật mới nhất hoặc hoàn nguyên về phiên bản cũ hơn .

  • Bạn có thể tạo một đơn nguyên nơi các phép tính có thể được tạm dừng , do đó bạn có thể tạm dừng chương trình của mình, truy cập và sửa lại dữ liệu trạng thái nội bộ, sau đó tiếp tục lại.

  • Bạn có thể thực hiện "tiếp tục" như một đơn nguyên. Điều này cho phép bạn phá vỡ tâm trí của mọi người!

Tất cả điều này và nhiều hơn nữa là có thể với các đơn nguyên. Tất nhiên, tất cả những điều này cũng hoàn toàn có thể xảy ra nếu không có các đơn vị quá. Nó chỉ đơn giản là dễ dàng hơn bằng cách sử dụng các đơn nguyên.


13
Tôi đánh giá cao câu trả lời của bạn, đặc biệt là sự nhượng bộ cuối cùng rằng tất cả những điều này tất nhiên là có thể nếu không có các đơn nguyên. Một điểm cần làm là hầu như dễ dàng hơn với các đơn nguyên, nhưng thường không hiệu quả bằng việc thực hiện mà không có chúng. Khi bạn cần liên quan đến máy biến áp, việc phân lớp thêm các lệnh gọi hàm (và các đối tượng hàm được tạo) có chi phí khó nhìn thấy và kiểm soát, được hiển thị vô hình bằng cú pháp thông minh.
seh

1
Trong Haskell ít nhất, phần lớn chi phí của các đơn vị bị tước đi bởi trình tối ưu hóa. Vì vậy, "chi phí" thực sự duy nhất là trong năng lực não cần thiết. (Điều này không đáng kể nếu "khả năng duy trì" là điều bạn quan tâm.) Nhưng thông thường, các đơn vị làm cho mọi thứ dễ dàng hơn , không khó hơn. (Nếu không, tại sao bạn lại bận tâm?)
Toán học,

Tôi không chắc liệu Haskell có hỗ trợ điều này hay không nhưng về mặt toán học, bạn có thể xác định một đơn nguyên theo >> = và trả lại hoặc tham gia và ap. >> = và trở lại là những gì làm cho các đơn vị thực sự hữu ích nhưng tham gia và ap cung cấp cho sự hiểu biết trực quan hơn về những gì một đơn vị là.
Danh sách Jeremy

15
Đến từ một nền tảng lập trình phi toán học, phi chức năng, câu trả lời này có ý nghĩa nhất đối với tôi.
jrahhali

10
Đây là câu trả lời đầu tiên thực sự đã cho tôi một số ý tưởng về một quái vật là gì. Cảm ơn bạn đã tìm cách giải thích nó!
robotmay

186

Trên thực tế, trái với sự hiểu biết chung về Monads, chúng không liên quan gì đến nhà nước. Monads chỉ đơn giản là một cách để bọc mọi thứ và cung cấp các phương thức để thực hiện các thao tác trên các công cụ được bọc mà không cần mở khóa.

Ví dụ: bạn có thể tạo một loại để bọc một loại khác, trong Haskell:

data Wrapped a = Wrap a

Để bọc thứ chúng tôi xác định

return :: a -> Wrapped a
return x = Wrap x

Để thực hiện các thao tác mà không hủy ghép nối, giả sử bạn có một hàm f :: a -> b, sau đó bạn có thể thực hiện việc này để nâng hàm đó lên hành động trên các giá trị được bọc:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

Đó là tất cả những gì cần phải hiểu. Tuy nhiên, hóa ra có một chức năng tổng quát hơn để thực hiện việc nâng này , đó là bind:

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bindcó thể làm nhiều hơn một chút fmap, nhưng không phải ngược lại. Trên thực tế, fmapcó thể được định nghĩa chỉ trong điều khoản bindreturn. Vì vậy, khi xác định một đơn nguyên .. bạn đưa ra loại của nó (đây là Wrapped a) và sau đó cho biết cách thức hoạt động returnbindhoạt động của nó .

Điều thú vị là điều này hóa ra là một mô hình chung đến mức nó xuất hiện ở khắp mọi nơi, đóng gói trạng thái theo cách thuần túy chỉ là một trong số đó.

Để có một bài viết hay về cách các đơn vị có thể được sử dụng để giới thiệu các phụ thuộc chức năng và do đó kiểm soát thứ tự đánh giá, giống như nó được sử dụng trong đơn vị IO của Haskell, hãy xem IO Inside .

Để hiểu về các đơn nguyên, đừng quá lo lắng về điều đó. Đọc về họ những gì bạn thấy thú vị và đừng lo lắng nếu bạn không hiểu ngay lập tức. Sau đó, chỉ cần lặn trong một ngôn ngữ như Haskell là cách để đi. Monads là một trong những điều mà sự hiểu biết nhỏ giọt vào não của bạn bằng cách thực hành, một ngày bạn đột nhiên nhận ra bạn hiểu chúng.


-> là ứng dụng chức năng phản chiếu bên phải, liên kết bên trái, do đó việc bỏ dấu ngoặc đơn không tạo ra sự khác biệt ở đây.
Matthias Benkard

1
Tôi không nghĩ rằng đây là một lời giải thích rất tốt. Monads chỉ đơn giản là một cách? được không Tại sao tôi không gói gọn bằng cách sử dụng một lớp thay vì một đơn nguyên?
Breton

4
@ mb21: Trong trường hợp bạn chỉ ra rằng có quá nhiều dấu ngoặc, lưu ý rằng a-> b-> c thực sự chỉ là viết tắt của a -> (b-> c). Viết ví dụ cụ thể này là (a -> b) -> (Ta -> Tb) nói đúng chỉ là thêm các ký tự không cần thiết, nhưng về mặt đạo đức là "việc phải làm" vì nó nhấn mạnh rằng fmap ánh xạ chức năng của loại a -> b đến một hàm loại Ta -> Tb. Và ban đầu, đó là những gì functor làm trong lý thuyết thể loại và đó là nơi các đơn vị đến từ.
Nikolaj-K

1
Câu trả lời này là sai lệch. Một số đơn nguyên hoàn toàn không có "trình bao bọc", các hàm như vậy từ một giá trị cố định.

1
@DanMandel Monads là các mẫu thiết kế cung cấp trình bao bọc kiểu dữ liệu của riêng nó. Monads được thiết kế theo cách để mã soạn sẵn trừu tượng. Vì vậy, khi bạn gọi một Monad trong mã của mình, nó sẽ thực hiện những điều phía sau hậu trường mà bạn không muốn lo lắng. Hãy suy nghĩ về Nullable <T> hoặc IEnumerable <T>, họ làm gì đằng sau hậu trường? Đó là Monad.
sksallaj

168

Nhưng, bạn có thể đã phát minh ra Monads!

sigfpe nói:

Nhưng tất cả những điều này giới thiệu các đơn nguyên như một cái gì đó bí truyền cần được giải thích. Nhưng điều tôi muốn tranh luận là họ không bí truyền gì cả. Trong thực tế, đối mặt với các vấn đề khác nhau trong lập trình chức năng, bạn sẽ được dẫn dắt, một cách khó hiểu, với các giải pháp nhất định, tất cả đều là ví dụ về các đơn nguyên. Trên thực tế, tôi hy vọng sẽ giúp bạn phát minh ra chúng ngay bây giờ nếu bạn chưa có. Sau đó, một bước nhỏ để nhận thấy rằng tất cả các giải pháp này trên thực tế là cùng một giải pháp được ngụy trang. Và sau khi đọc nó, bạn có thể ở vị trí tốt hơn để hiểu các tài liệu khác về các đơn nguyên vì bạn sẽ nhận ra mọi thứ bạn thấy là thứ gì đó bạn đã phát minh ra.

Nhiều vấn đề mà các đơn vị cố gắng giải quyết có liên quan đến vấn đề tác dụng phụ. Vì vậy, chúng tôi sẽ bắt đầu với họ. (Lưu ý rằng các đơn vị cho phép bạn làm nhiều hơn là xử lý các tác dụng phụ, đặc biệt là nhiều loại đối tượng chứa có thể được xem là các đơn nguyên. cai khac.)

Trong một ngôn ngữ lập trình bắt buộc như C ++, các hàm hoạt động không giống như các hàm của toán học. Ví dụ: giả sử chúng ta có hàm C ++ nhận một đối số dấu phẩy động đơn và trả về kết quả dấu phẩy động. Nhìn bề ngoài, nó có vẻ giống như một hàm toán học ánh xạ các số thực thành các số thực, nhưng một hàm C ++ có thể làm nhiều hơn là chỉ trả về một số phụ thuộc vào các đối số của nó. Nó có thể đọc và ghi các giá trị của các biến toàn cục cũng như ghi đầu ra ra màn hình và nhận đầu vào từ người dùng. Tuy nhiên, trong một ngôn ngữ chức năng thuần túy, một hàm chỉ có thể đọc những gì được cung cấp cho nó trong các đối số của nó và cách duy nhất nó có thể có ảnh hưởng đến thế giới là thông qua các giá trị mà nó trả về.


9
Cách tốt nhất không chỉ trên internet, mà bất cứ nơi nào. (Monad giấy ban đầu của Wadler cho lập trình chức năng mà tôi đã đề cập trong câu trả lời của tôi dưới đây cũng tốt.) Không có gì trong số hàng trăm hướng dẫn tương tự đến gần.
ShreevatsaR

13
Bản dịch JavaScript này của bài đăng của Sigfpe là cách tốt nhất mới để tìm hiểu các đơn nguyên, cho những người chưa tìm hiểu về Haskell tiên tiến!
Sam Watkins

1
Đây là cách tôi học được một đơn vị là gì. Đưa người đọc đi qua quá trình phát minh ra một khái niệm thường là cách tốt nhất để dạy khái niệm này.
Jordan

Tuy nhiên, một hàm chấp nhận đối tượng màn hình làm đối số và trả về bản sao của nó với văn bản được sửa đổi sẽ là thuần túy.
Dmitri Zaitsev

87

Một đơn nguyên là một kiểu dữ liệu có hai hoạt động: >>=(aka bind) và return(aka unit). returnlấy một giá trị tùy ý và tạo một thể hiện của đơn nguyên với nó. >>=lấy một thể hiện của đơn nguyên và ánh xạ một chức năng lên nó. (Bạn có thể thấy đã có một đơn nguyên là một loại kỳ lạ của kiểu dữ liệu, vì trong hầu hết các ngôn ngữ lập trình bạn không thể viết một hàm mang theo một giá trị tùy ý và tạo ra một loại từ nó. Monads sử dụng một loại đa hình tham số .)

Trong ký hiệu Haskell, giao diện đơn nguyên được viết

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

Các hoạt động này được cho là tuân theo một số "luật" nhất định, nhưng điều đó không quan trọng lắm: "luật" chỉ mã hóa cách thức triển khai hợp lý của các hoạt động phải hành xử (về cơ bản, >>=returnphải đồng ý về cách các giá trị được chuyển đổi thành các trường hợp đơn nguyên và đó >>=là kết hợp).

Monads không chỉ là về trạng thái và I / O: chúng trừu tượng một mô hình tính toán chung bao gồm làm việc với trạng thái, I / O, ngoại lệ và không xác định. Có lẽ các đơn vị đơn giản nhất để hiểu là danh sách và các loại tùy chọn:

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

trong đó []:là các hàm tạo danh sách, ++là toán tử ghép và JustNothinglà các hàm Maybetạo. Cả hai đơn nguyên này đều gói gọn các mẫu tính toán phổ biến và hữu ích trên các loại dữ liệu tương ứng của chúng (lưu ý rằng không có bất cứ điều gì liên quan đến tác dụng phụ hoặc I / O).

Bạn thực sự phải chơi xung quanh việc viết một số mã Haskell không tầm thường để đánh giá cao những gì các đơn vị nói về và tại sao chúng hữu ích.


Chính xác ý bạn là gì khi "ánh xạ một chức năng qua nó"?
Casebash

Casebash, tôi đang cố tình không chính thức trong phần giới thiệu. Xem các ví dụ ở gần cuối để có ý nghĩa về "ánh xạ một hàm" đòi hỏi gì.
Chris Conway

3
Monad không phải là một kiểu dữ liệu. Đó là quy tắc soạn thảo các hàm: stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev

@DmitriZaitsev đã đúng, Monads thực sự cung cấp kiểu dữ liệu riêng của mình, kiểu dữ liệu không có Monads
sksallaj

78

Trước tiên bạn nên hiểu functor là gì. Trước đó, hiểu các hàm bậc cao hơn.

Hàm bậc cao hơn chỉ đơn giản là hàm lấy hàm làm đối số.

Một functor là bất kỳ kiểu xây dựng Tnào tồn tại hàm bậc cao hơn, gọi nó map, biến đổi một hàm kiểu a -> b(được cung cấp bất kỳ hai loại ab) thành một hàm T a -> T b. mapHàm này cũng phải tuân theo luật định danh và thành phần sao cho các biểu thức sau trả về đúng cho tất cả pq(ký hiệu Haskell):

map id = id
map (p . q) = map p . map q

Ví dụ, một hàm tạo kiểu được gọi Listlà functor nếu nó được trang bị một hàm loại (a -> b) -> List a -> List btuân theo các luật trên. Việc thực hiện duy nhất là rõ ràng. Hàm kết quả List a -> List blặp lại qua danh sách đã cho, gọi (a -> b)hàm cho từng phần tử và trả về danh sách kết quả.

Một đơn nguyên là về cơ bản chỉ là một functor Tvới hai phương pháp bổ sung, join, kiểu T (T a) -> T a, và unit(đôi khi được gọi return, forkhoặc pure) loại a -> T a. Đối với danh sách trong Haskell:

join :: [[a]] -> [a]
pure :: a -> [a]

Tại sao nó hữu ích? Bởi vì bạn có thể, ví dụ, mapqua một danh sách có chức năng trả về danh sách. Joinlấy danh sách kết quả của danh sách và nối chúng. Listlà một đơn nguyên bởi vì điều này là có thể.

Bạn có thể viết một hàm mà làm map, sau đó join. Hàm này được gọi bind, hoặc flatMap, hoặc (>>=), hoặc (=<<). Đây thường là cách một thể hiện đơn nguyên được đưa ra trong Haskell.

Một đơn nguyên phải đáp ứng một số luật nhất định, cụ thể là joinphải liên kết. Điều này có nghĩa là nếu bạn có một giá trị xcủa loại [[[a]]]thì join (join x)nên bằng nhau join (map join x). Và purephải là một bản sắc cho joinđiều đó join (pure x) == x.


3
thêm một chút vào def của 'hàm bậc cao hơn': chúng có thể thực hiện các hàm HOẶC TRẢ LẠI. Đó là lý do tại sao họ 'cao hơn' vì họ làm mọi thứ với chính mình.
Kevin Won

9
Theo định nghĩa đó, phép cộng là hàm bậc cao hơn. Nó nhận một số và trả về một hàm thêm số đó cho một số khác. Vì vậy, không, các hàm bậc cao hơn là các hàm nghiêm ngặt có miền bao gồm các hàm.
Apocalisp

Video ' Brian Beckman: Đừng sợ Monad ' theo cùng một dòng logic này.
icc97

48

[Tuyên bố miễn trừ trách nhiệm: Tôi vẫn đang cố gắng hoàn toàn mò mẫm các đơn nguyên. Sau đây chỉ là những gì tôi đã hiểu cho đến nay. Nếu nó sai, hy vọng ai đó có kiến ​​thức sẽ gọi tôi trên thảm.]

Arnar đã viết:

Monads chỉ đơn giản là một cách để bọc mọi thứ và cung cấp các phương thức để thực hiện các thao tác trên các công cụ được bọc mà không cần mở khóa.

Chính xác là như vậy. Ý tưởng diễn ra như sau:

  1. Bạn lấy một số loại giá trị và bọc nó với một số thông tin bổ sung. Giống như giá trị thuộc một loại nhất định (ví dụ: một số nguyên hoặc một chuỗi), vì vậy thông tin bổ sung là một loại nhất định.

    Ví dụ, thông tin bổ sung đó có thể là một Maybehoặc một IO.

  2. Sau đó, bạn có một số toán tử cho phép bạn thao tác trên dữ liệu được bọc trong khi mang theo thông tin bổ sung đó. Các toán tử này sử dụng thông tin bổ sung để quyết định cách thay đổi hành vi của thao tác trên giá trị được bọc.

    Ví dụ, a Maybe Intcó thể là một Just Inthoặc Nothing. Bây giờ, nếu bạn thêm a Maybe Intvào a Maybe Int, toán tử sẽ kiểm tra xem liệu cả hai có Just Intở bên trong không, và nếu vậy, sẽ mở khóa Ints, chuyển cho chúng toán tử bổ sung, bọc lại kết quả Intthành một cái mới Just Int(hợp lệ Maybe Int), và do đó trả về a Maybe Int. Nhưng nếu một trong số họ là một Nothingbên trong, nhà điều hành này sẽ ngay lập tức trở lại Nothing, một lần nữa là hợp lệ Maybe Int. Bằng cách đó, bạn có thể giả vờ rằng số của bạn Maybe Intchỉ là những số bình thường và thực hiện phép toán thông thường trên chúng. Nếu bạn đã có được một Nothing, phương trình của bạn vẫn sẽ tạo ra kết quả đúng - mà không cần bạn phải kiểm tra rác Nothingở mọi nơi .

Nhưng ví dụ chỉ là những gì xảy ra cho Maybe. Nếu thông tin bổ sung là một IO, thì toán tử đặc biệt được xác định cho IOs sẽ được gọi thay thế và nó có thể làm điều gì đó hoàn toàn khác trước khi thực hiện bổ sung. (OK, cộng hai số IO Intlại với nhau có lẽ là vô nghĩa - Tôi chưa chắc chắn.) (Ngoài ra, nếu bạn chú ý đến Maybeví dụ này, bạn đã nhận thấy rằng Gói bọc một giá trị với các công cụ bổ sung không phải lúc nào cũng đúng. Nhưng thật khó chính xác, chính xác và chính xác mà không thể hiểu được.)

Về cơ bản, tiếng Pháp đơn giản là có nghĩa là mô hình của người Bỉ . Nhưng thay vì một cuốn sách đầy đủ các mẫu được giải thích không chính thức và được đặt tên cụ thể, giờ đây bạn có cấu trúc ngôn ngữ - cú pháp và tất cả - cho phép bạn khai báo các mẫu mới như những thứ trong chương trình của bạn . (Sự thiếu chính xác ở đây là tất cả các mẫu phải tuân theo một hình thức cụ thể, do đó, một đơn nguyên không hoàn toàn chung chung như một mẫu. Nhưng tôi nghĩ đó là thuật ngữ gần nhất mà hầu hết mọi người biết và hiểu.)

Và đó là lý do tại sao mọi người tìm thấy các đơn nguyên rất khó hiểu: bởi vì chúng là một khái niệm chung chung như vậy. Để hỏi những gì làm cho một cái gì đó một đơn nguyên tương tự mơ hồ như hỏi những gì làm cho một cái gì đó một mô hình.

Nhưng hãy nghĩ đến ý nghĩa của việc hỗ trợ cú pháp trong ngôn ngữ cho ý tưởng về một mẫu: thay vì phải đọc cuốn sách Gang of Four và ghi nhớ việc xây dựng một mẫu cụ thể, bạn chỉ cần viết mã thực hiện mô hình này theo thuyết bất khả tri, cách chung chung một lần và sau đó bạn đã hoàn tất! Sau đó, bạn có thể sử dụng lại mẫu này, như Khách truy cập hoặc Chiến lược hoặc Mặt tiền hoặc bất cứ điều gì, chỉ bằng cách trang trí các hoạt động trong mã của bạn với nó, mà không phải thực hiện lại nhiều lần!

Vì vậy, đó là lý do tại sao những người hiểu về các đơn vị tìm thấy chúng rất hữu ích : đó không phải là một khái niệm tháp ngà mà trí tuệ hợm hĩnh tự hào về sự hiểu biết (tất nhiên, điều đó cũng vậy, teehee), nhưng thực sự làm cho mã đơn giản hơn.


12
Đôi khi một lời giải thích từ một "người học" (như bạn) có liên quan nhiều hơn đến một người học khác hơn là một lời giải thích đến từ một chuyên gia. Người học cũng nghĩ như vậy :)
Adrian

Điều làm cho một cái gì đó là một đơn nguyên là sự tồn tại của một chức năng với loại M (M a) -> M a. Thực tế là bạn có thể biến nó thành một trong những loại M a -> (a -> M b) -> M blà những gì làm cho chúng hữu ích.
Danh sách Jeremy

"Đơn nguyên" đại khái có nghĩa là "mẫu" ... không.
Cảm ơn bạn

44

Sau nhiều phấn đấu, tôi nghĩ cuối cùng tôi cũng hiểu được đơn nguyên. Sau khi đọc lại bài phê bình dài dòng của riêng tôi về câu trả lời được bình chọn hàng đầu, tôi sẽ đưa ra lời giải thích này.

Có ba câu hỏi cần được trả lời để hiểu các đơn nguyên:

  1. Tại sao bạn cần một đơn nguyên?
  2. Một đơn nguyên là gì?
  3. Làm thế nào là một đơn nguyên được thực hiện?

Như tôi đã lưu ý trong các nhận xét ban đầu của mình, quá nhiều lời giải thích đơn nguyên bị cuốn vào câu hỏi số 3, mà không, và trước khi thực sự bao gồm đầy đủ câu hỏi 2, hoặc câu hỏi 1.

Tại sao bạn cần một đơn nguyên?

Các ngôn ngữ chức năng thuần túy như Haskell khác với các ngôn ngữ bắt buộc như C hoặc Java ở chỗ, một chương trình chức năng thuần túy không nhất thiết phải được thực hiện theo một thứ tự cụ thể, từng bước một. Một chương trình Haskell gần giống với một hàm toán học, trong đó bạn có thể giải "phương trình" trong bất kỳ số lượng đơn hàng tiềm năng nào. Điều này mang lại một số lợi ích, trong số đó là loại bỏ khả năng của một số loại lỗi nhất định, đặc biệt là những lỗi liên quan đến những thứ như "trạng thái".

Tuy nhiên, có một số vấn đề không đơn giản để giải quyết với phong cách lập trình này. Một số thứ, như lập trình giao diện điều khiển và tập tin i / o, cần những thứ xảy ra theo một thứ tự cụ thể hoặc cần duy trì trạng thái. Một cách để giải quyết vấn đề này là tạo ra một loại đối tượng đại diện cho trạng thái tính toán và một loạt các hàm lấy một đối tượng trạng thái làm đầu vào và trả về một đối tượng trạng thái được sửa đổi mới.

Vì vậy, hãy tạo một giá trị "trạng thái" giả định, đại diện cho trạng thái của màn hình giao diện điều khiển. chính xác cách thức giá trị này được xây dựng không quan trọng, nhưng giả sử đó là một mảng các ký tự ascii có độ dài byte đại diện cho những gì hiện có thể nhìn thấy trên màn hình và một mảng biểu thị dòng đầu vào cuối cùng được nhập bởi người dùng, bằng mã giả. Chúng tôi đã xác định một số chức năng có trạng thái giao diện điều khiển, sửa đổi nó và trả về trạng thái giao diện điều khiển mới.

consolestate MyConsole = new consolestate;

Vì vậy, để thực hiện lập trình giao diện điều khiển, nhưng theo cách thức chức năng thuần túy, bạn sẽ cần lồng rất nhiều lệnh gọi hàm bên trong nhau.

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

Lập trình theo cách này giữ cho phong cách chức năng "thuần túy", trong khi buộc các thay đổi đối với bàn điều khiển xảy ra theo một thứ tự cụ thể. Nhưng, có lẽ chúng ta sẽ muốn thực hiện nhiều hơn chỉ một vài thao tác tại một thời điểm như trong ví dụ trên. Các chức năng lồng nhau theo cách đó sẽ bắt đầu trở nên vô duyên. Những gì chúng ta muốn, là mã về cơ bản giống như trên, nhưng được viết giống như thế này một chút:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

Đây thực sự sẽ là một cách thuận tiện hơn để viết nó. Làm thế nào để chúng ta làm điều đó mặc dù?

Một đơn nguyên là gì?

Khi bạn có một loại (chẳng hạn như consolestate) mà bạn xác định cùng với một loạt các hàm được thiết kế riêng để hoạt động trên loại đó, bạn có thể biến toàn bộ gói của những thứ này thành một "đơn nguyên" bằng cách xác định một toán tử như :(liên kết) tự động cung cấp các giá trị trả về bên trái của nó, vào các tham số chức năng ở bên phải và lifttoán tử biến các hàm bình thường thành các hàm hoạt động với loại toán tử liên kết cụ thể đó.

Làm thế nào là một đơn nguyên được thực hiện?

Xem các câu trả lời khác, có vẻ như khá tự do để nhảy vào chi tiết về điều đó.


Trình tự không phải là lý do duy nhất để xác định một đơn nguyên. Một đơn nguyên chỉ là bất kỳ functor nào đã ràng buộc và trở lại. Ràng buộc và trả lại cho bạn trình tự. Nhưng họ cũng cho những thứ khác. Ngoài ra, lưu ý rằng ngôn ngữ mệnh lệnh yêu thích của bạn thực sự là một đơn vị IO ưa thích với các lớp OO. Làm cho nó dễ dàng xác định các đơn vị có nghĩa là dễ dàng sử dụng mẫu trình thông dịch - xác định một dsl là một đơn vị và giải thích nó!
Tên của


38

Sau khi đưa ra câu trả lời cho câu hỏi này vài năm trước, tôi tin rằng tôi có thể cải thiện và đơn giản hóa câu trả lời đó bằng ...

Một đơn vị là một kỹ thuật thành phần chức năng để xử lý bên ngoài đối với một số tình huống đầu vào bằng cách sử dụng chức năng soạn thảo bind, để xử lý trước đầu vào trong quá trình sáng tác.

Trong thành phần bình thường, hàm, compose (>>)được sử dụng để áp dụng hàm tổng hợp cho kết quả của tiền thân của nó theo trình tự. Điều quan trọng, chức năng được cấu thành là cần thiết để xử lý tất cả các kịch bản đầu vào của nó.

(x -> y) >> (y -> z)

Thiết kế này có thể được cải thiện bằng cách cơ cấu lại đầu vào để các trạng thái liên quan dễ dàng được thẩm vấn hơn. Vì vậy, thay vì chỉ đơn giản ylà giá trị có thể trở thành Mb, chẳng hạn, (is_OK, b)nếu ybao gồm một khái niệm về tính hợp lệ.

Ví dụ, khi đầu vào chỉ có thể là một số, thay vì trả về một chuỗi có thể chứa một số hay không, bạn có thể cấu trúc lại loại thành một số boolcho thấy sự hiện diện của một số hợp lệ và một số trong bộ dữ liệu, chẳng hạn như , bool * float. Các hàm tổng hợp giờ đây sẽ không còn cần phân tích chuỗi đầu vào để xác định xem một số có tồn tại hay không mà chỉ có thể kiểm tra boolphần của một tuple.

(Ma -> Mb) >> (Mb -> Mc)

Ở đây, một lần nữa, thành phần xảy ra một cách tự nhiên với composevà do đó, mỗi chức năng phải xử lý tất cả các kịch bản đầu vào của nó, mặc dù làm như vậy bây giờ dễ dàng hơn nhiều.

Tuy nhiên, điều gì sẽ xảy ra nếu chúng ta có thể ngoại trừ nỗ lực thẩm vấn trong những thời điểm mà việc xử lý một kịch bản là thường lệ. Ví dụ, nếu chương trình của chúng tôi không làm gì khi đầu vào là không OK như trong khi is_OKfalse. Nếu điều đó đã được thực hiện thì các hàm tổng hợp sẽ không cần phải tự xử lý kịch bản đó, đơn giản hóa đáng kể mã của chúng và thực hiện một mức độ tái sử dụng khác.

Để đạt được sự xuất hiện này, chúng ta có thể sử dụng một hàm bind (>>=), để thực hiện compositionthay vì compose. Như vậy, thay vì chỉ đơn giản chuyển các giá trị từ đầu ra của một chức năng sang đầu vào của một chức năng khác Bindsẽ kiểm tra Mphần Mavà quyết định xem và làm thế nào để áp dụng chức năng tổng hợp vào a. Tất nhiên, chức năng bindsẽ được xác định cụ thể cho riêng chúng tôi Mđể có thể kiểm tra cấu trúc của nó và thực hiện bất kỳ loại ứng dụng nào chúng tôi muốn. Tuy nhiên, acó thể là bất cứ điều gì vì bindchỉ đơn thuần chuyển giao không bị ảnh hưởng ađến hàm tổng hợp khi xác định ứng dụng cần thiết. Ngoài ra, các hàm tổng hợp không còn cần phải xử lýMmột phần của cấu trúc đầu vào, đơn giản hóa chúng. Vì thế...

(a -> Mb) >>= (b -> Mc) hoặc ngắn gọn hơn Mb >>= (b -> Mc)

Nói tóm lại, một đơn nguyên xuất hiện và do đó cung cấp hành vi tiêu chuẩn xung quanh việc xử lý các kịch bản đầu vào nhất định một khi đầu vào được thiết kế để phơi bày chúng đủ. Thiết kế này là một shell and contentmô hình trong đó trình bao chứa dữ liệu liên quan đến ứng dụng của hàm tổng hợp và được thẩm vấn bởi và chỉ có sẵn cho bindhàm.

Do đó, một đơn nguyên là ba điều:

  1. một Mvỏ để chứa thông tin liên quan đơn nguyên,
  2. một bindhàm được triển khai để sử dụng thông tin shell này trong ứng dụng của các hàm tổng hợp vào (các) giá trị nội dung mà nó tìm thấy trong shell và
  3. chức năng tổng hợp của biểu mẫu a -> Mb, tạo ra kết quả bao gồm dữ liệu quản lý đơn âm.

Nói chung, đầu vào của một chức năng hạn chế hơn nhiều so với đầu ra của nó có thể bao gồm những thứ như điều kiện lỗi; do đó, Mbcấu trúc kết quả thường rất hữu ích. Chẳng hạn, toán tử phép chia không trả về một số khi ước số là 0.

Ngoài ra, monads có thể bao gồm các hàm bọc bao bọc các giá trị, avào loại đơn nguyên Mavà các hàm chung a -> b, thành các hàm đơn trị a -> Mb, bằng cách gói kết quả của chúng sau khi ứng dụng. Tất nhiên, như bind, chức năng bọc như vậy là cụ thể M. Một ví dụ:

let return a = [a]
let lift f a = return (f a)

Thiết kế của bindhàm giả định cấu trúc dữ liệu bất biến và các hàm thuần túy, những thứ khác trở nên phức tạp và đảm bảo không thể được thực hiện. Như vậy, có luật đơn nguyên:

Được...

M_ 
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)

Sau đó...

Left Identity  : (return a) >>= f === f a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativitycó nghĩa là bindbảo tồn thứ tự đánh giá bất kể khi nào bindđược áp dụng. Đó là, trong định nghĩa của Associativitytrên, lực lượng đánh giá ban đầu của ngoặc bindingcủa fgsẽ chỉ dẫn đến một chức năng mà hy vọng Mađể hoàn thành bind. Do đó việc đánh giá Maphải được xác định trước khi giá trị của nó có thể được áp dụng fvà kết quả đó lần lượt được áp dụng g.


"... nhưng tôi hy vọng những người khác thấy nó hữu ích" nó thực sự hữu ích cho tôi, bất chấp tất cả các câu được nhấn mạnh: D

Đây là lời giải thích ngắn gọn và rõ ràng nhất về các đơn nguyên tôi từng đọc / xem / nghe. Cảm ơn bạn!
James

Có sự khác biệt quan trọng giữa Monad và Monoid. Monad là một quy tắc để "soạn thảo" các chức năng giữa các loại khác nhau, vì vậy chúng không tạo thành một hoạt động nhị phân theo yêu cầu cho Monoids, xem tại đây để biết thêm chi tiết: stackoverflow.com/questions/2704652/iêu
Dmitri Zaitsev

Đúng. Bạn nói đúng. Bài viết của bạn đã qua đầu tôi :). Tuy nhiên, tôi thấy cách điều trị này rất hữu ích (và thêm nó vào tôi như một hướng đi cho người khác). Cảm ơn những người đứng đầu của bạn: stackoverflow.com/a/7829607/1612190
George

2
Bạn có thể đã nhầm lẫn lý thuyết nhóm Đại số với lý thuyết Danh mục nơi Monad đến từ. Trước đây là lý thuyết về các nhóm đại số, không liên quan.
Dmitri Zaitsev

37

Một đơn vị, một cách hiệu quả, là một dạng "toán tử loại". Nó sẽ làm ba việc. Đầu tiên, nó sẽ "bọc" (hoặc chuyển đổi khác) một giá trị của một loại thành một loại khác (thường được gọi là "loại đơn nguyên"). Thứ hai, nó sẽ làm cho tất cả các hoạt động (hoặc chức năng) có sẵn trên loại cơ bản có sẵn trên loại đơn âm. Cuối cùng, nó sẽ cung cấp hỗ trợ cho việc kết hợp bản thân của nó với một đơn nguyên khác để tạo ra một đơn nguyên hỗn hợp.

"Có thể đơn nguyên" về cơ bản là tương đương với "loại không thể" trong Visual Basic / C #. Nó nhận một loại "T" không thể rỗng và chuyển đổi nó thành "Không thể <T>", và sau đó xác định tất cả các toán tử nhị phân có nghĩa là gì trên Nullable <T>.

Tác dụng phụ được đại diện cho simillarly. Một cấu trúc được tạo ra chứa các mô tả về tác dụng phụ bên cạnh giá trị trả về của hàm. Các hoạt động "nâng" sau đó sao chép xung quanh các hiệu ứng phụ khi các giá trị được truyền giữa các hàm.

Chúng được gọi là "đơn nguyên" thay vì tên dễ hiểu hơn của "toán tử loại" vì nhiều lý do:

  1. Các đơn vị có những hạn chế về những gì họ có thể làm (xem definiton để biết chi tiết).
  2. Những hạn chế đó, cùng với thực tế là có ba hoạt động liên quan, phù hợp với cấu trúc của một thứ gọi là đơn nguyên trong Lý thuyết loại, là một nhánh tối nghĩa của toán học.
  3. Chúng được thiết kế bởi những người đề xuất các ngôn ngữ chức năng "thuần túy"
  4. Những người đề xuất các ngôn ngữ chức năng thuần túy như các nhánh toán học tối nghĩa
  5. Bởi vì toán học tối nghĩa và các đơn nguyên được liên kết với các phong cách lập trình cụ thể, mọi người có xu hướng sử dụng từ đơn nguyên như một kiểu bắt tay bí mật. Bởi vì điều này không ai bận tâm đầu tư vào một cái tên tốt hơn.

1
Các đơn vị không được 'thiết kế', chúng được áp dụng từ một miền (lý thuyết danh mục) sang một miền khác (I / O trong các ngôn ngữ lập trình chức năng thuần túy). Có phải Newton đã 'thiết kế' tính toán?
Jared Updike

1
Điểm 1 và 2 ở trên là chính xác và hữu ích. Điểm 4 và 5 là loại vượn quảng cáo, ngay cả khi ít nhiều đúng sự thật. Họ không thực sự giúp giải thích các đơn nguyên.
Jared Updike

13
Re: 4, 5: Điều "Bắt tay bí mật" là cá trích đỏ. Lập trình đầy thuật ngữ. Haskell chỉ tình cờ gọi công cụ đó là gì mà không giả vờ khám phá lại một cái gì đó. Nếu nó tồn tại trong toán học, tại sao lại tạo nên một tên mới cho nó? Tên này thực sự không phải là lý do mọi người không nhận được đơn nguyên; chúng là một khái niệm tinh tế Một người bình thường có thể hiểu được phép cộng và phép nhân, tại sao họ không có khái niệm về một nhóm Abelian? Bởi vì nó trừu tượng và chung chung hơn và người đó đã không hoàn thành công việc để xoay quanh khái niệm này. Thay đổi tên sẽ không giúp được gì.
Jared Updike

16
Thở dài ... Tôi không thực hiện một cuộc tấn công vào Haskell ... Tôi đang làm một trò đùa. Vì vậy, tôi không thực sự hiểu chút gì về "hominem quảng cáo". Có, tính toán đã được "thiết kế". Đó là lý do tại sao, ví dụ, sinh viên tính toán được dạy ký hiệu Leibniz, thay vì những thứ kỳ quái mà Netwton sử dụng. Thiết kế tốt hơn. Tên tốt giúp hiểu rất nhiều. Nếu tôi gọi Nhóm Abelian là "vỏ nhăn", bạn có thể gặp khó khăn khi hiểu tôi. Bạn có thể nói "nhưng tên đó là vô nghĩa", không ai sẽ gọi họ như vậy. Đối với những người chưa bao giờ nghe về lý thuyết thể loại "đơn nguyên" nghe có vẻ vô nghĩa.
Scott Wisniewski

4
@Scott: xin lỗi nếu những bình luận sâu rộng của tôi làm cho có vẻ như tôi đang phòng thủ về Haskell. Tôi thích sự hài hước của bạn về cái bắt tay bí mật và bạn sẽ lưu ý rằng tôi đã nói nó ít nhiều đúng. : - là, nhưng không phải "những thứ mờ ấm" là gì (hay "biểu thức tính toán"). Nếu tôi hiểu việc bạn sử dụng thuật ngữ "toán tử loại" một cách chính xác, có rất nhiều toán tử loại khác ngoài các đơn vị.
Jared Updike

35

(Xem thêm câu trả lời tại Đơn vị là gì? )

Một động lực tốt cho Monads là sigfpe (Dan Piponi) Bạn có thể đã phát minh ra Monads! (Và có lẽ bạn đã có) . Có rất nhiều hướng dẫn đơn nguyên khác , nhiều trong số đó cố tình giải thích các đơn nguyên theo "thuật ngữ đơn giản" bằng cách sử dụng các phép loại suy khác nhau: đây là sai lầm hướng dẫn đơn nguyên ; tránh chúng

Như DR MacIver nói trong Hãy cho chúng tôi biết lý do tại sao ngôn ngữ của bạn tệ :

Vì vậy, những điều tôi ghét về Haskell:

Hãy bắt đầu với điều hiển nhiên. Monad hướng dẫn. Không, không phải đơn nguyên. Cụ thể là hướng dẫn. Họ là vô tận, thừa thãi và thần thân yêu là họ tẻ nhạt. Hơn nữa, tôi chưa bao giờ thấy bất kỳ bằng chứng thuyết phục nào cho thấy họ thực sự giúp đỡ. Đọc định nghĩa lớp, viết một số mã, vượt qua cái tên đáng sợ.

Bạn nói bạn hiểu có lẽ đơn nguyên? Tốt, bạn đang trên đường. Chỉ cần bắt đầu sử dụng các đơn nguyên khác và sớm hay muộn bạn sẽ hiểu những gì các đơn vị nói chung.

[Nếu bạn định hướng toán học, bạn có thể muốn bỏ qua hàng tá hướng dẫn và tìm hiểu định nghĩa hoặc theo dõi các bài giảng trong lý thuyết thể loại :) Phần chính của định nghĩa là Monad M bao gồm một "hàm tạo kiểu" định nghĩa cho mỗi loại hiện tại "T" một loại "MT" mới và một số cách để qua lại giữa các loại "thông thường" và loại "M".]

Ngoài ra, thật đáng ngạc nhiên, một trong những lời giới thiệu tốt nhất cho các đơn vị thực sự là một trong những bài báo học thuật đầu tiên giới thiệu các đơn vị, Monads của Philip Wadler cho lập trình chức năng . Nó thực sự có các ví dụ động lực thực tế, không tầm thường , không giống như nhiều hướng dẫn nhân tạo ngoài kia.


2
Vấn đề duy nhất với bài báo của Wadler là ký hiệu khác nhau nhưng tôi đồng ý rằng bài báo này khá hấp dẫn và một động lực ngắn gọn rõ ràng để áp dụng các đơn nguyên.
Jared Updike

+1 cho "ngụy biện hướng dẫn đơn nguyên". Hướng dẫn về các đơn nguyên giống như có một số hướng dẫn cố gắng giải thích khái niệm số nguyên. Một hướng dẫn sẽ nói, "1 tương tự như một quả táo"; một hướng dẫn khác nói, "2 giống như một quả lê"; người thứ ba nói, "3 về cơ bản là một quả cam". Nhưng bạn không bao giờ có được toàn bộ hình ảnh từ bất kỳ hướng dẫn duy nhất. Những gì tôi đã rút ra từ đó là các đơn nguyên là một khái niệm trừu tượng có thể được sử dụng cho nhiều mục đích khá khác nhau.
stakx - không còn đóng góp vào

@stakx: Vâng, đúng. Nhưng tôi không có nghĩa là các đơn nguyên là một sự trừu tượng mà bạn không thể học hoặc không nên học; chỉ có điều tốt nhất là học nó sau khi bạn đã thấy đủ các ví dụ cụ thể để nhận biết một sự trừu tượng hóa cơ bản duy nhất. Xem câu trả lời khác của tôi ở đây .
ShreevatsaR

5
Đôi khi tôi cảm thấy rằng có rất nhiều hướng dẫn cố gắng thuyết phục người đọc rằng các đơn nguyên là hữu ích bằng cách sử dụng mã làm những thứ phức tạp hoặc hữu ích. Điều đó cản trở sự hiểu biết của tôi trong nhiều tháng. Tôi không học theo cách đó. Tôi thích nhìn thấy mã cực kỳ đơn giản, làm điều gì đó ngu ngốc mà tôi có thể trải qua và tôi không thể tìm thấy loại ví dụ này. Tôi không thể học nếu ví dụ đầu tiên là một đơn vị phân tích ngữ pháp phức tạp. Tôi có thể tìm hiểu nếu đó là một đơn nguyên để tính tổng.
Rafael S. Calsaverini

Đề cập đến trình xây dựng kiểu không đầy đủ: stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev

23

Monads là để kiểm soát luồng dữ liệu trừu tượng là gì đối với dữ liệu.

Nói cách khác, nhiều nhà phát triển thoải mái với ý tưởng về Bộ, Danh sách, Từ điển (hoặc Băm hoặc Bản đồ) và Cây. Trong các kiểu dữ liệu đó, có nhiều trường hợp đặc biệt (ví dụ: InsertsOrderPreservingIdentityHashMap).

Tuy nhiên, khi đối mặt với "dòng chảy" của chương trình, nhiều nhà phát triển đã không được tiếp xúc với nhiều cấu trúc hơn so với khi, switch / case, do, while, goto (grr) và (có thể) đóng.

Vì vậy, một đơn vị chỉ đơn giản là một cấu trúc dòng điều khiển. Một cụm từ tốt hơn để thay thế đơn nguyên sẽ là 'loại kiểm soát'.

Như vậy, một đơn vị có các khe cho logic điều khiển hoặc các câu lệnh hoặc hàm - tương đương trong cấu trúc dữ liệu sẽ nói rằng một số cấu trúc dữ liệu cho phép bạn thêm dữ liệu và xóa dữ liệu đó.

Ví dụ: đơn vị "nếu":

if( clause ) then block

đơn giản nhất có hai vị trí - mệnh đề và khối. Các ifđơn nguyên thường được xây dựng để đánh giá kết quả của mệnh đề và nếu không sai, hãy đánh giá khối. Nhiều nhà phát triển không được giới thiệu về các đơn nguyên khi họ học 'nếu' và không cần thiết phải hiểu các đơn vị để viết logic hiệu quả.

Các đơn nguyên có thể trở nên phức tạp hơn, giống như cách các cấu trúc dữ liệu có thể trở nên phức tạp hơn, nhưng có nhiều loại đơn nguyên có thể có ngữ nghĩa tương tự, nhưng khác nhau về cách triển khai và cú pháp.

Tất nhiên, theo cùng một cách mà các cấu trúc dữ liệu có thể được lặp đi lặp lại hoặc đi qua, các đơn nguyên có thể được đánh giá.

Trình biên dịch có thể có hoặc không có hỗ trợ cho các đơn vị do người dùng xác định. Haskell chắc chắn làm. Ioke có một số khả năng tương tự, mặc dù thuật ngữ monad không được sử dụng trong ngôn ngữ.


14

Hướng dẫn Monad yêu thích của tôi:

http://www.haskell.org/haskellwiki/ ALL_About_Monads

(trong số 170.000 lượt truy cập trên một tìm kiếm của Google cho "hướng dẫn đơn nguyên"!)

@Stu: Quan điểm của các đơn nguyên là cho phép bạn thêm (thường) ngữ nghĩa tuần tự vào mã thuần; thậm chí bạn có thể soạn các đơn nguyên (sử dụng Monad Transformers) và nhận được các ngữ nghĩa kết hợp phức tạp và thú vị hơn, chẳng hạn như phân tích cú pháp với xử lý lỗi, trạng thái chia sẻ và ghi nhật ký, chẳng hạn. Tất cả điều này có thể ở mã thuần, các đơn vị chỉ cho phép bạn trừu tượng hóa nó và sử dụng lại nó trong các thư viện mô-đun (luôn luôn tốt trong lập trình), cũng như cung cấp cú pháp thuận tiện để làm cho nó trông bắt buộc.

Haskell đã quá tải toán tử [1]: nó sử dụng các lớp loại theo cách người ta có thể sử dụng các giao diện trong Java hoặc C # nhưng Haskell chỉ tình cờ cho phép các mã thông báo không phải là chữ và số như + && và> làm định danh infix. Đó chỉ là toán tử quá tải theo cách bạn nhìn vào nó nếu bạn có nghĩa là "quá tải dấu chấm phẩy" [2]. Nghe có vẻ như ma thuật đen và yêu cầu rắc rối để "làm quá tải dấu chấm phẩy" (hình ảnh tin tặc Perl nổi hứng với ý tưởng này) nhưng vấn đề là không có monad thì không có dấu chấm phẩy, vì mã đơn thuần không yêu cầu hoặc cho phép trình tự rõ ràng.

Tất cả điều này nghe có vẻ phức tạp hơn nhiều so với nó cần. Bài viết của sigfpe khá tuyệt nhưng sử dụng Haskell để giải thích nó, điều này không thể phá vỡ vấn đề về gà và trứng khi hiểu Haskell để mò mẫm Monads và hiểu Monads để mò mẫm Haskell.

[1] Đây là một vấn đề riêng biệt với các đơn vị nhưng các đơn vị sử dụng tính năng nạp chồng toán tử của Haskell.

[2] Đây cũng là một sự đơn giản hóa vì toán tử thực hiện các hành động đơn âm là >> = (phát âm là "liên kết") nhưng có đường cú pháp ("làm") cho phép bạn sử dụng dấu ngoặc nhọn và dấu chấm phẩy và / hoặc thụt lề và dòng mới.


9

Gần đây tôi đã nghĩ về Monads theo một cách khác. Tôi đã nghĩ về chúng như trừu tượng hóa thứ tự thực hiện theo cách toán học, điều này làm cho các loại đa hình mới có thể.

Nếu bạn đang sử dụng một ngôn ngữ bắt buộc và bạn viết một số biểu thức theo thứ tự, mã LUÔN LUÔN chạy chính xác theo thứ tự đó.

Và trong trường hợp đơn giản, khi bạn sử dụng một đơn nguyên, nó cũng có cảm giác tương tự - bạn xác định một danh sách các biểu thức xảy ra theo thứ tự. Ngoại trừ điều đó, tùy thuộc vào đơn vị bạn sử dụng, mã của bạn có thể chạy theo thứ tự (như trong IO monad), song song trên một số mục cùng một lúc (như trong Danh sách đơn nguyên), nó có thể dừng giữa chừng (như trong Đơn vị có thể) , nó có thể tạm dừng giữa chừng để được tiếp tục lại sau đó (như trong đơn vị Tiếp tục), nó có thể tua lại và bắt đầu lại từ đầu (như trong Đơn vị giao dịch) hoặc có thể tua lại giữa chừng để thử các tùy chọn khác (như trong đơn vị Logic) .

Và vì các đơn nguyên là đa hình, nên có thể chạy cùng một mã trong các đơn vị khác nhau, tùy thuộc vào nhu cầu của bạn.

Ngoài ra, trong một số trường hợp, có thể kết hợp các đơn nguyên với nhau (với máy biến áp đơn nguyên) để có được nhiều tính năng cùng một lúc.


9

Tôi vẫn chưa quen với các đơn nguyên, nhưng tôi nghĩ rằng tôi sẽ chia sẻ một liên kết mà tôi thấy rằng nó thực sự tốt để đọc (VỚI HÌNH ẢNH !!): http://www.matusiak.eu/numerodix/blog/2012/3/11/ monads-for-the-layman / (không liên kết)

Về cơ bản, khái niệm ấm áp và mờ mà tôi có được từ bài viết là khái niệm rằng các đơn nguyên về cơ bản là các bộ điều hợp cho phép các hàm khác nhau hoạt động theo kiểu có thể ghép được, tức là có thể xâu chuỗi nhiều chức năng và trộn và khớp với chúng mà không phải lo lắng về sự trở lại không nhất quán các loại và như vậy. Vì vậy, chức năng BIND chịu trách nhiệm giữ táo bằng táo và cam bằng cam khi chúng tôi đang cố gắng tạo ra các bộ điều hợp này. Và chức năng LIFT chịu trách nhiệm thực hiện các chức năng "cấp thấp hơn" và "nâng cấp" chúng để hoạt động với các chức năng BIND và cũng có thể ghép lại được.

Tôi hy vọng tôi đã hiểu đúng, và quan trọng hơn, hy vọng rằng bài viết có quan điểm hợp lệ về các đơn nguyên. Nếu không có gì khác, bài viết này đã giúp tôi thèm ăn để tìm hiểu thêm về các đơn nguyên.


Các ví dụ python làm cho nó dễ hiểu! Cám ơn vì đã chia sẻ.
Ryan Efendy

8

Ngoài các câu trả lời xuất sắc ở trên, tôi xin cung cấp cho bạn một liên kết đến bài viết sau (của Patrick Thomson) giải thích các đơn nguyên bằng cách liên kết khái niệm với thư viện JavaScript jQuery (và cách sử dụng "chuỗi phương thức" để thao tác DOM) : jQuery là một đơn nguyên

Bản thân tài liệu jQuery không đề cập đến thuật ngữ "đơn nguyên" mà nói về "mẫu xây dựng" có lẽ quen thuộc hơn. Điều này không thay đổi thực tế rằng bạn có một đơn nguyên thích hợp ở đó có thể mà không hề nhận ra.


Nếu bạn sử dụng jQuery, lời giải thích này có thể rất hữu ích, đặc biệt là nếu Haskell của bạn không mạnh
byteclub

10
JQuery rõ ràng không phải là một đơn nguyên. Các bài viết liên kết là sai.
Tony Morris

1
Là "nhấn mạnh" không phải là rất thuyết phục. Đối với một số cuộc thảo luận hữu ích về chủ đề này, hãy xem jQuery có phải là một đơn vị - Stack Overflow
nealmcb 25/03/13

1
Xem thêm Google Talk Monads và Gonads của Douglas Crackford và mã Javascript của anh ấy để thực hiện các bản mod, mở rộng về hành vi tương tự của các thư viện AJAX và Promise
nealmcb


7

Một đơn nguyên là một cách kết hợp các tính toán với nhau để chia sẻ một bối cảnh chung. Nó giống như xây dựng một mạng lưới đường ống. Khi xây dựng mạng, không có dữ liệu chảy qua mạng. Nhưng khi tôi đã hoàn thành việc ghép tất cả các bit cùng với 'liên kết' và 'trở lại' thì tôi gọi một cái gì đó giống như runMyMonad monad datavà dữ liệu chảy qua các đường ống.


1
Điều đó giống như Applicative hơn Monad. Với Monads, bạn phải lấy dữ liệu từ các đường ống trước khi bạn có thể chọn đường ống tiếp theo để kết nối.
Peaker

vâng, bạn mô tả Ứng dụng, không phải là Monad. Monad là, xây dựng đoạn ống tiếp theo tại chỗ, tùy thuộc vào dữ liệu đạt đến điểm đó, bên trong đường ống.
Will Ness

6

Trong thực tế, monad là một triển khai tùy chỉnh của toán tử thành phần chức năng, đảm nhiệm các tác dụng phụ và các giá trị đầu vào và trả về không tương thích (đối với chuỗi).


5

Nếu tôi hiểu chính xác, IEnumerable có nguồn gốc từ các đơn nguyên. Tôi tự hỏi nếu đó có thể là một góc tiếp cận thú vị cho những người trong chúng ta từ thế giới C #?

Đối với những gì nó có giá trị, đây là một số liên kết đến các hướng dẫn đã giúp tôi (và không, tôi vẫn chưa hiểu các đơn vị là gì).


5

Hai điều giúp tôi tốt nhất khi tìm hiểu về đó là:

Chương 8, "Trình phân tích chức năng", từ Lập trình sách của Graham Hutton ở Haskell . Trên thực tế, điều này không đề cập đến các đơn nguyên, nhưng nếu bạn có thể làm việc xuyên suốt chương và thực sự hiểu mọi thứ trong đó, đặc biệt là cách thức một chuỗi các hoạt động liên kết được đánh giá, bạn sẽ hiểu nội bộ của các đơn vị. Hy vọng điều này sẽ mất một vài lần thử.

Hướng dẫn Tất cả về Monads . Điều này đưa ra một số ví dụ tốt về việc sử dụng chúng và tôi phải nói rằng sự tương tự trong Appendex tôi đã làm việc cho tôi.


5

Monoid dường như là thứ gì đó đảm bảo rằng tất cả các hoạt động được xác định trên Monoid và loại được hỗ trợ sẽ luôn trả về loại được hỗ trợ bên trong Monoid. Ví dụ: Bất kỳ số + Bất kỳ số = Một số, không có lỗi.

Trong khi đó phân chia chấp nhận hai phân số và trả về một phân số, xác định phân chia bằng 0 là Infinity trong haskell bằng cách nào đó (điều này xảy ra là một phân số nào đó) ...

Trong mọi trường hợp, có vẻ như Monads chỉ là một cách để đảm bảo rằng chuỗi hoạt động của bạn hoạt động theo cách có thể dự đoán được và một hàm tự xưng là Num -> Num, được tạo bởi một chức năng khác của Num-> Num được gọi với x không nói, bắn tên lửa.

Mặt khác, nếu chúng ta có chức năng bắn tên lửa, chúng ta có thể kết hợp nó với các chức năng khác cũng bắn tên lửa, vì ý định của chúng ta rất rõ ràng - chúng tôi muốn bắn tên lửa - nhưng nó sẽ không thử in "Hello World" vì một số lý do kỳ lạ.

Trong Haskell, main thuộc loại IO () hoặc IO [()], sự phân biệt là lạ và tôi sẽ không thảo luận về nó nhưng đây là những gì tôi nghĩ xảy ra:

Nếu tôi có chính, tôi muốn nó thực hiện một chuỗi các hành động, lý do tôi chạy chương trình là để tạo hiệu ứng - thường là mặc dù IO. Do đó tôi có thể xâu chuỗi các hoạt động IO lại với nhau trong chính để - thực hiện IO, không có gì khác.

Nếu tôi cố gắng làm điều gì đó không "trả lại IO", chương trình sẽ phàn nàn rằng chuỗi không chảy hoặc về cơ bản "Làm thế nào điều này liên quan đến những gì chúng tôi đang cố gắng thực hiện - một hành động IO", nó dường như buộc lập trình viên để giữ cho dòng suy nghĩ của họ, mà không đi lạc và suy nghĩ về việc bắn tên lửa, trong khi tạo ra các thuật toán để sắp xếp - không chảy.

Về cơ bản, Monads dường như là một mẹo cho trình biên dịch rằng "này, bạn biết hàm này trả về một số ở đây, nó không thực sự luôn hoạt động, đôi khi nó có thể tạo ra một Số và đôi khi không có gì cả, chỉ cần giữ nó trong lí trí". Biết được điều này, nếu bạn cố gắng khẳng định một hành động đơn điệu, thì hành động đơn nguyên đó có thể đóng vai trò là một ngoại lệ thời gian biên dịch nói rằng "này, đây không thực sự là một con số, đây có thể là một con số, nhưng bạn không thể giả sử điều này, hãy làm gì đó để đảm bảo rằng dòng chảy được chấp nhận. " trong đó ngăn chặn hành vi chương trình không thể đoán trước - đến một mức độ công bằng.

Có vẻ như các đơn nguyên không phải là về độ tinh khiết, cũng không phải kiểm soát, mà là về việc duy trì danh tính của một danh mục mà tất cả các hành vi đều có thể dự đoán và xác định hoặc không biên dịch. Bạn không thể làm gì khi bạn dự kiến ​​sẽ làm một cái gì đó và bạn không thể làm gì nếu bạn dự kiến ​​sẽ không làm gì (hiển thị).

Lý do lớn nhất tôi có thể nghĩ đến cho Monads là - hãy xem mã Thủ tục / OOP và bạn sẽ nhận thấy rằng bạn không biết chương trình bắt đầu từ đâu, cũng không kết thúc, tất cả những gì bạn thấy là rất nhiều bước nhảy và rất nhiều toán học , ma thuật và tên lửa. Bạn sẽ không thể duy trì nó, và nếu bạn có thể, bạn sẽ dành khá nhiều thời gian để tập trung suy nghĩ xung quanh toàn bộ chương trình trước khi bạn có thể hiểu bất kỳ phần nào của nó, bởi vì tính mô đun trong bối cảnh này dựa trên các "phần" phụ thuộc lẫn nhau của mã, trong đó mã được tối ưu hóa có liên quan nhất có thể để hứa hẹn về hiệu quả / mối liên hệ. Các đơn nguyên rất cụ thể, và được xác định rõ ràng theo định nghĩa, và đảm bảo rằng dòng chương trình có thể phân tích và cô lập các phần khó phân tích - vì bản thân chúng là các đơn nguyên. Một đơn nguyên dường như là một " hoặc phá hủy vũ trụ hoặc thậm chí bóp méo thời gian - chúng tôi không có ý tưởng cũng như không có bất kỳ đảm bảo nào rằng NÓ LÀ GÌ. Một đơn vị bảo đảm rằng đó là những gì nó là. đó là rất mạnh mẽ. hoặc phá hủy vũ trụ hoặc thậm chí bóp méo thời gian - chúng tôi không có ý tưởng cũng như không có bất kỳ đảm bảo nào rằng NÓ LÀ GÌ. Một đơn vị bảo đảm rằng đó là những gì nó là. đó là rất mạnh mẽ.

Tất cả mọi thứ trong "thế giới thực" dường như là các đơn nguyên, theo nghĩa là nó bị ràng buộc bởi các định luật quan sát rõ ràng ngăn chặn sự nhầm lẫn. Điều này không có nghĩa là chúng ta phải bắt chước tất cả các hoạt động của đối tượng này để tạo các lớp, thay vào đó chúng ta có thể nói đơn giản là "hình vuông là hình vuông", không có gì ngoài hình vuông, thậm chí không phải hình chữ nhật cũng không phải hình vuông và "hình vuông có diện tích về chiều dài của một trong các kích thước hiện có của nó nhân với chính nó. Cho dù bạn có hình vuông nào, nếu là hình vuông trong không gian 2D, diện tích của nó hoàn toàn không thể là bất cứ thứ gì ngoài chiều dài của nó bình phương, nó gần như không đáng để chứng minh. chúng ta không cần phải đưa ra các xác nhận để đảm bảo rằng thế giới của chúng ta là như vậy, chúng ta chỉ sử dụng hàm ý của thực tế để ngăn chặn các chương trình của chúng ta không đi đúng hướng.

Tôi khá chắc chắn là sai nhưng tôi nghĩ điều này có thể giúp đỡ ai đó ngoài kia, vì vậy hy vọng nó sẽ giúp được ai đó.


5

Trong ngữ cảnh của Scala, bạn sẽ thấy sau đây là định nghĩa đơn giản nhất. Về cơ bản FlatMap (hoặc liên kết) là "liên kết" và tồn tại một danh tính.

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
      x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))

    // for every parameter X and x, there exists an id
    // such that the following holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

Ví dụ

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)

// Observe these are identical. Since Option is a Monad 
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)

scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)


// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)

// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

CHÚ THÍCH Nói một cách chính xác định nghĩa của Monad trong lập trình chức năng không giống với định nghĩa của Monad trong Lý thuyết Danh mục , được định nghĩa lần lượt mapflatten. Mặc dù chúng là loại tương đương dưới ánh xạ nhất định. Bài thuyết trình này rất hay: http://www.sl slideshoware.net/samthemonad/monad-presentation-scala-as-a-c Category


5

Câu trả lời này bắt đầu bằng một ví dụ tạo động lực, hoạt động thông qua ví dụ, lấy ra một ví dụ về một đơn nguyên và chính thức định nghĩa "đơn nguyên".

Hãy xem xét ba chức năng này trong mã giả:

f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x)          := <x, "">

flấy một cặp theo thứ tự của biểu mẫu <x, messages>và trả về một cặp đã ra lệnh. Nó để lại mục đầu tiên không bị ảnh hưởng và nối "called f. "vào mục thứ hai. Tương tự với g.

Bạn có thể soạn các hàm này và nhận giá trị ban đầu của mình, cùng với một chuỗi cho biết thứ tự các hàm được gọi trong:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">

Bạn không thích thực tế đó fgchịu trách nhiệm nối các thông điệp tường trình của riêng họ vào thông tin đăng nhập trước đó. (Chỉ cần tưởng tượng để tranh luận rằng thay vì nối các chuỗi fgphải thực hiện logic phức tạp trên mục thứ hai của cặp. Sẽ rất khó để lặp lại logic phức tạp đó trong hai hoặc nhiều chức năng khác nhau.)

Bạn thích viết các hàm đơn giản hơn:

f(x)    := <x, "called f. ">
g(x)    := <x, "called g. ">
wrap(x) := <x, "">

Nhưng hãy nhìn vào những gì xảy ra khi bạn soạn chúng:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">

Vấn đề là việc chuyển một cặp vào một hàm không cung cấp cho bạn những gì bạn muốn. Nhưng nếu bạn có thể đưa một cặp vào một hàm:

  feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">

Đọc feed(f, m)là "ăn mvào f". Để đưa một cặp <x, messages>vào một chức năng ftruyền x vào f, thoát <y, message>ra fvà quay trở lại <y, messages message>.

feed(f, <x, messages>) := let <y, message> = f(x)
                          in  <y, messages message>

Lưu ý những gì xảy ra khi bạn thực hiện ba điều với các chức năng của mình:

Đầu tiên: nếu bạn bọc một giá trị và sau đó đưa cặp kết quả vào một hàm:

  feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
  in  <y, "" message>
= let <y, message> = <x, "called f. ">
  in  <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)

Điều đó cũng giống như truyền giá trị vào hàm.

Thứ hai: nếu bạn cho một cặp vào wrap:

  feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
  in  <y, messages message>
= let <y, message> = <x, "">
  in  <y, messages message>
= <x, messages "">
= <x, messages>

Điều đó không thay đổi cặp.

Thứ ba: nếu bạn xác định một hàm lấy xg(x)đưa vào f:

h(x) := feed(f, g(x))

và cho một cặp vào đó:

  feed(h, <x, messages>)
= let <y, message> = h(x)
  in  <y, messages message>
= let <y, message> = feed(f, g(x))
  in  <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
  in  <y, messages message>
= let <y, message> = let <z, msg> = f(x)
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
  in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))

Điều đó cũng giống như cho cặp găn vào và cho cặp kết quả vào f.

Bạn có hầu hết các đơn nguyên. Bây giờ bạn chỉ cần biết về các loại dữ liệu trong chương trình của bạn.

Loại giá trị <x, "called f. ">nào? Vâng, điều đó phụ thuộc vào loại giá trị xlà gì. Nếu xlà loại t, thì cặp của bạn là một giá trị của loại "cặp tvà chuỗi". Gọi kiểu đó M t.

Mlà một hàm tạo kiểu: Mmột mình không đề cập đến một loại, nhưng M _chỉ một loại một khi bạn điền vào chỗ trống bằng một loại. An M intlà một cặp của một int và một chuỗi. An M stringlà một cặp của một chuỗi và một chuỗi. Vân vân.

Xin chúc mừng, bạn đã tạo ra một đơn nguyên!

Chính thức, đơn nguyên của bạn là tuple <M, feed, wrap>.

Một đơn nguyên là một tuple <M, feed, wrap>trong đó:

  • M là một nhà xây dựng kiểu.
  • feedlấy một (hàm lấy một tvà trả về một M u) và một M tvà trả về một M u.
  • wrapmất một vvà trả về một M v.

t, uvlà bất kỳ ba loại nào có thể giống hoặc không giống nhau. Một đơn vị thỏa mãn ba thuộc tính bạn đã chứng minh cho đơn nguyên cụ thể của mình:

  • Cho một gói tvào một chức năng cũng giống như chuyển các phần chưa được bao bọc tvào chức năng.

    Chính thức: feed(f, wrap(x)) = f(x)

  • Cho ăn M tvào wrapkhông làm gì cho M t.

    Chính thức: feed(wrap, m) = m

  • Cho một M t(gọi nó m) vào một chức năng

    • chuyển tvàog
    • nhận được M u(gọi nó n) từg
    • cho ăn nvàof

    giống như

    • cho ăn mvàog
    • nhận được ntừg
    • cho ăn nvàof

    Chính thức: feed(h, m) = feed(f, feed(g, m))ở đâuh(x) := feed(f, g(x))

Thông thường, feedđược gọi là bind(AKA >>=trong Haskell) và wrapđược gọi return.


5

Tôi sẽ cố gắng giải thích Monadtrong bối cảnh của Haskell.

Trong lập trình chức năng, thành phần chức năng là quan trọng. Nó cho phép chương trình của chúng tôi bao gồm các chức năng nhỏ, dễ đọc.

Hãy nói rằng chúng ta có hai chức năng: g :: Int -> Stringf :: String -> Bool.

Chúng ta có thể làm (f . g) x, điều này cũng giống như f (g x), đâu xlà một Intgiá trị.

Khi thực hiện thành phần / áp dụng kết quả của chức năng này cho chức năng khác, việc có các loại khớp với nhau là rất quan trọng. Trong trường hợp trên, loại kết quả được trả về gphải giống với loại được chấp nhận bởi f.

Nhưng đôi khi các giá trị nằm trong ngữ cảnh và điều này làm cho việc sắp xếp các loại dễ dàng hơn một chút. (Có các giá trị trong ngữ cảnh rất hữu ích. Ví dụ: Maybe Intloại đại diện cho một Intgiá trị có thể không có ở đó, IO Stringloại đại diện cho một Stringgiá trị ở đó do kết quả của việc thực hiện một số tác dụng phụ.)

Hãy nói rằng chúng ta bây giờ có g1 :: Int -> Maybe Stringf1 :: String -> Maybe Bool. g1f1rất giống gftương ứng.

Chúng ta không thể làm (f1 . g1) xhoặc f1 (g1 x), đâu xlà một Intgiá trị. Loại kết quả trả về g1không phải là những gì f1mong đợi.

Chúng tôi có thể sáng tác fgvới .nhà điều hành, nhưng bây giờ chúng tôi không thể sáng tác f1g1với .. Vấn đề là chúng ta không thể chuyển thẳng một giá trị trong ngữ cảnh sang một hàm mong đợi một giá trị không nằm trong ngữ cảnh.

Sẽ không hay nếu chúng tôi giới thiệu một nhà điều hành sáng tác g1f1, như vậy chúng tôi có thể viết (f1 OPERATOR g1) x? g1trả về một giá trị trong một bối cảnh. Giá trị sẽ được đưa ra khỏi bối cảnh và áp dụng cho f1. Và vâng, chúng tôi có một nhà điều hành như vậy. Đó là <=<.

Chúng tôi cũng có >>=toán tử thực hiện cho chúng tôi điều tương tự chính xác, mặc dù theo một cú pháp hơi khác nhau.

Chúng tôi viết : g1 x >>= f1. g1 xlà một Maybe Intgiá trị. Các >>=nhà điều hành giúp đi mà Intgiá trị ra khỏi "có lẽ-không-có" bối cảnh, và áp dụng nó vào f1. Kết quả của f1, là một Maybe Bool, sẽ là kết quả của toàn bộ >>=hoạt động.

Và cuối cùng, tại sao lại Monadhữu ích? Bởi vì Monadlớp loại định nghĩa >>=toán tử, rất giống với Eqlớp loại xác định toán tử ==/=toán tử.

Để kết luận, Monadlớp loại định nghĩa >>=toán tử cho phép chúng ta truyền các giá trị trong ngữ cảnh (chúng ta gọi các giá trị đơn trị này) cho các hàm không mong đợi các giá trị trong ngữ cảnh. Bối cảnh sẽ được chăm sóc.

Nếu có một điều cần nhớ ở đây, đó là Monads cho phép thành phần hàm liên quan đến các giá trị trong ngữ cảnh .



IOW, Monad là giao thức gọi hàm tổng quát.
Will Ness

Bạn trả lời là hữu ích nhất theo ý kiến ​​của tôi. Mặc dù tôi phải nói rằng tôi nghĩ rằng cần phải nhấn mạnh vào thực tế là các chức năng mà bạn đang đề cập không chỉ liên quan đến các giá trị trong ngữ cảnh, chúng chủ động đặt các giá trị trong ngữ cảnh. Vì vậy, ví dụ, một hàm, f :: ma -> mb sẽ rất dễ dàng soạn thảo với một hàm khác, g :: mb -> m c. Nhưng các đơn vị (liên kết cụ thể) cho phép chúng tôi soạn thảo vĩnh viễn các hàm đặt đầu vào của chúng trong cùng một bối cảnh mà không cần chúng tôi đưa giá trị ra khỏi bối cảnh đó trước (sẽ loại bỏ thông tin khỏi giá trị một cách hiệu quả)
James

@James Tôi nghĩ rằng nên được nhấn mạnh cho functor?
Jonas

@Jonas Tôi đoán tôi đã không giải thích propperly. Khi tôi nói rằng các hàm đặt các giá trị trong ngữ cảnh, tôi có nghĩa là chúng có kiểu (a -> mb). Điều này rất hữu ích vì việc đặt một giá trị vào ngữ cảnh sẽ thêm thông tin mới vào đó nhưng thường rất khó để kết hợp một (a -> mb) và a (b -> mc) vì chúng ta không thể lấy giá trị ra của bối cảnh. Vì vậy, chúng tôi sẽ phải sử dụng một số quy trình phức tạp để xâu chuỗi các chức năng này lại với nhau một cách hợp lý tùy thuộc vào bối cảnh cụ thể và các đơn vị chỉ cho phép chúng tôi thực hiện điều này một cách nhất quán, bất kể bối cảnh.
James

5

tl; dr

{-# LANGUAGE InstanceSigs #-}

newtype Id t = Id t

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Mở đầu

Toán tử ứng dụng $của hàm

forall a b. a -> b

được xác định theo quy tắc

($) :: (a -> b) -> a -> b
f $ x = f x

infixr 0 $

về mặt ứng dụng hàm nguyên thủy Haskell f x( infixl 10).

Thành phần .được định nghĩa theo $như

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x

infixr 9 .

và thỏa mãn tương đương forall f g h.

     f . id  =  f            :: c -> d   Right identity
     id . g  =  g            :: b -> c   Left identity
(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.là liên kết, và idlà bản sắc bên phải và bên trái của nó.

Bộ ba Kleisli

Trong lập trình, một đơn vị là một hàm tạo kiểu functor với một thể hiện của lớp loại đơn nguyên. Có một số biến thể tương đương của định nghĩa và thực hiện, mỗi biến thể mang một chút trực giác khác nhau về sự trừu tượng của đơn nguyên.

Một functor là một hàm tạo fkiểu * -> *với một thể hiện của lớp kiểu functor.

{-# LANGUAGE KindSignatures #-}

class Functor (f :: * -> *) where
   map :: (a -> b) -> (f a -> f b)

Ngoài việc tuân theo giao thức loại được thi hành tĩnh, các thể hiện của lớp loại functor phải tuân theo luật functor đại số forall f g.

       map id  =  id           :: f t -> f t   Identity
map f . map g  =  map (f . g)  :: f a -> f c   Composition / short cut fusion

Tính toán Functor có loại

forall f t. Functor f => f t

Một tính toán c rbao gồm các kết quả r trong bối cảnh c .

Các hàm đơn nguyên hoặc mũi tên Kleisli có loại

forall m a b. Functor m => a -> m b

Mũi tên Kleisi là các hàm lấy một đối số avà trả về một phép tính đơn trị m b.

Các đơn nguyên được định nghĩa theo quy tắc của bộ ba Kleisli forall m. Functor m =>

(m, return, (=<<))

thực hiện như lớp loại

class Functor m => Monad m where
   return :: t -> m t
   (=<<)  :: (a -> m b) -> m a -> m b

infixr 1 =<<

Bản sắc Kleisli return là một mũi tên Kleisli nhằm thúc đẩy một giá trị tvào bối cảnh đơn nguyên m. Ứng dụng mở rộng hoặc Kleisli =<< áp dụng một mũi tên Kleisli a -> m bcho kết quả tính toán m a.

Thành phần Kleisli <=< được định nghĩa theo nghĩa mở rộng là

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x

infixr 1 <=<

<=< kết hợp hai mũi tên Kleisli, áp dụng mũi tên trái vào kết quả của ứng dụng mũi tên phải.

Các thể loại của lớp loại đơn nguyên phải tuân theo luật đơn nguyên , được nêu rõ nhất theo thành phần của Kleisli:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identity
   return <=< g  =  g                :: b -> m c   Left identity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d   Associativity

<=<là liên kết, và returnlà bản sắc bên phải và bên trái của nó.

Danh tính

Loại nhận dạng

type Id t = t

là chức năng nhận dạng trên các loại

Id :: * -> *

Giải thích như một functor,

   return :: t -> Id t
=      id :: t ->    t

    (=<<) :: (a -> Id b) -> Id a -> Id b
=     ($) :: (a ->    b) ->    a ->    b

    (<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
=     (.) :: (b ->    c) -> (a ->    b) -> (a ->    c)

Trong Haskell chính tắc, đơn sắc được xác định

newtype Id t = Id t

instance Functor Id where
   map :: (a -> b) -> Id a -> Id b
   map f (Id x) = Id (f x)

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Lựa chọn

Một loại tùy chọn

data Maybe t = Nothing | Just t

mã hóa tính toán Maybe tmà không nhất thiết phải mang lại một kết quả t, tính toán có thể làm hỏng thất bại. Đơn vị tùy chọn được xác định

instance Functor Maybe where
   map :: (a -> b) -> (Maybe a -> Maybe b)
   map f (Just x) = Just (f x)
   map _ Nothing  = Nothing

instance Monad Maybe where
   return :: t -> Maybe t
   return = Just

   (=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
   f =<< (Just x) = f x
   _ =<< Nothing  = Nothing

a -> Maybe bchỉ được áp dụng cho một kết quả nếu Maybe amang lại kết quả.

newtype Nat = Nat Int

Các số tự nhiên có thể được mã hóa khi các số nguyên lớn hơn hoặc bằng 0.

toNat :: Int -> Maybe Nat
toNat i | i >= 0    = Just (Nat i)
        | otherwise = Nothing

Các số tự nhiên không được đóng dưới phép trừ.

(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)

infixl 6 -?

Các đơn vị tùy chọn bao gồm một hình thức xử lý ngoại lệ cơ bản.

(-? 20) <=< toNat :: Int -> Maybe Nat

Danh sách

Danh sách đơn nguyên, trên loại danh sách

data [] t = [] | t : [t]

infixr 5 :

và hoạt động đơn chất phụ gia của nó

(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[]       ++ ys = ys

infixr 5 ++

mã hóa tính toán phi tuyến[t] mang lại một lượng 0, 1, ...kết quả tự nhiên t.

instance Functor [] where
   map :: (a -> b) -> ([a] -> [b])
   map f (x : xs) = f x : map f xs
   map _ []       = []

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> [a] -> [b]
   f =<< (x : xs) = f x ++ (f =<< xs)
   _ =<< []       = []

Tiện ích mở rộng =<<nối ++tất cả các danh sách [b]từ các ứng dụng f xcủa mũi tên Kleisli a -> [b]thành các thành phần của [a]một danh sách kết quả [b].

Đặt các ước số thích hợp của một số nguyên dương n

divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]

divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)

sau đó

forall n.  let { f = f <=< divisors } in f n   =   []

Khi xác định lớp loại đơn, thay vì mở rộng =<<, tiêu chuẩn Haskell sử dụng toán tử lật của nó, toán tử liên kết>>= .

class Applicative m => Monad m where
   (>>=) :: forall a b. m a -> (a -> m b) -> m b

   (>>) :: forall a b. m a -> m b -> m b
   m >> k = m >>= \ _ -> k
   {-# INLINE (>>) #-}

   return :: a -> m a
   return = pure

Để đơn giản, giải thích này sử dụng hệ thống phân cấp lớp loại

class              Functor f
class Functor m => Monad m

Trong Haskell, hệ thống phân cấp tiêu chuẩn hiện tại là

class                  Functor f
class Functor p     => Applicative p
class Applicative m => Monad m

bởi vì không chỉ mỗi đơn vị là một functor, mà mỗi ứng dụng là một functor và mỗi đơn vị cũng là một ứng dụng.

Sử dụng danh sách đơn nguyên, mã giả mệnh lệnh

for a in (1, ..., 10)
   for b in (1, ..., 10)
      p <- a * b
      if even(p)
         yield p

tạm dịch là khối làm ,

do a <- [1 .. 10]
   b <- [1 .. 10]
   let p = a * b
   guard (even p)
   return p

sự hiểu biết đơn nguyên tương đương ,

[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]

và biểu thức

[1 .. 10] >>= (\ a ->
   [1 .. 10] >>= (\ b ->
      let p = a * b in
         guard (even p) >>       -- [ () | even p ] >>
            return p
      )
   )

Ký hiệu và hiểu đơn âm là đường cú pháp cho các biểu thức liên kết lồng nhau. Toán tử liên kết được sử dụng để liên kết tên cục bộ của các kết quả đơn âm.

let x = v in e    =   (\ x -> e)  $  v   =   v  &  (\ x -> e)
do { r <- m; c }  =   (\ r -> c) =<< m   =   m >>= (\ r -> c)

Ở đâu

(&) :: a -> (a -> b) -> b
(&) = flip ($)

infixl 0 &

Chức năng bảo vệ được xác định

guard :: Additive m => Bool -> m ()
guard True  = return ()
guard False = fail

trong đó loại đơn vị hoặc trống trống tuple

data () = ()

Các đơn vị cộng gộp hỗ trợ sự lựa chọnthất bại có thể được trừu tượng hóa bằng cách sử dụng một lớp loại

class Monad m => Additive m where
   fail  :: m t
   (<|>) :: m t -> m t -> m t

infixl 3 <|>

instance Additive Maybe where
   fail = Nothing

   Nothing <|> m = m
   m       <|> _ = m

instance Additive [] where
   fail = []
   (<|>) = (++)

ở đâu fail<|>tạo thành một monoidforall k l m.

     k <|> fail  =  k
     fail <|> l  =  l
(k <|> l) <|> m  =  k <|> (l <|> m)

faillà phần tử không hấp thụ / hủy diệt của các đơn vị phụ gia

_ =<< fail  =  fail

Nếu trong

guard (even p) >> return p

even plà đúng, sau đó bảo vệ tạo ra [()], và theo định nghĩa của >>hàm hằng số cục bộ

\ _ -> return p

được áp dụng cho kết quả (). Nếu sai, thì người bảo vệ tạo ra danh sách monad fail( []), điều này không mang lại kết quả nào cho một mũi tên Kleisli được áp dụng >>, vì vậy điều này pđược bỏ qua.

Tiểu bang

Khét tiếng, các đơn nguyên được sử dụng để mã hóa tính toán trạng thái.

Bộ xử lý trạng thái là một chức năng

forall st t. st -> (t, st)

chuyển trạng thái stvà mang lại kết quả t. Nhà nước st có thể là bất cứ điều gì. Không có gì, cờ, đếm, mảng, xử lý, máy, thế giới.

Loại bộ xử lý trạng thái thường được gọi là

type State st t = st -> (t, st)

Các bộ xử lý nhà nước là * -> *functor loại State st. Mũi tên Kleisli của bộ xử lý trạng thái là các chức năng

forall st a b. a -> (State st) b

Trong Haskell chính tắc, phiên bản lười biếng của bộ xử lý trạng thái được xác định

newtype State st t = State { stateProc :: st -> (t, st) }

instance Functor (State st) where
   map :: (a -> b) -> ((State st) a -> (State st) b)
   map f (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  (f x, s1)

instance Monad (State st) where
   return :: t -> (State st) t
   return x = State $ \ s -> (x, s)

   (=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
   f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  stateProc (f x) s1

Một bộ xử lý trạng thái được chạy bằng cách cung cấp trạng thái ban đầu:

run :: State st t -> st -> (t, st)
run = stateProc

eval :: State st t -> st -> t
eval = fst . run

exec :: State st t -> st -> st
exec = snd . run

Quyền truy cập trạng thái được cung cấp bởi người nguyên thủy getput, phương pháp trừu tượng hóa trên các đơn nguyên nhà nước :

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}

class Monad m => Stateful m st m -> st where
   get :: m st
   put :: st -> m ()

m -> sttuyên bố một sự phụ thuộc chức năng của loại trạng thái sttrên đơn nguyên m; rằng State t, ví dụ, sẽ xác định loại trạng thái là tduy nhất.

instance Stateful (State st) st where
   get :: State st st
   get = State $ \ s -> (s, s)

   put :: st -> State st ()
   put s = State $ \ _ -> ((), s)

với loại đơn vị được sử dụng tương tự voidtrong C.

modify :: Stateful m st => (st -> st) -> m ()
modify f = do
   s <- get
   put (f s)

gets :: Stateful m st => (st -> t) -> m t
gets f = do
   s <- get
   return (f s)

gets thường được sử dụng với các bộ truy cập trường bản ghi.

Các trạng thái đơn nguyên tương đương của luồng

let s0 = 34
    s1 = (+ 1) s0
    n = (* 12) s1
    s2 = (+ 7) s1
in  (show n, s2)

trong đó s0 :: Int, là minh bạch không kém phần tham khảo, nhưng vô cùng thanh lịch và thiết thực

(flip run) 34
   (do
      modify (+ 1)
      n <- gets (* 12)
      modify (+ 7)
      return (show n)
   )

modify (+ 1)là một tính toán của loại State Int (), ngoại trừ hiệu ứng của nó tương đương với return ().

(flip run) 34
   (modify (+ 1) >>
      gets (* 12) >>= (\ n ->
         modify (+ 7) >>
            return (show n)
      )
   )

Luật đơn nguyên của sự kết hợp có thể được viết theo >>= forall m f g.

(m >>= f) >>= g  =  m >>= (\ x -> f x >>= g)

hoặc là

do {                 do {                   do {
   r1 <- do {           x <- m;                r0 <- m;
      r0 <- m;   =      do {            =      r1 <- f r0;
      f r0                 r1 <- f x;          g r1
   };                      g r1             }
   g r1                 }
}                    }

Giống như trong lập trình hướng biểu thức (ví dụ Rust), câu lệnh cuối cùng của một khối biểu thị năng suất của nó. Toán tử liên kết đôi khi được gọi là dấu chấm phẩy có thể lập trình được.

Nguyên tắc cấu trúc điều khiển lặp từ lập trình mệnh lệnh có cấu trúc được mô phỏng đơn điệu

for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())

while :: Monad m => m Bool -> m t -> m ()
while c m = do
   b <- c
   if b then m >> while c m
        else return ()

forever :: Monad m => m t
forever m = m >> forever m

Đầu ra đầu vào

data World

Bộ xử lý trạng thái thế giới I / O là sự hòa giải của Haskell thuần túy và thế giới thực, về ngữ nghĩa hoạt động biểu thị và mệnh lệnh chức năng. Một sự tương tự gần gũi của việc thực hiện nghiêm ngặt thực tế:

type IO t = World -> (t, World)

Tương tác được tạo điều kiện bởi các nguyên thủy không tinh khiết

getChar         :: IO Char
putChar         :: Char -> IO ()
readFile        :: FilePath -> IO String
writeFile       :: FilePath -> String -> IO ()
hSetBuffering   :: Handle -> BufferMode -> IO ()
hTell           :: Handle -> IO Integer
. . .              . . .

Tạp chất của mã sử dụng IOnguyên thủy được giao thức vĩnh viễn bởi hệ thống loại. Bởi vì độ tinh khiết là tuyệt vời, những gì xảy ra IO, ở lại IO.

unsafePerformIO :: IO t -> t

Hoặc, ít nhất, nên.

Chữ ký loại của chương trình Haskell

main :: IO ()
main = putStrLn "Hello, World!"

mở rộng đến

World -> ((), World)

Một chức năng biến đổi một thế giới.

Phần kết

Các thể loại mà các đối tượng là các loại Haskell và các hình thái đó là các chức năng giữa các loại Haskell là, nhanh và lỏng lẻo, thể loại Hask.

Một functor Tlà một ánh xạ từ một thể loại Cđến một thể loại D; cho mỗi đối tượng trong Cmột đối tượng trongD

Tobj :  Obj(C) -> Obj(D)
   f :: *      -> *

và cho mỗi hình thái trong Cmột hình thái trongD

Tmor :  HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
 map :: (a -> b)   -> (f a -> f b)

trong đó X, Ylà các đối tượng trong C. HomC(X, Y)lớp đồng hình của tất cả các hình thái X -> Ytrong C. Functor phải bảo tồn bản sắc và thành phần hình thái, cấu trúc của thành Cphố D.

                    Tmor    Tobj

      T(id)  =  id        : T(X) -> T(X)   Identity
T(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

Các loại Kleisli thuộc một thể loại Cđược đưa ra bởi một Kleisli triple

<T, eta, _*>

của một endofunctor

T : C -> C

( f), một hình thái nhận dạng eta( return) và một toán tử mở rộng *( =<<).

Mỗi hình thái Kleisli trong Hask

      f :  X -> T(Y)
      f :: a -> m b

bởi toán tử mở rộng

   (_)* :  Hom(X, T(Y)) -> Hom(T(X), T(Y))
  (=<<) :: (a -> m b)   -> (m a -> m b)

được đưa ra một hình thái trong Haskthể loại Kleisli

     f* :  T(X) -> T(Y)
(f =<<) :: m a  -> m b

Thành phần trong danh mục Kleisli .Tđược đưa ra dưới dạng mở rộng

 f .T g  =  f* . g       :  X -> T(Z)
f <=< g  =  (f =<<) . g  :: a -> m c

và thỏa mãn các tiên đề thể loại

       eta .T g  =  g                :  Y -> T(Z)   Left identity
   return <=< g  =  g                :: b -> m c

       f .T eta  =  f                :  Z -> T(U)   Right identity
   f <=< return  =  f                :: c -> m d

  (f .T g) .T h  =  f .T (g .T h)    :  X -> T(U)   Associativity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d

trong đó, áp dụng các phép biến đổi tương đương

     eta .T g  =  g
     eta* . g  =  g               By definition of .T
     eta* . g  =  id . g          forall f.  id . f  =  f
         eta*  =  id              forall f g h.  f . h  =  g . h  ==>  f  =  g

(f .T g) .T h  =  f .T (g .T h)
(f* . g)* . h  =  f* . (g* . h)   By definition of .T
(f* . g)* . h  =  f* . g* . h     . is associative
    (f* . g)*  =  f* . g*         forall f g h.  f . h  =  g . h  ==>  f  =  g

về mặt mở rộng được đưa ra theo quy tắc

               eta*  =  id                 :  T(X) -> T(X)   Left identity
       (return =<<)  =  id                 :: m t -> m t

           f* . eta  =  f                  :  Z -> T(U)      Right identity
   (f =<<) . return  =  f                  :: c -> m d

          (f* . g)*  =  f* . g*            :  T(X) -> T(Z)   Associativity
(((f =<<) . g) =<<)  =  (f =<<) . (g =<<)  :: m a -> m c

Monads cũng có thể được định nghĩa theo thuật ngữ không phải là phần mở rộng của Kleislian, mà là một phép biến đổi tự nhiên mu, trong lập trình được gọi là join. Một đơn vị được định nghĩa theo nghĩa mulà một bộ ba trên một danh mục C, của một endofunctor

     T :  C -> C
     f :: * -> *

và hai sự yên tĩnh tự nhiên

   eta :  Id -> T
return :: t  -> f t

    mu :  T . T   -> T
  join :: f (f t) -> f t

thỏa mãn tương đương

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativity
  join . map join  =  join . join           :: f (f (f t)) -> f t

      mu . T(eta)  =  mu . eta       =  id  :  T -> T               Identity
join . map return  =  join . return  =  id  :: f t -> f t

Lớp loại đơn nguyên sau đó được định nghĩa

class Functor m => Monad m where
   return :: t -> m t
   join   :: m (m t) -> m t

Việc muthực hiện chính tắc của đơn vị tùy chọn:

instance Monad Maybe where
   return = Just

   join (Just m) = m
   join Nothing  = Nothing

các concatchức năng

concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat []       = []

joindanh sách đơn nguyên.

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> ([a] -> [b])
   (f =<<) = concat . map f

Việc triển khai joincó thể được dịch từ hình thức mở rộng bằng cách sử dụng tương đương

     mu  =  id*           :  T . T -> T
   join  =  (id =<<)      :: m (m t) -> m t

Bản dịch ngược từ musang dạng mở rộng được đưa ra bởi

     f*  =  mu . T(f)     :  T(X) -> T(Y)
(f =<<)  =  join . map f  :: m a -> m b

Nhưng tại sao một lý thuyết quá trừu tượng nên được sử dụng cho lập trình?

Câu trả lời rất đơn giản: là các nhà khoa học máy tính, chúng tôi coi trọng sự trừu tượng ! Khi chúng tôi thiết kế giao diện cho một thành phần phần mềm, chúng tôi muốn nó tiết lộ ít nhất có thể về việc triển khai. Chúng tôi muốn có thể thay thế việc triển khai bằng nhiều lựa chọn thay thế, nhiều 'trường hợp' khác của cùng một 'khái niệm'. Khi chúng tôi thiết kế một giao diện chung cho nhiều thư viện chương trình, điều quan trọng hơn nữa là giao diện chúng tôi chọn có nhiều cách triển khai. Đó là tính khái quát của khái niệm đơn nguyên mà chúng tôi đánh giá rất cao, đó là lý thuyết phạm trù quá trừu tượng nên các khái niệm của nó rất hữu ích cho lập trình.

Do đó, hầu như không ngạc nhiên khi việc khái quát hóa các đơn nguyên mà chúng tôi trình bày dưới đây cũng có mối liên hệ chặt chẽ với lý thuyết thể loại. Nhưng chúng tôi nhấn mạnh rằng mục đích của chúng tôi rất thiết thực: không phải là 'thực hiện lý thuyết thể loại', mà là tìm một cách tổng quát hơn để cấu trúc các thư viện kết hợp. Chỉ đơn giản là may mắn của chúng tôi mà các nhà toán học đã thực hiện nhiều công việc cho chúng tôi!

từ Tướng quân Monads đến Mũi tên của John Hughes


4

Những gì thế giới cần là một bài đăng blog đơn nguyên khác, nhưng tôi nghĩ rằng điều này hữu ích trong việc xác định các đơn nguyên hiện có trong tự nhiên.

Tam giác Sierpinki

Trên đây là một fractal gọi là tam giác Sierpinki, fractal duy nhất tôi có thể nhớ để vẽ. Fractal có cấu trúc tự tương tự như tam giác trên, trong đó các phần tương tự với tổng thể (trong trường hợp này chính xác bằng một nửa tỷ lệ như tam giác cha).

Monads là fractals. Với một cấu trúc dữ liệu đơn nguyên, các giá trị của nó có thể được tạo thành để tạo thành một giá trị khác của cấu trúc dữ liệu. Đây là lý do tại sao nó hữu ích để lập trình, và đây là lý do tại sao nó xảy ra trong nhiều tình huống.


3
Bạn có nghĩa là "những gì thế giới không cần ..."? Đẹp tương tự mặc dù!
Groverboy

@ icc97 bạn nói đúng - ý nghĩa là đủ rõ ràng. Sarcasm ngoài ý muốn, xin lỗi tác giả.
Groverboy

Những gì thế giới cần là một chủ đề bình luận khác xác nhận một sự mỉa mai, nhưng nếu đọc kỹ tôi đã viết nhưng điều đó sẽ làm cho nó rõ ràng.
Eugene Yokota


4

Hãy để "bên dưới" {| a |m}đại diện cho một số dữ liệu đơn nguyên. Một kiểu dữ liệu quảng cáo a:

        (I got an a!)
          /        
    {| a |m}

Chức năng, fbiết cách tạo ra một đơn nguyên, nếu chỉ có nó a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

Ở đây chúng ta thấy chức năng, fcố gắng đánh giá một đơn nguyên nhưng bị quở trách.

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

Funtion ,, ftìm cách trích xuất abằng cách sử dụng >>=.

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

Ít ai fbiết, các đơn nguyên và >>=đang thông đồng.

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

Nhưng họ thực sự nói về cái gì? Vâng, điều đó phụ thuộc vào các đơn nguyên. Nói chỉ trong trừu tượng đã sử dụng hạn chế; bạn phải có một số kinh nghiệm với các đơn vị cụ thể để hiểu rõ hơn.

Ví dụ, kiểu dữ liệu Có thể

 data Maybe a = Nothing | Just a

có một ví dụ đơn nguyên sẽ hoạt động như sau ...

Trong đó, nếu trường hợp là Just a

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

Nhưng đối với trường hợp Nothing

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

Vì vậy, đơn vị Có thể cho phép tính toán tiếp tục nếu nó thực sự chứa aquảng cáo, nhưng hủy bỏ tính toán nếu không. Tuy nhiên, kết quả vẫn là một phần của dữ liệu đơn âm, mặc dù không phải là đầu ra của f. Vì lý do này, đơn vị Có thể được sử dụng để đại diện cho bối cảnh thất bại.

Các đơn nguyên khác nhau hành xử khác nhau. Danh sách là các loại dữ liệu khác với các trường hợp đơn âm. Họ hành xử như sau:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

Trong trường hợp này, hàm biết cách tạo danh sách từ đầu vào của nó, nhưng không biết phải làm gì với đầu vào thêm và danh sách phụ. Các liên kết >>=, đã giúp đỡ fbằng cách kết hợp nhiều đầu ra. Tôi bao gồm ví dụ này để chỉ ra rằng trong khi >>=chịu trách nhiệm trích xuất a, nó cũng có quyền truy cập vào đầu ra ràng buộc cuối cùng của f. Thật vậy, nó sẽ không bao giờ trích xuất bất kỳ atrừ khi nó biết đầu ra cuối cùng có cùng loại bối cảnh.

Có những đơn nguyên khác được sử dụng để đại diện cho các bối cảnh khác nhau. Dưới đây là một số đặc điểm của một vài chi tiết. Các IOđơn vị thực sự không có a, nhưng nó biết một anh chàng và sẽ lấy nó acho bạn. Các State stđơn vị có một bí mật strằng nó sẽ vượt qua fdưới bàn, mặc dù fchỉ đến để yêu cầu một a. Các Reader rđơn nguyên tương tự State st, mặc dù nó chỉ cho phép fnhìn vào r.

Điểm chính trong tất cả những điều này là bất kỳ loại dữ liệu nào được tuyên bố là Monad đều đang khai báo một số loại bối cảnh xung quanh việc trích xuất một giá trị từ đơn nguyên. Lợi ích lớn từ tất cả những điều này? Chà, đủ dễ để thực hiện một phép tính với một số loại bối cảnh. Tuy nhiên, nó có thể trở nên lộn xộn khi xâu chuỗi nhiều phép tính theo ngữ cảnh. Các hoạt động đơn nguyên đảm nhiệm việc giải quyết các tương tác của bối cảnh để lập trình viên không phải làm.

Lưu ý, việc sử dụng đó >>=giúp giảm bớt một mớ hỗn độn bằng cách lấy đi một số quyền tự chủ f. Đó là, trong trường hợp trên Nothingchẳng hạn, fkhông còn quyết định phải làm gì trong trường hợp Nothing; nó được mã hóa vào >>=. Đây là sự đánh đổi. Nếu nó là cần thiết fđể quyết định phải làm gì trong trường hợp Nothing, thì đó fphải là một chức năng từ Maybe ađến Maybe b. Trong trường hợp này, Maybelà một đơn nguyên là không liên quan.

Tuy nhiên, lưu ý rằng đôi khi một loại dữ liệu không xuất các hàm tạo của nó (nhìn vào IO của bạn) và nếu chúng ta muốn làm việc với giá trị được quảng cáo, chúng ta có ít lựa chọn nhưng phải làm việc với giao diện đơn điệu.


3

Một đơn nguyên là một thứ được sử dụng để đóng gói các đối tượng có trạng thái thay đổi. Nó thường gặp nhất trong các ngôn ngữ mà nếu không thì không cho phép bạn có trạng thái có thể sửa đổi (ví dụ: Haskell).

Một ví dụ sẽ là cho tập tin I / O.

Bạn sẽ có thể sử dụng một đơn nguyên cho tệp I / O để cô lập bản chất trạng thái thay đổi thành mã đã sử dụng Monad. Mã bên trong Monad có hiệu quả có thể bỏ qua trạng thái thay đổi của thế giới bên ngoài Monad - điều này giúp dễ dàng hơn rất nhiều để suy luận về hiệu ứng tổng thể của chương trình của bạn.


3
Theo tôi hiểu, các đơn vị còn hơn thế. Đóng gói trạng thái có thể thay đổi trong một ngôn ngữ chức năng "thuần túy" chỉ là một ứng dụng của các đơn nguyên.
thSoft
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.