如何在IO代码中与纯算法交互



为了用一个简单的例子来说明这一点,假设我已经实现了filter:

filter :: (a -> Bool) -> [a] -> [a]

我有一个谓词p,它与现实世界相互作用:

p :: a -> IO Bool

它如何在不编写单独实现的情况下使其与filter一起工作:

filterIO :: (a -> IO Bool) -> [a] -> IO [a]

假设我能把p变成p':

p': IO (a -> Bool)

然后我可以做

main :: IO ()
main = do 
p'' <- p'
print $ filter p'' [1..100]

但我一直没能找到转换。

编辑:正如人们在评论中指出的那样,这样的转换没有意义,因为它会破坏IO Monad的封装。

现在的问题是,我是否可以构建我的代码,使纯版本和IO版本不会完全复制核心逻辑?

如何在不编写单独实现的情况下使用过滤器

这是不可能的,而且这种事情是不可能发生的,这是故意的——Haskell对它的类型有严格的限制,你必须遵守它们。你不能随意地把IO撒得到处都是。

现在的问题是,我是否可以构建我的代码,使纯版本和IO版本不会完全复制核心逻辑?

您将对filterM感兴趣。然后,您可以通过使用IOmonad获得filterIO的功能,也可以使用Identitymonad获得纯功能。当然,对于纯情况,您现在必须支付包装/展开(或coerce包装)Identity包装器的额外价格。(附带说明:由于Identitynewtype,这只是代码可读性成本,而不是运行时成本。)

ghci> data Color = Red | Green | Blue deriving (Read, Show, Eq)

这里是一个一元示例(请注意,仅包含RedBlueBlue的行是用户在提示下输入的):

ghci> filterM (x -> do y<-readLn; pure (x==y)) [Red,Green,Blue]
Red
Blue
Blue
[Red,Blue] :: IO [Color]

这里有一个纯粹的例子:

ghci> filterM (x -> Identity (x /= Green)) [Red,Green,Blue]
Identity [Red,Blue] :: Identity [Color]

如前所述,您可以将filterM用于此特定任务。然而,通常最好遵循Haskell的特点,严格分离IO和计算。在您的情况下,您可以一次性勾选所有必要的IO,然后用漂亮、可靠、易于测试的纯代码进行有趣的过滤(即,在这里,只需使用普通的filter):

type A = Int
type Annotated = (A, Bool)
p' :: Annotated -> Bool
p' = snd
main :: IO ()
main = do 
candidates <- forM [1..100] $ n -> do
permitted <- p n
return (n, permitted)
print $ fst <$> filter p' candidates

在这里,我们首先用一个标志来注释每个数字,该标志指示环境所说的内容。这个标志可以在实际的过滤步骤中简单地读出,而不需要任何进一步的IO

简而言之,这将被写为:

main :: IO ()
main = do 
candidates <- forM [1..100] $ n -> (n,) <$> p n
print $ fst <$> filter snd candidates

虽然这对于这个特定的任务是不可行的,但我还要补充一点,原则上,您可以用类似p'的东西来实现IO分离。这要求类型A"足够小",以便您可以使用所有可能的值来评估谓词。例如,

import qualified Data.Map as Map
type A = Char
p' :: IO (A -> Bool)
p' = (Map.!) . Map.fromList <$> mapM (c -> (c,) <$> p c) [''..]

这将为1114112个字符中的所有计算谓词一次,并将结果存储在查找表中。

最新更新