Генерация случайной строки во время компиляции или во время выполнения и использование ее в остальной части программы
Каков был бы лучший способ сделать это? unsafePerformIO
? Шаблон Haskell? Что-то другое? Я никогда не использовал ни одного из них, поэтому я не знаю многих деталей их использования.
Обратите внимание, что программа будет компилироваться при каждом запуске, поэтому не имеет значения, сгенерирую ли я строку во время компиляции или во время выполнения. Мне также нужно использовать эту строку в сотнях мест по всему коду, так что я не могу сделать это "правильным" способом, и это будет IO-действие, которое потребовало бы слишком большого количества другого кода для ввода в монаду IO,
6 ответов
С помощью unsafeperformIO
в данном конкретном случае все в порядке, так как в документации сказано:
Чтобы это было безопасно, вычисления IO не должны иметь побочных эффектов и не зависеть от окружающей среды.
Мы не беспокоимся о порядке newStdGen
,
import System.Random
import System.IO.Unsafe
randomStr :: String
randomStr = take 10 $ randomRs ('a','z') $ unsafePerformIO newStdGen
main = do
putStrLn randomStr
putStrLn randomStr
Я бы не рекомендовал использовать unsafePerformIO
, Я предполагаю, что в отчете Haskell не говорится, что постоянная функция запоминается, поэтому может случиться так, что
randStringUnsafe :: String
randStringUnsafe = unsafePerformIO $ liftM (take 10 . randomRs ('a','z')) newStdGen
даст разные результаты для разных звонков! С GHC это, скорее всего, запомнится, но без гарантий. Например, что если компилятор встроит функцию? (GHC, вероятно, достаточно умен, чтобы этого не делать, но опять же, никаких гарантий...). И например
randNumUnsafe :: (Random a, Num a) => [a]
randNumUnsafe = unsafePerformIO $ liftM (take 10 . randomRs (0, 9)) newStdGen
определенно даст вам разные результаты каждый раз, когда это называется.
Я предпочел бы пойти с шаблоном Haskell. Возможно, это немного сложнее, но безопасно. В одном модуле мы определяем
{-# LANGUAGE TemplateHaskell #-}
module RandomTH where
import Control.Monad
import System.Random
import Language.Haskell.TH
-- A standard function generating random strings.
randString :: IO String
randString = liftM (take 10 . randomRs ('a','z')) newStdGen
-- .. lifted to Q
randStringQ :: Q String
randStringQ = runIO randString
-- .. lifted to an Q Exp
randStringExp :: Q Exp
randStringExp = randStringQ >>= litE . stringL
-- | Declares a constant `String` function with a given name
-- that returns a random string generated on compile time.
randStringD :: String -> DecsQ
randStringD fname = liftM (: []) $
funD (mkName fname) [clause [] (normalB randStringExp) []]
(Может быть, randStringD
может быть написано более читабельным способом - если у вас есть идея, пожалуйста, отредактируйте ее или прокомментируйте.)
Затем в другом модуле мы можем использовать его для объявления постоянной функции с заданным именем:
{-# LANGUAGE TemplateHaskell #-}
$(randStringD "randStr")
main = do
putStrLn randStr
putStrLn randStr
Возможно, было бы легче ответить на этот вопрос, если бы мы знали больше об окружающем контексте, но подход, который я выбрал бы, заключался в передаче строки везде, где это было необходимо, и создании ее один раз в main
, Таким образом:
import Control.Monad
import System.Random
-- Some arbitrary functions
f :: String -> Int -> Int -> Int
f rstr x y = length rstr * x * y
-- This one doesn't depend on the random string
g :: Int -> Int
g x = x*x
h :: String -> String -> Int
h rstr str = sum . map fromEnum $ zipWith min rstr str
main :: IO ()
main = do
rstr <- randomString
putStr "The result is: "
print $ f rstr (g 17) (h rstr "other string")
randomString :: IO String
randomString = flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32)
Это, наверное, то, что я бы сделал.
С другой стороны, если у вас много этих функций, вам может показаться громоздким rstr
во всех них. Чтобы абстрагировать это, вы можете использовать Reader
монада; значения типа Reader r a
Или, в более общем смысле, значения типа MonadReader r m => m a
-могут ask
для значения типа r
, который передается за один раз, на верхнем уровне. Это даст вам:
{-# LANGUAGE FlexibleContexts #-}
import Control.Applicative
import Control.Monad.Reader
import System.Random
f :: MonadReader String m => Int -> Int -> m Int
f x y = do
rstr <- ask
return $ length rstr * x * y
g :: Int -> Int
g x = x*x
h :: MonadReader String m => String -> m Int
h str = do
rstr <- ask
return . sum . map fromEnum $ zipWith min rstr str
main :: IO ()
main = do
rstr <- randomString
putStr "The result is: "
print $ runReader (f (g 17) =<< h "other string") rstr
randomString :: IO String
randomString = flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32)
(На самом деле, так как (r ->)
это пример MonadReader r
вышеуказанные функции можно рассматривать как имеющие тип f :: Int -> Int -> String -> Int
и т. д., и вы можете пропустить вызов runReader
(и удалить FlexibleContexts
) - построенные вами монадические вычисления будут просто иметь тип String -> Int
, Но я бы не стал беспокоиться.)
Еще один подход, который, вероятно, является ненужным использованием языковых расширений (я, безусловно, предпочитаю два подхода выше), заключается в использовании неявного параметра, который представляет собой переменную, которая передается динамически и отражается в типе (вроде как MonadReader String m
ограничение). Это будет выглядеть так:
{-# LANGUAGE ImplicitParams #-}
import Control.Monad
import System.Random
f :: (?rstr :: String) => Int -> Int -> Int
f x y = length ?rstr * x * y
g :: Int -> Int
g x = x*x
h :: (?rstr :: String) => String -> Int
h str = sum . map fromEnum $ zipWith min ?rstr str
main :: IO ()
main = do
rstr <- randomString
let ?rstr = rstr
putStr "The result is: "
print $ f (g 17) (h "other string")
randomString :: IO String
randomString = flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32)
Сейчас. Я должен признать, что вы можете делать такие вещи на высшем уровне. Есть стандартный хак, который позволяет использовать unsafePerformIO
чтобы получить на высшем уровне IORef
s, например; а Template Haskell позволит вам выполнить действие ввода-вывода один раз, во время компиляции, и встроить результат. Но я бы избежал обоих этих подходов. Зачем? Что ж, по сути, есть некоторые споры о том, что "чистый" означает "точно определенный по синтаксису / не изменяется во время любого прогона программы" (интерпретация, которую я бы предпочел), или это означает "не меняется в течение этого прогона" программы ". В качестве одного из примеров проблем, которые это вызвало: Hashable
пакет, в какой-то момент, переключился с фиксированной соли на случайную соль. Это вызвало шум в Reddit и внесло ошибки в ранее работающий код. Пакет откатился назад и теперь позволяет пользователям подключаться к этому поведению через переменную среды, по умолчанию применяя чистоту между запусками.
Тем не менее, вот как использовать два подхода, которые вы упомянули, unsafePerformIO
и Template Haskell, чтобы получить случайные данные верхнего уровня - и почему бы, кроме вопросов о чистоте между прогонами, я не стал бы использовать эти методы. (Это единственные две техники для этого, которые я могу придумать.)
unsafePerformIO
хак, как его называют, очень хрупок; он полагается на то, что некоторые оптимизации не выполняются, и, как правило, не является популярным подходом. Это будет выглядеть так:import Control.Monad import System.Random import System.IO.Unsafe unsafeConstantRandomString :: String unsafeConstantRandomString = unsafePerformIO $ flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32) {-# NOINLINE unsafeConstantRandomString #-}
А если серьезно, посмотрим, сколько в слове
unsafe
используется в приведенном выше коде? Это потому, что с помощьюunsafePerformIO
будет кусать вас, если вы действительно не знаете, что делаете, и, возможно, даже тогда. Даже когдаunsafePerformIO
не кусает вас напрямую, равно как и авторы GHC сказали бы, что это, вероятно, не стоит использовать для этого (см. раздел "Преступление не платит"). Не делай этого.Использование шаблона Haskell для этого похоже на использование ядерной боеголовки для уничтожения комара. Уродливая ядерная боеголовка, чтобы загрузить. Такой подход будет выглядеть следующим образом:
{-# LANGUAGE TemplateHaskell #-} import Control.Monad import System.Random import Language.Haskell.TH thConstantRandomString :: String thConstantRandomString = $(fmap (LitE . StringL) . runIO $ flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32))
Также обратите внимание, что в версии Template Haskell вы не можете абстрагировать функциональность создания случайных строк в отдельное значение
randomString :: IO String
в том же модуле, или вы столкнетесь с ограничением стадии. Это безопасно, хотя, в отличие отunsafePerformIO
взломать; по крайней мере, безопасное по модулю беспокойство о чистоте между пробегами, упомянутое выше.
Генерация случайного числа в IO
не означает, что нижестоящие функции должны использовать IO
,
Вот пример чистой функции, которая зависит от значения типа A
:
f :: A -> B
... и вот IO
действие, которое генерирует A
:
io :: IO A
Я не должен изменять f
использовать IO
, Вместо этого я использую fmap
:
fmap f io :: IO B
Это именно та проблема, которую функторы должны решать: поднимать морфизмы над упакованными значениями, так что морфизмы не нужно модифицировать.
import System.Random
main = do
gen <- newStdGen
let str = take 10 $ randomRs ('a','z') gen
putStrLn str
putStrLn $ (reverse . (take 3)) str
В результате получается строка длиной десять символов, состоящая только из строчных букв. Этот код находится в монаде IO, но str является чистым, его можно передать чистым функциям. Вы не можете получить что-то случайное без IO Monad. Вы могли бы сделать unsafePerformIO, но я действительно не понимаю, почему. Вы можете передать значение str, если хотите всегда одно и то же. Если вы посмотрите на последнюю строку моего кода, вы увидите, что у меня есть чистая функция, которая работает со строкой, но так как я хочу увидеть ее, я вызываю putStrLn
который возвращает пустое действие ввода-вывода.
РЕДАКТИРОВАТЬ: Или это может быть место для чтения монады
Для строк, чисел и др.:
import System.Random ( newStdGen, randomRs, randomRIO )
main :: IO ()
main = do
s <- randomString 8 ""
putStrLn s
randomString :: Integer -> String -> IO String
randomString 0 str = return str
randomString size str = do
g <- newStdGen
t <- randomRIO ( 0, 2 )
let s = take 1 $ randomRs ( range t ) g
randomString ( size - 1 ) ( str ++ s )
where
range :: Integer -> ( Char, Char )
range i
| i == 0 = ('0', '9')
| i == 1 = ('A', 'Z')
| otherwise = ('a', 'z')