新手学Haskell,想输出一个列表。

5
我想大家都已经看过这种(或者至少类似的)问题了,但是我需要问一下,因为我在任何地方都找不到这个问题的答案(主要是因为我不知道我应该找什么)。
我写了这个小脚本,其中printTriangle应该打印出帕斯卡三角形。
fac = product . enumFromTo 2

binomial n k  = (product (drop (k-1) [2..n])) `div` (fac (n-k))

pascalTriangle maxRow = 
               do row<-[0..maxRow-1]
                  return (binomialRow row)
                  where
                  binomialRow row = 
                              do k<-[0..row]
                                 return (binomial row k)

printTriangle :: Int -> IO ()
printTriangle rows  = do row<-(triangle)
                         putStrLn (show row)
                         where 
                         triangle = pascalTriangle rows

现在,对于经过训练的眼睛来说可能很明显的原因,但对我来说完全是个谜,当我尝试在ghci中加载它时,会出现以下错误:

   Couldn't match expected type `IO t0' with actual type `[[Int]]'
    In a stmt of a 'do' expression: row <- (triangle)
    In the expression:
      do { row <- (triangle);
           putStrLn (show row) }
    In 
an equation for `printTriangle':
            printTriangle rows
              = do { row <- (triangle);
                     putStrLn (show row) }
              where
                  triangle = pascalTriangle rows

我想做的是像这样调用printTriangle:

printTriangle 3

我得到了这个输出:
[1]
[1,1]
[1,2,1]

如果有人能解释一下为什么我在这里做的事情不起作用(说实话,我不太确定我在这里到底在做什么;我习惯于命令式语言,而这个整个函数式编程的东西对我来说仍然非常困惑),以及如何以更聪明的方式做到这一点,那就太好了。
提前致谢。
4个回答

6
你在评论中说过你认为列表是单子,但现在你不确定了——好吧,你是对的,列表确实是单子!那么为什么你的代码会有问题呢?
原因在于IO也是一个单子。因此当编译器看到printTriangle :: Int -> IO ()以及do-notation时,就会认为“啊哈!我知道该怎么做了!他正在使用IO单子!”当它发现里面使用的是列表单子而不是IO单子时,它可能会感到震惊和绝望!
所以问题在于:要打印并处理外部世界的信息,你需要使用IO单子;而在函数内部,你试图使用列表作为单子。
让我们看看这会带来什么问题。do-notation是Haskell的语法糖,它诱使我们进入代码块并享用其中……我的意思是它是>>=(又称bind),让我们使用单子(并享受其中)的语法糖。因此,让我们使用bind来写printTriangle
printTriangle rows = (pascalTriangle rows) >>= (\row -> 
                     putStrLn $ show row)

好的,这很直接。现在我们有没有遇到任何问题呢?让我们看看类型。bind的类型是什么?Hoogle说:(>>=) :: Monad m => m a -> (a -> m b) -> m b。好的,谢谢Hoogle。所以基本上,bind想要一个包装类型为a的monad类型,一个将类型为a的personality转换为(相同的)包装类型为b的monad类型的函数,并以(相同的)包装类型为b的personality结束。
那么在我们的printTriangle中,我们有什么?
- pascalTriangle rows :: [[Int]] - 所以我们的monad是[],个性是[Int] - (\row -> putStrLn $ show row) :: [Int] -> IO () - 这里的monad是IO,个性是() 糟糕。当Hoogle告诉我们必须匹配monad类型时,我们给>>=提供了列表monad和产生IO monad的函数。这使得Haskell表现得像一个小孩子:它闭上眼睛,在地板上跺脚大喊“不!不!不!”甚至不会看你的程序,更不用说编译它了。
那么我们如何取悦Haskell呢?其他人已经提到了mapM_。在顶层函数中添加显式类型签名也是一个好主意 - 它有时可以帮助您更早地获得编译错误(您肯定会遇到这些错误;毕竟这是Haskell :)),这使得理解错误消息变得容易得多。
我建议编写一个将[[Int]]转换为字符串的函数,然后单独打印出该字符串。通过将转换成字符串与IO-nastiness分离,这将允许您继续学习Haskell,而不必担心mapM_和朋友们,直到您准备好为止。
showTriangle :: [[Int]] -> String
showTriangle triangle = concatMap (\line -> show line ++ "\n") triangle

或者

showTriangle = concatMap (\line -> show line ++ "\n")

那么printTriangle就容易多了:
printTriangle :: Int -> IO ()
printTriangle rows = putStrLn (showTriangle $ pascalTriangle rows)

或者

printTriangle = putStrLn . showTriangle . pascalTriangle

哇,这正是我正在寻求的 - 谢谢,现在我有点明白发生了什么。 - Cubic
@Cubic -- 如果你对学习 Haskell 感兴趣,但需要一个好的资源,请查看 Learn You A Haskell。这是一本非常有趣且深入的关于 Haskell 及其惯用语的读物。 - Matt Fenwick

4
如果您想将列表元素打印在新行上,您可以参考这个问题的解答。
因此,
printTriangle rows  = mapM_ print $ pascalTriangle rows

并且
λ> printTriangle 3
[1]
[1,1]
[1,2,1]

最后,您所要求的似乎是 mapM_

这个可以运行,但我仍然不知道实际发生了什么。 - Cubic
(当然,我知道mapM是什么(至少我认为我知道 - 和map差不多,只是用单子,对吧?),但我仍然想知道为什么我会收到那个错误消息 - 在Haskell中,我经常遇到类型错误,所以我想这是我现在需要关注的问题) - Cubic
其实,我也是个初学者。有一点我可以告诉你的是,在printTriangle函数中使用<-和'rows'参数并不太有效。所以,举个例子,当你移除其中一个参数时,你会得到可行的代码(虽然它实际上并不是你想要的,并且看起来相当奇怪)。就像这样http://hpaste.org/53912 - ДМИТРИЙ МАЛИКОВ
@dmitry.malikov -- 这是因为有两个不同的单子,[]IO。你需要更高级的东西(即单子变换器)来混合它们。 - Matt Fenwick

3
每当我在Haskell中编码时,我总是尝试声明至少顶级定义的类型。这不仅可以通过记录函数来帮助,而且还可以更容易地捕获类型错误。因此,pascalTriangle具有以下类型: pascalTriangle :: Int -> [[Int]]
当编译器看到下面的代码时:
row<-(triangle)
...
where 
triangle = pascalTriangle rows

它将推断三角形的类型为:

triangle :: [[Int]]

<- 操作符期望其右侧的参数是一个单子。因为您声明了函数在 IO 单子上工作,所以编译器期望三角形具有以下类型:

triangle :: IO something

这显然与类型 [[Int]] 不匹配。这就是编译器试图以自己扭曲的方式告诉您的内容。

正如其他人所说,这种编码风格不是 Haskell 的惯用法。它看起来像我在早期学习 Haskell 时会产生的代码,当时我仍然有着“命令式导向”的思维方式。如果您试图摆脱命令式思维方式,并开放您的思维方式,尝试使用函数式风格,您将发现可以以非常优雅和整洁的方式解决大多数问题。


是的,我想这应该是这样的。我认为问题在于我仍然不太明白Monad和普通函数之间的区别(我以为我可以将列表用作Monad - 显然是错误的)哦,而且我非常想打开我的思维方式,采用函数式风格,但我真的不知道大多数问题的函数式方法会是什么样子... - Cubic
你可以将列表用作单子(monad)。只是在这种情况下,编译器期望IO单子,而你却提供了列表单子。单子不是一个容易理解的概念。如果你错过了它们,这里有一些不错的资源可以帮助你:http://www.cs.utah.edu/~hal/docs/daume02yaht.pdf, http://www.learnyouahaskell.com/, http://book.realworldhaskell.org/read/ - Pedro Rodrigues

0

尝试从ghci提示符中执行以下操作:

> let {pascal 1 = [1]; pascal n = zipWith (+) (l++[0]) (0:l) where l = pascal (n-1)}
> putStr $ concatMap ((++"\n") . show . pascal) [1..20]

您的代码非常不符合 Haskell 的习惯用法。在 Haskell 中,您使用高阶函数来构建其他函数。这样,您就可以编写非常简洁的代码。

在此我使用 zipWith 来惰性地组合两个列表,生成帕斯卡三角形的下一行,而且这几乎是按手工计算的方式进行的。然后使用 concatMap 生成可打印的三角形字符串,并由 putStr 打印出来。


1
他正在使用列表单子和点无样式。与其试图进一步改进他的代码(对于初学者来说,我认为它已经很不错了),不如告诉他导致他看到的实际错误消息的原因是什么? - user395760
2
帕斯卡三角形并不是我的最终目标,我实际上想编写一个计算二项式分布的函数,所以我有了我的二项式函数,并且我想通过三角形作为示例来学习Haskell IO。此外,我不知道什么是惯用语,因为我几乎不了解这种语言习惯用法。 - Cubic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接