Random OCaml中的随机数生成

Random OCaml中的随机数生成,random,ocaml,Random,Ocaml,当使用严格的函数式语言时,您必须使用一种编写程序的方法。我遇到了使用OCaml生成大量伪随机数的问题,我不确定我是否使用这种语言生成这些数字的最佳方法 我所做的是用一个函数(gen)创建一个模块,该函数接受一个整数作为大小,并返回一个空列表,然后返回一个大小为的伪随机数列表。问题是当大小太大时,它会断言一个堆栈溢出,这是预期的结果 我应该使用尾部递归吗?我是否应该使用我不知道的更好的方法 module RNG = struct (* Append a number n in the end

当使用严格的函数式语言时,您必须使用一种编写程序的方法。我遇到了使用OCaml生成大量伪随机数的问题,我不确定我是否使用这种语言生成这些数字的最佳方法

我所做的是用一个函数(gen)创建一个模块,该函数接受一个整数作为大小,并返回一个空列表,然后返回一个大小为的伪随机数列表。问题是当大小太大时,它会断言一个堆栈溢出,这是预期的结果

我应该使用尾部递归吗?我是否应该使用我不知道的更好的方法

module RNG =
struct
  (* Append a number n in the end of the list l *)
  let rec append l n =
    match l with
    | [] -> [n]
    | h :: t -> h :: (append t n)

  (* Generate a list l with size random numbers *)
  let rec gen size l =
    if size = 0 then
      l
    else
      let n = Random.int 1000000 in
      let list = append l n in
      gen (size - 1) list
end
测试代码以生成10亿个伪随机数返回:

# let l = RNG.gen 1000000000 [];;
Stack overflow during evaluation (looping recursion?).

以相反的顺序生成列表,然后在最后反转一次,这将是一个很大的改进。将连续值添加到列表末尾的速度非常慢。添加到列表前面可以在固定时间内完成


更好的是,只需以相反的顺序生成列表并以这种方式返回即可。是否注意列表的生成顺序与生成值的顺序相同?

标准
list
模块有一个
init
函数,可用于在一行中写入所有这些内容:

let upperbound = 10

let rec gen size =
  List.init size (fun _ -> Random.int upperbound)

为什么需要显式计算完整列表?另一种选择可能是使用新的序列模块延迟(和确定地)生成元素:

   let rec random_seq state () =
     let state' = Random.State.copy state in
     Seq.Cons(Random.State.int state' 10, random_seq state')

然后,随机序列的状态完全由初始状态决定:它既可以顺利重用,也可以根据需要生成新元素。

问题在于附加函数不是尾部递归函数。每个递归都会占用一点堆栈空间来存储它的状态,随着列表变长,append函数占用越来越多的堆栈空间。在某些情况下,堆栈不够大,代码失败

正如您在问题中所建议的,避免这种情况的方法是使用尾部递归。使用列表时,通常意味着以相反的顺序构造列表。然后,append函数变成了简单的

如果结果列表的顺序很重要,则需要在末尾颠倒列表。因此,代码返回
List.rev acc
并不少见。这需要O(n)个时间,但需要恒定的空间,并且是尾部递归的。因此,堆栈没有限制

因此,您的代码将变成:

let rec gen size l =
  if size = 0 then
    List.rev l
  else
    let n = Random.int 1000000 in
    let list = n :: l in
    gen (size - 1) list
还有一些事情需要优化:

当通过递归逐位构建结果时,结果通常是names
acc
,是累加器的缩写,并首先传递:

let rec gen acc size =
  if size = 0 then
    List.rev acc
  else
    let n = Random.int 1000000 in
    let list = n :: acc in
    gen list (size - 1)
然后允许使用函数和模式匹配,而不是大小参数和if构造:

let rec gen acc = function
| 0 -> List.rev acc
| size ->
    let n = Random.int 1000000 in
    let list = n :: acc in
    gen list (size - 1)
随机数的列表通常也是一样好的。除非您想要不同大小的列表,但使用相同的种子以相同的数字序列开始,否则您可以跳过List.rev。acc是这样一个简单的构造,它通常不会将其绑定到变量

let rec gen acc = function
| 0 -> acc
| size ->
    let n = Random.int 1000000 in
    gen (n :: acc) (size - 1)
最后,您可以利用可选参数。虽然这使代码的阅读变得更复杂,但大大简化了它的使用:

let rec gen ?(acc=[]) = function
  | 0 -> acc
  | size ->
      let n = Random.int 1000000 in
      gen ~acc:(n :: acc) (size - 1)

# gen 5;;
- : int list = [180439; 831641; 180182; 326685; 809344]
您不再需要指定空列表来生成随机数列表

注意:另一种方法是使用包装函数:

let gen size =
  let rec loop acc = function
    | 0 -> acc
    | size ->
        let n = Random.int 1000000 in
        loop (n :: acc) (size - 1)
  in loop [] size

第一种方法是否可以改进为现在遍历整个列表以添加一个数字?这样做时,我的内存遇到了一些性能问题。它消耗大量内存(大量分配),当它达到~15GiB时,我不得不停止它。有没有办法避免这个问题?您可以看到实现。如果不需要列表按特定顺序排列,则可以使用尾部递归实现,而不需要列表反转,这将把内存需求减少一半。15GB似乎有点过分,但如果分配10亿个cons单元,它的数量级是正确的。也许列表不是适合您的用例的正确数据结构。正如@octachron所建议的那样,一个序列似乎更合适。我不需要显式地生成整个列表,我只是想这样做。感谢您提供的信息,我将尝试一下。我不知道在OCaml中我可以使用惰性构造。很抱歉,这个问题有一个更好的答案,因此我将其更改为有利于整个SO社区。请注意,这在很大程度上取决于您的用例。在处理随机变量序列时,通常会最终对一些减少的可观测值感兴趣,如平均值、少量矩或直方图。在这种情况下,最好使用序列(或惰性列表)动态计算缩减后的数据,而不是先在内存中计算完整序列,然后将缩减器应用于完整数据集。我不在乎列表的生成顺序,我会照你说的做。回答得很好!在你回答之前,我做了一些你提到的事情,但你给了我更多+1.