对于这个,有什么比unsafePerformIO更好的东西吗?

时间:2014-03-03 17:56:35

标签: haskell

到目前为止,我已经避免需要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甚至需要关心库选择多层次的子模块?)。

2 个答案:

答案 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中的ClientServer来代替免费的monad。