到目前为止,我已经避免需要unsafePerformIO
,但今天可能需要改变....我想看看社区是否同意,或者是否有人有更好的解决方案。
我有一个库需要使用存储在一堆文件中的一些配置数据。这些数据保证是静态的(在运行期间),但需要在无法编译Haskell程序的最终用户编辑的文件中(极少数情况下)。 (细节是不重要的,但是将“/etc/mime.types”看作是一个非常好的近似值。它是一个在很多程序中使用的大型静态数据文件)。
如果这不是一个库,我会使用IO monad ....但是因为它是一个在我的代码中被调用的库,它实际上迫使IO monad冒泡了几乎一切我用多个模块写的!虽然我需要一次性读取数据文件,但这个低级别的调用 是有效的纯粹,所以这是一个非常不可接受的结果。
仅供参考,我计划将调用包装在unsafeInterleaveIO中,以便只加载所需的文件。我的代码看起来像这样......
dataDir="<path to files>"
datafiles::[FilePath]
datafiles =
unsafePerformIO $
unsafeInterleaveIO $
map (dataDir </>)
<$> filter (not . ("." `isPrefixOf`))
<$> getDirectoryContents dataDir
fileData::[String]
fileData = unsafePerformIO $ unsafeInterleaveIO $ sequence $ readFile <$> datafiles
鉴于读取的数据是引用透明的,我非常确定unsafePerformIO是安全的(这已在很多地方讨论过,例如“Use of unsafePerformIO appropriate?”)。不过,如果有更好的方法,我很乐意听到它。
UPDATE -
回应Anupam的评论......
有两个原因导致我无法将lib分解为IO和非IO部分。
首先,数据量很大,我不想一次将其全部读入内存。请记住,IO总是严格阅读....这就是我需要进行unsafeInterleaveIO
调用以使其变得懒惰的原因。恕我直言,一旦你使用unsafeInterleaveIO
,你也可以使用unsafePerformIO
,因为风险已经存在。
其次,打破IO特定部分只是替换了IO monad的冒泡和IO读取代码的冒泡,以及数据的传递(我可能实际上选择使用传递数据)无论如何,状态monad,所以将IO monad替换为状态monad并不是一个改进。)如果低级函数本身不是纯粹的,那就不会那么糟糕了(想想我上面的/etc/mime.types示例,想象一下Haskell extensionToMimeType
函数,它基本上是纯粹的,但是需要从文件中获取数据库数据....突然,堆栈中从低到高的所有内容都需要调用或通过readMimeData::IO String
。为什么每个main
甚至需要关心库选择多层次的子模块?)。
答案 0 :(得分:5)
我同意Anupam Jain的意见,你最好在IO中以更高的级别读取这些数据文件,然后纯粹将其中的数据传递给你的其余程序。
例如,您可以将需要fileData
结果的函数放入Reader [String]
,这样他们就可以根据需要询问结果(或者Reader Config
,其中Config
type AppResult = String
fileData :: IO [String]
fileData = undefined -- read the files
myApp :: String -> Reader [String] AppResult
myApp s = do
files <- ask
return undefined -- do whatever with s and config
main = do
config <- fileData
return $ runReader (myApp "test") config
拥有这些字符串以及您需要的任何其他内容。
我所建议的草图如下:
{{1}}
答案 1 :(得分:1)
我认为你不想一次读取所有数据,因为这样做会很昂贵。也许你并不真正知道你需要加载哪些文件,所以在开始时加载所有文件都会很浪费。
这是尝试解决方案。它要求你在一个免费的monad中工作,并将副作用操作转移给一个翻译。一些初步进口:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString as B
import Data.Monoid
import Data.List
import Data.Functor.Compose
import Control.Applicative
import Control.Monad
import Control.Monad.Free
import System.IO
我们为免费monad定义了一个仿函数。它将提供值p
做解释器并在收到值b
后继续计算:
type LazyLoad p b = Compose ((,) p) ((->) b)
请求加载文件的便利功能:
lazyLoad :: FilePath -> Free (LazyLoad FilePath B.ByteString) B.ByteString
lazyLoad path = liftF $ Compose (path,id)
虚拟解释器功能,从stdin
读取“文件内容”:
interpret :: Free (LazyLoad FilePath B.ByteString) a -> IO a
interpret = iterM $ \(Compose (path,next)) -> do
putStrLn $ "Enter the contents for file " <> path <> ":"
B.hGetLine stdin >>= next
一些愚蠢的示例函数:
someComp :: B.ByteString -> B.ByteString
someComp b = "[" <> b <> "]"
takesAwhile :: Int
takesAwhile = foldl' (+) 0 $ take 400000000 $ intersperse (negate 1) $ repeat 1
示例程序:
main :: IO ()
main = do
r <- interpret $ do
r1 <- someComp <$> lazyLoad "file1"
r2 <- return takesAwhile
if (r2 == 1)
then return r1
else someComp <$> lazyLoad "file2"
putStrLn . show $ r
执行时,此程序将请求一行,花一些时间计算takesAwhile
,然后再请求另一行。
如果想要允许不同类型的“请求”,可以使用类似Data types à la carte的内容扩展此解决方案,以便每个函数只需要知道它所需的精确效果。
如果您满足于仅允许一种类型的请求,您还可以使用Pipes.Core中的Client
和Server
来代替免费的monad。