如果您发现do
符号令人困惑,我的建议是根本不要使用它。您可以使用>>=
. 假装它的类型是
(>>=) :: IO a -> (a -> IO b) -> IO b
也就是说,让我们看看你的代码。
let
在一个do
块中为某个值命名。这与它在 之外所做的事情是一样的do
,所以这在这里没有帮助(它没有给你额外的权力)。
<-
更有趣的是:它充当“从本地 IO 中提取值”的构造(如果你眯着眼睛看的话)。
hoo :: [String] -> IO [String]
hoo (xs:ys:yss) = do
-- The right-hand side (goo xs ys) has type IO [String], ...
rs <- goo xs ys
-- ... so rs :: [String].
-- We can apply the same construct to our recursive call:
hs <- hoo yss
-- hoo yss :: IO [String], so hs :: [String].
let gs = rs ++ hs
return gs
如上所述,let
只是将名称绑定到一个值,所以我们在这里并不需要它:
hoo :: [String] -> IO [String]
hoo (xs:ys:yss) = do
rs <- goo xs ys
hs <- hoo yss
return (rs ++ hs)
或者,没有do
符号,<-
我们将按如下方式进行。
(>>=) :: IO a -> (a -> IO b) -> IO b
>>=
接受一个值和一个回调函数,并在“展开”值 ( )IO
上运行该函数。a
这意味着在函数中,只要整个事情的结果IO b
再次出现(对于某些任意类型b
),我们就可以本地访问该值。
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys -- :: IO [String]
...
我们有一个IO [String]
,我们需要做一些事情[String]
,所以我们使用>>=
:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= (\rs -> ...)
如果看>>=
's 的类型签名,这里 ( )a
起到的作用也是 is (因为整体需要 return )。[String]
rs :: [String]
b
[String]
hoo
IO [String]
那么我们在这...
部分做什么呢?我们需要对 进行递归调用hoo
,这又会产生一个IO [String]
值,因此我们>>=
再次使用:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= (\rs -> hoo yss >>= (\hs -> ...))
同样,hs :: [String]
最好...
有类型IO [String]
来使整个事情进行类型检查。
现在我们有了rs :: [String]
and hs :: [String]
,我们可以简单地连接它们:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= (\rs -> hoo yss >>= (\hs -> rs ++ hs)) -- !
这是一个类型错误。rs ++ hs :: [String]
, 但上下文需要IO [String]
. 幸运的是,有一个函数可以帮助我们:
return :: a -> IO a
现在它进行类型检查:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= (\rs -> hoo yss >>= (\hs -> return (rs ++ hs)))
由于 Haskell 语法的工作方式(函数体尽可能向右延伸),这里的大多数括号实际上是可选的:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= \rs -> hoo yss >>= \hs -> return (rs ++ hs)
并且通过一些重新格式化,可以使整个事情看起来很有启发性:
hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
goo xs ys >>= \rs ->
hoo yss >>= \hs ->
return (rs ++ hs)