Câu hỏi Một đơn nguyên là gì?


Có một thời gian ngắn xem xét Haskell gần đây, những gì sẽ là một ngắn gọn, súc tích, thực tế giải thích 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 tôi đã đi qua được khá không thể tiếp cận và thiếu chi tiết thực tế.


1228


gốc


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 biệt. - P Shved
Đây là một bài giới thiệu mới sử dụng javascript - tôi thấy nó rất dễ đọc. - Benjol
Xem thêm Những cách khác nhau để xem một đơn nguyên. - Petr Pudlák
Xem thêm Monads trong ảnh - cibercitizen1
Một đơn nguyên là một mảng các hàm có các hoạt động trợ giúp. Xem câu trả lời này - cibercitizen1


Các câu trả lời:


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

Bạn yêu cầu các ví dụ thực tế:

Ví dụ 1: Hiểu danh sách:

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

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

Nó chỉ ra điều này thực sự chỉ là cú pháp cú pháp cho một số hoạt động trong danh sách đơn nguyên. Cùng một danh sách hiểu có thể được viết như sau:

do
   x <- [1..10]
   if odd x 
       then [x * 2] 
       else []

Hoặc thậm chí:

[1..10] >>= (\x -> if odd x then [x*2] else [])

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 monads, AKA computation builders. Chủ đề chung là đơn nguyên chuỗi hoạt động theo một cách cụ thể, hữu ích nào đó. Trong danh sách hiểu, các hoạt động được xích như vậy nếu một hoạt động trả về một danh sách, sau đó 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, IO monad thực hiện các thao tác, nhưng chuyển một "biến ẩn" dọc theo, đại diện cho "trạng thái của thế giới", cho phép chúng ta viết mã I / O một cách thuần túy.

Nó chỉ ra mô hình của chuỗi hoạt động khá hữu ích và được sử dụng cho rất nhiều thứ khác nhau trong Haskell.

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

Cả hai cú pháp list-comprehension và do-notation là cú pháp đường cho các hoạt động chuỗi sử dụng >>= nhà điều hành. Một đơn nguyên về cơ bản chỉ là một loại hỗ trợ >>= nhà điều 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, digit, vv là khá đơn giản. Chúng khớp hoặc không khớp. Phép thuật là đơn vị 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 một kết quả khớp không thành công, trong trường hợp đó, monad sẽ quay lại phiên 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 nằm trong Haskell, nhưng hóa ra F # cũng hỗ trợ monads. Ví dụ này bị lấy 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 tìm nạp một trang web. Đường đục lỗ là việc sử dụng GetResponseAsync - nó thực sự chờ phản hồi trên một sợi 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 ra 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 nguyên có thể tự "tách" khối đó và trì hoãn việc thực thi nửa sau. (Các 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.)

Cách họ làm việc

Vậy làm thế nào một monad 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 gì thực sự xảy ra trong một khối (hoặc một biểu thức tính toán khi chúng được gọi trong F #), là mọi hoạt động (về cơ bản mọi dòng) đều được bao bọc trong một hàm ẩn danh riêng biệt. Các hàm này sau đó được kết hợp bằng cách sử dụng bind toán tử (đánh vần >>= trong Haskell). Kể từ khi bind hoạt động kết hợp các chức năng, nó có thể thực hiện 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 hiện một số trên một thread riêng biệt khi nó cảm thấy thích nó và như vậy.

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 >>= toán tử là thành phần ma thuật: Nó lấy một giá trị (ở phía bên trái) và kết hợp nó với một hàm (ở bên phải), để tạo ra một giá trị mới. Giá trị mới này sau đó sẽ được tiếp theo >>= toán tử và một lần nữa kết hợp với một hàm để tạo ra một giá trị mới. >>= có thể được xem như một bộ đánh giá mini.

Lưu ý rằng >>= bị quá tải cho các loại khác nhau, vì vậy mọi đơn nguyên đều có triển khai riêng của nó >>=. (Tất cả các hoạt động trong chuỗi phải thuộc loại của cùng một đơn vị, 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ỉ cần 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ên, điều gì làm cho toàn bộ mẫu hữu ích là khi có cái gì đó đang diễn ra trong quá trình thực hiện của monad >>=.

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

Tổng hợp

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

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

Tại sao monads khó?

Đối với nhiều người học Haskell, monads là một chướng ngại vật mà họ đánh trúng như một bức tường gạch. Nó không phải là monads mình là phức tạp, nhưng việc thực hiện dựa trên nhiều tính năng Haskell tiên tiến khác như các loại tham số, loại lớp, và như vậy. Vấn đề là Haskell I / O dựa trên monads, 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 khi tất cả, nó không có nhiều thú vị để tạo ra các chương trình mà không tạo ra bất kỳ đầu ra. Tôi không có giải pháp ngay lập tức cho vấn đề gà và trứng này, ngoại trừ việc xử lý 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 trên monads: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


966



Là một người đã có rất nhiều vấn đề để hiểu các monads, 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. Trong cách nào là danh sách hiểu một đơn? Có dạng mở rộng của ví dụ đó không? Một điều khác thực sự làm tôi băn khoăn về hầu hết các giải thích đơn nguyên, kể cả cái này- Liệu chúng có tiếp tục trộn lẫn "một đơn nguyên là gì không?" với "một đơn nguyên tốt cho cái gì?" và "Một đơn nguyên được thực hiện như thế nào?". 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 kiểu hỗ trợ toán tử >> =." Mà chỉ có tôi ... - Breton
Ngoài ra tôi không đồng ý với kết luận của bạn về lý do tại sao monads là khó khăn. Nếu bản thân monads không phức tạp, thì bạn sẽ có thể giải thích những gì họ đang có mà không có một 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 "một monad là gì", tôi muốn biết những gì ngứa nó có nghĩa là để gãi. Cho đến nay có vẻ như câu trả lời là "Bởi vì các tác giả của haskell là sadomasochists và quyết định rằng bạn nên làm điều gì đó ngu ngốc phức tạp để hoàn thành những điều đơn giản, vì vậy bạn phải học monads để sử dụng haskell, chứ không phải vì chúng hữu ích bản thân họ "... - Breton
Nhưng .. không thể đúng, phải không? Tôi nghĩ rằng monads là khó bởi vì 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 thực hiện 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 mà tiêu thụ một sản phẩm dầu mỏ tinh chế để lái xe trong một chu kỳ một số piston kim loại, do đó xoay một trục quay gắn liền với một số bánh răng mà lái một số bánh xe. Các bánh xe có túi cao su thổi phồng xung quanh chúng mà giao diện với một bề mặt nhựa đường để gây ra một bộ sưu tập chỗ ngồi để di chuyển về phía trước. Những chiếc ghế di chuyển về phía trước vì ... - Breton
Tôi đọc tất cả điều này và vẫn không biết một đơn nguyên là gì, ngoài thực tế là nó là một cái gì đó mà các lập trình viên của Haskell không hiểu đủ để giải thích. Các ví dụ không giúp được gì nhiều, bởi vì đây là tất cả những thứ mà người ta có thể làm mà không có monads, và câu trả lời này không làm cho nó rõ ràng như thế nào monads làm cho chúng dễ dàng hơn, chỉ gây nhầm lẫn hơn. Một phần của câu trả lời này gần như hữu ích là khi đường cú pháp của ví dụ số 2 bị loại bỏ. Tôi nói đến gần bởi vì, ngoài dòng đầu tiên, việc mở rộng không mang bất kỳ sự tương đồng thực sự với bản gốc. - Laurence Gonsalves
Một vấn đề khác dường như là đặc hữu đối với sự giải thích của các monads là nó được viết trong 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ữ xấu để giải thích các monads. Nếu tôi biết Haskell tôi đã hiểu được các monads, vì vậy nếu bạn muốn giải thích các monads, hãy bắt đầu bằng cách sử dụng một ngôn ngữ mà những người không biết monads có nhiều khả năng hiểu hơn. nếu bạn phải sử dụng Haskell, không sử dụng đường cú pháp chút nào - 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à không giả định một sự hiểu biết về Haskell IO. - Laurence Gonsalves


Giải thích "một đơn nguyên" là gì giống như nói "số là gì?" Chúng tôi sử dụng số lượng mọi lúc. Nhưng hãy tưởng tượng bạn đã gặp một người không biết gì về con số. Như thế nào heck bạn sẽ giải thích những con số nào? Và làm thế nào bạn thậm chí sẽ bắt đầu để mô tả lý do tại sao mà 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 chuỗi với 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 cùng với "hàm kết buộc". (Trong Haskell, nó được đặt tên >>=.) Bạn có thể tự mình viết các cuộc gọi tới toán tử bind, hoặc bạn có thể sử dụng đường cú pháp làm cho trình biên dịch chèn các cuộc gọi hàm đó cho bạn. Nhưng một trong hai cách, mỗi bước được phân tách bằng một cuộc gọi đến hàm liên kết này.

Vì vậy, hàm kết buộc giống như dấu chấm phẩy; nó tách các bước trong một quá 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à nạp nó vào bước tiếp theo.

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

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

(Một đơn vị làm không có gì đặc biệt được gọi là "danh tính đơn sắc". Đúng hơn là chức năng nhận dạng, điều này nghe có vẻ như là một điều hoàn toàn vô nghĩa, nhưng hóa ra không phải là ... Nhưng đó là một câu chuyện khác ™.)

Về cơ bản, mỗi đơn nguyên có triển khai thực hiện chức năng liên kết của chính nó. Và bạn có thể viết một hàm kết buộc sao cho nó thực hiện những điều khó hiểu 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. Bằng 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. (Các Thất bại Monad.)

  • Mở rộng ý tưởng này, bạn có thể thực hiện "ngoại lệ". (Các Lỗi Monad hoặc là Ngoại lệ Monad.) Bởi vì bạn tự xác định chúng thay vì nó 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 một thứ ba ngoại lệ được ném.)

  • Bạn có thể thực hiện từng bước trở lại nhiều kết quả, và có vòng lặp chức năng liên kết trên chúng, cho mỗi bước vào bước tiếp theo cho bạn. Bằng cách này, bạn không phải tiếp tục viết vòng quanh nơi khi giao dịch với nhiều kết quả. Chức năng liên kết "tự động" thực hiện tất cả những gì cho bạn. (Các Liệt kê Monad.)

  • Cũng như chuyển "kết quả" từ bước này sang bước khác, bạn có thể có hàm liên kết chuyển dữ liệu thừa xung quanh là tốt. 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 dữ liệu từ bất kỳ đâu, mà không phải chuyển nó theo cách thủ công cho mọi chức năng. (Các Reader Monad.)

  • Bạn có thể làm cho nó để các "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ập nhật phá hoại. (Các State Monad và người em họ của nó Writer Monad.)

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

  • Bạn có thể tạo một đơn vị có thể tính toán đã tạm dừng, vì vậy bạn có thể tạm dừng chương trình của bạn, đi vào và tinker với dữ liệu trạng thái nội bộ, và sau đó tiếp tục nó.

  • 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 monads. Tất nhiên, tất cả điều này cũng hoàn toàn có thể không có monads quá. Nó chỉ là quyết liệt dễ dàng hơn sử dụng monads.


638



Tôi đánh giá cao câu trả lời của bạn - đặc biệt là 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ể quá mà không có monads. Một điểm cần làm là nó chủ yếu dễ dàng hơn với monads, nhưng nó thường không hiệu quả như làm nó mà không có chúng. Một khi bạn cần phải liên quan đến máy biến áp, các lớp bổ sung của các cuộc gọi chức năng (và các đối tượng chức năng được tạo ra) có chi phí khó xem và kiểm soát, hiển thị vô hình bằng cú pháp thông minh. - seh
Trong Haskell ít nhất, hầu hết các chi phí của monads bị tước đi bởi các optimiser. Vì vậy, chỉ có "chi phí" thực sự là cần thiết trong não bộ. (Điều này là không đáng kể nếu "bảo trì" là một cái gì đó bạn quan tâm.) Nhưng thường, monads 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 sẽ bận tâm?) - MathematicalOrchid
Đế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
Đây là câu trả lời đầu tiên thực sự đã cho tôi một số ý tưởng về cái quái gì là địa ngục. Cảm ơn bạn đã tìm cách giải thích nó! - robotmay
Câu trả lời của bạn là hoàn toàn rực rỡ! cảm ơn một tấn! - Swapnil B.


Trên thực tế, trái với sự hiểu biết chung của Monads, họ không có gì để làm với nhà nước. Monads chỉ đơn giản là cách để gói 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 gói mà không cần mở nó.

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

data Wrapped a = Wrap a

Để bọc nội dung 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 cần unwrapping, hãy nói rằng bạn có một hàm f :: a -> b, sau đó bạn có thể làm điều này để thang máy hàm đó hoạt động trên các giá trị được bao 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, nó chỉ ra rằng có một chức năng tổng quát hơn để làm điều này Nâng, đó là bind:

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

bind có thể làm nhiều hơn một chút fmap, nhưng không phải ngược lại. Thực ra, fmap chỉ có thể được xác định theo bind và return. Vì vậy, khi xác định một monad .. bạn cung cấp cho loại của nó (ở đây nó đã được Wrapped a) và sau đó nói như thế nào return và bind hoạt động.

Điều thú vị là điều này hóa ra là một mô hình chung mà nó bật lên khắp nơi, trạng thái đóng gói một cách thuần khiết chỉ là một trong số chúng.

Đối với một bài viết tốt về cách monads 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 nguyên IO của Haskell, hãy kiểm tra IO Inside.

Đối với sự hiểu biết monads, đừng lo lắng quá nhiều về nó. Đọc về 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à con đường để đi. Monads là một trong những điều mà sự hiểu biết nhỏ nhặt vào bộ 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 rằng bạn hiểu chúng.


164



-> là ứng dụng chức năng phản chiếu, liên kết bên phải, có liên kết bên trái, vì vậy để các dấu ngoặc đơn không tạo ra sự khác biệt ở đây. - Matthias Benkard
Lời giải thích của bạn đã làm điều đó cho tôi. Tôi đã thêm vào mặc dù một số tiền giới hạn của một số monads tiêu chuẩn (người đọc, tiểu bang, có thể, ...) để minh họa một số sử dụng thực tế và bao bì - Rabarberski
Tôi không nghĩ rằng đây là một lời giải thích rất tốt cả. Monads chỉ đơn giản là một cách? được rồi, theo cách nào? Tại sao tôi không đóng gói bằng cách sử dụng một lớp thay vì một đơn? - Breton
Một giải thích dài hơn về ý tưởng này: blog.sigfpe.com/2007/04/trivial-monad.html - sdcvvc
@ mb21: Trong trường hợp bạn chỉ cần chỉ ra rằng có quá nhiều dấu ngoặc, hãy lưu ý rằng a-> b-> c thực sự chỉ viết tắt cho một -> (b-> c). Viết ví dụ cụ thể này là (a -> b) -> (Ta -> Tb) nói đúng là chỉ thêm các ký tự không cần thiết, nhưng về mặt đạo đức là "điều đúng để làm" vì nó nhấn mạnh rằng fmap ánh xạ một hàm kiểu a -> b đến một hàm của kiểu Ta -> Tb. Và ban đầu, đó là những gì các nhà làm trong lý thuyết thể loại và đó là nơi mà các nhà sư đến từ. - Nikolaj-K


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

sigfpe nói:

Nhưng tất cả những giới thiệu này như một điều 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 chút nào. Trong thực tế, phải đố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, không thể tránh khỏi, với một số giải pháp nhất định, tất cả đều là những ví dụ về monads. Trong 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ó. Đó là một bước nhỏ để nhận thấy rằng tất cả các giải pháp trên thực tế là giải pháp tương tự trong ngụy trang. Và sau khi đọc điều này, bạn có thể ở một vị trí tốt hơn để hiểu các tài liệu khác về monads bởi vì bạn sẽ nhận ra tất cả mọi thứ bạn nhìn thấy như một cái gì đó bạn đã phát minh ra.

Nhiều vấn đề mà monads cố 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 monads cho phép bạn làm nhiều hơn là xử lý các hiệu ứng phụ, đặc biệt là nhiều kiểu đối tượng chứa container có thể được xem như là các monads. Một số phần giới thiệu cho các monads thấy khó hòa hợp hai cách sử dụng khác nhau này và tập trung vào một hoặc 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ó một hàm C ++ lấy một đối số dấu chấm động và trả về một kết quả dấu chấm động. Bề ngoài nó có vẻ giống như một hàm toán học ánh xạ các thực sang 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ầu cũng như ghi đầu ra vào 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ó hiệu ứng trên thế giới là thông qua các giá trị nó trả về.


161



… Cách tốt nhất không chỉ trên internet, mà ở bất cứ đâu. (Bản gốc của Wadler Monads cho lập trình hàm mà tôi đã đề cập trong câu trả lời của tôi dưới đây cũng tốt.) Không ai trong số hàng chục bài hướng dẫn bằng cách tương tự đến gần. - ShreevatsaR
Bản dịch JavaScript này của bài đăng của Sigfpe là cách tốt nhất mới để học monads, cho những người không đã hk nâng cao Haskell! - Sam Watkins
Đây là cách tôi học được một đơn nguyên là gì. Đi bộ người đọc thông 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 cho khái niệm. - 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 đã sửa đổi sẽ là thuần túy. - Dmitri Zaitsev


Một đơn nguyên là một kiểu dữ liệu có hai hoạt động: >>= (aka bind) và return (aka unit). return có 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 hàm trên nó. (Bạn có thể thấy rằng một đơn nguyên là một kiểu dữ liệu lạ, 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 có giá trị tùy ý và tạo ra một kiểu từ nó. -tính đa hình tham số.)

Trong ký hiệu Haskell, giao diện đơn nhất đượ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 thủ một số "luật" nhất định, nhưng điều đó không quan trọng lắm: "luật" chỉ đơn giản hóa việc thực hiện các hoạt động hợp lý của các hoạt động phải hoạt động (về cơ bản, >>= và return nên đồng ý về cách các giá trị được chuyển thành các trường hợp đơn nguyên và >>= là kết hợp).

Monads không chỉ về trạng thái và I / O: chúng trừu tượng một mẫu 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ẽ những đơn giản nhất để hiểu là danh sách và 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

Ở đâu [] và : là những người xây dựng danh sách, ++là toán tử ghép nối và Just và Nothing là Maybe nhà thầu. Cả hai monads này đóng gói các mẫu tính toán thông dụng và hữu ích trên các kiểu dữ liệu tương ứng của chúng (lưu ý rằng không có liên quan gì đến các tác dụng phụ hoặc I / O).

Bạn thực sự phải chơi xung quanh bằng văn bản một số mã Haskell không tầm thường để đánh giá cao những gì monads là về và tại sao họ là hữu ích.


77



Chính xác những gì bạn có ý nghĩa của "bản đồ một chức năng trên 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 để hiểu ý nghĩa của việc "ánh xạ một hàm". - Chris Conway
Monad không phải là kiểu dữ liệu. Đó là một quy tắc của các chức năng soạn thảo: stackoverflow.com/a/37345315/1614973 - Dmitri Zaitsev


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

A hàm bậc cao chỉ đơn giản là một hàm nhận hàm làm đối số.

A functor là bất kỳ loại xây dựng nào T mà tồn tại một hàm bậc cao hơn, gọi nó là map, điều đó biến đổi một hàm kiểu a -> b (cho hai loại bất kỳ a và b) vào một hàm T a -> T b. Điều này map chức năng cũng phải tuân theo các định luật về nhận dạng và bố cục sao cho các biểu thức sau đây trả về đúng cho tất cả p và q (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 là List là một functor nếu nó được trang bị chức năng kiểu (a -> b) -> List a -> List b tuân thủ luật pháp ở trên. Việc thực hiện chỉ thực tế là hiển nhiên. Kết quả List a -> List b chức năng lặp lại trên danh sách đã cho, gọi (a -> b) cho mỗi phần tử và trả về danh sách kết quả.

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

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

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

Bạn có thể viết một hàm map, sau đó join. Hàm này được gọi bind, hoặc là flatMap, hoặc là (>>=), hoặc là (=<<). Điều này là bình thường như thế nào một ví dụ monad được đưa ra trong Haskell.

Một đơn nguyên phải thỏa mãn một số luật, cụ thể là join phải là kết hợp. Điều này có nghĩa là nếu bạn có một giá trị x loại [[[a]]] sau đó join (join x) nên bằng join (map join x). Và pure phải là một bản sắc cho join như vậy mà join (pure x) == x.


71



bổ sung nhẹ vào def của 'hàm thứ tự cao hơn': chúng có thể thực hiện các hàm OR RETURN. Đó là lý do tại sao họ là 'cao hơn' 'cos họ làm những việc với chính mình. - Kevin Won
Theo định nghĩa đó, bổ sung là một hàm bậc cao hơn. Nó lấy một số và trả về một hàm bổ sung 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 đúng 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


[Disclaimer: Tôi vẫn cố gắng hoàn toàn grok monads. Sau đây là những gì tôi đã hiểu cho đến nay. Nếu sai, hy vọng ai đó có kiến ​​thức sẽ gọi cho tôi trên thảm.]

Arnar đã viết:

Monads chỉ đơn giản là cách để gói 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 gói mà không cần mở nó.

Đó chính là nó. Ý tưởng đi như thế này:

  1. Bạn có một số loại giá trị và quấn nó với một số thông tin bổ sung. Cũng giống như giá trị của 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à Maybe hoặc một IO.

  2. Sau đó, bạn có một số toán tử cho phép bạn hoạt động trên dữ liệu được bao 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 hoạt động trên giá trị được bao bọc.

    Ví dụ: a Maybe Int có thể là một Just Int hoặc là Nothing. Bây giờ, nếu bạn thêm Maybe Int đến một Maybe Int, toán tử sẽ kiểm tra xem chúng có vừa là Just Intbên trong, và nếu có, sẽ mở khóa Ints, chuyển cho họ toán tử cộng, kết quả lại Int vào một Just Int (đó là một giá trị Maybe Int), và do đó trả lại Maybe Int. Nhưng nếu một trong số đó là Nothing bên trong, toán tử 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 Maybe Ints chỉ là số bình thường và thực hiện toán học 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 sẽ vẫn tạo ra kết quả đúng - mà không cầ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, sau đó toán tử đặc biệt được xác định cho IOs sẽ được gọi là thay vào đó, và nó có thể làm một cái gì đó hoàn toàn khác nhau trước khi thực hiện việc bổ sung. (OK, thêm hai IO Ints với nhau có lẽ là vô nghĩa - tôi vẫn chưa chắc chắn.) (Ngoài ra, nếu bạn chú ý đến Maybe Ví dụ, bạn đã nhận thấy rằng "gói một giá trị với các công cụ bổ sung" không phải lúc nào cũng chính xác. Nhưng thật khó để chính xác, chính xác và chính xác mà không bị khó hiểu.)

Về cơ bản, “Đơn nguyên” có nghĩa là “mô hình”. Nhưng thay vì một cuốn sách có đầy đủ các giải thích không chính thức và được đặt tên cụ thể, bạn đã có một ngôn ngữ xây dự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ự không 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ể, vì vậy một đơn nguyên không hoàn toàn giống như một khuôn mẫu. Nhưng tôi nghĩ đó là thuật ngữ gần nhất mà hầu hết mọi người đều biết và hiểu.)

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

Nhưng hãy nghĩ về những hàm ý của việc có sự hỗ trợ cú pháp trong ngôn ngữ cho ý tưởng về một khuôn mẫu: thay vì phải đọc Gang of Four cuốn sách và ghi nhớ việc xây dựng một mô hình cụ thể, bạn chỉ cần viết mã thực hiện mẫu này theo cách tổng quát, không thuyết phục 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 hay bất kỳ thứ 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 cần phải triển khai lại nó!

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


42



Đôi khi một lời giải thích từ một "người học" (giống như bạn) là phù hợp hơn với người học khác hơn là một lời giải thích đến từ một chuyên gia. Học viên nghĩ giống nhau :) - Adrian
Điều gì làm cho một cái gì đó một đơn nguyên là sự tồn tại của một hàm với kiểu 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 b là những gì làm cho chúng hữu ích. - Jeremy List
"monad" có nghĩa là "mẫu" ... không. - user633183