| README.md | generator based randomness | |
| RNG.md | more detailed spec of random fixnums and flonums | |
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->fixnumrandom-bytes->flonum
(srfi NNN get-entropy!)
get-entropy!
(srfi NNN random-source-descriptors)
make-rsdrsd?state?rsd-state?-procedureget-bytes!rsd-get-bytes!-procedurecopy-statersd-copy-state-procedurestate=?rsd-state=?-procedureserialized-state?rsd-serialized-state?-procedurestate->datumrsd-state->datum-proceduredatum->statersd-datum->state-procedure
(srfi NNN default-rsd)
ser-rsdmake-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:
- The state.
- A bytevector.
- An exact integer (the start position).
- 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 apseudo-random-generatorobject, 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.