在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中进行递归时(例如,“主”函数)我不能。我查找了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个后代。我的想法是将前两个父母列入清单,繁殖两个后代,取得列表中的下两个父母,培育另一对后代,依此类推。)
如果您发现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中进行递归的主要内容,如果未能解决你的问题,请参考以下文章
进阶学习4:函数式编程FP——函子FunctorMayBe函子Either函子IO函子FolktalePointer函子Monad
为啥 Haskell 异常只能在 IO monad 中捕获?