Trước tiên chúng ta hãy phân biệt giữa việc học các khái niệm trừu tượng và học các ví dụ cụ thể về chúng.
Bạn sẽ không đi quá xa mà bỏ qua tất cả các ví dụ cụ thể, vì lý do đơn giản là chúng hoàn toàn phổ biến. Trong thực tế, sự trừu tượng tồn tại phần lớn bởi vì chúng thống nhất những điều bạn sẽ làm bằng mọi cách với các ví dụ cụ thể.
Mặt khác, bản tóm tắt chắc chắn rất hữu ích , nhưng chúng không cần thiết ngay lập tức. Bạn có thể nhận được khá nhiều bỏ qua hoàn toàn trừu tượng và chỉ sử dụng các loại khác nhau trực tiếp. Cuối cùng bạn sẽ muốn hiểu chúng, nhưng bạn luôn có thể quay lại với nó sau. Trên thực tế, tôi gần như có thể đảm bảo rằng nếu bạn làm điều đó, khi bạn quay lại, bạn sẽ tát vào trán mình và tự hỏi tại sao bạn lại dành toàn bộ thời gian để làm mọi việc một cách khó khăn thay vì sử dụng các công cụ đa năng tiện lợi.
Lấy Maybe a
một ví dụ. Nó chỉ là một kiểu dữ liệu:
data Maybe a = Just a | Nothing
Đó là tất cả nhưng tự viết tài liệu; đó là một giá trị tùy chọn. Hoặc bạn có "chỉ" một cái gì đó thuộc loại a
, hoặc bạn không có gì. Giả sử bạn có một chức năng tra cứu nào đó, trả về Maybe String
để đại diện cho việc tìm kiếm một String
giá trị có thể không có. Vì vậy, bạn khớp mẫu trên giá trị để xem đó là:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Đó là tất cả!
Thực sự, không có gì khác bạn cần. Không có Functor
s hoặc Monad
s hoặc bất cứ điều gì khác. Những cách này thể hiện những cách sử dụng Maybe a
giá trị phổ biến ... nhưng chúng chỉ là thành ngữ, "mẫu thiết kế", bất cứ điều gì bạn có thể muốn gọi nó.
Nơi mà bạn thực sự không thể tránh được hoàn toàn là với IO
, nhưng dù sao đó cũng là một hộp đen bí ẩn, vì vậy không đáng để cố gắng hiểu ý nghĩa của nó là Monad
gì hay bất cứ điều gì.
Trên thực tế, đây là một bảng cheat cho tất cả những gì bạn thực sự cần biết IO
bây giờ:
Nếu một cái gì đó có một loại IO a
, điều đó có nghĩa là nó là một thủ tục làm một cái gì đó và phun ra một a
giá trị.
Khi bạn có một khối mã bằng cách sử dụng do
ký hiệu, hãy viết một cái gì đó như thế này:
do -- ...
inp <- getLine
-- etc...
... Có nghĩa là thực hiện thủ tục ở bên phải <-
và gán kết quả cho tên bên trái.
Trong khi đó nếu bạn có một cái gì đó như thế này:
do -- ...
let x = [foo, bar]
-- etc...
... Nó có nghĩa là gán giá trị của biểu thức đơn giản (không phải là thủ tục) ở bên phải của =
tên bên trái.
Nếu bạn đặt một cái gì đó ở đó mà không gán giá trị, như thế này:
do putStrLn "blah blah, fishcakes"
... Nó có nghĩa là thực hiện một thủ tục và bỏ qua bất cứ điều gì nó trả về. Một số thủ tục có loại IO ()
- ()
loại là loại giữ chỗ không nói gì, vì vậy điều đó chỉ có nghĩa là thủ tục làm gì đó và không trả về giá trị. Sắp xếp giống như một void
chức năng trong các ngôn ngữ khác.
Thực hiện cùng một quy trình nhiều lần có thể cho kết quả khác nhau; Đó là loại ý tưởng. Đây là lý do tại sao không có cách nào để "loại bỏ" IO
khỏi một giá trị, bởi vì thứ gì đó IO
không phải là một giá trị, đó là một thủ tục để có được một giá trị.
Dòng cuối cùng trong một do
khối phải là một thủ tục đơn giản không có phép gán, trong đó giá trị trả về của thủ tục đó trở thành giá trị trả về cho toàn bộ khối. Nếu bạn muốn giá trị trả về sử dụng một số giá trị đã được gán, return
hàm sẽ lấy một giá trị đơn giản và cung cấp cho bạn một quy trình không trả về giá trị đó.
Ngoài ra, không có gì đặc biệt IO
; các thủ tục này thực sự là các giá trị đơn giản và bạn có thể vượt qua chúng và kết hợp chúng theo các cách khác nhau. Chỉ khi họ bị xử tử trong một do
khối được gọi ở đâu main
đó thì họ mới làm gì.
Vì vậy, trong một cái gì đó như chương trình ví dụ hoàn toàn nhàm chán, rập khuôn này:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
putStrLn msg
return name
... bạn có thể đọc nó giống như một chương trình bắt buộc. Chúng tôi đang xác định một thủ tục được đặt tên hello
. Khi được thực thi, đầu tiên nó thực hiện một thủ tục để in một thông báo hỏi tên của bạn; tiếp theo nó thực hiện một thủ tục đọc một dòng đầu vào và gán kết quả cho name
; sau đó nó gán một biểu thức cho tên msg
; sau đó nó in thông điệp; sau đó nó trả về tên người dùng là kết quả của toàn bộ khối. Vì name
là a String
, điều này có nghĩa hello
là thủ tục trả về a String
, vì vậy nó có kiểu IO String
. Và bây giờ bạn có thể thực hiện thủ tục này ở nơi khác, giống như nó thực thi getLine
.
Pfff, đơn nguyên. Ai cần họ?