2012-06-27 33 views
19

Tôi gặp sự cố với Haskell. Tôi có tệp văn bản như sau:Làm cách nào để phân tích chuỗi IO trong Haskell?

5. 
7. 
[(1,2,3),(4,5,6),(7,8,9),(10,11,12)]. 

Tôi không biết làm cách nào để có được 2 số đầu tiên (2 và 7 ở trên) và danh sách từ dòng cuối cùng. Có dấu chấm ở cuối mỗi dòng.

Tôi đã cố gắng xây dựng trình phân tích cú pháp, nhưng chức năng được gọi là 'readFile' trả lại Monad được gọi là Chuỗi IO. Tôi không biết làm thế nào tôi có thể lấy thông tin từ loại chuỗi đó.

Tôi thích làm việc trên một mảng ký tự. Có thể có một hàm có thể chuyển đổi từ 'Chuỗi IO' thành [Char]?

+2

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

+1 vì bạn đã thu hút một số câu trả lời tuyệt vời ở đây :) – CoR

Trả lời

57

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à:

  1. 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)
  2. 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 

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[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 mainIO() (đó 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ì.

+3

Tôi cảm thấy như chúng ta không nên đề cập đến * chức năng sẽ không được đặt tên * trong các loại câu trả lời này, nếu chỉ vì một số sinh viên của Haskell có thể thấy nó, hãy xem định nghĩa loại và chỉ đơn giản là nói "* aha! Đây là những gì tôi đã được sau khi tất cả cùng! *" – Ashe

+0

Nó không phải là ngay cả khi bạn giữ chức năng tinh khiết và chức năng trong IO monad ngoài tỉ mỉ trình biên dịch sẽ inline nó anyways bằng cách nào đó? –

+0

Tôi gặp lỗi kiểu khi cố gắng áp dụng 'dòng' cho đầu ra của' readFile' –

4

Nếu bạn có một phân tích cú pháp thuộc loại này:

myParser :: String -> Foo 

và bạn đọc các tập tin sử dụng

readFile "thisfile.txt" 

sau đó bạn có thể đọc và phân tích các tập tin sử dụng

fmap myParser (readFile "thisfile.txt") 

Các kết quả của việc đó sẽ có loại IO Foo.

fmap có nghĩa là myParser chạy "bên trong" IO.

Một cách khác để nghĩ về điều này là trong khi myParser :: String -> Foo, fmap myParser :: IO String -> IO Foo.

8

Là một lập trình noob, tôi cũng bị nhầm lẫn bởi IO s. Chỉ cần nhớ rằng nếu bạn đi IO bạn không bao giờ đi ra ngoài. Chris đã viết một số great explanation on why. Tôi chỉ nghĩ rằng nó có thể giúp đưa ra một số ví dụ về cách sử dụng IO String trong một đơn nguyên. Tôi sẽ sử dụng getLine để đọc dữ liệu của người dùng và trả về một số IO String.

line <- getLine 

Tất cả điều này là ràng buộc người dùng nhập từ getLine đến một giá trị mang tên line. Nếu bạn nhập mã này vào ghci và nhập :type line nó sẽ trả về:

:type line 
line :: String 

Nhưng chờ đã! getLine trả về một IO String

:type getLine 
getLine :: IO String 

Vậy điều gì đã xảy ra với IO Ness từ getLine? <- là những gì đã xảy ra. <- là bạn của bạn IO. Nó cho phép bạn đưa ra giá trị bị nhiễm độc bởi IO trong một đơn vị và sử dụng nó với các chức năng bình thường của bạn. Monads dễ dàng được xác định bởi vì chúng bắt đầu bằng do.Cũng giống như vậy:

main = do 
    putStrLn "How much do you love Haskell?" 
    amount <- getLine 
    putStrln ("You love Haskell this much: " ++ amount) 

Nếu bạn đang như tôi, bạn sớm sẽ khám phá ra rằng liftIO là bên cạnh người bạn tốt nhất của bạn đơn nguyên, và rằng $ giúp giảm số lượng các ngoặc bạn cần phải viết.

Vậy làm thế nào để bạn nhận được thông tin từ readFile? Vâng, nếu sản lượng readFile 's là IO String như vậy:

:type readFile 
readFile :: FilePath -> IO String 

Sau đó, tất cả bạn cần là bạn thân thiện <-:

yourdata <- readFile "samplefile.txt" 

Bây giờ nếu kiểu đó trong ghci và kiểm tra các loại yourdata bạn sẽ nhận thấy nó đơn giản là String.

:type yourdata 
text :: String 
+0

là có một phiên bản de-sugared của 'số tiền <- getLine' ?? – matthias

+0

@matthias 'getLine >> = (\ số -> ...)'. –

6

Khi mọi người đã nói, nếu bạn có hai chức năng, một là readStringFromFile :: FilePath -> IO String, và khác là doTheRightThingWithString :: String -> Something, sau đó bạn không thực sự cần phải thoát khỏi một chuỗi từ IO, vì bạn có thể kết hợp hai chức năng này trong nhiều cách sau:

với fmap cho IO (IOFunctor):

fmap doTheRightThingWithString readStringFromFile 

với (<$>) cho IO (IOApplicative(<$>) == fmap):

import Control.Applicative 

... 

doTheRightThingWithString <$> readStringFromFile 

Với liftM cho IO (liftM == fmap):

import Control.Monad 

... 

liftM doTheRightThingWithString readStringFromFile 

Với (>>=) cho IO (IOMonad, fmap == (<$>) == liftM == \f m -> m >>= return . f):

readStringFromFile >>= \string -> return (doTheRightThingWithString string) 
readStringFromFile >>= \string -> return $ doTheRightThingWithString string 
readStringFromFile >>= return . doTheRightThingWithString 
return . doTheRightThingWithString =<< readStringFromFile 

Với do ký hiệu:

do 
    ... 
    string <- readStringFromFile 
    --^you escape String from IO but only inside this do-block 
    let result = doTheRightThingWithString string 
    ... 
    return result 

Mỗi lần bạn sẽ nhận được IO Something.

Tại sao bạn muốn làm như vậy? Vâng, với điều này, bạn sẽ có tinh khiếtreferentially transparent chương trình (chức năng) bằng ngôn ngữ của bạn. Điều này có nghĩa là mọi chức năng loại là không có IO là tinh khiếttham chiếu trong suốt, do đó đối với cùng một đối số, hàm sẽ trả về cùng giá trị. Ví dụ: doTheRightThingWithString sẽ trả lại cùng một số Something cho cùng một số String. Tuy nhiên readStringFromFile không phải IO-free có thể trả về các chuỗi khác nhau mỗi lần (vì tệp có thể thay đổi), do đó bạn không thể thoát khỏi giá trị không phân biệt như vậy từ IO.

Các vấn đề liên quan