【问题标题】:How to read data from IO into data-structure and then process the data-structure?如何将数据从 IO 读取到数据结构中,然后处理数据结构?
【发布时间】:2011-07-09 11:59:03
【问题描述】:

首先对不起,我做了典型的“我从哪里开始”的事情,但我完全迷失了。

我已经阅读了“Learn you a haskell for great good”这个网站,感觉现在已经有一个时代了(差不多半个学期。我即将完成“输入和输出”一章,我仍然不知道如何编写多行程序。

我看过 do 语句,你只能使用它来将 IO 操作连接到一个函数中,但我不知道我将如何编写一个真实的应用程序。

谁能指出我正确的方向。

我来自 C 背景,基本上我在大学这个学期的一个模块中使用了 haskell,我想将 C++ 与 haskell 进行比较(在很多方面)。我希望创建一系列搜索和排序程序,以便我可以评论它们在各自语言中的易用性与速度。

但是,我真的开始对使用 Haskell 失去信心,因为使用 Haskell 已经六周了,我仍然不知道如何编写完整的应用程序,而且我正在阅读的网站中的章节似乎越来越多越来越长。

我基本上需要创建一个将存储在结构中的基本对象(我知道该怎么做),更多我正在努力解决的是,我如何创建一个从某个文本文件中读取数据的程序,并首先用该数据填充结构,然后继续处理它。由于 haskell 似乎将 IO 和其他操作分开,它不仅让我在程序中编写多行,我正在寻找这样的东西:

main = data <- getContent
       let allLines = lines data
       let myStructure = generateStruct allLines
       sort/search/etc
       print myStructure

我该怎么做呢?有什么好的教程可以帮助我开始实际的程序吗?

-A

【问题讨论】:

  • 必填链接:haskell.org/tutorial 以防万一被绕过——当然,“温和”是相对的。
  • 我正在学习一个haskell for great good,不过只有几天时间。
  • 除了需要使用 do 表示法或放弃赋值 (&lt;-) 并转而使用绑定 (&gt;&gt;=) 之外,您不能按原样命名 data保留字。
  • Haskell 确实很难学,尤其是对于有命令式语言编程经验的人来说。 可以编写真正的应用程序的;从你习惯于思考的方式来看,一切都是“由内而外”的。坚持下去!
  • 听起来您需要为#haskell 开处方,每周三次,持续一个月。

标签: haskell


【解决方案1】:

您提到看到do 表示法,现在该学习如何使用do。考虑您的示例 mainIO,您应该使用 do 语法或绑定:

main = do
  dat <- getContent
  let allLines = lines dat
      myStructure = generateStruct allLines
      sorted = mySort myStructure
      searchResult = mySearch myStructure
  print myStructure
  print sorted
  print searchResult

所以现在你有一个获取stdin 的main,通过lines 将其转换为[String],大概将其解析为一个结构并在该结构上运行排序和搜索。请注意,有趣的代码都是纯代码 - mySortmySearchgenerateStruct 不需要是 IO(也不能是,在 let 绑定中),因此您实际上是在正确使用纯且有效的代码在一起。

我建议你看看 bind 是如何工作的 (&gt;&gt;=) 以及如何将符号去糖化成 bind。 This SO question 应该会有所帮助。

【讨论】:

  • 请注意,从学术的角度来看,包括 Haskell 中的 IO 在内的一切都是纯粹的——唯一的副作用是不终止。但是,通常将“纯/有效”作为“非单子/单子”的同义词说。为了让新手更难学习纯度,有一个名为pure的函数。
  • @nponeccop 使用“monadic”作为“有效”的同义词并不完全正确,人们这样做的事实可能会让初学者感到困惑。 IO 值可能有效; &gt;&gt;= 和其他与 monad 相关的函数是纯函数,但可以具有有效的参数和/或结果。
  • 我的看法是相反的:我希望大家不要再谈论副作用,而是开始谈论单子代码。它是否代表“真实”效果与程序员无关,只会令人困惑。例如,ST 与州。无论如何,这不是一个扩展讨论的好地方
【解决方案2】:

另请参阅 Neil Mitchell 的 Explaining Haskell IO without Monads

【讨论】:

  • 这是我所知道的两个最好的“monad 教程”之一。非常推荐。
【解决方案3】:

我将尝试从一个简化的示例开始。假设这是我们想要做的:

  1. 打开一个包含整数列表的文件并返回它。
  2. 对此列表进行排序
  3. 让我们也颠倒一下列表
  4. 在屏幕上打印结果

假设我们有这些函数可以使用:

getContent :: IO [Int]
sort :: [Int] -> [Int]
reverse :: [Int] -> [Int]
show :: a -> String
putStrLn :: String -> IO ()

为了让我们清楚,我会谈谈这些功能:

  • getContent:这个函数是我编的,但是如果有这样的函数 that 就是它的签名(你可以使用getContent = return [3,7,2,1] 进行测试)。我敢肯定你以前见过这样的签名,至少隐约明白,既然它做 IO,它的签名不能只是getContent :: [Int]
  • sort:是Data.List模块中定义的函数,用法很简单:sort [3,1,2]返回[1,2,3]
  • reverse:也在Data.List模块中定义:reverse [1,3,2]返回[2,3,1]
  • show:不需要导入任何东西,直接使用即可:show 11返回字符串"11"show [1,2,3] 返回字符串 "[1,2,3]" 等。
  • putStrLn:获取一个字符串,将其放在屏幕上并返回 IO (),现在又一次,因为它执行 IO,所以它的签名不能只是putStrLn :: Stiring -&gt; ()

好的,现在我们已经有了创建程序所需的一切,现在的问题是将这些功能连接在一起。让我们从连接函数开始:

getContent :: IO [Int]sort :: [Int] -&gt; [Int]

我认为如果你得到了这部分,你也很容易得到其余部分。所以,问题是因为getContent 返回IO [Int] 而不仅仅是[Int],你不能忽略或去掉IO 部分并将其推入sort。也就是说,这是你不能做的来连接这些功能:

sort (getRidOfIO getContent)

这就是&gt;&gt;= :: m a -&gt; (a -&gt; m b) -&gt; m b 操作的用武之地。现在注意到mab类型变量,所以如果我们用m 代替IOa 代替[Int]b 代替@987654354 @,我们得到签名:

&gt;&gt;= :: IO [Int] -&gt; ([Int] -&gt; IO [Int]) -&gt; IO [Int]

再次查看getContentsort 函数及其签名,并尝试考虑如何将它们融入&gt;&gt;=。我相信你会注意到你可以直接使用getContent 作为&gt;&gt;= 的第一个参数。到目前为止,&gt;&gt;= 所做的是将[Int] 取出getContent 并将其推入作为第二个参数提供的函数中。但是第二个参数中的函数是什么?我们不能直接使用sort :: [Int] -&gt; [Int],我们可以尝试的下一个最好的方法是

\listOfInts -&gt; sort listOfInts

但这仍然有签名[Int] -&gt; [Int],所以这并没有多大帮助。这里是另一个英雄来的地方,

return :: a -&gt; m a.

同样,am 是类型变量,让我们替换它们,我们将得到

return :: [Int] -&gt; IO [Int]

所以将\listOfInts -&gt; sort listOfIntsreturn 加在一起,我们将得到:

\listOfInts -&gt; return $ sort listOfInts :: [Int] -&gt; IO [Int]

这正是我们想要作为&gt;&gt;= 的第二个参数的内容。因此,让我们最终使用我们的胶水将getContentsort 连接在一起:

getContent &gt;&gt;= (\listOfInts -&gt; return $ sort listOfInts)

这与(使用do 表示法)相同:

do listOfInts <- getContent
   return $ sort listOfInts

到这里,最可怕的部分就结束了。现在可能是 aha 时刻之一,试着想想我们刚刚建立的连接的结果类型是什么。我会为你破坏它,......的类型

getContent &gt;&gt;= (\listOfInts -&gt; return $ sort listOfInts) 又是IO [Int]

让我们总结一下:我们采用了 IO [Int] 类型的东西和 [Int] -&gt; [Int] 类型的东西,将这两个东西粘在一起,再次得到 IO [Int] 类型的东西!

现在继续尝试完全相同的事情:获取我们刚刚创建的IO [Int] 对象并将其与reverse :: [Int] -&gt; [Int] 粘合在一起(使用&gt;&gt;=return)。

我觉得我写的太多了,但是如果有什么不清楚的地方或者您需要其他方面的帮助,请告诉我。

到目前为止,我所描述的内容可能如下所示:

getContent :: IO [Int]   
getContent = return [5,2,1,7]

main :: IO ()
main = do
  listOfInts <- getContent
  return $ sort listOfInts
  return ()                   -- This is only to sattisfy the signature of main

【讨论】:

    【解决方案4】:

    如果是从stdin 读取并将结果写入stdout 的问题,没有进一步的用户输入——正如你提到的getContents 所暗示的——那么古老的interact :: (String -&gt; String) -&gt; IO (),或者其他几个版本,例如Data.ByteString.interact :: (ByteString -&gt; ByteString) -&gt; IO ()Data.Text.interact :: (Text -&gt; Text) -&gt; IO() 都是需要的。 interact 基本上是“用这个函数制作一个小 unix 工具”函数——它将正确类型的 pure 函数映射到可执行操作(即 IO() 类型的值。)所有Haskell 教程应在第三页或第四页提及,并附上编译说明。

    所以如果你写

    main = interact arthur
    
    arthur :: String -> String
    arthur = reverse
    

    并使用ghc --make -O2 Reverse.hs -o reverse 进行编译,然后通过管道传输到./reverse 的任何内容都将被理解为字符列表并反转出现。同样,无论您使用什么管道

    main = interact (unlines . meredith  . lines)
    
    meredith :: [String] -> [String]
    meredith = filter (not.null)
    

    将出现省略空行。更有趣的是,

    main = interact ( unlines . map show . luther . map read . lines)
    
    luther :: [Int] -> [Int]
    luther = filter even 
    

    将采用由换行符分隔的字符流,将它们读取为Ints,删除奇数,并产生适当过滤的流。

    main = interact ( unlines . map show . emma . map read . lines)
    
    emma :: [Int] -> Int
    emma = sum . map square 
      where square x = x * x
    

    将打印换行符分隔数字的平方和。

    在最后两种情况下,lutheremma 的内部“数据结构”是 [Int],这很乏味,当然,应用于它的函数也很简单。要点是让interact 的一种形式处理所有的IO,从而让你的脑海中浮现出像“填充结构”和“处理它”这样的图像。要使用interact,您需要使用组合来使整体产生某种String -&gt; String 函数。但即使在这里,就像在 runt 第一个示例 arthur:: String -&gt; String 中一样,您正在定义一个更像数学意义上的真正函数。 StringByteString 类型中的值与 BoolInt 中的值一样纯。

    在这种基本 interact 类型的更复杂的情况下,您的任务首先是考虑如何将您将关注的函数的所需 pure 值映射到 @987654349 @ 值(这里,对于 Int,它只是 show,对于 [Int],它只是 unlines . map show)。 interact 知道如何处理字符串。 -- 然后弄清楚如何定义一个 pure mapping 从 Strings 或 ByteString(将包含您的“原始”数据)到您的主体函数作为参数的类型中的值。在这里,我只是使用map read . lines 导致[Int]。如果您正在处理一些更复杂的问题,比如说树结构,您需要一个从[Int]MyTree Int 的函数。当然,要放在这个位置上的更复杂的函数是解析器。

    然后你可以去城里,在这种情况下:根本没有理由认为自己是“编程”、“填充”和“处理”。这就是LYAH 的所有酷设备发挥作用的地方。您的职责是定义特定定义学科内的映射。在最后两种情况下,它们是从[Int][Int] 和从[Int]Int,但这里有一个类似的示例,它源自super-excellent Vector package 上的excellent, still incomplete, tutorial,其中初始数字结构是处理的是Vector Int

    {-# LANGUAGE BangPatterns #-}
    import qualified Data.ByteString.Lazy.Char8      as L
    import qualified Data.Vector.Unboxed            as U
    import System.Environment
    
    main = L.interact (L.pack . (++"\n") . show . roman . parse)
        where 
        parse :: L.ByteString -> U.Vector Int
        parse bytestr = U.unfoldr step bytestr
        step !s = case L.readInt s of
            Nothing       -> Nothing
            Just (!k, !t) -> Just (k, L.tail t)
    
    -- now the IO and stringy nonsense is out of the way 
    -- so we can calculate properly:
    
    roman :: U.Vector Int -> Int
    roman = U.sum
    

    这里roman 又是白痴,从 Ints 的 Vector 到 Int 的任何函数,无论多么复杂,都可以代替它。写一个更好的roman 永远不会是“填充”“多行编程”“处理”等问题,尽管我们当然是这样说的;这只是通过组合 Data.Vector 和其他地方的函数来定义真正函数的问题。天空是极限,也请查看该教程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-28
      • 1970-01-01
      • 1970-01-01
      • 2012-04-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多