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

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

  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. Использование шаблона 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')
Другие вопросы по тегам