我有一个包含一些数据的文件。这些数据永远不会改变,我想让它在IO monad之外可用。我怎么能这样做?
示例(请注意,这只是一个示例,我的数据不可计算):
primes.txt:
2 3 5 7 13
code.hs:
primes :: [Int]
primes = map read . words . unsafePerformIO . readFile $ "primes.txt"
这是unsafePerformIO
的“合法”使用吗?还有其他选择吗?
答案 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
好处是