Загрузить чистую глобальную переменную из файла
У меня есть файл с некоторыми данными. Эти данные никогда не меняются, и я хочу сделать их доступными вне монады IO. Как я могу это сделать?
Пример (обратите внимание, что это всего лишь пример, мои данные не являются вычислимыми):
primes.txt:
2 3 5 7 13
code.hs:
primes :: [Int]
primes = map read . words . unsafePerformIO . readFile $ "primes.txt"
Является ли это "законным" использованием unsafePerformIO
? Существуют ли альтернативы?
Ответы
Ответ 1
Вы можете использовать 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|]
.
Ответ 2
Использование unsafePerformIO
таким образом не является большим.
В декларации primes :: [Int]
говорится, что primes
- это список чисел. Один конкретный список чисел, который ничем не зависит.
Фактически, однако, это зависит от состояния файла "primes.txt", когда определение оценивается. Кто-то может изменить этот файл, чтобы изменить значение, которое имеет primes
, что не должно быть возможным в соответствии с его типом.
При наличии гипотетической оптимизации, которая решает, что primes
следует пересчитать по требованию, а не хранить в памяти полностью (в конце концов, ее тип говорит, что мы получим то же самое каждый раз, когда мы его пересчитаем), primes
может даже показаться, что он имеет два разных значения в течение одного прогона программы. Это проблема, которая может возникнуть при использовании unsafePerformIO
для компиляции.
На практике все вышеперечисленное, вероятно, вряд ли будет проблемой.
Но теоретически правильная задача - не сделать primes
глобальной константой (потому что она не является константой). Вместо этого вы делаете требуемое для него вычисление (т.е. Принимаете primes
в качестве аргумента), а во внешней IO
программе вы читаете файл, а затем вызываете чистое вычисление, передавая чистое значение IO
программа, извлеченная из файла. Вы получаете лучшее из обоих миров; вам не нужно лгать компилятору, и вам не нужно вставлять всю вашу программу в IO
. Вы можете использовать конструкции, такие как монада Reader, чтобы избежать необходимости вручную передавать primes
повсюду, если это помогает.
Итак, вы можете использовать unsafePerformIO
, если хотите просто продолжить с ним. Это теоретически неправильно, но вряд ли вызовет проблемы на практике.
Или вы можете реорганизовать свою программу, чтобы отразить, что происходит на самом деле.
Или, если primes
действительно является глобальной константой, и вы просто не хотите буквально включать огромный фрагмент данных в свой источник программы, вы можете использовать TemplateHaskell, как показано dflemstr.
Ответ 3
Да, все должно быть хорошо. Вы можете добавить прагму {-# NOINLINE primes #-}
, чтобы быть в безопасности - не уверен, что GHC когда-либо будет встроить CAF.
Единственная альтернатива, о которой я могу думать, - это сделать то же самое во время компиляции (используя Template Haskell), по сути вставляя простые числа в двоичный файл. Тем не менее, я предпочитаю вашу версию - обратите внимание, что список primes
будет фактически прочитан и создан лениво!
Ответ 4
Ваша программа точно не определяет, когда этот файл загружается. Если файл не существует, это вызовет исключение, и не будет точно указано, где это произойдет. (То есть, возможно, после того, как ваша программа уже сделала некоторые наблюдаемые вещи в реальном мире.) Аналогичные замечания применяются, если кто-то решает изменить содержимое файла; вы точно не знаете, когда читаете, и какое содержимое вы получите. (Вряд ли будет проблемой, если файл не должен изменяться.)
Что касается альтернатив: одна из возможностей - создать глобальную изменяемую переменную [которая сама по себе немного зла] и вставить содержимое файла в эту переменную из основного потока ввода-вывода. Таким образом, файл читается в четко определенный момент. [Я замечаю, что вы используете ленивый ввод-вывод, поэтому вы должны определять только, когда файл открывается.]
Действительно, "правильная" вещь заключается в том, чтобы вручную вносить данные в каждую нужную ей функцию. Я могу понять, почему вы можете этого не делать; это боль. Возможно, вы использовали бы какую-то государственную монаду, чтобы избежать этого вручную, хотя...
Ответ 5
Это основано на ответе 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 во время компиляции
- Нет конверсий во время выполнения, чтобы замедлить или выбросить исключения.
- Потенциальные улучшения размера кода, так как вам не нужно хранить весь файл raw.