函数式语言如何处理随机数?


68

我的意思是,在我阅读过的几乎所有有关函数式语言的教程中,都是关于函数的妙处之一,就是如果您两次调用具有相同参数的函数,那么总会得到同样的结果。

您究竟如何创建一个以种子为参数,然后基于该种子返回随机数的函数?

我的意思是,这似乎与功能方面的优势之一背道而驰,对吧?还是我在这里完全错过了什么?

Answers:


89

您不能创建一个纯函数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,并且不必对状态进行初始化也要小心。


6
+1是应用函子/ Monad实际用法的一个很好的例子。
jozefg

9
不错的答案,但是在某些步骤上进展太快了。例如,为什么会bad :: Random a -> a引入不一致之处?这有什么不好?请在说明中慢慢进行,尤其是对于第一步:)如果您可以解释“有用”功能为何有用的话,那么答案可能是1000分!:)
Andres F.

@AndresF。好的,我会稍作修改。
dan_waterworth 2013年

1
@AndresF。我已经修改了答案,但是我认为我没有充分解释您如何使用此做法,因此我可能稍后再讲。
dan_waterworth 2013年

3
出色的答案。我不是函数程序员,但我确实了解大多数概念,并且已经“玩过” Haskell。这是一种答案,既可以告知提问者,又可以激发其他人更深入地学习和更多地了解该主题。我希望我能在我的投票中给你10分以上的几点。
RLH 2013年

10

你没看错 如果将同一种子两次分配给RNG,则它返回的第一个伪随机数将是相同的。这与功能性编程与副作用编程无关。种子的定义是,特定的输入会导致特定的输出,这些输出具有分布良好但绝对是非随机的值。这就是为什么它被称为伪随机的原因,并且通常是一件好事,例如编写可预测的单元测试,在同一问题上可靠地比较不同的优化方法等。

如果您实际上需要计算机的非伪随机数,则必须将其连接到真正随机的事物,例如粒子衰减源,计算机所在的网络中发生不可预测的事件等。这很难做到即使正确,它也会正确运行并且通常很昂贵,但这是获取伪随机值的唯一方法(通常,即使没有显式提供值,您从编程语言接收的值也基于某种种子。)

仅此而已,这将损害系统的功能性质。由于非伪随机数生成器很少见,因此这种情况很少出现,但是是的,如果您确实有一种生成真正的随机数的方法,那么至少您的编程语言中的那一点不能成为100%纯函数。语言是否会例外,这仅仅是语言实现者的实际程度的问题。


9
真正的RNG根本不是计算机程序,无论它是否是纯功能性的。我们都知道冯·诺伊曼(von Neumann)的话,是关于产生随机数的算术方法的(那些不知道的人,请查找它-最好是整件事,而不仅仅是第一句话)。您需要与一些不确定的硬件进行交互,当然这也是不纯的。但这仅仅是I / O,它已经在非常不同的环境中多次与纯度协调。没有任何可用的语言会完全禁止I / O-否则您将看不到程序的结果。

否决票是什么?
l0b0 2013年

6
为什么外部和真正随机的来源会损害系统的功能性质?仍然是“相同的输入->相同的输出”。除非您将外部源视为系统的一部分,否则它将不是“外部”,对吗?
Andres F.

4
这与PRNG与TRNG无关。您不能具有type的非常量函数() -> Integer。您可以使用类型为纯粹的功能性PRNG PRNG_State -> (PRNG_State, Integer),但是必须通过不纯的方法对其进行初始化)。
吉尔斯

4
@Brian Agreed,但是措辞(“将其连接到真正随机的东西上”)表明随机源在系统外部。因此,系统本身仍保持纯功能。不是输入源。
Andres F.

6

一种方法是将其视为无限随机数序列:

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

2
@dan_waterworth实际上很有意义。整数不能说是随机的。数字列表可以具有此属性。因此,事实是,随机数生成器可以具有int-> [int]类型,即获取种子并返回随机整数列表的函数。当然,您可以在周围设置一个州monad以获得haskell的do表示法。但是,作为对该问题的一般性回答,我认为这确实很有帮助。
西蒙·贝格

5

非功能性语言也是如此。在这里忽略真正随机数稍微分开的问题。

随机数生成器始终获取种子值,并且对于相同的种子,返回相同的随机数序列(如果您需要测试使用随机数的程序,则非常有帮助)。基本上,它从您选择的种子开始,然后将最后的结果用作下一次迭代的种子。因此,大多数实现都是您描述它们时的“纯”函数:取一个值,对于相同的值始终返回相同的结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.