从文件加载纯全局变量

时间:2012-10-03 20:19:07

标签: haskell global-variables

我有一个包含一些数据的文件。这些数据永远不会改变,我想让它在IO monad之外可用。我怎么能这样做?

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

primes.txt:

  

2 3 5 7 13

code.hs:

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

这是unsafePerformIO的“合法”使用吗?还有其他选择吗?

5 个答案:

答案 0 :(得分:20)

您可以使用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|]部分。

答案 1 :(得分:6)

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

声明primes :: [Int]表示primes是一个数字列表。一个特定的数字列表,不依赖于任何内容。

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

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

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

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

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

或者你可以重构你的程序以反映真实情况。

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

答案 2 :(得分:4)

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

我能想到的唯一选择是在编译期间(使用Template Haskell)执行相同的操作,实质上是将素数嵌入到二进制文件中。但是,我更喜欢您的版本 - 请注意primes列表实际上会被阅读&懒惰地创造了!

答案 3 :(得分:4)

当加载此文件时,您的程序没有准确定义。如果该文件不存在,则会抛出异常,并且无法确切知道将在何处发生。 (即,可能在您的程序已经完成了一些可观察的实际内容之后。)如果有人决定更改文件的内容,则类似的评论也适用;你不知道它何时被读取,以及你将获得哪些内容。 (如果文件不应该改变,则不太可能成为问题。)

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

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

答案 4 :(得分: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文件的语法
  • 在运行时没有转换会降低您的速度或抛出异常。
  • 潜在的代码大小改进,因为您不需要将整个文件存储为原始文件。