我有一个文件,里面有一些数据。这些数据永远不会改变,我想让它在 IO monad 之外可用。我怎样才能做到这一点?
示例(请注意,这只是一个示例,我的数据不可计算):
素数.txt:
2 3 5 7 13
代码.hs:
primes :: [Int]
primes = map read . words . unsafePerformIO . readFile $ "primes.txt"
这是“合法”使用unsafePerformIO
吗?有替代品吗?
我有一个文件,里面有一些数据。这些数据永远不会改变,我想让它在 IO monad 之外可用。我怎样才能做到这一点?
示例(请注意,这只是一个示例,我的数据不可计算):
素数.txt:
2 3 5 7 13
代码.hs:
primes :: [Int]
primes = map read . words . unsafePerformIO . readFile $ "primes.txt"
这是“合法”使用unsafePerformIO
吗?有替代品吗?
您可以在编译时使用 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|]
零件所在的位置。
以unsafePerformIO
这种方式使用并不是很好。
声明primes :: [Int]
说这primes
是一个数字列表。一个特定的数字列表,它不依赖于任何东西。
然而,事实上,当定义恰好被评估时,它取决于文件“primes.txt”的状态。有人可以更改此文件以更改primes
似乎具有的值,根据其类型,这应该是不可能的。
如果存在一个假设的优化,它决定primes
应该按需重新计算而不是完全存储在内存中(毕竟,它的类型表明我们每次重新计算都会得到相同的东西),primes
甚至可能看起来有两个不同的在程序的单次运行期间的值。这是使用unsafePerformIO
欺骗编译器时可能出现的问题。
在实践中,以上所有可能都不太可能成为问题。
但是理论上正确的做法是不要做primes
一个全局常量(因为它不是一个常量)。相反,您将需要它的计算参数化(即primes
作为参数),并在外部IO
程序中读取文件,然后通过传递IO
程序从文件中提取的纯值来调用纯计算。你得到两全其美;您不必对编译器撒谎,也不必将整个程序放入IO
. 如果有帮助,您可以使用诸如 Reader monad 之类的结构来避免必须primes
在任何地方手动传递。
因此,unsafePerformIO
如果您想继续使用它,您可以使用它。这在理论上是错误的,但在实践中不太可能引起问题。
或者您可以重构您的程序以反映实际情况。
或者,如果primes
真的是一个全局常量,而您只是不想在程序源中包含大量数据,则可以使用 TemplateHaskell,如 dflemstr 所示。
是的,应该没问题。您可以添加一个{-# NOINLINE primes #-}
pragma 以确保安全——不确定 GHC 是否会内联 CAF。
我能想到的唯一选择是在编译时做同样的事情(使用 Template Haskell),基本上将素数嵌入到二进制文件中。然而,我更喜欢你的版本——注意这个primes
列表实际上会被懒惰地读取和创建!
您的程序没有准确定义何时加载此文件。如果文件不存在,这将引发异常,并且无法确切说明会发生在哪里。(即,可能在您的程序已经做了一些可观察到的现实世界的事情之后。)如果有人决定更改文件的内容,则适用类似的评论;你不知道它什么时候被读取,也不知道你会得到哪些内容。(如果文件不应该更改,则不太可能成为问题。)
至于替代方案:一种可能性是创建一个全局可变变量[这本身有点邪恶],并将文件的内容从主 I/O 线程插入到该变量中。这样,文件就会在明确定义的时刻被读入。[我注意到您也在使用惰性 I/O,因此您只需定义文件何时打开。]
真的,“正确”的事情是将数据手动线程化到每个需要它的函数。我能理解你为什么不想那样做;这是一种痛苦。您可能会使用某种状态单子来避免手动执行此操作...
这是基于 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
好处是