9

最好的方法是什么?unsafePerformIO? 模板 Haskell?还有什么?我从来没有使用过其中任何一个,所以我不知道使用它们的许多细节。

请注意,程序每次运行时都会被编译,所以我在编译时或运行时生成字符串都没有关系。我还需要在整个代码的很多地方使用这个字符串,所以我不能真正以“正确”的方式来做它,让它成为一个 IO 操作,这需要太多其他代码放入 IO monad .

4

6 回答 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) []]

(也许randStringD可以以更易读的方式编写 - 如果您有想法,请编辑或评论。)

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

{-# LANGUAGE TemplateHaskell #-}

$(randStringD "randStr")

main = do
    putStrLn randStr
    putStrLn randStr
于 2013-07-06T07:59:40.827 回答
8

如果我们对周围的上下文有更多了解,可能会更容易回答这个问题,但我会采取的方法是在任何需要的地方传递字符串,并在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,所以上面的函数可以被视为具有 typef :: Int -> Int -> String -> Int等,并且您可以省略对runReader(和 remove FlexibleContexts)的调用——您构建的一元计算将只是 type 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来获取顶级IORefs;并且 Template Haskell 将允许您在编译时运行一次 IO 操作并嵌入结果。但我会避免这两种方法。为什么?好吧,从根本上说,关于“纯”是指“完全由语法确定/不会在程序的任何运行中改变”(我喜欢的解释),还是意味着“在这次运行中不会改变”,存在一些争论的节目。” 作为这导致的问题的一个例子:Hashable在某一时刻,从固定盐切换到随机盐。,并将错误引入以前工作的代码中。该软件包已退后,现在允许用户通过环境变量选择加入此行为,默认为运行间纯度。

也就是说,这里是如何使用您提到的两种方法unsafePerformIO和 Template Haskell 来获取顶级随机数据 - 以及为什么,除了对运行间纯度的担忧之外,我不会使用这些技术。(这是我能想到的仅有的两种技术。)

  1. 所谓的unsafePerformIOhack非常脆弱。它依赖于某些未执行的优化,通常不是一种受欢迎的方法。这样做看起来像这样:

    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. 为此使用 Template 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黑客不同,它是安全的;至少,安全模数上述关于运行间纯度的担忧。

于 2013-07-06T07:35:37.513 回答
4

正如文档所说,在这种特殊情况下使用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
于 2013-07-06T06:37:57.803 回答
4

生成随机数IO并不意味着下游函数必须使用IO.

这是一个依赖于 type 值的纯函数示例A

f :: A -> B

...这是一个IO生成的动作A

io :: IO A

我不必修改f即可使用IO. 相反,我使用fmap

fmap f io :: IO B

这正是仿函数应该解决的问题:将态射提升到包装值上,以便不需要修改态射。

于 2013-07-06T13:46:53.797 回答
1
import System.Random

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

   putStrLn str

   putStrLn $ (reverse . (take 3)) str

这会生成一个只有小写字母的十个字符长的字符串。此代码在 IO monad 中,但 str 是纯的,它可以传递给纯函数。如果没有 IO Monad,您将无法获得随机的东西。你可以做一个 unsafePerformIO 但我真的不明白为什么。如果您总是想要相同的值,则可以传递 str 值。如果您查看我的代码的最后一行,您可以看到我有一个对字符串进行操作的纯函数,但是因为我想看到它,所以我调用putStrLn它返回一个空的 IO 操作。

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

于 2013-07-06T06:15:08.057 回答
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')
于 2021-08-14T14:53:21.503 回答