Answers:
您不能创建一个纯函数random
,该函数每次调用都会产生不同的结果。实际上,您甚至不能“调用”纯函数。您应用它们。因此,您什么都不会丢失,但这并不意味着随机数在函数编程中是不可取的。让我演示一下,我将始终使用Haskell语法。
来自命令式背景,您可能最初希望random具有如下类型:
random :: () -> Integer
但这已经被排除,因为随机不能是纯函数。
考虑一下价值观念。值是一成不变的。它永远不会改变,并且您可以对它所做的每一次观察始终是一致的。
显然,随机不能产生整数值。相反,它会产生一个Integer随机变量。它的类型可能看起来像这样:
random :: () -> Random Integer
除了完全不需要传递参数外,函数是纯函数,因此一个函数random ()
与另一个函数一样好random ()
。从这里开始,我将随机地给出这种类型:
random :: Random Integer
一切都很好,但不是很有用。您可能希望能够编写类似的表达式random + 42
,但是您不能,因为它不会进行类型检查。您还不能对随机变量做任何事情。
这就提出了一个有趣的问题。应该存在哪些函数来操纵随机变量?
该函数不存在:
bad :: Random a -> a
以任何有用的方式,因为这样您可以编写:
badRandom :: Integer
badRandom = bad random
这就引入了不一致。badRandom应该是一个值,但它也是一个随机数。矛盾。
也许我们应该添加以下功能:
randomAdd :: Integer -> Random Integer -> Random Integer
但这只是更一般模式的特例。您应该能够将任何函数应用于随机事物,以便获得其他随机事物,例如:
randomMap :: (a -> b) -> Random a -> Random b
random + 42
现在不用写,而是可以写randomMap (+42) random
。
如果您只拥有randomMap,则将无法将随机变量组合在一起。例如,您无法编写此函数:
randomCombine :: Random a -> Random b -> Random (a, b)
您可以尝试这样写:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
但是它的类型错误。除了以a结尾Random (a, b)
,我们以a结尾Random (Random (a, b))
可以通过添加另一个功能来解决此问题:
randomJoin :: Random (Random a) -> Random a
但是,出于最终可能会变得清楚的原因,我不会这样做。相反,我将添加以下内容:
randomBind :: Random a -> (a -> Random b) -> Random b
目前尚不很明显,这实际上可以解决问题,但是可以:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
实际上,可以根据randomJoin和randomMap编写randomBind。也可以根据randomBind编写randomJoin。但是,我将保留此作为练习。
我们可以简化一下。请允许我定义此功能:
randomUnit :: a -> Random a
randomUnit将值转换为随机变量。这意味着我们可以拥有实际上不是随机的随机变量。但是,情况总是如此。我们本来可以做的randomMap (const 4) random
。定义randomUnit的一个好主意是,现在我们可以根据randomUnit和randomBind定义randomMap:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
好吧,现在我们到了某个地方。我们有可以操纵的随机变量。然而:
我将处理伪随机数。可以为真正的随机数实现这些功能,但是这个答案已经很长了。
本质上,这将起作用的方式是我们将在各处传递种子值。每当我们生成一个新的随机值时,我们都会生成一个新的种子。最后,当我们完成构造随机变量时,我们将要使用此函数从中进行采样:
runRandom :: Seed -> Random a -> a
我将定义如下的Random类型:
data Random a = Random (Seed -> (Seed, a))
然后,我们只需要提供randomUnit,randomBind,runRandom和random的实现,这是非常简单的:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
对于随机性,我将假设已经有一种类型的函数:
psuedoRandom :: Seed -> (Seed, Integer)
在这种情况下,随机是正义的Random psuedoRandom
。
Haskell具有句法糖,可以使这种外观更好看。这就是所谓的do-notation(注释),要使用它,我们就必须为Random创建Monad的实例。
instance Monad Random where
return = randomUnit
(>>=) = randomBind
做完了 randomCombine
现在可以这样写:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
如果我为自己这样做,那么我什至可以更进一步,并创建一个Applicative实例。(不要担心,如果这没有意义)。
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
然后可以编写randomCombine:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
现在我们有了这些实例,我们可以使用>>=
而不是randomBind,用join代替randomJoin,用fmap代替randomMap,用return代替randomUnit。我们还免费提供了全部功能。
这值得么?您可能会争辩说,进入现阶段,使用随机数并不完全是可怕的,并且是漫长的。我们为此付出了什么呢?
最直接的收获是,我们现在可以准确地看到程序的哪些部分依赖于随机性,哪些部分完全是确定性的。以我的经验,像这样强制严格分离会极大地简化事情。
到目前为止,我们已经假设我们只希望从生成的每个随机变量中获取一个样本,但是如果事实证明,将来我们实际上希望看到更多的分布,则这是微不足道的。您可以在具有不同种子的同一随机变量上多次使用runRandom。当然,这在命令式语言中是可能的,但是在这种情况下,我们可以确定每次采样随机变量时都不会执行意外的IO,并且不必对状态进行初始化也要小心。
bad :: Random a -> a
引入不一致之处?这有什么不好?请在说明中慢慢进行,尤其是对于第一步:)如果您可以解释“有用”功能为何有用的话,那么答案可能是1000分!:)
你没看错 如果将同一种子两次分配给RNG,则它返回的第一个伪随机数将是相同的。这与功能性编程与副作用编程无关。种子的定义是,特定的输入会导致特定的输出,这些输出具有分布良好但绝对是非随机的值。这就是为什么它被称为伪随机的原因,并且通常是一件好事,例如编写可预测的单元测试,在同一问题上可靠地比较不同的优化方法等。
如果您实际上需要计算机的非伪随机数,则必须将其连接到真正随机的事物,例如粒子衰减源,计算机所在的网络中发生不可预测的事件等。这很难做到即使正确,它也会正确运行并且通常很昂贵,但这是不获取伪随机值的唯一方法(通常,即使没有显式提供值,您从编程语言接收的值也基于某种种子。)
仅此而已,这将损害系统的功能性质。由于非伪随机数生成器很少见,因此这种情况很少出现,但是是的,如果您确实有一种生成真正的随机数的方法,那么至少您的编程语言中的那一点不能成为100%纯函数。语言是否会例外,这仅仅是语言实现者的实际程度的问题。
() -> Integer
。您可以使用类型为纯粹的功能性PRNG PRNG_State -> (PRNG_State, Integer)
,但是必须通过不纯的方法对其进行初始化)。
一种方法是将其视为无限随机数序列:
IEnumerable<int> randomNumberGenerator = new RandomNumberGenerator(seed);
也就是说,只需将其视为无底的数据结构,就像Stack
只能调用的地方Pop
,但是可以永远调用它。就像普通的不可变堆栈一样,从顶部移走一个堆栈会得到另一个(不同的)堆栈。
因此,一个不变的(具有惰性评估)随机数生成器可能看起来像:
class RandomNumberGenerator
{
private readonly int nextSeed;
private RandomNumberGenerator next;
public RandomNumberGenerator(int seed)
{
this.nextSeed = this.generateNewSeed(seed);
this.RandomNumber = this.generateRandomNumberBasedOnSeed(seed);
}
public int RandomNumber { get; private set; }
public RandomNumberGenerator Next
{
get
{
if(this.next == null) this.next = new RandomNumberGenerator(this.nextSeed);
return this.next;
}
}
private static int generateNewSeed(int seed)
{
//...
}
private static int generateRandomNumberBasedOnSeed(int seed)
{
//...
}
}
那是功能。
pseudoRandom :: Seed -> (Seed, Integer)
。您甚至可能最终编写了这种类型的函数[Integer] -> ([Integer], Integer)