【问题标题】:String concatenation with each line in a text file in HaskellHaskell中文本文件中每一行的字符串连接
【发布时间】:2021-09-20 06:29:28
【问题描述】:

我正在尝试编写一个将给定文本文件作为其读取的代码 输入并传递相同的文本文件,每行每个字符串的长度与该字符串的长度连接。

为此,我创建了一个每行一个字符串的文本文件。 我已经设法编写了一个代码,该代码从文本文件中获取一行并输出该行,其长度显示为其中的一部分,但我无法编写此代码的递归版本,因此它会继续执行这与文本文件的每一行,直到没有更多的行。如果我一直在使用列表,但我不能对充满字符串的文本文件使用模式匹配,这不会是一个问题。

我只需要获取第二个代码示例以将其自身应用于整个列表,但我不能。 如何在不使用仿函数/fmap 的情况下更改我的代码以使其正常工作? 对于这个愚蠢的问题,我真的很抱歉,我对编程还很陌生。

import System.IO
main :: IO()
main = do
   file <- openFile ".txt" ReadMode
   x <- hGetContents file >>=
   xs <- hGetLine 
   if null xs 
    then return ()
    else do
        putStrLn $ xs ++ " has a length of " ++ show (length xs)
   hClose file

main :: IO()
main = do
file <- openFile ".txt" ReadMode
x <- hGetLine file
xs <- hGetLine file
if null x 
  then return ()
  else do 
      putStrLn $ x ++ " has a length of " ++ show (length x)
      putStrLn $ xs ++ " has a length of " ++ show (length xs)
 hClose file

【问题讨论】:

    标签: haskell recursion io


    【解决方案1】:

    您似乎想要一个递归解决方案。但是main 操作不太适合递归调用自身,因为它的特定职责只发生一次:打开和关闭文件。

    所以你需要一个单独的递归操作,它假设文件管理是由上面的某个层完成的,并且只处理一个预先准备好的文件句柄。假设我们称之为processFileHandle

    使用那种类型签名:

    processFileHandle :: Handle -> IO ()  -- for now
    

    但是,等一下!我们有这个文本转换要做:

    xs ++ " has a length of " ++ show (length xs)
    

    我们是否要将这种代码硬连线到我们的processFileHandle 函数中?绝对不 !我们希望将文本处理与文件 I/O 分开。这样一来,我们就不必在每次我们必须进行的线路转换发生变化时都重新编写processFileHandle

    所以更好的类型签名是:

    processFileHandle :: (String -> String) ->  Handle -> IO ()
    

    我们提供线变换作为额外的功能参数。在我们的例子中,这是:

    transformLine1 :: String -> String
    transformLine1 str =
        let  ln = length str
        in   str ++ " has a length of " ++ (show ln)
    

    现在,要继续处理processFileHandle,我们需要一种优雅地检测文件结束条件的方法。但是为此的函数必然具有类型签名:Handle -&gt; IO Bool

    所以我们将此类型签名提交到Hoogle 专用搜索引擎。 Hoogle 将我们引向 hIsEOF 库函数,这正是我们所需要的。

    我们现在可以编写我们的 main 动作,这只是:

    main :: IO ()
    main = do
        fh <- openFile  "foo.txt"  ReadMode
        processFileHandle transformLine1 fh
        hClose fh
    

    现在,我们可以提供processFileHandle 的代码,因为我们可以测试文件结尾:

    processFileHandle :: (String -> String) -> Handle -> IO ()
    processFileHandle fn fh =
      do
          atTheEnd <- hIsEOF fh    -- are we done ?
          if atTheEnd then
                          return ()  -- nothing left to do
                      else
                          do
                              line0 <- hGetLine fh
                              let  line1 = fn line0
                              putStrLn line1
                              processFileHandle fn fh  -- recursive call
    

    测试:

    $ 
    $ cat foo.txt
    alpha
    beta
    epsilon
    eta
    $ 
    $ ghc --version
    The Glorious Glasgow Haskell Compilation System, version 8.8.4
    $ 
    $ ghc q68324502.hs -o ./q68324502.x
    [1 of 1] Compiling Main             ( q68324502.hs, q68324502.o )
    Linking ./q68324502.x ...
    $ 
    $ ./q68324502.x
    alpha has a length of 5
    beta has a length of 4
    epsilon has a length of 7
    eta has a length of 3
    $ 
    

    【讨论】:

    • 非常感谢您的详细解释和cmets!我在搜索过程中遇到了 hIsEOF,但没有设法正确实现它,所以你在多个方面帮助了我!
    【解决方案2】:

    要递归地写这个,你需要一个函数来调用它自己。你已经有一个函数main可以调用自己,但你不想多次打开文件,所以最好将打开(和关闭)文件的部分分开使用辅助函数读取行的部分:

    main :: IO ()
    main = do
      file <- openFile "test.txt" ReadMode
      processLines
      hClose file
    

    现在我们可以编写递归的processLines函数了:

    processLines :: Handle -> IO ()
    processLines file = do
      x <- hGetLine file
      putStrLn $ x ++ " has a length of " ++ show (length x)
      processLines file
    

    这可行,但它会无条件地调用自身,因此它会一直读取行,直到到达文件末尾并引发异常。我们可以使用函数hIsEOF 来解决这个问题:

    processLines :: Handle -> IO ()
    processLines file = do
      eof <- hIsEOF file
      if eof
        then return ()
        else do
          x <- hGetLine file
          putStrLn $ x ++ " has a length of " ++ show (length x)
          processLines file
    

    因为processLines 只是一个辅助函数并且在main 之外没有合理的用途,大多数Haskell 程序员会使用where 子句或let 定义将它拉入main,并且可能会给出它一个较短的一次性名称,例如 loopprocessgo。带有where 子句的结果是:

    main :: IO ()
    main = do
      file <- openFile "test.txt" ReadMode
      process file
      hClose file
    
      where
        process file = do
          eof <- hIsEOF file
          if eof
            then return ()
            else do
              x <- hGetLine file
              putStrLn $ x ++ " has a length of " ++ show (length x)
              process file
    

    使用let 的一个优点是您可以在作用域内使用file 变量定义帮助程序,因此您不必将它作为参数传递:

    main :: IO ()
    main = do
      file <- openFile "test.txt" ReadMode
      let loop = do
            eof <- hIsEOF file
            if eof
              then return ()
              else do
                x <- hGetLine file
                putStrLn $ x ++ " has a length of " ++ show (length x)
                loop
      loop
      hClose file
    

    Haskell 提供了更合理的方式以循环形式编写此程序,避免显式递归和定义帮助程序的需要。例如:

    -- using `whileM_` from `monad-loops`
    main1 :: IO ()
    main1 = withFile "test.txt" ReadMode $ \h ->
      whileM_ (not <$> hIsEOF h) $ do
          x <- hGetLine h
          putStrLn $ x ++ " has a length of " ++ show (length x)
    

    或:

    -- using `forM_` from `Control.Monad`
    main2 :: IO ()
    main2 = do
      contents <- readFile "test.txt"
      let lns = lines contents
      forM_ lns $ \x ->
        putStrLn $ x ++ " has a length of " ++ show (length x)
    

    然而,这些都不是编写这个程序的好方法。从根本上说,你对一行输入有一个纯粹的转换:

    annotate :: String -> String
    annotate x = x ++ " has a length of " ++ show (length x)
    

    并且您想将其应用于文件中的所有行,您可以使用单行来完成(诚然在 IO 上使用 fmap Functor):

    main = putStr =<< unlines . map annotate . lines <$> readFile "test.txt"
    

    或更明确地说:

    main = do
      content <- readFile "test.txt"
      let lns = lines content
          lns' = [annotate l | l <- lns]
      putStr $ unlines lns'
    

    【讨论】:

    • 非常感谢!我特别喜欢这样一个事实,即您可以使用本地定义将所有内容打包到单个递归主函数中,这要归功于“let”。
    猜你喜欢
    • 1970-01-01
    • 2018-10-20
    • 1970-01-01
    • 1970-01-01
    • 2016-08-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-24
    相关资源
    最近更新 更多