Haskell 从文件中加载纯全局变量

Haskell 从文件中加载纯全局变量,haskell,global-variables,Haskell,Global Variables,我有一个文件,里面有一些数据。这个数据永远不会改变,我想让它在IO monad之外可用。我该怎么做 示例(请注意,这只是一个示例,我的数据不可计算): primes.txt: 2 3 5 7 13 代码.hs: primes :: [Int] primes = map read . words . unsafePerformIO . readFile $ "primes.txt" 这是对unsafePerformIO的“合法”使用吗?有其他选择吗?是的,应该可以。您可以添加一个{-#NOINL

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

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

primes.txt:

2 3 5 7 13

代码.hs:

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

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

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


我能想到的唯一替代方法是在编译时做同样的事情(使用模板Haskell),本质上是将素数嵌入二进制文件中。但是,我更喜欢您的版本-注意,
primes
列表实际上是惰性地读取和创建的

您的程序没有准确定义加载此文件的时间。如果该文件不存在,这将抛出一个异常,并且不知道会在哪里发生。(即,可能在您的程序已经执行了一些可观察到的真实世界内容之后)如果有人决定更改文件的内容,也可以使用类似的注释;你不知道它什么时候被阅读,也不知道你会得到什么内容。(如果文件不应更改,则不太可能出现问题。)

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


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

您可以使用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
作为参数),然后在外部
IO
程序中读取文件,然后通过将纯值传递给从文件中提取的
IO
程序来调用纯计算。你可以两全其美;您不必对编译器撒谎,也不必将整个程序放入
IO
。如果有帮助的话,您可以使用诸如Reader monad之类的构造来避免在任何地方手动传递
素数

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

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


或者,如果
primes
确实是一个全局常量,并且您不想在程序源代码中包含大量数据,那么您可以使用TemplateHaskell,如dflemstr所示。

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

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文件的语法
  • 在运行时没有转换来降低您的速度或引发异常
  • 由于不需要原始存储整个文件,因此可能会提高代码大小

从“对付尴尬的队伍”的论文中,这似乎是
不安全的原因。你可以在以下位置找到这篇论文:如果所讨论的数据永远不会改变,那么我建议这是最好的答案。@MathematicalArchid,如果所讨论的数据可能会改变,然后,您应该使用
readFile
并将结果绑定到
IO
monad中,以反映这不是一个引用透明操作的事实。另一种可能是打开预处理器并执行
#包括“primes.txt”
(我实际上没有尝试过)。如果我将文件名存储在一个变量中呢?例如,我不能做
[litFile | jsonFilename |]
。在这种情况下,你有什么建议?