为了用一个简单的例子来说明这一点,假设我已经实现了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
感兴趣。然后,您可以通过使用IO
monad获得filterIO
的功能,也可以使用Identity
monad获得纯功能。当然,对于纯情况,您现在必须支付包装/展开(或coerce
包装)Identity
包装器的额外价格。(附带说明:由于Identity
是newtype
,这只是代码可读性成本,而不是运行时成本。)
ghci> data Color = Red | Green | Blue deriving (Read, Show, Eq)
这里是一个一元示例(请注意,仅包含Red
、Blue
和Blue
的行是用户在提示下输入的):
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个字符中的所有计算谓词一次,并将结果存储在查找表中。