在IO monad中进行递归

时间:2018-06-26 04:40:19

标签: haskell recursion io-monad

我一直在尝试找出如何在IO monad中进行递归。 我熟悉使用纯函数进行递归,但是无法将这些知识传递给IO monad。

具有纯函数的递归
我对使用纯函数(例如下面的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”功能)时,我无法执行。我查找了liftMreplicateM和undo-the-IO <-运算符或函数。我想要一个hoohoo'之类的IO monad(对随后出现的乱码表示歉意。)

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个后代。我必须接受列表中的前两个父母,繁殖两个后代,列表中的后两个父母,再繁殖一对后代,依此类推。)

3 个答案:

答案 0 :(得分:5)

如果您发现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值和一个回调函数,并在“ unwrapped”值(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 -> ...)

如果您查看>>=的类型签名,a在这里扮演[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]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)

答案 1 :(得分:2)

do表示法非常方便。使用它,是您的朋友。我们只需要遵循它的规则,到位的每一件事都必须相应地具有正确的类型。

您非常亲密:

goo :: String -> String -> IO [String]

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

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

do中,用x <- foo表示,当foo :: IO a时,我们有x :: a。而已。 (更多说明,例如here)。

对于递归,它是用do表示法实现的,就像在纯代码中实现的那样:通过命名事物,并在定义该名称的表达式内部引用相同的名称,无论它是否是纯表达式或do表示法。

递归是信仰的飞跃。我们不在乎如何定义事物-我们假设它定义正确,因此我们可以通过其名称来引用它。只要类型合适。

答案 2 :(得分:1)

要使用do表示法执行此操作,您需要 bind 每个IO操作的结果,以便在诸如{{1} }…let rs =…,就像这样:

++

但是,通常您不想为每个操作的结果引入一个临时名称,因此在典型的Haskell代码中,有一些组合器使这种事情变得更紧凑。在这里您可以使用hoo :: [String] -> IO [String] hoo (xs:ys:yss) = do g <- goo xs ys h <- hoo yss let rs = g ++ h return rs

liftA2

赞:

liftA2
  :: Applicative f

  -- Given a pure function to combine an ‘a’ and a ‘b’ into a ‘c’…
  => (a -> b -> c)

  -- An action that produces an ‘a’…
  -> f a

  -- And an action that produces a ‘b’…
  -> f b

  -- Make an action that produces a ‘c’.
  -> f c

hoo (xs:ys:yss) = liftA2 (++) (goo xs ys) (hoo yss) 仅适用于两个参数的函数;要应用其他数量的参数的函数,可以使用liftA2运算符Functor(别名<$>)和fmap运算符Applicative

<*>

这些可以像这样组合:

(<$>)
  :: Functor f

  -- Given a pure function to transform an ‘a’ into a ‘b’…
  => (a -> b)

  -- And an action that produces an ‘a’…
  -> f a

  -- Make an action that produces a ‘b’.
  -> f b

(<*>)
  :: Applicative f

  -- Given an action that produces a function from ‘a’ to ‘b’…
  => f (a -> b)

  -- And an action that produces an ‘a’…
  -> f a

  -- Make an action that produces a ‘b’.
  -> f b

也就是说,使用(++) <$> goo xs ys :: IO ([String] -> [String]) -- f (a -> b) hoo yss :: IO [String] -- f a hoo (xs:ys:yss) = (++) <$> goo xs ys <*> hoo yss :: IO [String] -- f b (++)映射到goo xs ys的结果上是返回部分应用函数的动作,而<$>则产生一个应用此函数的动作到<*>的结果。

(有一条法律规定hoo yss等同于f <$> x,也就是说,如果您有一个仅返回函数pure f <*> x的动作pure f,则请取消包装将该动作并使用f将其应用于x的结果,这与仅将纯函数应用于<*>的动作相同。)

将其与3个参数一起使用的另一个示例:

<$>

您可以将所有这些组合器视为不同类型的应用程序运算符,例如cat3 a b c = a ++ b ++ c main = do -- Concatenate 3 lines of input result <- cat3 <$> getLine <*> getLine <*> getLine putStrLn result

($)
  • ($) :: (a -> b) -> a -> b (<$>) :: (a -> b) -> f a -> f b (<*>) :: f (a -> b) -> f a -> f b (=<<) :: (a -> f b) -> f a -> f b pure 函数应用于 pure 参数
  • ($) pure 函数应用于 action
  • 的结果
  • (<$>)将一个动作的函数产生的应用于另一个动作
  • 的结果
  • (<*>)(=<<)的翻转版本)将函数返回 action 应用于 action