在IO monad中进行递归

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在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中进行递归时(例如,“主”函数)我不能。我查找了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个后代。我的想法是将前两个父母列入清单,繁殖两个后代,取得列表中的下两个父母,培育另一对后代,依此类推。)

答案

如果您发现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 >>= (
s -> ...)

如果你看看>>=的类型签名,a的角色由[String]在这里(rs :: [String])和b也是[String](因为hoo整体需要返回IO [String])。

那么我们在...部分做什么呢?我们需要对hoo进行递归调用,这再次导致IO [String]值,所以我们再次使用>>=

hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
    goo xs ys >>= (
s -> hoo yss >>= (hs -> ...))

同样,hs :: [String]...更好地使用IO [String]类型来完成整个事情。

现在我们有rs :: [String]hs :: [String],我们可以简单地连接它们:

hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
    goo xs ys >>= (
s -> hoo yss >>= (hs -> rs ++ hs))  -- !

这是一个类型错误。 rs ++ hs :: [String],但背景需要IO [String]。幸运的是,有一个功能可以帮助我们:

return :: a -> IO a

现在它是typechecks:

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

由于Haskell语法的工作方式(函数体尽可能向右扩展),这里的大多数parens实际上是可选的:

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

通过一些重新格式化,整个事情可以看起来很有启发性:

hoo :: [String] -> IO [String]
hoo (xs : ys : yss) =
    goo xs ys >>= 
s ->
    hoo yss   >>= hs ->
    return (rs ++ hs)
另一答案

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表示法。

递归是一种信仰的飞跃。我们不关心事物是如何定义的 - 我们假设它被正确定义,所以我们可以通过它的名称来引用它。只要类型合适。

另一答案

要使用do表示法执行此操作,您需要绑定每个IO操作的结果,以便在纯表达式中使用这些结果,例如let rs = ... ++ ...,如下所示:

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

但是,通常您不希望为每个操作的结果引入临时名称,因此在典型的Haskell代码中,有一些组合器使这种事情更紧凑。在这里你可以使用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
  • ($)将纯函数应用于纯粹的参数
  • (<$>)将纯函数应用于动作的结果
  • (<*>)将一个动作产生的纯函数应用于另一个动作的结果
  • (=<<)(>>=)的翻转版本)应用一个函数将一个动作返回给一个动作的结果

以上是关于在IO monad中进行递归的主要内容,如果未能解决你的问题,请参考以下文章

用于 IO monad 的复杂 monad 转换器

进阶学习4:函数式编程FP——函子FunctorMayBe函子Either函子IO函子FolktalePointer函子Monad

为啥 Haskell 异常只能在 IO monad 中捕获?

函数式夜点心:IO Monad 与副作用处理

如何为continuation monad实现stack-safe chainRec操作符?

如何使用 Monad.Writer 进行跟踪?