【问题标题】:Doing recursion within an IO monad在 IO monad 中进行递归
【发布时间】:2018-06-26 04:40:18
【问题描述】:

我一直试图弄清楚如何在 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”函数)中进行递归时,我不能。我查过 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 个后代。我的想法是取前两个列表中的父母,繁殖两个后代,取出列表中接下来的两个父母,再繁殖一对后代,依此类推。)

【问题讨论】:

  • 在掌握(&gt;&gt;=)(&lt;$&gt;)liftA2 等组合符之前,请确保始终绑定 @987654339 的结果@ 行动。 rs &lt;- goo xs ys 是正确的,但你必须对 hoo' 做同样的事情。
  • 关于术语的注释:没有“IO monad”这样的东西。 IO(类型)单子。
  • 为了扩展 @melpomene,你所说的 IO monad,我称之为“IO 动作”(= 一个类型为 IO something 的值)。
  • 为胡言乱语道歉也许尝试一个简单的现实世界问题,例如读取文件并将其内容作为字符串列表返回,会更有效率。

标签: haskell recursion io-monad


【解决方案1】:

如果您发现 do 表示法令人困惑,我的建议是根本不要使用它。您可以使用&gt;&gt;= 做任何您需要的事情。假装它的类型是

(>>=) :: IO a -> (a -> IO b) -> IO b

也就是说,让我们看看你的代码。

do 块中的let 为某个值命名。这与它在 do 之外所做的事情相同,所以在这里没有帮助(它没有给你额外的权力)。

&lt;- 更有趣:它充当“从本地 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 符号和&lt;-,我们将按如下方式进行。

(>>=) :: IO a -> (a -> IO b) -> IO b

&gt;&gt;= 接受一个 IO 值和一个回调函数,并在“未包装”值 (a) 上运行该函数。这意味着在函数中,只要整个结果再次为IO b(对于某些任意类型b),我们就可以本地访问该值。

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

我们有一个IO [String],我们需要对[String]做一些事情,所以我们使用&gt;&gt;=

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

如果你看&gt;&gt;=的类型签名,a的作用在这里由[String]扮演 (rs :: [String]) 和b 也是[String](因为hoo 整体需要返回IO [String])。

那么我们在... 部分做了什么?我们需要对hoo 进行递归调用,这再次导致IO [String] 值,因此我们再次使用&gt;&gt;=

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)

【讨论】:

  • 我刚刚确认,在添加一行 mk20 [] = return ([]) 以处理递归过程中的空列表后,您对我的 hoo 函数的第一次重制是有效的。
【解决方案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 &lt;- foo,当foo :: IO a,我们有x :: a。而已。 (更多解释例如here)。

至于递归,它是通过do 符号实现的,就像在纯代码中实现的一样:通过命名事物,并从定义该名称的表达式内部引用相同的名称,无论是纯表达式还是do 符号。

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

【讨论】:

    【解决方案3】:

    要使用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 运算符&lt;$&gt;fmap 的别名)和Applicative 运算符&lt;*&gt;

    (<$>)
      :: 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
    

    也就是说,使用&lt;$&gt;(++) 映射到goo xs ys 的结果上是一个返回部分应用函数的操作,&lt;*&gt; 生成一个将该函数应用到hoo yss 的结果的操作。

    (有一条定律规定f &lt;$&gt; x 等价于pure f &lt;*&gt; x——也就是说,如果您有一个动作pure f 只返回一个函数f,则解开该动作并将其应用于结果x 使用 &lt;*&gt;,那么这与使用 &lt;$&gt; 将纯函数应用于动作相同。)

    另一个使用 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 参数
    • (&lt;$&gt;)pure 函数应用于 action 的结果
    • (&lt;*&gt;) 将一个函数resulting从一个动作应用到另一个动作的结果
    • (=&lt;&lt;)(&gt;&gt;=) 的翻转版本)将一个函数 returning 一个 action 应用于 action 的结果

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多