Haskell--美化打印列表的问题

11

我对Haskell还不是很熟悉,但我已经仔细阅读并消化了《为了更好的学习Haskell》,在学习过程中尝试着做了一些实验。对于我的第一个项目,我想尝试一下经典的FizzBuzz游戏。因此,我写出了以下代码:

import System.IO

fizzBuzz :: (Integral a) => a -> String
fizzBuzz num
    | fizz && buzz = "FizzBuzz"
    | fizz = "Fizz"
    | buzz = "Buzz"
    | otherwise = show num
    where fizz = num `mod` 3 == 0
          buzz = num `mod` 5 == 0

main = print $ map fizzBuzz [1..100]

之前的方法很好用,但是返回的列表看起来比较密集,不太容易阅读。因此我尝试使用了以下主要函数:

main = map putStrLn $ map fizzBuzz [1..100]

这导致我出现了错误 Couldn't match expected type 'IO t' against inferred type '[IO ()]'。我尝试了半打方法,但似乎都没有帮助。我想做的事情应该怎么做才是正确的呢?

1个回答

26
map :: (a -> b) -> [a] -> [b]
putStrLn :: Show a => a -> IO ()
map putStrLn :: Show a => [a] -> [IO ()]

您有一个 IO () 操作列表。

main :: IO ()

您需要将它们合并为单个IO ()操作。

您想要做的是在sequence/sequence_中执行这些IO ()操作:

sequence :: Monad m => [m a] -> m [a]
sequence_ :: Monad m => [m a] -> m ()

为方便起见,mapM/mapM_会将函数映射到列表中,并按顺序处理生成的单调结果。

mapM :: Monad m => (a -> m b) -> [a] -> m [b]
mapM_ :: Monad m => (a -> m b) -> [a] -> m ()

所以你修改后的代码应该是这样的:

main = mapM_ putStrLn $ map fizzBuzz [1..100]

虽然我可能会这样写:

main = mapM_ (putStrLn . fizzBuzz) [1..100]

或者甚至是这个:

main = putStr $ unlines $ map fizzBuzz [1..100]

让我们编写自己的序列。我们希望它做什么呢?

sequence [] = return []
sequence (m:ms) = do
    x <- m
    xs <- sequence ms
    return $ x:xs
  • 如果列表里面没有元素了,就返回(注入到Monad中)一个空结果列表。
  • 否则,在Monad内部,
    • 绑定(对于IO Monad,这意味着执行)第一个结果。
    • sequence剩下的列表;绑定那个结果列表。
    • 返回第一个结果和其他结果列表的cons。

GHC的库使用类似于foldr (liftM2 (:)) (return [])的东西,但是对于新手来说更难解释;现在,只要相信我的话它们是等价的。

sequence_更容易理解,因为它不会费心追踪结果。 GHC的库将其实现为sequence_ ms = foldr (>>) (return ()) ms。 让我们展开 foldr 的定义:

  sequence [a, b, c, d]
= foldr (>>) (return ()) [a, b, c, d]
= a >> (b >> (c >> (d >> return ())))

换句话说,“做a,丢弃结果; 做b; 丢弃结果,... 最后返回()”。
mapM  f xs = sequence  $ map f xs
mapM_ f xs = sequence_ $ map f xs

另一方面,使用备选的unlines解决方案甚至无需了解单子(monads)。

unlines是什么?这里有一个例子:lines "a\nb\nc\nd\n" = ["a", "b", "c", "d"],所以unlines ["a", "b", "c", "d"] = "a\nb\nc\nd\n"

unlines $ map fizzBuzz [1..100] = unlines ["1", "2", "Fizz", ..] = "1\n2\nFizz\n..." ,然后传递给putStr。由于Haskell的惰性求值魔法,完整字符串永远不需要在内存中构建,因此即使处理[1..1000000]或更高也能快乐运行 :)


谢谢,我知道 Haskell 有类似的东西,但我不知道怎么用!现在就要理解它为什么能工作... :) - RCIX
正如我所解释的那样,您生成了一个 IO() 操作列表,但您需要说明您想要对它们执行什么操作。sequence 按顺序运行一系列单子,这恰好是您在此处想要的。相反,我的最后一个解决方案将 fizz/buzz 字符串连接成一个多行字符串,然后进入 IO 的奇怪领域。 - ephemient
是的,我只是没有将单子与IO操作联系起来 - 那个教程仍然有些模糊。 - RCIX

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