Генерируйте случайную строку во время компиляции или выполнения и используйте ее в остальной части программы


Как лучше всего это сделать? unsafePerformIO? Шаблон Хаскелл? Что-то еще? Я никогда не использовал ни один из них, поэтому я не знаю многих деталей их использования.

Обратите внимание, что программа будет компилироваться каждый раз, когда она запускается, поэтому не имеет значения, генерирую ли я строку во время компиляции или во время выполнения. Мне также нужно использовать эту строку в тоннах мест по всему коду, поэтому я не могу сделать это "правильным" способом и сделать это действием ввода-вывода, что потребует слишком много других действий. код, который будет введен в монаду ИО.

5 8

5 ответов:

Использование 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. Я предполагаю, что отчет Хаскелла не утверждает, что постоянная функция запоминается, поэтому может случиться, что

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

Определенно даст вам разные результаты каждый время называется.


Я бы предпочел пойти с шаблоном Хаскелла. Это, возможно, немного сложнее, но безопасно. В одном модуле мы определяем

{-# 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 для получения верхнего уровня IORefs, например экземпляр; и шаблон Haskell позволит вам выполнить действие ввода-вывода один раз, во время компиляции, и внедрить результат. Но я бы избегал обоих этих подходов. Почему? Ну, в принципе, есть некоторые дебаты о том, означает ли "чистый" "определяется точно синтаксисом/не изменяется при любом запуске программы" (интерпретация, которую я бы предпочел), или это означает "не изменяется при этом запуске программы."В качестве одного из примеров проблем, которые это вызвало: пакет Hashable , в одном точка, переключенная с фиксированной соли на случайную соль. Это вызвалошум на Reddit и внесло ошибки в ранее работавший код. Пакет отклонился назад, и теперь позволяет пользователям выбирать это поведение через переменную окружения, по умолчанию устанавливая чистоту между запусками.

Тем не менее, вот как использовать два подхода, которые вы упомянули, unsafePerformIO и шаблон Haskell, чтобы получить случайные данные верхнего уровня-вместе с why, отдельно от проблем, связанных с в перерывах между пробежками я бы не стал использовать эти техники. (Это единственные два метода для этого, которые я могу придумать.)
  1. Хак 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 сказали бы, что , вероятно, не стоит использовать для этого (см. раздел "преступление не платит"). Не делай этого.

  2. Использование шаблона Хаскелла для этого похоже на использование ядерной боеголовки, чтобы убить комара. Уродливая ядерная боеголовка в придачу. Такой подход будет выглядеть следующим образом: следующее:
    {-# 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))
    
    Обратите внимание также, что в версии шаблона 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 чист, он может быть передан чистым функциям. Вы не можете получить что-то случайное без монады ИО. Вы могли бы сделать unsafePerformIO, но я действительно не понимаю, почему. Вы можете передать значение str, если всегда хотите получить одно и то же. Если вы посмотрите на последнюю строку моего кода, Вы увидите, что у меня есть чистая функция, которая работает со строкой, но поскольку я хочу ее видеть, я вызываю putStrLn который возвращает пустое действие ввода-вывода.

EDIT: или это может быть место для монады читателя