为了用一个简单的例子说明这一点,请说我已经实现了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版本不会完全复制核心逻辑吗?
答案 0 :(得分:5)
如何在不编写单独的实现的情况下使其与过滤器一起使用
这是不可能的,事实上这种事情是不可能的是设计--Haskell对其类型设置了严格的限制,你必须遵守它们。你不能无所事事地洒遍IO
。
现在的问题是,我可以构建我的代码,以便纯版本和IO版本不会完全复制核心逻辑吗?
您会对filterM
感兴趣。然后,您可以使用filterIO
monad获得IO
的功能,使用Identity
monad获得纯功能。当然,对于纯粹的情况,您现在必须支付包装/展开(或coerce
} Identity
包装器的额外费用。 (旁注:因为Identity
是newtype
,这只是代码可读性成本,而不是运行时成本。)
ghci> data Color = Red | Green | Blue deriving (Read, Show, Eq)
以下是一个monadic示例(请注意,只有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]
答案 1 :(得分:1)
如前所述,您可以使用filterM
执行此特定任务。但是,通常最好保持Haskell特有的严格分离IO和计算。在您的情况下,您可以一次性勾选所有必要的IO
,然后使用漂亮,可靠,易于测试的纯代码进行有趣的过滤(即,这里只需使用正常的filter
):< / p>
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
虽然这项特定任务不可行,但我还要补充一点, 原则上可以通过类似IO
的内容实现p'
分隔。这要求类型A
“足够小”,您可以使用所有值来评估谓词。例如,
import qualified Data.Map as Map
type A = Char
p' :: IO (A -> Bool)
p' = (Map.!) . Map.fromList <$> mapM (\c -> (c,) <$> p c) ['\0'..]
这会对1114112个字符的所有评估一次谓词,并将结果存储在查找表中。