Skip to main content

Bài 11 - I/O cơ bản

TÓM TẮT
  • Hàm thuần túy
  • Giới thiệu về hành vi IO (Hành động IO)
  • Hiểu về IO actions
  • IO actions trong thực tế
    • Kiểu ()
  • Tương tác với người dùng
    • getChargetLine, và putStrLn
  • Hành động là giá trị hàng đầu
  • Chuỗi các hành động IO (toán tử >> và >>=)
  • Khối do
    • Sử dụng let, lồng "do-blocks", thoát IOreturn
  • Hàm main
  • Tóm tắt lại
Video bài giảng
Chúng tôi đang dịch thuyết minh bài giảng sang tiếng Việt

Hàm thuần túy

Từ trước đến giờ, chúng ta đã làm việc với các hàm thuần túy. Các hàm này không có hiệu ứng phụ (side effects) và lấy tất cả các đối số của chúng làm đầu vào (Input) và tạo ra một giá trị làm đầu ra (Output) chỉ phụ thuộc vào các đối số đó.

pic

Ý nghĩa của đầu vào và đầu ra là rất quan trọng ở đây. Đầu vào của một hàm chỉ có thể là các giá trị mà chúng ta cung cấp dưới dạng đối số và đầu ra của nó là giá trị mà nó trả về.

Ví dụ:

pic

Hàm lame nhận một tham số số duy nhất và trả về giá trị nhân với ba. Đầu ra của hàm này hoàn toàn phụ thuộc vào giá trị mà chúng ta cung cấp làm đầu vào. Vì vậy, mỗi khi chúng ta áp dụng lame hàm này cho bốn, chúng ta sẽ nhận được mười hai.

pic

Ngay cả khi chúng ta không hiển thị rõ ràng đầu vào của max6, chúng ta biết rằng vì filter được áp dụng một phần nên nó sẽ nhận danh sách Integers. (Tôi đã đơn giản hóa phần khai báo Kiểu một chút.) Giống như trước đây, đầu ra hoàn toàn phụ thuộc vào giá trị mà chúng ta cung cấp làm đầu vào. Vì vậy, mỗi khi chúng ta áp dụng max6 hàm này cho cùng một danh sách, chúng ta sẽ nhận được kết quả tương tự.

Và, tất nhiên, các hàm được xử lý. Vì vậy, nếu một hàm có vẻ như nhận nhiều đối số, thì thực ra hàm đó nhận một tham số duy nhất và trả về một hàm nhận một tham số đơn khác, v.v., cho đến khi tất cả các tham số được áp dụng và chúng ta nhận được giá trị cuối cùng. Nếu chúng ta sử dụng các tham số giống nhau, chúng ta sẽ nhận được kết quả tương tự. Và trong mỗi bước trung gian, chúng ta cũng nhận được kết quả trung gian tương tự.

Tất cả đều tốt cho đến nay. Nhưng nếu chúng ta chưa có đầu vào (input) thì sao?

Nếu chúng ta muốn tạo một chương trình có tương tác thì sao? Một trang web chẳng hạn? Hay một trò chơi chẳng hạn? Khi chúng ta viết chương trình của mình, chúng ta không biết người dùng sẽ làm gì với nó. Chúng ta không thể biết trước liệu một người chơi trong trò chơi của chúng ta sẽ di chuyển sang trái hay phải. Hoặc nếu người dùng trên trang web của chúng ta sẽ nhấp vào một nút cụ thể hay không.

Những điều đó xảy ra trong khi chạy chương trình, vì vậy không có cách nào để lập trình viên đưa chúng làm đầu vào của một hàm.

Ý tôi là, chúng ta có thể, nhưng hãy mường tượng nếu chúng ta chọn chúng trước. Một trò chơi luôn diễn ra và kết thúc theo cùng một cách. Không có cách nào để người chơi tương tác với nó. Nghe giống phim hơn. Vẫn không tệ. Cho đến khi bạn nhận ra rằng bạn thậm chí không thể hiển thị hình ảnh trên màn hình vì điều đó sẽ dẫn đến việc gửi và nhận thông tin từ màn hình máy tính của bạn trong khi chạy chương trình. Vì vậy, nếu bạn chạy "trò chơi" này, thì về cơ bản, bạn đang sử dụng máy tính của mình như một lò sưởi rất đắt tiền.

Cách duy nhất để cung cấp cho chương trình của chúng ta thông tin và khả năng mà nó cần là cung cấp cho nó một phương pháp để tương tác với thế giới thực.

Và để làm được điều đó, Haskell sử dụng các hành động IO (IO actions).

Giới thiệu về IO actions

Trước khi bắt đầu với các hành động IO, tôi sẽ nói chuyện với con voi trong phòng. Tôi chỉ nói với bạn rằng mọi thứ chúng ta viết mã cho đến nay đều thuần túy và chúng ta không thể tương tác với nó. Nhưng chúng ta đã tương tác với các hàm và truyền đối số kể từ bài học đầu tiên! Đó là bởi vì chúng ta đã "gian lận" bằng cách sử dụng GHCi, GHCi thực hiện các tác vụ IO trong nền mà không thông báo rõ ràng cho chúng ta biết. Vì vậy, cuối cùng, nếu chúng ta muốn chương trình của mình tương tác với thế giới thực, chúng ta vẫn cần các hành động IO.

IO actions (hành động hoặc tính toán ) có thể thay đổi và tương tác thế giới bên ngoài chương trình của chúng ta.

Cái tên IO actions có thể bị hiểu sai. 

Khi chúng ta nói về các hành động IO, chúng ta không nói về đầu vào và đầu ra của hàm. Chúng ta nói về đầu vào và đầu ra giữa chương trình và thế giới thực

Các hành động IO có thể thay đổi và tương tác thế giới bên ngoài chương trình của chúng ta. Chúng có tương tác hoặc không tương tác với thế giới, nhưng chúng CÓ THỂ. Chúng được phép tương tác với thế giới. Đây là chìa khóa. 

Những hành động IO này là cái mà chúng ta gọi là hiệu ứng phụ (side effect).

Hiệu ứng phụ là bất kỳ hiệu ứng nào có thể quan sát được ngoài việc chỉ trả về một giá trị.

Đó là lý do tại sao tất cả các hàm chúng ta đã viết cho đến nay đều thuần túy. Điều duy nhất chúng làm là trả về một giá trị. Nghe có vẻ hơi trừu tượng, vì vậy hãy xem một vài ví dụ để chúng ta có thể hình dung được:

  • Lấy kết quả của những gì một người dùng gõ trên bàn phím.

pic

Lưu ý rằng các hành động IO nằm ở phía trên sơ đồ. Chúng ta biểu diễn các hàm bằng các hộp lấy dữ liệu đầu vào của chúng từ bên trái và trả về kết quả đầu ra ở bên phải. Nhưng bây giờ chúng ta đang xử lý các hành động, chúng ta chỉ ra các hiệu ứng phụ ở phía trên đầu sơ đồ bằng các mũi tên đi vào và ra vì các hiệu ứng phụ có thể gửi và nhận thông tin.

  • Hiển thị một số văn bản, hình ảnh hoặc nội dung nào đó trên màn hình.

pic

  • Gọi API hoặc cơ sở dữ liệu (bạn làm gì không thực sự quan trọng).

pic

Ok, vậy là chúng ta đã rõ về ý tưởng của các hành động IO. Nhưng Haskell xử lý các hành động IO đó như thế nào?

Hiểu về IO actions

Bây giờ, tôi sẽ chỉ cho bạn định nghĩa về Kiểu (type) cho phép chúng ta tương tác an toàn với thế giới thực bằng cách sử dụng các hành động IO. Xin cảnh báo! Đừng cố gắng hiểu mã code. Chúng ta sẽ chỉ sử dụng nó để tạo ra một mô hình diễn giải những gì đang diễn ra bên trong.

Nếu không có thêm rắc rối, đây là Kiểu IO:

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

Hàm tạo kiểu IO có một hàm tạo giá trị IO đơn giản.

Phần thú vị là hàm được truyền dưới dạng tham số của hàm tạo giá trị. Nó lấy trạng thái của thế giới thực, thực hiện điều gì đó với nó và trả về một bộ chứa trạng thái mới của thế giới thực và một giá trị Kiểu a được tạo trong khi tất cả điều đó đang diễn ra.

Không phải các nhà thiết kế của Haskell là một nhóm thông minh sao? Nếu bạn cho một hàm trạng thái của toàn bộ thế giới thực, thì hàm đó có thể làm bất cứ điều gì! Nói chuyện với cơ sở dữ liệu, truy cập tệp trên máy tính của bạn, cho phép chim cánh cụt bay, hay bất cứ thứ gì!

Tuy nhiên, chúng ta có thể sử dụng nó để in nội dung trên màn hình.

Tất nhiên, đây không phải là điều đang thực sự xảy ra dưới kiểu có vẻ kỳ diệu này. Nhưng chúng ta có thể nghĩ về nó theo cách này. Kiểu IO thực sự là một Kiểu trừu tượng.

Kiểu dữ liệu trừu tượng là Kiểu có biểu diễn bị ẩn nhưng cung cấp các hoạt động liên quan.

Vì vậy, hoạt động bên trong của IO được ẩn giấu. Và thay vào đó, chúng ta nhận được một loạt các hàm và hành động sử dụng Kiểu này. Nghĩa là:

Chúng ta không sử dụng trực tiếp hàm tạo IO. Chúng ta sử dụng các hàm và hành động hoạt động với các giá trị IO.

Vậy là xong, đó là tất cả những gì chúng ta sẽ làm về vấn đề này. Nếu bạn muốn biết thêm, có rất nhiều hướng dẫn trên mạng hoặc thậm chí bạn có thể tự khám phá mã nguồn! Nhưng tôi khuyên bạn nên đợi cho đến khi bạn thông thạo Haskell hơn để giải quyết thử thách đó.

Bây giờ, hãy chuyển sang khía cạnh thực tế. Trong thực tế, chúng ta không quan tâm đến các chi tiết về cách xử lý tương tác với thế giới thực. Chúng ta thậm chí không quan tâm đến cách IO xác định Kiểu! Điều duy nhất chúng ta quan tâm là, nếu chúng ta sử dụng nó đúng cách, trình biên dịch sẽ xử lý các chi tiết và chúng ta sẽ không gặp bất kỳ sự bất ngờ nào khi chạy mã của mình. Vì vậy, hãy học cách sử dụng đúng Kiểu IO.

Hành động IO trong thực tế

pic

Kiểu IO a cho chúng ta biết something là một hành động IO tương tác với thế giới thực trước tiên và sau đó trả về giá trị Kiểu a.

Ví dụ:

action1 :: IO Bool             -- Performs IO and returns a Bool

action2 :: IO Int -- Performs IO and returns an Int

action3 :: IO (Double -> Char) -- Performs IO and returns a function

action4 :: IO () -- Performs IO and returns ()
Có ba điều quan trọng cần lưu ý ở đây:
  1. Một là, sau khi thực hiện hành động, chúng ta nhận được GIÁ TRỊ của Kiểu được chỉ định mà chúng ta có thể sử dụng trong mã của mình.
  2. Khác là hành động trả về một giá trị SAU KHI thực hiện hành động IO. Chúng ta KHÔNG THỂ có được giá trị đó nếu không tương tác với thế giới thực.
  3. Và cuối cùng, chúng ta thấy một Kiểu mới trong hành động cuối cùng đó là Kiểu đơn vị.

Kiểu đơn vị (Unit type)

Đây là Kiểu đơn vị:

data () = ()

Ta thấy nó chỉ có một hàm tạo giá trị không nhận tham số (còn gọi là hàm tạo rỗng). Vì vậy, đây là Kiểu chỉ có một giá trị có thể có (()). Ngoài ra, lưu ý rằng Kiểu và giá trị có cùng tên.

Nhưng đợi đã! Tại sao chúng ta tìm hiểu về điều này ngay bây giờ? Vâng, bởi vì, cho đến bây giờ, chúng ta đã làm việc với các hàm thuần túy. Nếu điều duy nhất mà một hàm thuần túy làm là trả về (), thì tại sao bạn còn bận tâm sử dụng hàm đó? Chỉ cần sử dụng giá trị trực tiếp! Và nếu một hàm lấy giá trị đơn vị làm tham số, tại sao bạn thậm chí còn bận tâm yêu cầu nó nếu nó luôn có cùng giá trị? Chỉ cần loại bỏ tham số đó và sử dụng trực tiếp đơn vị bên trong hàm!

Khi bạn nghĩ như vậy, chúng ta có thể bỏ Kiểu đơn vị khỏi bất kỳ hàm thuần túy nào và không mất gì (điều này đúng). NHƯNG! Bây giờ chúng ta đang xử lý các hành động và hiệu ứng phụ, có nhiều trường hợp khi chúng ta không quan tâm đến hành động đó trả về giá trị gì vì chúng ta chỉ quan tâm đến hiệu ứng phụ mà nó thực hiện. Hãy in một cái gì đó trên màn hình hoặc xóa một tập tin. Chúng ta không cần một cái gì đó phản hồi lại. Chúng ta chỉ quan tâm đến hiệu ứng phụ.

Đó là lý do tại sao, bây giờ, thật hợp lý khi có Kiểu này. Để đại diện cho một giá trị mà chúng ta không quan tâm.

Đừng lo lắng, chúng ta sẽ thấy một vài ví dụ thực tế trong bài học này. Hãy bắt đầu bằng cách lấy thông tin đầu vào từ người dùng.

Tương tác với thế giới thực (getChargetLine và putStrLn)

IO actions cơ bản nhất mà tôi có thể nghĩ đến là getChar :

pic

Hàm này thực hiện hành động IO yêu cầu người dùng viết một ký tự cho đầu vào tiêu chuẩn. Ngay khi người dùng viết ký tự đó, nó sẽ ghi nhận ký tự đó và kết thúc hành động IO. Giá trị của Kiểu Char mà nó trả về là ký tự mà người dùng đã viết. Chúng ta có thể chạy hàm ở đây để xem nó trông như thế nào.

    -- getChar :: IO Char
getChar
    <stdin>: hGetChar: end of file

Khi chúng ta chạy getChar, một vùng văn bản sẽ xuất hiện để chúng ta có thể nhập một ký tự vào. Chỉ một ký tự thôi.

Có những trường hợp khi một ký tự là đủ, chẳng hạn như khi bạn cần xác nhận (y) hoặc từ chối (n) một hành động trong CLI. Nhưng còn cả một cụm từ thì sao? Đối với điều đó, chúng ta có thể sử dụng một hàm khác gọi là getLine :

pic

Cái này thực hiện hành động IO yêu cầu người dùng viết một dòng cho đầu vào tiêu chuẩn. Và giá trị của Kiểu String mà nó trả về là văn bản mà người dùng đã viết cho đến khi nhấn Enter.

    -- getLine :: IO String
getLine
    <stdin>: hGetLine: end of file

Các hàm getChar và getLine tuyệt vời. Tuy nhiên, nếu chúng ta chạy một chương trình chỉ với những thứ này, nó sẽ nhắc chúng ta về một ký tự hoặc chuỗi mà không có bất kỳ lời giải thích nào. Giống như khi con chó của bạn đột nhiên đứng trước mặt bạn và im lặng nhưng nhìn chằm chằm vào bạn với đôi mắt cún con. Bạn biết nó muốn một cái gì đó, nhưng là những gì?

Chúng ta không chỉ cần một cách để nhận vào mà còn cần để gửi thông điệp ra thế giới bên ngoài. Và để làm điều đó, chúng ta sử dụng putStrLn :

pic

putStrLn là một hàm lấy một chuỗi làm đầu vào và trả về một hành động IO. Và hành động được trả về cảu putStrLn làm gì?

pic

Nó in thông số mà String chúng ta đã truyền trước đây dưới dạng tham số cho đầu ra tiêu chuẩn.

Như một ví dụ:

putStrLn "Hello World from inside the program!"

Kết quả in ra sẽ là: Hello World from inside the program!

Trong trường hợp này, putStrLn được áp dụng cho String và trả về một hành động thuộc Kiểu IO ()

Nhưng hãy kiểm tra thử điều này:

x = putStrLn "Hello World from inside the program!" 

Nếu chúng ta chạy dòng này, chúng ta sẽ không nhận được String đầu ra tiêu chuẩn của mình. Đó là bởi vì chúng ta đã không yêu cầu hành động được thực hiện. Chúng ta chỉ đặt tên cho nó là x, và chỉ như vậy. Chúng ta không thực sự cần hành động được thực hiện, vì vậy Haskell đã không thực hiện nó.

Và đó là một ý nghĩa khác của hành động. Chúng là những gì trong lập trình được gọi là first-class value.

Hành động là first-class value

Điều đó có nghĩa là bạn có thể coi các hành động giống như bất kỳ giá trị nào khác. Ví dụ:

-- putStrLn is an example of a function that returns an action

-- Bind to names
x = putStrLn "Hello World from inside the program!"

-- Put them inside lists or other data structures
listOfActions :: [IO ()]
listOfActions = [putStrLn "a", putStrLn "b"]

-- Pass them as function parameters
fakeLength :: [IO ()] -> Int
fakeLength list = 1 + length list

Và, nếu chúng ta chạy fakeLenghth hàm chuyển các listOfActions tham số dưới dạng…

fakeLength listOfActions

3

Chúng ta đã không nhận được bất kỳ thông báo nào trong đầu ra tiêu chuẩn vì không có hành động nào trong listOfActions được thực hiện! Tại sao chúng không được thực hiện? Chúng ta chỉ hỏi về độ dài của danh sách chứ không phải giá trị mà nó chứa. Hãy nhớ rằng Haskell còn lười hơn cả một con mèo đang tắm nắng. Nó sẽ không làm bất cứ điều gì trừ khi nó phải làm.

Vì vậy, cho đến khi chúng được thực hiện, các hành động IO chỉ là kế hoạch để làm điều gì đó, chương trình sẽ chạy hoặc hành động sẽ được thực hiện. Và bởi vì các hiệu ứng phụ không được thực hiện trong khi đánh giá tất cả các biểu thức đó, nên chúng ta có thể tiếp tục suy luận về mã của mình như chúng ta đã quen. Và chỉ quan tâm đến các hiệu ứng phụ thực tế khi chúng ta yêu cầu Haskell thực hiện chúng.

Vì vậy, làm thế nào chúng ta có thể yêu cầu Haskell thực hiện một vài hành động khác nhau? Bằng cách soạn chúng với các toán tử đặc biệt.

Soạn các hành động IO (>> và >>= toán tử)

Chúng ta sẽ tạo một vài bot đơn giản để tìm hiểu cách soạn các hành động IO

Bot "thô lỗ"

Bầu tiên là một con bot "thô lỗ". Ngay khi bạn tương tác với nó, nó sẽ hét vào mặt bạn.

Nó bắt đầu với một thông điệp đơn giản:

rudeBot :: IO ()
rudeBot = putStrLn "Hey!"

rudeBot

Hey!

Bot phải thở trước khi hét vào mặt bạn lần nữa, vì vậy chúng ta sẽ thêm cụm từ tiếp theo vào hành động thứ hai. Để làm điều đó, chúng ta sẽ giới thiệu toán tử "then":

(>>) :: IO a -> IO b -> IO b

Đây là một khai báo chuyên dụng dành riêng cho các hành động IO. Và như bạn có thể thấy, toán tử thực hiện hai hành động làm đầu vào: IO a và IO b. Đầu tiên, nó thực thi IO a, bỏ qua kết quả (bất kể a là gì) và trả về hành động IO b.

Nếu đây là một toán tử thuần túy, thì việc triển khai sẽ như thế này:

pureThen :: a -> b -> b
x `pureThen` y = y

3 `pureThen` 5

3 `pureThen` 5 `pureThen` 6 `pureThen` 'a'

5

'a'

Chúng sẽ bỏ đi giá trị đầu tiên và trả lại giá trị thứ hai.

Nhưng! Bởi vì chúng ta đang xử lý các hành động IO, nên toán tử này có một số bí mật cho phép nó thực hiện hành động IO đầu tiên trước sau đó mới trả lại hành động thứ hai. Cụ thể hơn:

(>>) :: IO a -> IO b -> IO b

Toán tử >> sắp xếp tuần tự hai hành động, bỏ qua bất kỳ giá trị nào do hành động đầu tiên tạo ra.

Ví dụ:

abc :: IO ()
abc = putStrLn "a" >> putStrLn "b" >> putStrLn "c"

abc

a b c

abc là một hành động được hợp thành bởi nhiều hành động.

Khi chúng ta yêu cầu Haskell thực hiện abc hành động, trước tiên, nó sẽ thực hiện putStrLn "a" hành động và loại bỏ kết quả, sau đó thực hiện putStrLn "b" hành động và loại bỏ kết quả, cuối cùng, thực hiện putStrLn "c" hành động và trả về bất kỳ thứ gì mà hành động cuối cùng trả về. Vì vậy, bởi vì putStrLn "c" trả lại đơn vị sau các hiệu ứng phụ, abc hành động cũng trả lại đơn vị sau các hiệu ứng phụ.

Hướng của các mũi tên chỉ hướng của chuỗi các hành động. Chúng ta thực hiện các hành động từ trái sang phải.

Bây giờ, hãy sử dụng toán tử này để hoàn thành bot "thô lỗ":

rudeBot :: IO ()
rudeBot = putStrLn "Hey!" >> putStrLn "Get out of my lawn!"

rudeBot

Kết quả:

Hey!
Get out of my lawn!

Chính xác những gì chúng ta muốn!

Nhưng có lẽ chúng ta có thể làm cho nó thậm chí còn "thô lỗ" hơn.

Một bot "thô lỗ" hơn

Làm thế nào chúng ta có thể làm cho bot này khó tính hơn? Hừm… tôi biết! Hãy làm cho nó có vẻ quan tâm đến chúng ta và sau đó hét vào mặt chúng ta! Vì vậy, nó cũng lãng phí thời gian của chúng ta.

Vì vậy, chúng ta sẽ làm cho bot hỏi tên của chúng ta, hoàn toàn phớt lờ nó và sau đó la mắng chúng ta. Tất cả điều đó với mã đơn giản này:

evenRuderBot :: IO ()
evenRuderBot =
putStrLn "What's your name?" -- IO ()
>> getLine -- IO String
>> putStrLn "like I care! Get lost!" -- IO ()

evenRuderBot

<stdin>: hGetLine: end of file

Nó hơi dài, vì vậy chúng ta sẽ chia nó thành nhiều dòng.

Bên cạnh đó, không có nhiều thay đổi. Thay đổi thực sự duy nhất là chúng ta đã thêm một getLine hàm giữa các hàm putStrLn. Nhưng nếu chúng ta chạy nó bây giờ, chúng ta sẽ thấy rằng

  1. Đầu tiên, nó thực hiện hiệu ứng phụ là hỏi tên của chúng ta trong đầu ra tiêu chuẩn.
  2. Sau đó, nó thực hiện hiệu ứng phụ là đợi chúng ta nhập và trả về giá trị đó. Nhưng vì chúng ta đang sử dụng >> toán tử, chúng ta bỏ qua nó.
  3. Và cuối cùng, nó gửi tin nhắn cuối cùng.

Nó chắc chắn "thô lỗ" hơn trước, nhưng có lẽ chúng ta đã đi quá xa. Hãy tìm hiểu cách sử dụng giá trị được trả về getLine bằng cách sử dụng bot "thô lỗ" của chúng ta để trị liệu.

Bot "thô lỗ" sau khi trị liệu

Sau khi trị liệu, bot của chúng ta cảm thấy tốt hơn và muốn trở thành bạn của chúng ta. Và vì điều đó, nó phải dùng tên của chúng ta. Chúng ta không thể sử dụng >> vì toán tử này bỏ qua việc dùng tên mà chúng ta nhập vào. Vì vậy, chúng ta cần một toán tử khác thực hiện tương tự nhưng chuyển kết quả của hành động đầu tiên sang hành động thứ hai. Và đó là toán tử "bind" (>>).

(>>=) :: IO a -> (a -> IO b) -> IO b

Đây cũng là một khai báo chuyên dụng cho các hành động IO. Trông có vẻ phức tạp, nhưng nó không khác mấy so với toán tử >>.

Toán tử này thực hiện một hành động IO a và một hàm nhận một giá trị cùng Kiểu a với giá trị được tạo bởi hành động đầu tiên và trả về một hành động khác, IO b.

Công việc của toán tử này là: Nó thực hiện hành động IO để nhận giá trị a, sau đó chuyển giá trị đó cho hàm để nhận hành động IO b và trả về kết quả đó. Toán tử không thực hiện hành động IO b, chỉ thực hiện hành động IO a.

Vì thế:

tip

Toán >>= tử kết hợp tuần tự hai hành động, chuyển bất kỳ giá trị do hành động đầu tiên tạo ra dưới dạng đối số cho hành động thứ hai.

Ví dụ:

-- Remainders:
-- (>>=) :: IO a -> (a -> IO b) -> IO b
-- getLine :: IO String


yellIt :: String -> IO ()
yellIt str = putStrLn (str ++ "!!!!!!")

yellItBack :: IO ()
yellItBack = getLine >>= yellIt

yellIt "Hey"
--yellItBack

Hey!!!!!!

Như bạn có thể thấy, chúng ta có  hàm yellIt lấy một Chuỗi và trả về hành động in chuỗi đó bằng một loạt dấu chấm than.

Và trong hàm yellItBack, chúng ta thực hiện hành động getLine trả về một Chuỗi. Nhưng thay vì ném chuỗi đi, lần này, chúng ta sử dụng >>= toán tử để chuyển nó thành tham số của hàm yellItyellIt trả về hành động đó và vì đó là hành động cuối cùng nên nó cũng được đánh giá. Gửi cho thế giới bên ngoài cùng một thông điệp nhưng nhấn mạnh hơn.

Bây giờ, hãy sử dụng toán tử này cho bot trên (bây giờ "tử tế" hơn):

response :: String -> IO ()
response name = putStrLn $ "Nice to meet you, " ++ name ++ "!"

rudeBotAfterTherapy :: IO ()
rudeBotAfterTherapy =
putStrLn "What's your name?"
>> getLine
>>= response

Chúng ta tạo hàm rudeBotAfterTherapy đầu tiên yêu cầu tên của bạn. Sau đó, nó đợi bạn ra lệnh. Và sau khi bạn thực hiện, nó sẽ chuyển giá trị đó cho  hàm response sử dụng để in một thông báo.

Nó hoạt động rồi, nhưng  hàm response này là một hàm đơn giản mà chúng ta chỉ sử dụng một lần. Và chúng ta đã biết phải làm gì trong những trường hợp này. Chúng ta sử dụng hàm lambda:

rudeBotAfterTherapy :: IO ()
rudeBotAfterTherapy =
putStrLn "What's your name?"
>> getLine
>>= (\name -> putStrLn $ "Nice to meet you, " ++ name ++ "!")


rudeBotAfterTherapy

<stdin>: hGetLine: end of file

Vậy đấy Một hàm đơn giản và nhỏ gọn chỉ với một chút ồn ào.

Bây giờ nó là một bot trò chuyện!

Bot của chúng ta hiện đã được khôi phục hoàn toàn và hóa ra đó là một bot trò chuyện! Hãy thêm một vài tính năng trò chuyện!

Ví dụ: hãy thêm một thông báo khác cho chúng ta biết số lượng chữ cái mà tên của chúng ta có:

lettersInName :: String -> String
lettersInName name =
"Your name has "
++ show (length name)
++ " letters, in case you where wandering..."

Hàm lettersInName là một hàm thuần túy lấy tên và trả về một nhận xét nhỏ ngớ ngẩn về nó. Để thêm nó vào bot trò chuyện của chúng ta, chúng ta cần làm như sau:

chattyBot :: IO ()
chattyBot =
putStrLn "Hey! What's your name?"
>> getLine
>>= ( \name ->
putStrLn ("Nice to meet you, " ++ name ++ "!")
>> putStrLn (lettersInName name)
)


chattyBot

<stdin>: hGetLine: end of file

Chúng ta cần giá trị name (được cung cấp dưới dạng kết quả của hành động thứ hai), vì vậy chúng ta cần soạn hành động của mình bên trong hàm lambda đó.

Và bây giờ nó giống nhau hơn. Chúng ta có thể tiếp tục thêm ngày càng nhiều hành động theo cùng một cách. Đây là một cái gì đó phức tạp hơn một chút:

finalChattyBot :: IO ()
finalChattyBot =
putStrLn "Hey! What's your name?"
>> getLine
>>= ( \name ->
putStrLn ("Nice to meet you, " ++ name ++ "!")
>> putStrLn (lettersInName name)
>> putStrLn ("So, " ++ name ++ ", what do you do for fun?")
>> getLine
>>= ( \hobby ->
putStrLn ("Are you kidding, " ++ name ++ "! I love " ++ hobby ++ "!")
)
)
>> putStrLn "OK, bye!"


finalChattyBot

<stdin>: hGetLine: end of file

Như bạn có thể thấy, nếu chúng ta tiếp tục tăng tương tác, chúng ta sẽ bắt đầu thấy một khuôn mẫu. Một mô hình xấu xí và khó đọc.

May mắn thay, chúng ta có một sự thay thế ngọt ngào hơn. Hãy tìm hiểu cú pháp "Do":

Cú pháp do

Cú pháp do chỉ là đường cú pháp cho các biểu thức được tạo bởi toán tử >> và >>=.

Chúng ta sẽ viết lại tất cả các biểu thức trước đó với cú pháp do để có thể thấy sự khác biệt. Bắt đầu với bot "thô lỗ":

rudeBot :: IO ()
rudeBot = putStrLn "Hey!"

Có thể viết lại thành

rudeBotDo :: IO ()
rudeBotDo = do
putStrLn "Hey!"
rudeBotDo

Hey!

Như bạn có thể thấy, chúng ta viết từ khóa do sau dấu bằng. Và sau đó, chúng ta bắt đầu một khối với các hành động. Trong vó dụ này, cú pháp do có vẻ không hữu ích lắm. Sẽ đơn giản hơn khi viết nó mà không cần cú pháp do!

Hãy xem phiên bản thứ hai của bot "thô lỗ":

rudeBot :: IO ()
rudeBot = putStrLn "Hey!" >> putStrLn "Get out of my lawn!"

Có thể viết lại thành

rudeBotDo :: IO ()
rudeBotDo = do
putStrLn "Hey!"
putStrLn "Get out of my lawn!"
rudeBotDo

Kết quả trả về là:

Hey!

Get out of my lawn!

Bây giờ chúng ta bắt đầu thấy một số cải tiến nhỏ. Nó không nhiều nhưng chúng ta có thể thấy rằng mỗi hành động nằm trong một dòng khác nhau, giúp dễ dàng xác định hơn.

Không có cú pháp do, các hành động đi từ trái sang phải. Với cú pháp do, chúng đi từ trên xuống dưới.

Còn với bot "thô lỗ" thì sao?

evenRuderBot :: IO ()
evenRuderBot =
putStrLn "What's your name?"
>> getLine
>> putStrLn "like I care! Get lost!"

Có thể viết lại thành

evenRuderBotDo :: IO ()
evenRuderBotDo = do
putStrLn "What's your name?"
getLine
putStrLn "like I care! Get lost!"

evenRuderBotDo

<stdin>: hGetLine: end of file

Tương tự như trước đây, hãy thêm cú pháp do, xóa >> toán tử và bạn đã sẵn sàng. Lưu ý rằng bây giờ chúng ta đang sắp xếp mọi thứ ở cùng một mức độ thụt lề trái.

Bây giờ là khi những thứ thú vị bắt đầu. Ở đây chúng ta có bot "thô lỗ" sau khi trị liệu:

rudeBotAfterTherapy :: IO ()
rudeBotAfterTherapy =
putStrLn "What's your name?"
>> getLine
>>= (\name -> putStrLn $ "Nice to meet you, " ++ name ++ "!")

Làm thế nào để chúng ta xử lý name? Trong bot cũ, chúng ta đã bỏ qua nó, nhưng bây giờ chúng ta cần một cách để liên kết kết quả getLine với một tên. Để làm được điều đó, chúng ta sẽ tìm hiểu <- (mũi tên trái hoặc liên kết):

rudeBotAfterTherapyDo :: IO ()
rudeBotAfterTherapyDo = do
putStrLn "What's your name?"
name <- getLine -- (getline :: IO String) so (name :: Sring)
putStrLn $ "Nice to meet you, " ++ name


rudeBotAfterTherapyDo

Mũi tên trái này liên kết kết quả của việc chạy putStrLn hành động với name. Và một khi bạn có biến name, bạn có thể sử dụng nó ở bất cứ đâu sau hành động đó.

Bây giờ hãy xem cách chúng ta có thể sử dụng cú pháp do cho bot trò chuyện:

chattyBot :: IO ()
chattyBot =
putStrLn "Hey! What's your name?"
>> getLine
>>= ( \name ->
putStrLn ("Nice to meet you, " ++ name ++ "!")
>> putStrLn (lettersInName name)
)

Có thể viết lại thành

chattyBotDo :: IO ()
chattyBotDo = do
putStrLn "Hey! What's your name?"
name <- getLine
putStrLn ("Nice to meet you, " ++ name ++ "!")
putStrLn $ lettersInName name


chattyBotDo

<stdin>: hGetLine: end of file

Bây giờ sự khác biệt bắt đầu trở nên ro ràng hơn! Bạn phải dừng lại chattyBot vài giây để nắm bắt những gì nó đang làm, nhưng chattyBotDo thực sự rất dễ làm theo!

Cuối cùng, hãy so sánh cái phức tạp nhất:

finalChattyBot :: IO ()
finalChattyBot =
putStrLn "Hey! What's your name?"
>> getLine
>>= ( \name ->
putStrLn ("Nice to meet you, " ++ name ++ "!")
>> putStrLn (lettersInName name)
>> putStrLn ("So, " ++ name ++ ", what do you do for fun?")
>> getLine
>>= ( \hobby ->
putStrLn ("Are you kidding, " ++ name ++ "! I love " ++ hobby ++ "!")
)
)
>> putStrLn "OK, bye!"

Có thể viết lại thành

finalChattyBotDo :: IO ()
finalChattyBotDo = do
putStrLn "Hey! What's your name?"
name <- getLine
putStrLn ("Nice to meet you, " ++ name ++ "!")
putStrLn (lettersInName name)
putStrLn ("So, " ++ name ++ ", what do you do for fun?")
hobby <- getLine
putStrLn ("Are you kidding, " ++ name ++ "! I love " ++ hobby ++ "!")
putStrLn "OK, bye!"

Luôn giữ tất cả các câu lệnh được căn chỉnh ở cùng một mức độ thụt đầu dòng lề trái để tránh làm trình biên dịch bối rối và mắc phải những lỗi ngớ ngẩn.

Như bạn có thể thấy, cú pháp do này rất dễ đọc và viết! Nó cho phép mã sạch hơn và ngắn gọn hơn. Nhưng đợi một phút! Mã này có vẻ bắt buộc một cách kỳ lạ! Chúng ta đang nêu những việc cần làm từng bước theo trình tự! Không phải mã khai báo rất tuyệt sao?

Cho đến bây giờ, tất cả các hàm chúng ta đã viết là thuần túy, điều đó có nghĩa là chúng ta sẽ luôn có cùng một kết quả, bất kể thứ tự đánh giá. Đó là lý do tại sao chúng ta có thể viết các khai báo và để trình biên dịch xử lý các chi tiết.

Bây giờ chúng ta có hiệu ứng phụ, thứ tự xảy ra mới quan trọng! Chúng ta không muốn chạy hiệu ứng phụ của việc lấy tên trước tên in "Tên của bạn là gì?" câu hỏi. Đó là lý do tại sao chúng ta áp dụng một phong cách bắt buộc hơn với cú pháp do.

Về bản chất, đó là cú pháp do. Nhưng có một vài điều khác liên quan đến khía cạnh thực tế của việc sử dụng cú pháp do. Nó trở nên phức tạp hơn một chút, nhưng không nhiều.

Hãy bắt đầu với cách sử dụng let bên trong cú pháp do:

Sử dụng let bên trong cú pháp do

Chúng ta có thể sử dụng let từ khóa bên trong một khối do như thế này:

unscramble :: IO ()
unscramble = do
putStrLn "Write a bunch of numbers and letters scrambled together:"
arg <- getLine
let numbers = filter (` elem` "1234567890") arg
letters = filter (` notElem` numbers) arg
putStrLn $ "Numbers: " ++ numbers
putStrLn $ "Letters: " ++ letters


unscramble
Có một vài chi tiết cần ghi nhớ:
  • Các cấu trúc được giới hạn bằng let từ khóa là lười biếng. Vì vậy, mặc dù chúng chiếm một dòng trong khối do, nhưng chúng chỉ được tính toán khi cần thiết.
  • Chúng ta không cần từ khóa in như chúng ta làm bên ngoài cú pháp do. Mọi thứ sau ràng buộc let đều có thể truy cập được. Tương tự như các biến được giới hạn bằng mũi tên trái (<-).
  • Nếu bạn có một vài ràng buộc let lần lượt. Từ khóa let chỉ cần thiết cho lần đầu tiên và bạn phải duy trì tất cả ở cùng một mức thụt đầu dòng. Tính năng này không bắt buộc phải sử dụng (bạn có thể viết let cho từng cái một), nhưng nó rất tiện lợi.

Cú pháp ràng buộc này let khá thuận tiện. Tương tự như khả năng lồng do các khối.

Lồng các khối do

Chúng ta có thể lồng nhiều khối do vào nhau nếu chúng ta muốn. Ví dụ:

plusADecade :: IO ()
plusADecade = do
putStrLn "What is your age?:"
ageString <- getLine
let validAge = all (` elem` "1234567890") ageString
if validAge
then do
let age = read ageString :: Int
putStrLn $ "Int 10 years you will be " ++ show (age + 10) ++ " years old."
else do
putStrLn "Your age should contain only digits from 0-9. (Press CTRL+C to close)"
plusADecade



plusADecade

<stdin>: hGetLine: end of file

Hàm plusADecade đầu tiên yêu cầu tuổi của người dùng. Sau đó, nó kiểm tra xem Chuỗi do người dùng cung cấp chỉ chứa các số. Nếu đúng như vậy, chúng ta sử dụng read hàm này để lấy biểu diễn số, thêm 10 vào đó và in thông báo về độ tuổi của người dùng sau 10 năm nữa.

Nếu nó không chỉ chứa các số, chúng ta sẽ in một thông báo cho người dùng biết rằng chỉ các số mới được phép và gọi hàm plusADecade để khởi động lại chương trình.

Chi tiết quan trọng

Câu lệnh cuối cùng trong khối do phải là một biểu thức trả về một hành động IO thuộc Kiểu chính xác.

Nếu chúng ta xem xét cách hàm plusADecade kết thúc, chúng ta có hai trường hợp có thể xảy ra tùy thuộc vào nếu validAge là True hoặc False.

Bên trong then, chúng ta thấy một khối do kết thúc bằng một putStrLn được áp dụng cho một chuỗi, vì vậy biểu thức có kiểu IO () như chúng ta đã chỉ ra ở plusADecade khai báo. Tất cả đều tốt.

Bên trong else, chúng ta thấy một khối do kết thúc bằng một lệnh gọi đệ quy của plusADecade hàm, vì vậy nó thuộc Kiểu IO (). Tất cả đều tốt.

Tất nhiên, chúng ta không thể kết thúc với thứ gì đó trả về giá trị kiểu IO Int hoặc IO Bool. Nhưng có nhiều hơn nữa! Chúng ta không thể làm điều gì đó như thế này:

twice :: IO String
twice = do
str <- getLine
let tw = str ++ str -- Error

Tại sao? Chà, chúng ta hãy xem mã này mà không cần cú pháp ngắn gọn:

twice :: IO String
twice =
getLine >>= \str ->
let tw = str ++ str in -- Error

Đúng rồi! Nếu chúng ta xem kỹ, chúng ta sẽ thấy rằng let liên kết mở rộng thành một let biểu thức hoàn chỉnh. Và vấn đề đã lộ rõ: Đó là cách diễn đạt không đầy đủ! Gán (let) tw vào cái gì?

Và đó là khi ý tưởng tuyệt vời mà tất cả chúng ta có khi lần đầu học Haskell nảy ra trong đầu. Nếu tôi trả lại String như thế này thì sao?

twice :: IO String
twice =
getLine >>= \str ->
let tw = str ++ str in tw -- Error: Couldn't match type!

Biểu thức let đã hoàn tất. Nhưng bây giờ chúng ta gặp lỗi Kiểu vì chúng ta phải trả lại một IO String và chúng ta đang trả về một String thuần túy. Sửa chữa dễ dàng thôi! Chúng ta có thể thay đổi Kiểu twice thành String vì đó là những gì chúng ta sẽ trả lại!

twice :: String
twice =
getLine >>= \str ->
let tw = str ++ str in tw -- Error: Couldn't match type!

Và, để hoàn thiện, đây là mã tương đương với cú pháp do:

twice :: String
twice = do
str <- getLine
let tw = str ++ str -- tw :: String
tw -- Error: Couldn't match type!

Nếu chúng ta cố gắng biên dịch bất kỳ phiên bản nào trong hai phiên bản cuối cùng của twice, chúng ta sẽ gặp lỗi Kiểu. Nhưng chúng ta đã nói rằng Kiểu đó là Chuỗi và chúng ta đang trả về một Chuỗi. Vấn đề là gì?

Vấn đề là chúng ta đã sử dụng getLine hành động 'impurity' bên trong hai lần và chúng ta đang cố gắng trả về một giá trị thuần túy! Trong Haskell, một khi bạn 'impurity' ('impurity'), bạn không thể quay lại. Bạn không thể yêu cầu sự tha thứ, và không có sự cứu trợ. Nói cách khác, bạn không thể thoát khỏi IO.

Thoát IO và hàm return

Đừng nhầm lẫn

Hàm return TRONG HASKELL KHÔNG HOẠT ĐỘNG NHƯ TRONG CÁC NGÔN NGỮ LẬP TRÌNH KHÁC! ĐÓ LÀ MỘT ĐIỀU KHÁC BIỆT!

Nếu bạn không biết bất kỳ ngôn ngữ lập trình nào khác, đừng lo lắng về điều đó. Tôi muốn mở đầu bằng điều đó để tránh nhầm lẫn hoặc trong trường hợp bạn sắp bỏ qua phần này.

OK, vậy chúng ta hãy quay lại vấn đề hiện tại: Tại sao đoạn mã này không biên dịch được?:

twice :: String
twice = do
str <- getLine -- (str :: String) because (getLine :: IO String)
let tw = str ++ str -- tw :: String
tw -- Compiler error: Couldn't match type!

Bởi vì nếu chúng ta cho phép nó biên dịch, thì đối với cái sử dụng hàm twice, có vẻ như nó đang sử dụng một hàm thuần túy! Mã thuần túy và 'impurity' sẽ có thể trộn lẫn mà không có cách nào phân biệt chúng! Và nếu đúng như vậy, chúng ta sẽ không bao giờ chắc chắn liệu mã của mình có thuần túy hay không, điều đó có nghĩa là chúng ta sẽ không bao giờ biết liệu các hàm của mình có hoạt động như chúng ta nghĩ hay không. Chúng ta sẽ phải đối xử với mọi thứ như thể nó 'impurity', và chúng ta sẽ mất tất cả những lợi ích của sự thuần túy! Không còn minh bạch về tham chiếu, không còn mã dễ suy luận, không còn chắc chắn rằng việc sửa đổi một hàm sẽ không làm hỏng bất kỳ thứ gì mà bạn không biết về nó, v.v.

Đó là lý do tại sao chúng ta không thể trả về một giá trị thuần túy nếu chúng ta thực hiện một hành động 'impurity' bên trong hàm. Phải có một cách để cả trình biên dịch và nhà phát triển xác định xem chúng ta có sử dụng một hành động không trong sáng hay không.

Giải pháp rất đơn giản: Một khi chúng ta sử dụng bất kỳ hiệu ứng phụ nào, chúng ta sẽ không bao giờ có thể thoát ra khỏi 'impurity'.

Làm thế nào để điều này làm việc trong thực tế? Chà, chúng ta có thể thực hiện các hành động IO để nhận giá trị kết quả và làm việc với nó (giống như chúng ta đã làm với getLine để có được str) miễn là chúng ta thực hiện nó trong ngữ cảnh IO khác! Trình biên dịch thực thi điều này bằng cách kiểm tra xem: Một hàm thực hiện một hành động IO bên trong phải trả về một hành động IO.

Vì thế:

twice :: IO ... -- IO of something because we use getLine inside
twice = do
str <- getLine -- getLine :: IO String
let tw = str ++ str
-- ... We have to do something else

Bởi vì chúng ta đã thực hiện getLine hành động bên trong twice, bây giờ twice phải trả lại một IO hành động.

Ok, vì vậy chúng ta muốn trở lại tw. Nhưng tw là thuộc Kiểu String và chúng ta cần trả về giá trị thuộc Kiểu IO String. Bây giờ chúng ta có thể làm gì đây?

Đó là khi hàm return này có ích:

return :: a -> IO a

Hàm return đưa vào một giá trị bên trong một hành động IO không làm gì cả. Nó nhận một giá trị thuộc bất kỳ Kiểu nào và trả về một hành động dẫn đến giá trị đó.

Bạn có nhớ khi chúng ta trình bày ý tưởng về các hành động IO mà chúng ta nói rằng chúng CÓ THỂ tương tác với thế giới thực không? Vâng, trong trường hợp này, nó không. Hành động được hàm return trả về là một hành động giả không tạo ra bất kỳ hiệu ứng phụ nào và trả về giá trị.

Điều này là hoàn hảo cho trường hợp sử dụng của chúng ta. Chúng ta cần một giá trị được bao bọc trong một hành động IO, nhưng chúng ta không cần thực hiện bất kỳ hành động nào nữa.

Vì vậy, sử dụng hàm mới của chúng ta, chúng ta có được phiên bản cuối cùng của twice :

twice :: IO String
twice = do
str <- getLine
let tw = str ++ str
return tw

twice

<stdin>: hGetLine: end of file

Nếu không viết mã tinh gọn thì nó trông như thế này:

twice' :: IO String
twice' =
getLine >>= \str ->
let tw = str ++ str in return tw

twice'

<stdin>: hGetLine: end of file

Hãy nhớ rằng return này chỉ là một hàm. Cuối cùng, nó không cần được sử dụng và bạn có thể sử dụng nó nhiều lần như bất kỳ hàm nào khác.

Và đó là cách chúng ta làm việc với các hiệu ứng phụ.

Và bạn có thể đang nghĩ: "Nhưng, nếu một khi chúng ta sử dụng một hành động 'impurity', chúng ta phải mang Kiểu IO đó mãi mãi, thì nó sẽ kết thúc ở đâu? Và nếu chúng ta cần tương tác với thế giới khi bắt đầu chương trình của mình, như thế nào? người dùng nhấp vào nút bắt đầu, điều đó không có nghĩa là toàn bộ chương trình sẽ 'impurity' sao?"

Đó là những câu hỏi hợp lệ. Hãy trả lời từng câu một.

Giống như nhiều ngôn ngữ lập trình khác, Haskell có cái được gọi là "điểm vào" ("entry point").

Cú pháp main

Điểm khởi đầu là vị trí quan trọng trong chương trình bắt đầu thực thi chương trình. Điều đầu tiên chạy khi bạn chạy chương trình.

Nhiều ngôn ngữ lập trình có điểm vào. Và trong hầu hết các trường hợp, điểm vào được đặt tên là 'main'. Đôi khi nó là một hàm, một số khác là một phương thức tĩnh. Rust, Java, Dart, C, C++, và một vài ngôn ngữ khác cũng có. Và, tất nhiên, Haskell cũng dùng nó. Trong trường hợp của Haskell, đó là một hành động IO được gọi là main:

main :: IO ()

Hành động này là hành động duy nhất được thực thi khi chúng ta chạy bất kỳ chương trình nào được viết bằng Haskell. Và nó luôn có Kiểu IO (). Trên thực tế, bạn có thể trả về một giá trị khác với (), nhưng nó sẽ bị bỏ qua vì không có gì ở trên hành động này. Vì vậy, không có điểm nào.

Bây giờ, khi bạn tương tác với thế giới bên ngoài và phải thực hiện các Kiểu IO, đó là nơi nó kết thúc. Trong hành động chính.

Một điều quan trọng cần biết là nếu bạn muốn một hàm hoặc hành động được đánh giá và thực hiện khi chạy chương trình của mình, thì nó phải—trực tiếp hoặc gián tiếp—trong main. Cuối cùng, khi soạn thảo các hành động và hàm, chúng ta sẽ dán mọi thứ lại với nhau trong một IO () hành động duy nhất, khi chạy, sẽ thực hiện mọi thứ như chúng ta đã chỉ định.

Tại sao bây giờ chúng ta mới nói về điều này? Bởi vì chúng ta đã sử dụng GHCi để xử lý việc này ở hậu trường. Tuy nhiên, bài học tiếp theo, chúng ta sẽ bắt đầu làm việc với các chương trình được biên dịch thực sự. Vì vậy, chúng ta nên bắt đầu thực hành ngay bây giờ.

Trên ghi chú đó, hãy để tôi giới thiệu với bạn một trong những chương trình ngắn nhất mà bạn có thể viết bằng Haskell:

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

Đây chính là nó! Một chương trình Haskell hoàn chỉnh! Bạn có thể đặt cái này vào một tệp Haskell và biên dịch nó bằng cách sử dụng ghc fileName.hs, và bạn sẽ nhận được một chương trình in ra "Hello World!" khi được thực thi!

Ok, vậy nếu mỗi khi bạn thực hiện một hành động, bạn phải mang theo IO và điều đầu tiên mà chương trình Haskell chạy là một hành động có tất cả mã bên trong… Tại thời điểm nào chúng ta viết mã thuần túy?

Các chương trình Haskell trong thế giới thực thường có một main hàm nhỏ với một vài hành động và hàm thuần túy. Và bên trong các hàm thuần túy đó là nơi chứa phần lớn mã. Bằng cách đó, bạn giảm thiểu sự tương tác với các hiệu ứng phụ và hầu hết mã của bạn là thuần túy và dễ xử lý.

Sắp xếp như thế này:

main :: IO ()
main = do
config <- getConfig -- Custom IO action
let result = pureFunction config -- Huge pure function
putStrLn $ "Done! The result is: " ++ show result

Phần lớn mã là thuần túy và bên trong mã pureFunction có config giá trị thuần túy. Và một phần nhỏ của chương trình tương tác với thế giới thực để lấy cấu hình và hiển thị kết quả cuối cùng.

Và đó gần như là tất cả những gì tôi muốn trình bày trong bài học này. Có rất nhiều thứ để tiếp thu, nhưng với mọi thứ chúng ta đề cập hôm nay và một chút luyện tập, bạn sẽ không gặp vấn đề gì khi viết các chương trình tương tác!

Và bởi vì tôi biết chúng ta đã đề cập đến rất nhiều khái niệm và cú pháp mới, đây là bản tóm tắt để bạn sử dụng làm điểm ôn tập khi làm bài tập về nhà:

Bản tóm tắt IO tinh khiết VS không tinh khiết:

PureImpure
Luôn tạo ra cùng một kết quả khi có cùng tham sốCó thể tạo ra các kết quả khác nhau cho cùng một tham số
Không bao giờ có hiệu ứng phụCó thể có hiệu ứng phụ
Không bao giờ thay đổi trạng tháiCó thể thay đổi trạng thái chung của chương trình, hệ thống hoặc thế giới
Không có IO IO
Các hàm luôn hoạt động trơn truHãy cẩn thận hơn

Tóm tắt cú pháp

Biểu tượngNghĩa
>>Thực hiện liên tục hai hành động, loại bỏ bất kỳ giá trị nào được tạo bởi hành động đầu tiên
>>=Thực hiện tục hai hành động, chuyển tiếp giá trị do hành động đầu tiên tạo ra sang hành động thứ hai
returnChức năng thêm một giá trị bên trong ngữ cảnh
doBắt đầu một khối do
<-Bên trong khối do: Kết quả ràng buộc của việc thực hiện một hành động
letBên trong khối do: Liên kết lười biếng của bất kỳ biểu thức nào (nó không thực hiện hành động)
mainĐiểm vào của một chương trình Haskell. (Tất cả các hàm/hành động kết thúc bằng main.)

Nếu bạn tò mò về cách tạo các chương trình của riêng mình, đừng lo lắng. Bài học tiếp theo, chúng ta sẽ học cách thiết lập môi trường phát triển cục bộ của riêng mình, cách sử dụng các mô-đun và cách quản lý các dự án Haskell. Chúng ta sẽ vẫn cung cấp mọi thứ trong sổ ghi chép Jupyter trực tuyến và môi trường phát triển trực tuyến, nhưng chúng ta sẽ xóa tất cả các bánh xe đào tạo để bạn có thể tự quản lý dự án của mình nếu muốn.

Hãy đảm bảo hoàn thành bài tập về nhà của bạn và tôi sẽ gặp bạn trong bài tiếp theo!

Nguồn bài viết tại đây


Picture