首页 > 解决方案 > 在 IO monad 中进行递归

问题描述

我一直试图弄清楚如何在 IO monad 中进行递归。我熟悉使用纯函数进行递归,但无法将这些知识转移到 IO monads。

使用纯函数递归
我很乐意使用纯函数进行递归,例如foo下面的函数。

foo (x:y:ys) = foo' x y ++ foo ys

具有 IO [String] 输出
的函数我创建了一个如下goo所示的函数,它可以满足我的需要并具有 IO 输出。

goo :: String -> String -> IO [String]
goo xs ys = goo' xs ys 

尝试在 IO monad
中进行递归 当我尝试在 IO monad 中进行递归时(例如,“main”函数),我做不到。我查找了liftM,replicateM和 undo-the-IO<-运算符或函数。我想要一个类似hooor的 IO monad hoo'(为接下来的胡言乱语道歉)。

hoo :: [String] -> IO [String]
hoo (xs:ys:yss) = do
                  let rs = goo xs ys ++ hoo yss
                  return rs

或者

hoo' :: [String] -> IO [String]
hoo' (xs:ys:yss) = do
                  rs <- goo xs ys 
                  let gs = rs ++ hoo' yss
                  return gs

(顺便说一句,如果你想知道我的项目是什么,我正在为一门课程从头开始编写遗传算法程序。我的函数需要两个父母并繁殖两个后代,因为使用随机数生成器goo,它们作为 IO 返回。goo我需要做的是使用递归hoo函数goo从 20 个父母的列表中繁殖 20 个后代。我的想法是取列表中的前两个父母,繁殖两个后代,带上接下来的两个父母名单,繁殖另一对后代,依此类推。)

标签: haskellrecursionio-monad

解决方案


如果您发现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]hooIO [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)

推荐阅读