我一直在尝试找出如何在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”功能)时,我无法执行。我查找了liftM
,replicateM
和undo-the-IO <-
运算符或函数。我想要一个hoo
或hoo'
之类的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个后代。我必须接受列表中的前两个父母,繁殖两个后代,列表中的后两个父母,再繁殖一对后代,依此类推。)
答案 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