有没有什么比unsafePerformIO更适合这个....?
到目前为止,我避免需要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?”)。 不过,如果有更好的方法,我很乐意听到它。
最新情况:
为了回应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
甚至需要关心一个子模块深层的库选择? )。
我同意Anupam Jain的观点,你最好是在IO级中读取这些数据文件的更高级别,然后纯粹通过其他程序将数据传递给它们。
例如,您可以将需要fileData
结果的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
我收集到你不想一次读完所有的数据,因为那样会很昂贵。 也许你并不真正知道你需要加载哪些文件,因此在开始时加载所有这些文件会很浪费。
这是一个解决方案的尝试。 它要求你在一个免费的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定义了一个functor。 它将提供一个值, 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
,然后才请求另一条线。
如果想要允许不同种类的“请求”,这个解决方案可以扩展为类似单点数据类型的东西,这样每个功能只需要知道它需要的精确效果。
如果满足于仅允许一种类型的请求,则还可以使用Pipes.Core中的Client
和Server
而不是免费的monad。
上一篇: Is there something better than unsafePerformIO for this....?
下一篇: is GHC's implementation of Haskell semantically broken?