Repository: racket/math
Pull request #112: Allow supplying a prng
Author: login: samth
Created: 2025-04-09T20:00:51Z | Merged: 2025-04-10T15:43:14Z



Diff:
diff --git a/math-doc/math/scribblings/math-base.scrbl b/math-doc/math/scribblings/math-base.scrbl
index 748c297..e207336 100644
--- a/math-doc/math/scribblings/math-base.scrbl
+++ b/math-doc/math/scribblings/math-base.scrbl
@@ -82,23 +82,29 @@ Like @racket[(apply + xs)], but incurs rounding error only once when adding inex
 
 @section{Random Number Generation}
 
-@defproc[(random-natural [k Integer]) Natural]{
+@defproc[(random-natural [k Integer] [p Pseudo-Random-Generator (current-pseudo-random-generator)]) Natural]{
 Returns a random natural number less than @racket[k], which must be positive.
 Use @racket[(random-natural k)] instead of @racket[(random k)] when @racket[k]
 could be larger than @racket[4294967087].
+
+Uses @racket[p] for the underlying generation of randomness. 
 }
 
-@defproc[(random-integer [a Integer] [b Integer]) Integer]{
+@defproc[(random-integer [a Integer] [b Integer] [p Pseudo-Random-Generator (current-pseudo-random-generator)]) Integer]{
 Returns a random integer @racket[n] such that @racket[(a . <= . n)] and @racket[(n . < . b)].
+
+Uses @racket[p] for the underlying generation of randomness. 
 }
 
-@defproc[(random-bits [num Integer]) Natural]{
+@defproc[(random-bits [num Integer] [p Pseudo-Random-Generator (current-pseudo-random-generator)]) Natural]{
 Returns a random natural smaller than @racket[(expt 2 num)]; @racket[num] must be non-negative.
 For powers of two, this is faster than using @racket[random-natural], which
 is implemented in terms of @racket[random-bits], using biased rejection sampling.
 
 As an example of use, the significands of the numbers returned by @racket[bfrandom]
 are chosen by @racket[(random-bits (bf-precision))].
+
+Uses @racket[p] for the underlying generation of randomness. 
 }
 
 
diff --git a/math-lib/math/private/base/base-random.rkt b/math-lib/math/private/base/base-random.rkt
index b5860a0..2cef52c 100644
--- a/math-lib/math/private/base/base-random.rkt
+++ b/math-lib/math/private/base/base-random.rkt
@@ -5,39 +5,49 @@
 (provide random-bits random-natural random-integer)
 
 ;; Random bits are taken in blocks of this size:
-(define block-bits 29)
-(define block-size (arithmetic-shift 1 block-bits))
+(define block-bits : Nonnegative-Fixnum 29)
+(define block-size : Natural (arithmetic-shift 1 block-bits))
 
-(: random-bits (Integer -> Natural))
-(define (random-bits bits)
+(: random-bits (case-> (Integer -> Natural)
+                       (Integer Pseudo-Random-Generator -> Natural)))
+(define (random-bits bits [prng (current-pseudo-random-generator)])
   (cond [(bits . < . 0)  (raise-argument-error 'random-bits "Non-Negative-Integer" bits)]
         [(bits . = . 0) 0]
+        [(not (fixnum? bits))  (raise-argument-error 'random-bits "reasonably sized integer" bits)]
+        [(bits . <= . block-bits)  (random (ann (fxlshift 1 bits) Nonnegative-Fixnum) prng)]
+        [(bits . <= . (fx* 2 block-bits))
+	 ;; this case is not always in the fixnum range on 32-bit platforms
+         (define rem-bits (fx- bits block-bits))
+         (bitwise-ior (arithmetic-shift (random block-size prng) rem-bits)
+		      (random (ann (fxlshift 1 rem-bits) Nonnegative-Fixnum) prng))]
         [else
          (define max-blocks (assert (quotient bits block-bits) index?))
          (define rem-bits (remainder bits block-bits))
          (let: loop : Natural ([blocks : Nonnegative-Fixnum  0]
-                               [r : Natural  (random (fxlshift 1 rem-bits))])
+                               [r : Natural  (random (fxlshift 1 rem-bits) prng)])
            (cond [(blocks . fx< . max-blocks)
                   (loop (fx+ blocks 1)
                         (bitwise-ior (arithmetic-shift r block-bits)
-                                     (random block-size)))]
+                                     (random block-size prng)))]
                  [else  r]))]))
 
 (define random-max 4294967087)
 
-(: random-natural (Integer -> Natural))
+(: random-natural (case-> (Integer -> Natural)
+                          (Integer Pseudo-Random-Generator -> Natural)))
 ;; Returns a random integer in the interval [0..n)
-(define (random-natural n)
+(define (random-natural n [prng (current-pseudo-random-generator)])
   (cond
     [(n . <= . 0)  (raise-argument-error 'random-natural "Positive-Integer" n)]
-    [(n . <= . random-max)  (random n)]
+    [(n . <= . random-max)  (random n prng)]
     [else
      (define bits (integer-length (- n 1)))
      (let loop ()
-       (define r (random-bits bits))
+       (define r (random-bits bits prng))
        (if (r . >= . n) (loop) r))]))
 
-(: random-integer (Integer Integer -> Integer))
-(define (random-integer a b)
+(: random-integer (case-> (Integer Integer -> Integer)
+                          (Integer Integer Pseudo-Random-Generator -> Integer)))
+(define (random-integer a b [prng (current-pseudo-random-generator)])
   (let ([a  (min a b)] [b  (max a b)])
-    (+ a (random-natural (- b a)))))
+    (+ a (random-natural (- b a) prng))))
diff --git a/math-test/math/tests/random-tests.rkt b/math-test/math/tests/random-tests.rkt
index b81250b..eca60cf 100644
--- a/math-test/math/tests/random-tests.rkt
+++ b/math-test/math/tests/random-tests.rkt
@@ -2,6 +2,8 @@
 (require math/base)
 (require typed/rackunit)
 
+(define g (current-pseudo-random-generator))
+
 (check-true
   (for/or ([n (in-range 100)])
     (> (random-natural (expt 2 64)) 4294967087)))
@@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
+
+(for ([n (in-range 100)])
+  (define x (random-integer 1 100 g))
+  (check-true (and (<= 1 x) (< x 100))))
+
+(for ([n (in-range 100)])
+  (define x (random-bits n g))
+  (check-true (and (<= 0 x) (< x (expt 2 n)))))


Reviews:
user: login: pavpanchekha
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:03:02Z
user: login: pavpanchekha
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:06:52Z
user: login: samth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:09:07Z
user: login: samth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:09:25Z
user: login: bdeket
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T04:58:16Z
user: login: rfindler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T12:25:44Z
user: login: bdeket
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:04:29Z
user: login: pavpanchekha
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:30:09Z

Review comments:
diff_hunk: @@ -5,33 +5,42 @@
 (provide random-bits random-natural random-integer)
 
 ;; Random bits are taken in blocks of this size:
-(define block-bits 29)
-(define block-size (arithmetic-shift 1 block-bits))
+(define block-bits : Nonnegative-Fixnum 29)
+(define block-size : Natural (arithmetic-shift 1 block-bits))
path: math-lib/math/private/base/base-random.rkt
user: login: pavpanchekha
body: Are these doing something (enabling more optimizations?) or just stylistic? Fine, I think, either way.
created_at: 2025-04-09T21:03:02Z
author_association: COLLABORATOR
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: pavpanchekha
body: I'm sorry—doesn't this test fail one in every ~4 billion times? I know it's not a lot but... is this the best test?
created_at: 2025-04-09T21:06:40Z
author_association: COLLABORATOR
diff_hunk: @@ -5,33 +5,42 @@
 (provide random-bits random-natural random-integer)
 
 ;; Random bits are taken in blocks of this size:
-(define block-bits 29)
-(define block-size (arithmetic-shift 1 block-bits))
+(define block-bits : Nonnegative-Fixnum 29)
+(define block-size : Natural (arithmetic-shift 1 block-bits))
path: math-lib/math/private/base/base-random.rkt
user: login: samth
body: Stylistic here. 
created_at: 2025-04-09T21:09:07Z
author_association: MEMBER
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: samth
body: This is copied from the test above, so maybe both should change?
created_at: 2025-04-09T21:09:25Z
author_association: MEMBER
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: bdeket
body: the test uses `for/or` so the failure rate is `(1 in 4294967505)^100` = `(1 in 2e963)` (still not 0)
created_at: 2025-04-10T04:58:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: rfindler
body: What is this test testing? Would it be testing the same things if the loop were replaced with just `(natural? (random-natural (expt 2 64) g))`?
created_at: 2025-04-10T12:25:44Z
author_association: MEMBER
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: bdeket
body: I understood the test as: can `random-natural` generate numbers bigger than what `racket`s `random` can generate (`random` has a maximum integer argument of `4294967087`)
created_at: 2025-04-10T13:04:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,3 +15,16 @@
 (for ([n (in-range 100)])
   (define x (random-bits n))
   (check-true (and (<= 0 x) (< x (expt 2 n)))))
+
+
+(check-true
+  (for/or ([n (in-range 100)])
+    (> (random-natural (expt 2 64) g) 4294967087)))
path: math-test/math/tests/random-tests.rkt
user: login: pavpanchekha
body: Ah, I missed the `for/or`. This is a low enough chance of failure that we can "risk it".
created_at: 2025-04-10T15:30:09Z
author_association: COLLABORATOR

PR comments:
user: login: samth
created_at: 2025-04-09T20:58:40Z
body: Note that this changes behavior slightly -- previously if you mutated the `current-pseudo-random-generator` parameter in another thread while the loop in `random-natural` or `random-bits` was running, the effect could change the behavior of later loop iterations. I don't think that's a problem though -- the old behavior was probably surprising.
author_association: MEMBER
user: login: pavpanchekha
created_at: 2025-04-10T15:43:25Z
body: Thank you sam!
author_association: COLLABORATOR