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ế.
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ế.
Câu trả lời:
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
, digit
vv 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 bind
toán tử (đánh vần là >>=
Haskell). Vì bind
hoạ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
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.
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
bind
có 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ế, fmap
có thể được định nghĩa chỉ trong điều khoản bind
và return
. 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 return
và bind
hoạ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.
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ề.
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
lấ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, >>=
và return
phả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 đó []
và :
là các hàm tạo danh sách, ++
là toán tử ghép và Just
và Nothing
là các hàm Maybe
tạ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.
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 T
nà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 a
và b
) thành một hàm T a -> T b
. map
Hà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ả 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 List
là functor nếu nó được trang bị một hàm loại (a -> b) -> List a -> List b
tuâ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 b
lặ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 T
với hai phương pháp bổ sung, join
, kiểu T (T a) -> T a
, và unit
(đôi khi được gọi return
, fork
hoặ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ụ, map
qua một danh sách có chức năng trả về danh sách. Join
lấy danh sách kết quả của danh sách và nối chúng. List
là 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à join
phải liên kết. Điều này có nghĩa là nếu bạn có một giá trị x
của loại [[[a]]]
thì join (join x)
nên bằng nhau join (map join x)
. Và pure
phải là một bản sắc cho join
điều đó join (pure x) == x
.
[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:
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 Maybe
hoặc một IO
.
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 Int
có thể là một Just Int
hoặc Nothing
. Bây giờ, nếu bạn thêm a Maybe Int
và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 Int
s, chuyển cho chúng toán tử bổ sung, bọc lại kết quả Int
thà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 Nothing
bê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 Int
chỉ 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 IO
s 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 Int
lạ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 Maybe
ví 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.
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.
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:
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à lift
toá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 đó.
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 y
là giá trị có thể trở thành Mb
, chẳng hạn, (is_OK, b)
nếu y
bao 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ố bool
cho 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 bool
phầ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 compose
và 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_OK
là false
. 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 composition
thay 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 Bind
sẽ kiểm tra M
phần Ma
và 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 bind
sẽ đượ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, a
có thể là bất cứ điều gì vì bind
chỉ đơ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ýM
mộ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 content
mô 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 bind
hàm.
Do đó, một đơn nguyên là ba điều:
M
vỏ để chứa thông tin liên quan đơn nguyên, bind
hà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à 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 đó, Mb
cấ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, monad
s có thể bao gồm các hàm bọc bao bọc các giá trị, a
vào loại đơn nguyên Ma
và 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 bind
hà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)
Associativity
có nghĩa là bind
bả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 Associativity
trên, lực lượng đánh giá ban đầu của ngoặc binding
của f
và g
sẽ 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á Ma
phải được xác định trước khi giá trị của nó có thể được áp dụng f
và kết quả đó lần lượt được áp dụng g
.
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:
(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.
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ữ.
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.
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.
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.
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.
Monads không phải là phép ẩn dụ , nhưng một sự trừu tượng thực tế hữu ích nổi lên từ một mô hình phổ biến, như Daniel Spiewak giải thích.
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 data
và dữ liệu chảy qua các đường ống.
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).
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ì).
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.
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 đó.
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 map
và flatten
. 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
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, "">
f
lấ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ế đó f
và g
chị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 f
và g
phả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 m
vào f
". Để đưa một cặp <x, messages>
vào một chức năng f
là truyền x
vào f
, thoát <y, message>
ra f
và 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 x
và g(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ị x
là gì. Nếu x
là loại t
, thì cặp của bạn là một giá trị của loại "cặp t
và chuỗi". Gọi kiểu đó M t
.
M
là một hàm tạo kiểu: M
mộ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 int
là một cặp của một int và một chuỗi. An M string
là 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.feed
lấy một (hàm lấy một t
và trả về một M u
) và một M t
và trả về một M u
.wrap
mất một v
và trả về một M v
.t
, u
và v
là 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 t
vào một chức năng cũng giống như chuyển các phần chưa được bao bọc t
vào chức năng.
Chính thức: feed(f, wrap(x)) = f(x)
Cho ăn M t
vào wrap
khô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
t
vàog
M u
(gọi nó n
) từg
n
vàof
giống như
m
vàog
n
từg
n
và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
.
Tôi sẽ cố gắng giải thích Monad
trong 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 -> String
và f :: 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 x
là một Int
giá 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ề g
phả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 Int
loại đại diện cho một Int
giá trị có thể không có ở đó, IO String
loại đại diện cho một String
giá 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 String
và f1 :: String -> Maybe Bool
. g1
và f1
rất giống g
và f
tương ứng.
Chúng ta không thể làm (f1 . g1) x
hoặc f1 (g1 x)
, đâu x
là một Int
giá trị. Loại kết quả trả về g1
không phải là những gì f1
mong đợi.
Chúng tôi có thể sáng tác f
và g
với .
nhà điều hành, nhưng bây giờ chúng tôi không thể sáng tác f1
và g1
vớ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 g1
và f1
, như vậy chúng tôi có thể viết (f1 OPERATOR g1) x
? g1
trả 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 x
là một Maybe Int
giá trị. Các >>=
nhà điều hành giúp đi mà Int
giá 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 Monad
hữu ích? Bởi vì Monad
lớp loại định nghĩa >>=
toán tử, rất giống với Eq
lớp loại xác định toán tử ==
và /=
toán tử.
Để kết luận, Monad
lớ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à Monad
s cho phép thành phần hàm liên quan đến các giá trị trong ngữ cảnh .
{-# 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
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à id
là bản sắc bên phải và bên trái của nó.
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 f
kiể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 r
bao 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ố a
và 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ị t
và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 b
cho 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à return
là bản sắc bên phải và bên trái của nó.
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
Một loại tùy chọn
data Maybe t = Nothing | Just t
mã hóa tính toán Maybe t
mà 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 b
chỉ được áp dụng cho một kết quả nếu Maybe a
mang 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 đơ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 x
củ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
là
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ọn và thấ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
và <|>
tạo thành một monoidforall k l m.
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
và fail
là 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 p
là đú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.
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 st
và 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 get
và put
, 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 -> st
tuyên bố một sự phụ thuộc chức năng của loại trạng thái st
trên đơn nguyên m
; rằng State t
, ví dụ, sẽ xác định loại trạng thái là t
duy 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ự void
trong 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
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 IO
nguyê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.
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 T
là 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 C
một đối tượng trongD
Tobj : Obj(C) -> Obj(D)
f :: * -> *
và cho mỗi hình thái trong C
một hình thái trongD
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
trong đó X
, Y
là các đối tượng trong C
. HomC(X, Y)
là lớp đồng hình của tất cả các hình thái X -> Y
trong 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 C
phố 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 Hask
thể 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 mu
là 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 mu
thự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 concat
chức năng
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
là join
danh 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 join
có 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ừ mu
sang 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
Philip Wadler: Monads cho lập trình chức năng
Simon L Peyton Jones, Philip Wadler: Lập trình chức năng bắt buộc
Jonathan MD Hill, Keith Clarke: Giới thiệu về lý thuyết thể loại, monads lý thuyết thể loại, và mối quan hệ của họ để lập trình chức năng '
Eugenio Moggi: Khái niệm tính toán và đơn nguyên
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à vì 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
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.
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.
http://code.google.com.vn/p/monad-tutorial/ là một công việc đang tiến hành để giải quyết chính xác câu hỏi này.
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, f
biế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, f
cố 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 ,, f
tìm cách trích xuất a
bằ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 f
biế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 a
quả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 đỡ f
bằ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ỳ a
trừ 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ó a
cho bạn. Các State st
đơn vị có một bí mật st
rằng nó sẽ vượt qua f
dưới bàn, mặc dù f
chỉ đế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 f
nhì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 Nothing
chẳng hạn, f
khô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ì đó f
phải là một chức năng từ Maybe a
đến Maybe b
. Trong trường hợp này, Maybe
là 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.
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.