2009-03-23 26 views
177

Có rất nhiều cuộc nói chuyện về monads những ngày này. Tôi đã đọc một vài bài viết/bài đăng blog, nhưng tôi không thể đi đủ xa với các ví dụ của họ để nắm bắt đầy đủ khái niệm. Lý do là monads là một khái niệm ngôn ngữ chức năng, và do đó các ví dụ được viết bằng ngôn ngữ mà tôi chưa từng làm việc (vì tôi chưa từng sử dụng một ngôn ngữ chức năng theo chiều sâu). Tôi không thể nắm bắt được cú pháp đủ sâu để làm theo các bài viết hoàn toàn ... nhưng tôi có thể nói có điều gì đó đáng để hiểu ở đó.Giúp nhà phát triển C# hiểu: Một đơn nguyên là gì?

Tuy nhiên, tôi biết C# khá tốt, bao gồm các biểu thức lambda và các tính năng chức năng khác. Tôi biết C# chỉ có một tập con của các tính năng chức năng, và vì vậy có lẽ monads không thể được thể hiện trong C#.

Tuy nhiên, chắc chắn có thể truyền đạt khái niệm này? Ít nhất tôi hy vọng như vậy. Có lẽ bạn có thể trình bày một ví dụ C làm nền tảng, và sau đó mô tả những gì một nhà phát triển C# sẽ muốn anh ta có thể làm từ đó nhưng không thể vì ngôn ngữ thiếu các tính năng lập trình hàm. Điều này sẽ là tuyệt vời, bởi vì nó sẽ truyền đạt ý định và lợi ích của các monads. Vì vậy, đây là câu hỏi của tôi: Giải thích tốt nhất mà bạn có thể cung cấp cho monads cho một nhà phát triển C# 3 là gì?

Cảm ơn!

(EDIT: Nhân tiện, tôi biết có ít nhất 3 câu hỏi "một đơn nguyên" đã có trên SO. Tuy nhiên, tôi phải đối mặt với cùng một vấn đề với họ ... vì vậy câu hỏi này là cần thiết, vì C# - nhà phát triển tập trung. Cảm ơn.)

+0

Xin lưu ý rằng nó thực sự là một nhà phát triển C# 3.0. Đừng nhầm lẫn với .NET 3.5. Ngoài ra, câu hỏi hay. – Razzie

+3

Đó là giá trị chỉ ra rằng biểu thức truy vấn LINQ là một ví dụ về hành vi monadic trong C# 3. –

+1

Tôi vẫn nghĩ rằng đó là một câu hỏi trùng lặp. Một trong những câu trả lời trong http://stackoverflow.com/questions/2366/can-anyone-explain-monads liên kết tới http://channel9vip.orcsweb.com/shows/Going+Deep/Brian-Beckman-Dont-fear- the Monads /, trong đó một trong các bình luận có một ví dụ C# rất đẹp. :) – jalf

Trả lời

9

Một đơn nguyên về cơ bản là xử lý hoãn lại. Nếu bạn đang cố gắng viết mã có tác dụng phụ (ví dụ I/O) bằng ngôn ngữ không cho phép chúng, và chỉ cho phép tính toán thuần túy, một dodge là nói, "Ok, tôi biết bạn sẽ không làm các tác dụng phụ cho tôi, nhưng bạn có thể vui lòng tính toán điều gì sẽ xảy ra nếu bạn đã làm không? "

Đó là loại gian lận.

Bây giờ, lời giải thích đó sẽ giúp bạn hiểu được ý định hình ảnh lớn của các monads, nhưng ma quỷ ở trong các chi tiết. Làm thế nào chính xác làm bạn tính toán hậu quả? Đôi khi, nó không phải là đẹp.

Cách tốt nhất để cung cấp tổng quan về cách người dùng sử dụng chương trình bắt buộc là nói rằng bạn đặt một DSL trong đó các hoạt động trông giống như những gì bạn đang sử dụng bên ngoài đơn vị được sử dụng để xây dựng chức năng đó sẽ làm những gì bạn muốn nếu bạn có thể (ví dụ) ghi vào một tập tin đầu ra. Hầu như (nhưng không thực sự) như thể bạn đang xây dựng mã trong một chuỗi để sau này được đánh giá.

+1

Giống như trong cuốn sách I Robot? Trường hợp các nhà khoa học yêu cầu một máy tính để tính toán du lịch không gian và yêu cầu họ bỏ qua các quy tắc nhất định? :) :) :) :) – OscarRyz

+3

Hmm, A Monad _can_ được sử dụng để xử lý trì hoãn và đóng gói các hàm tác dụng phụ, thực sự đó là ứng dụng thực sự đầu tiên trong Haskell, nhưng nó thực sự là một mẫu chung chung hơn nhiều. Các ứng dụng phổ biến khác bao gồm xử lý lỗi và quản lý trạng thái. Đường cú pháp (làm trong Haskell, Biểu thức tính toán trong F #, cú pháp LINQ trong C#) chỉ đơn thuần là điều cơ bản đối với Monads như vậy. –

+0

@MikeHadlow: Các trường hợp đơn lẻ để xử lý lỗi ('Có thể' và' Hoặc e') và quản lý trạng thái ('State s',' ST s') tấn công tôi là trường hợp cụ thể "Vui lòng tính toán điều gì sẽ xảy ra nếu bạn đã làm [ tác dụng phụ cho tôi] ". Một ví dụ khác là không xác định ('[]'). – pyon

4

Tôi chắc rằng người dùng khác sẽ đăng bài chuyên sâu, nhưng tôi thấy this video hữu ích trong một phạm vi, nhưng tôi sẽ nói rằng tôi vẫn chưa đến mức lưu loát với khái niệm mà tôi có thể (hoặc nên) bắt đầu giải quyết các vấn đề một cách trực giác với Monads.

+1

Điều tôi thấy hữu ích hơn nữa là nhận xét chứa ví dụ C# bên dưới video. – jalf

+0

Tôi không biết về * nhiều hơn nữa * hữu ích, nhưng nó chắc chắn đưa các ý tưởng vào thực tế. – TheMissingLINQ

0

Bạn có thể nghĩ về một đơn nguyên là a C# interface that classes have to implement. Đây là một câu trả lời thực tế bỏ qua tất cả các loại lý thuyết toán học đằng sau lý do tại sao bạn muốn chọn có những khai báo trong giao diện của bạn và bỏ qua tất cả các lý do tại sao bạn muốn có monads trong một ngôn ngữ mà cố gắng để tránh tác dụng phụ, nhưng tôi thấy nó là một khởi đầu tốt như một người hiểu (C#) giao diện.

+0

Bạn có thể xây dựng? Nó là gì về một giao diện liên quan đến giao diện với monads? –

+1

Tôi nghĩ rằng bài đăng trên blog sẽ mở ra một số đoạn dành cho câu hỏi đó. – hao

141

Hầu hết những gì bạn làm trong lập trình cả ngày là kết hợp một số chức năng với nhau để xây dựng các chức năng lớn hơn từ chúng. Thông thường bạn không chỉ có chức năng trong hộp công cụ mà còn cả những thứ khác như toán tử, bài tập biến và những thứ tương tự, nhưng nói chung chương trình của bạn kết hợp rất nhiều "tính toán" với các phép tính lớn hơn sẽ được kết hợp với nhau thêm.

Một đơn nguyên là một số cách để thực hiện điều này "kết hợp tính toán".

Thông thường cơ bản nhất "điều hành" của mình để kết hợp hai tính toán với nhau là ;:

a; b 

Khi bạn nói điều này bạn có nghĩa là "đầu tiên làm a, sau đó làm b". Kết quả a; b về cơ bản lại là một tính toán có thể được kết hợp cùng với nhiều thứ hơn. Đây là một đơn nguyên đơn giản, nó là một cách để chải các tính toán nhỏ cho những cái lớn hơn. Các ; nói "làm điều bên trái, sau đó làm điều trên bên phải".

Một thứ khác có thể được xem là đơn nguyên trong ngôn ngữ hướng đối tượng là .. Thường thì bạn tìm thấy những thứ như thế này:

a.b().c().d() 

Các . về cơ bản có nghĩa là "đánh giá tính toán ở bên trái, và sau đó gọi phương thức trên phải vào kết quả của điều đó". Đó là một cách khác để kết hợp các hàm/tính toán với nhau, phức tạp hơn một chút so với ;. Và khái niệm về chuỗi những thứ cùng với . là một đơn nguyên, vì nó là một cách kết hợp hai phép tính với nhau để tính toán mới.

Một đơn nguyên khá phổ biến, mà không có cú pháp đặc biệt, là mô hình này:

rv = socket.bind(address, port); 
if (rv == -1) 
    return -1; 

rv = socket.connect(...); 
if (rv == -1) 
    return -1; 

rv = socket.send(...); 
if (rv == -1) 
    return -1; 

Một giá trị trả về là -1 chỉ ra thất bại, nhưng không có cách nào thực sự để trừu tượng ra kiểm tra lỗi này, ngay cả khi bạn có rất nhiều cuộc gọi API mà bạn cần phải kết hợp trong thời trang này. Về cơ bản, đây chỉ là một đơn nguyên khác kết hợp hàm gọi theo quy tắc "nếu hàm ở bên trái trả về -1, trả về -1, nếu không gọi hàm ở bên phải". Nếu chúng ta có một nhà điều hành >>= rằng đã làm điều này, chúng tôi chỉ đơn giản là có thể viết:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...) 

Nó sẽ làm cho mọi việc dễ đọc hơn và giúp đỡ để trừu tượng ra cách đặc biệt của chúng tôi các chức năng kết hợp, vì vậy mà chúng ta không cần phải lặp lại chính mình lặp đi lặp lại. Có rất nhiều cách để kết hợp các chức năng/tính toán hữu ích như một mẫu chung và có thể được trừu tượng hóa trong một đơn nguyên, cho phép người dùng đơn lẻ viết nhiều đoạn ngắn gọn và rõ ràng hơn, vì tất cả các cuốn sách- giữ và quản lý các chức năng được sử dụng được thực hiện trong đơn nguyên.

Ví dụ: >>= ở trên có thể được mở rộng thành "thực hiện kiểm tra lỗi và sau đó gọi bên phải trên ổ cắm mà chúng tôi nhận là đầu vào", để chúng tôi không cần chỉ định rõ ràng socket nhiều lần:

new socket() >>= bind(...) >>= connect(...) >>= send(...); 

Định nghĩa chính thức phức tạp hơn một chút vì bạn phải lo lắng về cách lấy kết quả của một hàm làm đầu vào cho hàm tiếp theo, nếu hàm đó cần đầu vào đó và vì bạn muốn đảm bảo các chức năng bạn kết hợp phù hợp với cách bạn cố gắng kết hợp chúng trong đơn nguyên của bạn. Nhưng khái niệm cơ bản chỉ là bạn chính thức hóa các cách khác nhau để kết hợp các chức năng với nhau.

+26

Câu trả lời hay! Tôi sẽ đưa ra lời trích dẫn của Oliver Steele, cố gắng liên hệ Monads với nhà điều hành quá tải à la C++ hoặc C#: Monads cho phép bạn quá tải ';' nhà điều hành. –

+5

@ JörgWMittag Tôi đã đọc câu nói đó trước đây, nhưng nó nghe có vẻ như vô nghĩa. Bây giờ tôi hiểu các monads và đọc lời giải thích này về cách ';' là một, tôi hiểu rồi. Nhưng tôi nghĩ nó thực sự là một tuyên bố không hợp lý đối với hầu hết các nhà phát triển bắt buộc. ';' không được xem như một toán tử nữa so với // là nhiều nhất. –

+0

Bạn có chắc chắn rằng bạn biết một đơn nguyên là gì? Monads không phải là "chức năng" hoặc tính toán, có các quy tắc cho monads. – Luis

41

Đã một năm kể từ khi tôi đăng câu hỏi này. Sau khi đăng nó, tôi đã đi sâu vào Haskell trong vài tháng. Tôi rất thích nó, nhưng tôi đặt nó sang một bên khi tôi đã sẵn sàng đi sâu vào Monads.Tôi trở lại làm việc và tập trung vào các công nghệ mà dự án của tôi yêu cầu.

Và đêm qua, tôi đã đến và đọc lại các câu trả lời này. Quan trọng nhất là, tôi đã đọc lại the specific C# example trong các nhận xét văn bản của the Brian Beckman video ai đó mentions above. Nó hoàn toàn rõ ràng và chiếu sáng mà tôi đã quyết định đăng trực tiếp tại đây.

Do nhận xét này, không chỉ làm tôi cảm thấy như tôi hiểu chính xác gì Monads là ... Tôi nhận ra tôi đã thực sự viết một số thứ trong C# mà Monads ... hoặc ít nhất là rất gần, và phấn đấu để giải quyết các vấn đề tương tự.

Vì vậy, đây là những nhận xét - đây là tất cả các trích dẫn trực tiếp từ the comment here bởi sylvan:

này là khá mát mẻ. Đó là một chút trừu tượng mặc dù. Tôi có thể tưởng tượng những người không biết những gì monads đã bị nhầm lẫn do thiếu các ví dụ thực tế.

Vì vậy, hãy để tôi cố gắng tuân thủ, và chỉ để thực sự rõ ràng tôi sẽ làm một ví dụ trong C#, mặc dù nó sẽ trông xấu xí. Tôi sẽ thêm Haskell tương đương vào cuối và cho bạn thấy đường cú pháp Haskell tuyệt vời, ở đâu, IMO, monads thực sự bắt đầu có ích.

Được rồi, vì vậy một trong những Monads đơn giản nhất được gọi là "Có thể đơn nguyên" trong Haskell. Trong C#, loại Có thể được gọi là Nullable<T>. Về cơ bản, nó là một lớp nhỏ chỉ gói gọn khái niệm về một giá trị hợp lệ và có giá trị, hoặc là "null" và không có giá trị.

Một điều hữu ích để gắn bên trong một đơn nguyên để kết hợp các giá trị thuộc loại này là khái niệm thất bại. I E. chúng tôi muốn có thể xem xét nhiều giá trị nullable và trả lại null ngay sau khi bất kỳ giá trị nào trong số đó là null. Điều này có thể hữu ích nếu bạn, ví dụ, tra nhiều khóa trong từ điển hay gì đó, và cuối cùng bạn muốn xử lý tất cả các kết quả và kết hợp chúng bằng cách nào đó, nhưng nếu bất kỳ khóa nào không có trong từ điển, bạn muốn trả lại null cho toàn bộ điều. Nó sẽ là tẻ nhạt để tự kiểm tra từng tra cứu cho null và trả về, vì vậy chúng ta có thể ẩn kiểm tra này bên trong toán tử liên kết (đó là loại điểm của monads, chúng ta che giấu sổ sách trong toán tử bind. để sử dụng vì chúng ta có thể quên chi tiết).

Đây là chương trình khuyến khích toàn bộ sự việc (tôi sẽ xác định Bind sau này, đây chỉ là để cho bạn thấy lý do tại sao nó đẹp).

class Program 
    { 
     static Nullable<int> f(){ return 4; }   
     static Nullable<int> g(){ return 7; } 
     static Nullable<int> h(){ return 9; } 


     static void Main(string[] args) 
     { 


      Nullable<int> z = 
           f().Bind(fval => 
           g().Bind(gval => 
           h().Bind(hval => 
           new Nullable<int>(fval + gval + hval)))); 

      Console.WriteLine(
        "z = {0}", z.HasValue ? z.Value.ToString() : "null"); 
      Console.WriteLine("Press any key to continue..."); 
      Console.ReadKey(); 
     } 
    } 

Bây giờ, bỏ qua trong chốc lát rằng đã có sự ủng hộ để làm điều này cho Nullable trong C# (bạn có thể thêm ints nullable với nhau và bạn nhận được null nếu một trong hai là null). Hãy giả vờ rằng không có tính năng như vậy, và nó chỉ là một lớp người dùng định nghĩa không có phép thuật đặc biệt. Vấn đề là chúng ta có thể sử dụng hàm Bind để liên kết một biến với nội dung của giá trị Nullable và giả sử rằng không có gì lạ xảy ra và sử dụng chúng như ints bình thường và chỉ cần thêm chúng lại với nhau. Chúng tôi bọc kết quả trong một nullable ở cuối, và nullable sẽ là null (nếu bất kỳ f, g hoặc h trả về null) hoặc nó sẽ là kết quả của việc tổng hợp f, gh cùng nhau. (điều này tương tự như cách chúng ta có thể ràng buộc một hàng trong một cơ sở dữ liệu với một biến trong LINQ, và làm việc với nó, an toàn khi biết rằng toán tử Bind sẽ đảm bảo rằng biến sẽ chỉ được thông qua giá trị hàng hợp lệ).

Bạn có thể chơi với điều này và thay đổi bất kỳ số f, gh để trả về null và bạn sẽ thấy toàn bộ điều sẽ trả về giá trị rỗng. Vì vậy, rõ ràng các nhà điều hành liên kết đã làm điều này kiểm tra cho chúng tôi, và bảo lãnh trả lại null nếu nó gặp một giá trị null, và nếu không vượt qua cùng giá trị bên trong cấu trúc Nullable vào lambda.

Dưới đây là các nhà điều hành Bind:

public static Nullable<B> Bind<A,B>(this Nullable<A> a, Func<A,Nullable<B>> f) 
    where B : struct 
    where A : struct 
{ 
    return a.HasValue ? f(a.Value) : null; 
} 

Các loại ở đây cũng giống như trong đoạn video. Phải mất một cú pháp M a (Nullable<A> trong C# cho trường hợp này) và hàm từ a đến M b (Func<A, Nullable<B>> theo cú pháp C#) và trả về một M b (Nullable<B>).

Mã này chỉ kiểm tra nếu giá trị rỗng có chứa một giá trị và nếu trích xuất nó và chuyển nó vào hàm, nếu không nó sẽ trả về giá trị rỗng. Điều này có nghĩa là toán tử Bind sẽ xử lý tất cả logic kiểm tra null cho chúng tôi. Nếu và chỉ khi giá trị mà chúng tôi gọi là Bind trên là không null thì giá trị đó sẽ được "chuyển cùng" sang hàm lambda, nếu không chúng tôi sẽ gửi tiền sớm và toàn bộ biểu thức là không. Điều này cho phép mã mà chúng tôi viết bằng cách sử dụng đơn nguyên hoàn toàn không có hành vi kiểm tra không có giá trị này, chúng tôi chỉ sử dụng Bind và nhận một biến ràng buộc với giá trị bên trong giá trị đơn nguyên (fval, gvalhval trong mã ví dụ) và chúng tôi có thể sử dụng chúng an toàn khi biết rằng Bind sẽ xử lý việc kiểm tra chúng trước khi chuyển chúng.

Có các ví dụ khác về những điều bạn có thể làm với một đơn nguyên. Ví dụ: bạn có thể làm cho toán tử Bind xử lý luồng đầu vào của các ký tự và sử dụng nó để viết trình kết hợp phân tích cú pháp. Mỗi trình kết hợp phân tích cú pháp sau đó có thể hoàn toàn không biết gì về những thứ như theo dõi lại, lỗi phân tích cú pháp, và chỉ kết hợp các trình phân tích cú pháp nhỏ hơn với nhau như thể mọi thứ sẽ không bao giờ sai, an toàn với kiến ​​thức rằng việc thực hiện một cách thông minh của Bind các bit khó. Sau đó, sau đó có thể ai đó thêm đăng nhập vào đơn nguyên, nhưng mã sử dụng đơn nguyên không thay đổi, bởi vì tất cả phép thuật xảy ra trong định nghĩa của toán tử Bind, phần còn lại của mã không thay đổi.

Cuối cùng, đây là việc triển khai cùng một mã trong Haskell (-- bắt đầu dòng nhận xét).

-- Here's the data type, it's either nothing, or "Just" a value 
-- this is in the standard library 
data Maybe a = Nothing | Just a 

-- The bind operator for Nothing 
Nothing >>= f = Nothing 
-- The bind operator for Just x 
Just x >>= f = f x 

-- the "unit", called "return" 
return = Just 

-- The sample code using the lambda syntax 
-- that Brian showed 
z = f >>= (\fval -> 
    g >>= (\gval -> 
    h >>= (\hval -> return (fval+gval+hval)))) 

-- The following is exactly the same as the three lines above 
z2 = do 
    fval <- f 
    gval <- g 
    hval <- h 
    return (fval+gval+hval) 

Như bạn có thể thấy ký hiệu đẹp ở cuối làm cho nó trông giống như mã mệnh lệnh thẳng. Và thực sự điều này là do thiết kế. Monads có thể được sử dụng để đóng gói tất cả các công cụ hữu ích trong lập trình bắt buộc (trạng thái có thể thay đổi, IO, v.v.) và sử dụng cú pháp giống như mệnh lệnh này, nhưng đằng sau rèm cửa, tất cả chỉ là các monads và thực hiện thông minh toán tử bind! Điều thú vị là bạn có thể thực hiện các monads của riêng mình bằng cách triển khai >>=return. Và nếu bạn làm như vậy những monads cũng sẽ có thể sử dụng ký hiệu do, có nghĩa là bạn về cơ bản có thể viết các ngôn ngữ nhỏ của riêng bạn bằng cách chỉ định nghĩa hai hàm!

+2

Cá nhân tôi thích phiên bản của F #, nhưng trong cả hai trường hợp, chúng đều tuyệt vời. – ChaosPandion

+3

Cảm ơn bạn đã quay lại đây và cập nhật bài đăng của bạn. Theo dõi những thứ này giúp các lập trình viên đang tìm kiếm một khu vực cụ thể thực sự hiểu cách các lập trình viên đồng ý cuối cùng nói về khu vực đó, thay vì chỉ đơn giản là "làm thế nào để tôi làm x trong công nghệ y". Bạn da man! – kappasims

+0

Tôi đã thực hiện cùng một con đường mà bạn có về cơ bản và đến cùng một nơi để hiểu các monads, cho biết đây là lời giải thích tốt nhất về hành vi ràng buộc của một đơn nguyên mà tôi từng thấy đối với một nhà phát triển bắt buộc. Mặc dù tôi nghĩ rằng bạn không hoàn toàn chạm vào tất cả mọi thứ về monads đó là một chút giải thích ở trên bởi sth. –

0

Xem answer của tôi là "Một đơn nguyên là gì?"

Nó bắt đầu với một ví dụ động cơ thúc đẩy, hoạt động thông qua ví dụ này, xuất phát một ví dụ về một đơn nguyên, và chính thức định nghĩa 'đơn nguyên'.

Nó giả định không có kiến ​​thức về lập trình chức năng và nó sử dụng mã giả với function(argument) := expression cú pháp với các biểu thức đơn giản nhất có thể

chương trình

C này # là một thực hiện các đơn nguyên giả.. (Để tham khảo:. M là các nhà xây dựng loại, feed là "ràng buộc" hoạt động, và wrap là "trở lại" hoạt động)

using System.IO; 
using System; 

class Program 
{ 
    public class M<A> 
    { 
     public A val; 
     public string messages; 
    } 

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x) 
    { 
     M<B> m = f(x.val); 
     m.messages = x.messages + m.messages; 
     return m; 
    } 

    public static M<A> wrap<A>(A x) 
    { 
     M<A> m = new M<A>(); 
     m.val = x; 
     m.messages = ""; 
     return m; 
    } 

    public class T {}; 
    public class U {}; 
    public class V {}; 

    public static M<U> g(V x) 
    { 
     M<U> m = new M<U>(); 
     m.messages = "called g.\n"; 
     return m; 
    } 

    public static M<T> f(U x) 
    { 
     M<T> m = new M<T>(); 
     m.messages = "called f.\n"; 
     return m; 
    } 

    static void Main() 
    { 
     V x = new V(); 
     M<T> m = feed<U, T>(f, feed(g, wrap<V>(x))); 
     Console.Write(m.messages); 
    } 
} 
Các vấn đề liên quan