Tôi nghĩ bạn có hiểu lầm cơ bản về IO trong Haskell. Cụ thể, bạn nói điều này:
Có thể có một hàm có thể chuyển đổi từ 'Chuỗi IO' thành [Char]?
Không, không có và thực tế là không có chức năng như vậy là một trong những điều quan trọng nhất về Haskell.
Haskell là ngôn ngữ rất nguyên tắc. Nó cố gắng duy trì sự khác biệt giữa các hàm "thuần túy" (không có bất kỳ tác dụng phụ nào và luôn trả về cùng một kết quả khi đưa ra cùng một đầu vào) và các hàm "không tinh khiết" (có tác dụng phụ như đọc từ tệp, in vào màn hình, ghi vào đĩa vv). Các quy tắc là:
- Bạn có thể sử dụng một hàm thuần túy bất cứ nơi nào (trong các chức năng tinh khiết khác, hoặc trong các chức năng bất tịnh)
- Bạn chỉ có thể sử dụng chức năng bất tịnh bên trong chức năng bất tịnh khác.
Cách mã được đánh dấu là thuần khiết hoặc không tinh khiết là sử dụng hệ thống loại. Khi bạn thấy chữ ký chức năng như
digitToInt :: String -> Int
bạn biết rằng hàm này thuần túy. Nếu bạn cung cấp cho nó String
, nó sẽ trả về một số Int
và hơn nữa là nó sẽ luôn trả về cùng một số Int
nếu bạn cung cấp cho cùng một số String
.Mặt khác, một chữ ký chức năng như
getLine :: IO String
là bất tịnh, vì kiểu trả về của String
được đánh dấu bằng IO
. Rõ ràng getLine
(đọc dòng đầu vào của người dùng) sẽ không luôn trả về cùng một String
, bởi vì nó phụ thuộc vào những gì người dùng nhập. Bạn không thể sử dụng hàm này trong mã thuần túy, vì việc thêm ngay cả tạp chất nhỏ nhất cũng sẽ gây ô nhiễm mã thuần túy. Khi bạn đi IO
bạn không bao giờ có thể quay lại.
Bạn có thể xem IO
làm trình bao bọc. Khi bạn thấy một loại cụ thể, ví dụ: x :: IO String
, bạn nên hiểu nghĩa là "x
là một hành động, khi được thực hiện, thực hiện một số I/O tùy ý và sau đó trả về một loại nào đó String
" (lưu ý rằng trong Haskell, String
và [Char]
giống hệt nhau).
Vậy làm cách nào bạn có thể truy cập vào các giá trị từ hành động IO
? May mắn thay, loại chức năng main
là IO()
(đó là một hành động thực hiện một số I/O và trả về ()
, giống như trả lại không có gì). Vì vậy, bạn luôn có thể sử dụng các hàm IO
bên trong main
. Khi bạn thực thi chương trình Haskell, những gì bạn đang làm đang chạy hàm main
, làm cho tất cả I/O trong định nghĩa chương trình được thực thi - ví dụ, bạn có thể đọc và ghi từ tệp, yêu cầu người dùng nhập, viết thư cho stdout vv vv
bạn có thể nghĩ rằng cấu trúc một chương trình Haskell như thế này:
- Tất cả các mã mà không I/O nhận được
IO
thẻ (về cơ bản, bạn đặt nó trong một khối do
)
- Mã không cần thực hiện I/O không cần phải ở trong khối
do
- đây là các loại "thuần túy" nctions.
- Các chuỗi chức năng
main
của bạn cùng với các hành động I/O bạn đã xác định theo thứ tự làm cho chương trình thực hiện những gì bạn muốn làm (xen kẽ với các hàm thuần túy ở bất cứ đâu bạn muốn).
- Khi bạn chạy
main
, bạn làm cho tất cả các hành động I/O đó được thực thi.
Vì vậy, cho tất cả những điều đó, làm cách nào để bạn viết chương trình? Vâng, hàm
readFile :: FilePath -> IO String
đọc tệp dưới dạng String
. Vì vậy, chúng tôi có thể sử dụng để lấy nội dung của tệp. Chức năng
lines:: String -> [String]
chia tách một String
trên dòng mới, vì vậy bây giờ bạn có một danh sách các String
s, mỗi tương ứng với một dòng của tập tin. Hàm
init :: [a] -> [a]
Thả phần tử cuối cùng khỏi danh sách (sẽ loại bỏ phần cuối cùng .
trên mỗi dòng).Chức năng
read :: (Read a) => String -> a
mất một String
và biến nó thành một kiểu dữ liệu tùy ý Haskell, chẳng hạn như Int
hoặc Bool
. Kết hợp các chức năng này một cách hợp lý sẽ cung cấp cho bạn chương trình của bạn.
Lưu ý rằng thời gian duy nhất bạn thực sự cần thực hiện bất kỳ I/O nào là khi bạn đang đọc tệp. Do đó, đó là phần duy nhất của chương trình cần sử dụng thẻ IO
. Phần còn lại của chương trình có thể được viết "hoàn toàn".
Có vẻ như những gì bạn cần là bài viết The IO Monad For People Who Simply Don't Care, điều này sẽ giải thích rất nhiều câu hỏi của bạn. Đừng sợ hãi bởi thuật ngữ "monad" - bạn không cần phải hiểu những gì một monad là viết các chương trình Haskell (chú ý rằng đoạn này là câu duy nhất trong câu trả lời của tôi sử dụng từ "monad", mặc dù thừa nhận rằng tôi đã sử dụng nó gấp bốn lần bây giờ ...)
đây là chương trình đó (tôi nghĩ) bạn muốn viết
run :: IO (Int, Int, [(Int,Int,Int)])
run = do
contents <- readFile "text.txt" -- use '<-' here so that 'contents' is a String
let [a,b,c] = lines contents -- split on newlines
let firstLine = read (init a) -- 'init' drops the trailing period
let secondLine = read (init b)
let thirdLine = read (init c) -- this reads a list of Int-tuples
return (firstLine, secondLine, thirdLine)
để trả lời npfedwards
bình luận về việc áp dụng lines
đến đầu ra của readFile text.txt
, bạn cần nhận ra rằng readFile text.txt
cung cấp cho bạn một số IO String
và chỉ khi bạn liên kết nó đến một biến (sử dụng contents <-
) mà bạn có quyền truy cập vào các String
cơ bản, để bạn có thể áp dụng lines
cho nó.
Hãy nhớ rằng: khi bạn đi IO
, bạn sẽ không bao giờ quay lại nữa.
Tôi đang cố tình lờ đi unsafePerformIO
bởi vì, như ngụ ý của tên, nó là rất không an toàn! Đừng bao giờ sử dụng nó trừ khi bạn thực sự biết bạn đang làm gì.
Lưu ý rằng String chỉ là bí danh loại cho [Char], vì vậy điều duy nhất đứng giữa bạn và [Char] là IO (bạn không thể loại bỏ - bạn cần phải làm việc bên trong đơn nguyên IO (xem bất kỳ hướng dẫn đơn lẻ nào để biết thêm thông tin)). Cũng lưu ý rằng [Char] là một danh sách các ký tự (liên kết), không phải là một mảng. – sepp2k
+1 vì bạn đã thu hút một số câu trả lời tuyệt vời ở đây :) – CoR