13

我有一个文件,里面有一些数据。这些数据永远不会改变,我想让它在 IO monad 之外可用。我怎样才能做到这一点?

示例(请注意,这只是一个示例,我的数据不可计算):

素数.txt:

2 3 5 7 13

代码.hs:

primes :: [Int]
primes = map read . words . unsafePerformIO . readFile $ "primes.txt"

这是“合法”使用unsafePerformIO吗?有替代品吗?

4

5 回答 5

21

您可以在编译时使用 TemplateHaskell 读取文件。然后,文件的数据将作为实际字符串存储在程序中。

在一个模块中(Text/Literal/TH.hs在本例中),定义如下:

module Text.Literal.TH where

import Language.Haskell.TH
import Language.Haskell.TH.Quote

literally :: String -> Q Exp
literally = return . LitE . StringL

lit :: QuasiQuoter
lit = QuasiQuoter { quoteExp = literally }

litFile :: QuasiQuoter
litFile = quoteFile lit

在您的模块中,您可以执行以下操作:

{-# LANGUAGE QuasiQuotes #-}
module MyModule where

import Text.Literal.TH (litFile)

primes :: [Int]
primes = map read . words $ [litFile|primes.txt|]

当您编译程序时,GHC 将打开primes.txt文件并将其内容插入到[litFile|primes.txt|]零件所在的位置。

于 2012-10-03T21:26:25.917 回答
6

unsafePerformIO这种方式使用并不是很好。

声明primes :: [Int]说这primes是一个数字列表。一个特定的数字列表,它不依赖于任何东西。

然而,事实上,当定义恰好被评估时,它取决于文件“primes.txt”的状态。有人可以更改此文件以更改primes似乎具有的值,根据其类型,这应该是不可能的。

如果存在一个假设的优化,它决定primes应该按需重新计算而不是完全存储在内存中(毕竟,它的类型表明我们每次重新计算都会得到相同的东西),primes甚至可能看起来有两个不同的在程序的单次运行期间的值。这是使用unsafePerformIO欺骗编译器时可能出现的问题。

在实践中,以上所有可能都不太可能成为问题。

但是理论上正确的做法是不要做primes一个全局常量(因为它不是一个常量)。相反,您将需要它的计算参数化(即primes作为参数),并在外部IO程序中读取文件,然后通过传递IO程序从文件中提取的纯值来调用纯计算。你得到两全其美;您不必对编译器撒谎,也不必将整个程序放入IO. 如果有帮助,您可以使用诸如 Reader monad 之类的结构来避免必须primes在任何地方手动传递。

因此,unsafePerformIO如果您想继续使用它,您可以使用它。这在理论上是错误的,但在实践中不太可能引起问题。

或者您可以重构您的程序以反映实际情况。

或者,如果primes真的是一个全局常量,而您只是不想在程序源中包含大量数据,则可以使用 TemplateHaskell,如 dflemstr 所示。

于 2012-10-04T06:25:13.817 回答
4

是的,应该没问题。您可以添加一个{-# NOINLINE primes #-}pragma 以确保安全——不确定 GHC 是否会内联 CAF。

我能想到的唯一选择是在编译时做同样的事情(使用 Template Haskell),基本上将素数嵌入到二进制文件中。然而,我更喜欢你的版本——注意这个primes列表实际上会被懒惰地读取和创建!

于 2012-10-03T20:28:19.807 回答
4

您的程序没有准确定义何时加载此文件。如果文件不存在,这将引发异常,并且无法确切说明会发生在哪里。(即,可能在您的程序已经做了一些可观察到的现实世界的事情之后。)如果有人决定更改文件的内容,则适用类似的评论;你不知道它什么时候被读取,也不知道你会得到哪些内容。(如果文件不应该更改,则不太可能成为问题。)

至于替代方案:一种可能性是创建一个全局可变变量[这本身有点邪恶],并将文件的内容从主 I/O 线程插入到该变量中。这样,文件就会在明确定义的时刻被读入。[我注意到您也在使用惰性 I/O,因此您只需定义文件何时打开。]

真的,“正确”的事情是将数据手动线程化到每个需要它的函数。我能理解你为什么不想那样做;这一种痛苦。您可能会使用某种状态单子来避免手动执行此操作...

于 2012-10-03T21:15:00.643 回答
2

这是基于 dflemstr 的回答。鉴于您想要加载整数列表,您可能希望read在编译时也执行。我只是把它写出来,因为看到这个例子对我很有用,我希望它对其他人有帮助。

import Language.Haskell.TH
import Language.Haskell.TH.Quote

intArray' :: String -> Q Exp
intArray' s = return $ ListE e
    where
        e = map (LitE . IntegerL . read) $ words s

intArray :: QuasiQuoter
intArray = QuasiQuoter { quoteExp = intArray' }


intArrayFile :: QuasiQuoter
intArrayFile = quoteFile intArray

要使用它...

{-# LANGUAGE QuasiQuotes #-}
import TT

primes :: [Int]
primes = [intArrayFile|primes.txt|]

main = print primes

好处是

  • 在编译时检查您的 primes.txt 文件的语法
  • 运行时没有任何转换会减慢您的速度或引发异常。
  • 由于您不需要将整个文件原始存储,因此可能会改进代码大小。
于 2014-04-14T06:11:33.353 回答