在编译时或运行时生成一个随机字符串,并在程序的其余部分使用它。

10
什么是最好的方法来做到这一点?使用unsafePerformIO?模板Haskell?还是其他什么?我从未使用过其中任何一个,所以不了解它们的详细信息。
请注意,每次运行程序时都将编译该程序,因此在编译时或运行时生成字符串并无影响。我还需要在代码的许多地方使用此字符串,因此我不能按照“正常”的方式将其作为IO操作处理,否则将需要大量其他代码放入IO单子中。
6个回答

11

我不建议使用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

每次调用它时,都会给你不同的结果。


我宁愿选择Template 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) []]
< p >(也许 randStringD 可以用更易读的方式编写 - 如果您有想法,请编辑或评论。)

然后,在另一个模块中,我们可以使用它来声明具有给定名称的常量函数:

{-# LANGUAGE TemplateHaskell #-}

$(randStringD "randStr")

main = do
    putStrLn randStr
    putStrLn randStr

7
假如我们能更了解周边背景,回答这个问题可能会更容易些,但我会选择在每处必要的地方传递该字符串,并在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 monad; 类型为 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)


现在,我必须承认你可以在顶层做这些事情。例如,有一个标准的hack允许使用unsafePerformIO来获取顶层的IORef;而模板Haskell将允许您在编译时运行一次IO操作,并嵌入结果。但我会避免这两种方法。为什么?基本上,存在一些争议,即“纯粹”是否意味着“完全由语法确定/在程序的任何运行中都不会改变”(我支持这种解释),还是它的意思是“在此次运行中不会改变。”由于这个问题引起了一些问题,例如:Hashable包在某个时候从固定盐切换到随机盐。这在Reddit上引起了轩然大波,并在之前工作正常的代码中引入了错误。该软件包后退了一步,现在允许用户通过环境变量选择此行为,默认为在运行之间的清洁度。

话虽如此,下面介绍如何使用你提到的两种方法——unsafePerformIO和Template Haskell来获取顶层随机数据,以及为什么我不会使用这些技术(除了运行间纯度之外的原因)。 (这是我能想到的仅有的两种方法。)

  1. The unsafePerformIO hack, as it's called, is very fragile; it relies on certain optimizations not being performed, and is generally not a well-liked approach. Doing it this way would look like so:

    import Control.Monad
    import System.Random
    import System.IO.Unsafe
    
    unsafeConstantRandomString :: String
    unsafeConstantRandomString = unsafePerformIO $
      flip replicateM (randomRIO (' ','~')) =<< randomRIO (1,32)
    {-# NOINLINE unsafeConstantRandomString #-}
    

    Seriously, though, see how much the word unsafe is used in the above code? That's because using unsafePerformIO will bite you unless you really know what you're doing, and possibly even then. Even when unsafePerformIO doesn't bite you directly, no less than the authors of GHC would say that it's probably not worth using for this (see the section titled "Crime Doesn't Pay"). Don't do this.

  2. Using Template Haskell for this is like using a nuclear warhead to kill a gnat. An ugly nuclear warhead, to boot. That approach would look like the following:

    {-# 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))
    

    Note also that in the Template Haskell version, you can't abstract the random-string-creation functionality into a separate value randomString :: IO String in the same module, or you'll run afoul of the stage restriction. It is safe, though, unlike the unsafePerformIO hack; at least, safe modulo the concerns about between-run purity mentioned above.


4
IO 中生成随机数并不意味着下游函数必须使用 IO。以下是一个示例纯函数,它依赖于类型为 A 的值:
f :: A -> B

... 这里是生成 AIO 动作:

io :: IO A

我不必修改f就可以使用IO。相反,我使用fmap

fmap f io :: IO B

这正是functor所要解决的问题:将变形器提升到包装值上,以便不需要修改变形器。

3
在这种情况下使用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

1
是的,但这能保证randomStr在程序中不会改变吗? - Drew
它不会改变。randomStr是一个值(而不是函数)。由于Haskell是惰性的,这个值将在您第一次使用它时生成,然后始终保持相同。 - Ankur
5
好的。这个回答(https://dev59.com/4Wcs5IYBdhLWcg3wmlIK#12721453)提出可能存在一种假想的编译器优化,导致其被重新计算。我知道在实践中它不会改变,但这并不等同于编译器的保证它不会改变。 - Drew
5
如果你要在这里使用 unsafePerformIO(我会 - 并且确实使用,见我的回答 - 但不建议),请确保至少正确地指定 {-# NOINLINE randomStr #-}。否则,GHC 可能会决定优化方式,这会破坏你的程序。从技术上讲,Haskell 不是惰性的,而是非严格的;惰性只是一种实现策略,并且没有保证“第一次使用值时将生成该值,然后始终相同”。(我说“使用 unsafePerformIO 伤害你”是有原因的。) - Antal Spector-Zabusky

1
import System.Random

main = do
   gen <- newStdGen
   let str = take 10 $ randomRs ('a','z') gen 

   putStrLn str

   putStrLn $ (reverse . (take 3)) str

这会生成一个只有小写字母的10个字符长的字符串。虽然这段代码在IO单子中,但str是纯净的,它可以传递给纯函数。如果没有IO Monad,你无法得到随机结果。你可以使用unsafePerformIO,但我不太清楚为什么要这样做。如果你总是想要相同的值,那么可以传递str值。如果你查看我的代码的最后一行,你会发现我有一个操作该字符串的纯函数,但由于我想看到它,所以我调用了putStrLn,它返回一个空的IO操作。

编辑:或者这可能是Reader Monad的地方


问题在于 str 会随着每次 main 函数的运行而改变,这对我来说是不可行的。想象一下,如果 main 是一个一元函数,并作为参数传递给另一个函数,那么该函数将应用一堆值到 main 中以推断其某些属性。这就是我在处理的问题。 - Drew
如果您想在编译时生成随机字符串,并且每次运行程序都会编译它,那么与在编译时或运行时生成有何不同? main 不是您传递的函数,而是程序入口点。我认为您唯一的选择是使用模板Haskell,但我不确定如何做到这一点,因为它处于状态或IO单子中,或者使用unsafeIO。 - DiegoNolan
是的,在这种情况下,main函数是入口点,但在我的情况下,该值用于传递、分析等函数中。除非我将该函数的定义移动到main函数中,否则我无法使用“str”。 - Drew
2
@Drew,你可以将你的纯函数参数化,以接受“str”作为参数。 - Gabriella Gonzalez

0

对于字符串、数字和其他类型:

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')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接