1
0
Fork
You've already forked user-configurable-random
0
User configurable random pre-SRFI
Find a file
2026年03月22日 23:03:53 -04:00
README.md generator based randomness 2026年03月22日 23:03:53 -04:00
RNG.md more detailed spec of random fixnums and flonums 2025年12月22日 08:31:05 -05:00

Primitives for User-Configurable Random Number Generation

Abstract

This specifies an API for random byte sources that does not use global state and allows for random number generators to be defined by the user. This library is designed to be minimal and low-level so that other generators (generating random values of a type, or of a specified range) can be built off of this one.

Rationale

The main SRFI for random-number generation is SRFI-27. It supplies an API with global state (default-random-source) and local state (random-source-make-integers). ("Local state" refers to APIs where the state is explicitly passed to procedures that generate random numbers.)

The SRFI has the following defficiencies:

  • There is no portable way to make custom random-source that use the same API as the default random number generator. There is an explicitly allowed extension mechanism in (make-random-source), but no implementation I surveyed uses this. In any case, any extension would be non-portable.
  • The state of a random-source is extremely underspecified. It's not clear that state taken from a random source does not share mutable state with the random-source it was taken from (i.e. it could be a vector). It also makes portably seeding the random-source with a set value difficult.
  • The only source of entropy random-source-randomize!: the entropy source cannot be read from itself.
  • random-source-pseudo-randomize! is difficult to implement properly.
  • There is no procedure to read multiple random numbers at once and put them into a vector (or in modern Scheme, a bytevector). This makes generating a large number of random numbers inefficient.

Many sizable implementations, like Bigloo, CHICKEN, Chez, Gauche, and Sagittarius supply their own, different random number generation APIs. (A large list of Scheme implementations and what random number generators they have is given in the appendix.) Many of them have different issues (some only have global state) and are usually less flexible than SRFI-27 or this SRFI.

This SRFI proposes an API based around "random-source descriptors" (RSDs) to solve the issues of SRFI-27 and the RNGs of the surveyed implementations. The random-source descriptor contains a procedure that generates random bytes from a source object, along with a predicate for recognizing what the source object is.

This SRFI supports serializable RNGs. Serializable RNGs have RSDs that have procedures to copy the state, to turn the state into a datum, and turn a datum into a state. This means that the state can be duplicated to allow two runs of the same randomly generated data in parallel, and also allows the state to be saved and restored across program invocations and Scheme implementations (if they use the same algorithm).

This SRFI includes procedures that operate on bytevectors of random bits, so they can be used for non-serializable RNGs, such as /dev/random on Linux.

RSDs are immutable objects. The state that the RSD manipulates is supplied as a separate value to the random number generator functions. This allows external procedures to manipulate the state (for instance, to implement versions of random-source-pseudo-randomize!) without needing to extend or modify this API.

This SRFI is low-level. It only supplies procedures to randomly generate uniform byte data, uniformly distributed fixnums, and uniformly distributed flonums. Other distributions can be built on top of the procedures described here. This API can be used to implement SRFI 27. The primitives could be used for exotic APIs like a monadic random number generator, or a RNG that uses an algebraic effect handler.

API

The API uses the same conventions for its procedure descriptions as described in the R7RS, §1.3.3. An argument rsd denotes a position that expects an object that satisfies rsd?, and state denotes an object that must satisfy (rsd-state? rsd state) for the rsd passed before it.

Libraries

The identifiers listed in the subheadings below must be available in the library (srfi NNN) on implementations conforming to the R7RS. Implementations conforming to SRFI 97 and the R6RS should make the library available as (srfi :NNN) and (srfi :NNN random).

The exported identifiers are also organized into sublibraries (using R7RS conventions):

(srfi NNN fundamental)

  • random-bytes->fixnum
  • random-bytes->flonum

(srfi NNN get-entropy!)

  • get-entropy!

(srfi NNN random-source-descriptors)

  • make-rsd
  • rsd?
  • state?
  • rsd-state?-procedure
  • get-bytes!
  • rsd-get-bytes!-procedure
  • copy-state
  • rsd-copy-state-procedure
  • state=?
  • rsd-state=?-procedure
  • serialized-state?
  • rsd-serialized-state?-procedure
  • state->datum
  • rsd-state->datum-procedure
  • datum->state
  • rsd-datum->state-procedure

(srfi NNN default-rsd)

  • ser-rsd
  • make-ser-state

Fundamental Conversion Procedures

These procedures are pure functions that operate on streams of bytes. They are here to separate the RNG algorithm from the production of useful data from random output.

Implementations are expected to aggresively optimize the uses of these procedures.

(random-bytes->fixnum thunk)

thunk must return unsigned 8-bit integers.

Random bytes are read from thunk and a fixnum is returned. The limits of the fixnum are described as in the R6RS or in SRFI 143. For implementations that support neither, an exact integer in an implementation defined range that supports the spirit of a useful RNG for integers is returned.

Given a uniform distribution of bytevectors, the result of random-bytes->fixnum is uniformly distributed over the return range.

Note: On most implementations, this is a simple memory copy onto the fixnum representation.

Rationale: This generates the whole representational range of fixnums because this is very easy to do. Subsets can be formed using rejection sampling.

(random-bytes->flonum thunk)

thunk must return unsigned 8-bit integers.

Random bytes are read from thunk and a flonum is returned. Return a flonum in [0.0,1.0]. On systems that do not support flonums as defined in the R6RS or in SRFI 144, this returns an inexact real.

Given a uniform distribution of bytevectors, the result of random-bytes->flonum is uniformly distributed over the return range.

Rationale: This range is easy and quick to generate given implementation specific details. The range (0.0,1.0), etc. can be made using rejection sampling.

(get-entropy! bv [start [end]])

Get random bytes from a system entropy source. The procedure acts like get-bytes! but without a rsd.

This source SHOULD be a cryptographically secure source of entropy, such as the /dev/random source on Linux. The implementation MUST document the source of random bytes.

Random Source Descriptors

The definition of a random source descriptor (RSD) is equivalent to

(define-record-type <rsd>
 (make-rsd state? get-bytes! copy-state state=? serialized-state?
 state->datum datum-state)
 rsd?
 (state? rsd-state?-procedure)
 (get-bytes! rsd-get-bytes!-procedure)
 (copy-state rsd-copy-state-procedure)
 (state=? rsd-state=?-procedure)
 (serialized-state? rsd-serialized-state?-procedure)
 (state->datum rsd-state->datum-procedure)
 (datum->state rsd-datum->state-procedure))

The arguments copy-state, state->datum, state=?, serialized-state?, and datum->state to make-rsd must be procedures that operate as described below.

RSDs are immutable objects.

Wrapper Procedures

If an implementation raises errors due to invalid arguments, the checks are to be done in the wrapper procedures.

Each procedure (name rsd args ...) is equivalent to ((rsd-name-procedure rsd) args ...).

(state? rsd obj)

state? returns #t if obj is an acceptable state for rsd.

(get-bytes! rsd state bytevector [start [end]])

The stored procedure must take 4 arguments:

  1. The state.
  2. A bytevector.
  3. An exact integer (the start position).
  4. An exact integer (the end position).

The start position is the byte index of the first place to put a random byte, and the end position is one past the last place to put a random byte.

If the start and end values are not specified, they default to 0 and (- (bytevector-length bytevector) 1) respectively. This is handled by the wrapper procedure.

(copy-state rsd state)

The procedure must take one argument, the state, and must return another state that is usable by the RSD that does not share any mutable state with the input state. The returned state is state=? to the original state.

(state->datum rsd state)
(datum->state rsd state)
(serialized-state? rsd obj)

The state->datum must take one argument, the state, and must return a value that has read-write invariance that represents the internal state of the random number generator. The returned value must not share mutable state with the input state. The returned value is serialized-state? for the rsd that state->datum was invoked with.

The datum->state takes a datum that is serialized-state? for that rsd and returns a state such that state? returns #t on it. It is an error to invoke datum->state on something that is not serialized-state? for that rsd.

state->datum and datum->state are inverses according to equal?:

(equal? datum (state->datum rsd (datum->state rsd datum)))
(state=? rsd state1 state2)

This procedure checks if the same sequence of random bits is returned from usages of state1 and `state2.

Let state1 and state2 be state? for the RSD, and let L be any integer accepted by make-bytevector. Let datum1 and datum2 be serialized-state? for the RSD.

The procedure state=? is defined such that the following always evaluate to #t:

;;;; Property 1: Equality implies equivalence of output.
(if (state=? rsd state state2)
 (let ((bv1 (make-bytevector L))
 (bv2 (make-bytevector L)))
 (get-bytes! rsd state1 bv1)
 (get-bytes! rsd state2 bv2)
 (and (equal? bv1 bv2) (state=? rsd state1 state2)))
 #t)
;;;; Property 2: datum->state and state->datum are inverses for states
(state=? rsd (datum->state rsd (state->datum rsd state1)) state1)
;;;; Property 3: Equal datums make equal states
(if (equal? datum1 datum2)
 (state=? rsd (datum->state rsd datum1) (datum->state rsd datum2))
 #t)

Default Random Source Descriptors

ser-rsd
(make-ser-state [obj])

This is a serializable RSD. Implementations MUST document the algorithm for this RSD.

The state for this RSD is made by calling make-ser-state. This procedure must be able to take an exact integer as its argument, and may take other implementation-defined objects. If called with no arguments, it returns a state seeded from a system entropy source.

For each obj, (state=? ser-rsd (make-ser-state obj) (make-ser-state obj)).

Example Usage

This example uses SRFI 158 generators to reduce the amount of boilerplate.

#!r6rs
(import (rnrs) (srfi :158) (srfi :NNN))
(define make-random-fixnum-generator
 (case-lambda
 (() (make-random-fixnum-generator (make-ser-state)))
 ((state)
 (let ((generate-byte (lambda ()
 (let ((bv (make-bytevector 1)))
 (get-bytes! ser-rsd state bv)
 (bytevector-u8-ref bv 0)))))
 (lambda () (random-bytes->fixnum generate-byte))))))
(define very-random-generator (make-random-fixnum-generator))
(display "Entropic numbers, should be different every run: ")
(write (generator->list (gtake (make-random-fixnum-generator))))
(newline)
(display "Entropic numbers, should be different: ")
(write (generator->list (gtake (make-random-fixnum-generator))))
(newline)
(display "Random numbers with known seed, should be the same every run: ")
(write (generator->list (gtake (make-random-fixnum-generator 0))))
(newline)
(display "Random numbers with known seed, should be the same: ")
(write (generator->list (gtake (make-random-fixnum-generator 0))))
(newline)

Future Directions

  • A set of standard RNGs for well-known PRNGS, like ChaCha20 or mersenne twsiter.
  • Distributions like SRFI 194.
  • Randomly generating directly into integer, floating-point, and complex vectors like those in SRFI 4 and SRFI 160.

Sample Implementation

(TODO)

A portable implementation of the foundation and get-entropy! library in the R7RS is not possible. One could write an implementation in pure R6RS, but such a implementation would be of questionable use as it both easier and more performant to implement those at a low level.

An implementation for CHICKEN 6 is presented.

Acknowledgements

The idea of RSDs came from the concept of DTOs in SRFI-225.

Appendix: Scheme Implementations with RNG APIs

  • Bigloo has global state and generates pseudo-random numbers seeded with 1.
  • CHICKEN 5.3.0 has global state and generates pseudo-random numbers seeded with the OS's entropy source.
  • Chez 10 has an RNG with dynamic state with an (undocumented) seed of #xFFFFFFFF. Chez also has an RNG interface with local state, where the state is encapsulated in a pseudo-random-generator object, where the object is seeded with the local time.
  • Gambit 4.9.7 uses a custom implementation of SRFI-27 with extra procedures for generating u8vectors.
  • Gauche 0.9.15 has a custom implementation of SRFI-27, and its own Merssene Twister generator.
  • Gerbil 0.18.1 has a source of cryptographic random bytes without any way to seed it.
  • Guile 3 has a random number generator with both global scoped and local state. It allows for serialization and deserialization of the state of the generator, and has multiple different distributions.
  • MIT-Scheme 12.1 has an interface that is "a mixture of the Common Lisp and SRFI 27 systems." The SRFI-27 interface is included, along with a local and global state based generator that allows for serialization/deserialization of the random state.
  • Racket has a random number generator with dynamic and local state seeded by a vector of exact integers.
  • s7 has a random float generator with a savable and loadable state.
  • Sagittarius 0.9.13 has multiple random number generator algorithms with the possibility of custom algorithms.
  • Foment, Cyclone 0.36, Chibi 0.11, STklos 2.10, Skint v0.6.7 uses SRFI-27 with no major API changes.

The following implementations did not seem to have a standard random number generator: Kawa, Loko, Mosh, SCM, TR7.

Other notable additions:

License

Copyright © 2025–2026 Peter McGoron

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.