Подтвердить что ты не робот

Загрузить чистую глобальную переменную из файла

У меня есть файл с некоторыми данными. Эти данные никогда не меняются, и я хочу сделать их доступными вне монады IO. Как я могу это сделать?

Пример (обратите внимание, что это всего лишь пример, мои данные не являются вычислимыми):

primes.txt:

2 3 5 7 13

code.hs:

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

Является ли это "законным" использованием unsafePerformIO? Существуют ли альтернативы?

4b9b3361

Ответ 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.