Haskell IO 的多行读取技术

Technique for reading in multiple lines for Haskell IO

基本上我想找到一种方法,让用户可以输入测试用例的数量,然后输入他们的测试用例。然后程序可以运行这些测试用例并按照测试用例出现的顺序打印出结果。

所以基本上我有 main 读取测试用例的数量并将其输入到一个函数中,该函数将从 IO 读取多次。它看起来像这样:

main = getLine >>= \tst -> w (read :: String -> Int) tst [[]]

这是 w 的方法签名:w :: Int -> [[Int]]-> IO ()

所以我的计划是读取测试用例的数量并让 w 运行一个函数,该函数接受每个测试用例并将结果存储到 [[]] 变量中。所以列表中的每个列表都会是一个输出。 w 将递归运行直到它到达 0 并在单独的行上打印出每个列表。我想知道是否有更好的方法来执行此操作,因为我必须将一个空列表传递给 w,这似乎是无关紧要的。

正如@bheklilr 提到的,您无法更新 [[]] 这样的值。标准的函数方法是通过一组递归调用传递一个累加器。在下面的示例中,loop 函数的 acc 参数就是这个累加器 - 它包含到目前为止收集的所有输出。在循环结束时我们 return 它。

myTest :: Int -> [String]
myTest n = [ "output line " ++ show k ++ " for n = " ++ show n | k <- [1..n] ]

main = do
  putStr "Enter number of test cases: "
  ntests <- fmap read getLine :: IO Int
  let loop k acc | k > ntests = return $ reverse acc
      loop k acc = do
        -- we're on the kth-iteration
        putStr $ "Enter parameter for test case " ++ show k ++ ": "
        a <- fmap read getLine :: IO Int
        let output = myTest a         -- run the test
        loop (k+1) (output:acc)
  allOutput <- loop 1 []
  print allOutput

随着您对这种模式越来越熟悉,您会把它识别为 fold(实际上是 monadic fold,因为我们正在做 IO)并且您可以实现它foldM.

Update:为了帮助解释 fmap 的工作原理,这里是不使用 fmap:

的等效表达式
With fmap:                                 Without fmap:

n <- fmap read getLine :: IO [Int]         line <- getLine
                                           let n = read line :: Int

vals <- fmap (map read . words) getLine    line <- getLine
             :: IO [Int]                   let vals = (map read . words) line :: [Int]

使用fmap 允许我们消除中间变量line,我们无论如何都不会再引用它。我们仍然需要提供类型签名,以便 read 知道该怎么做。

惯用的方法是使用 replicateM:

runAllTests :: [[Int]] -> IO ()
runAllTests = {- ... -}

main = do
    numTests <- readLn
    tests <- replicateM numTests readLn
    runAllTests tests
-- or:
-- main = readLn >>= flip replicateM readLn >>= runAllTests