I think you are right about global mutable state.
I am not completely sure what we consider comptime state.
I think using comptime vars, you at least get locally scoped comptime state, for example with zig 0.13 I can do this:
const std = @import("std");
const builtin = @import("builtin");
const ComptimePrng = struct {
const Prng = std.rand.DefaultPrng;
rng: Prng,
pub fn comptimeInit() ComptimePrng {
comptime {
return .{
.rng = Prng.init(0), // imagine a different seed being passed from build system via build option
};
}
}
pub fn getRandomColor(self: *ComptimePrng) [3]u8 {
var color: [3]u8 = undefined;
self.rng.random().bytes(&color);
// copy to const is not strictly needed here,
// but it is a useful pattern as soon as you have pointers to other things
// and later want to use these at runtime.
// The pointers that get exposed to the runtime need to be const pointers
// that only contain other const pointers.
//
// However you can build quite interesting things with comptime vars
// accumulating things, then once things are built,
// you can create a deep copy with all const pointers.
//
// This requires that there aren't cycles because we still don't have
// a way to create cyclic data structures with const pointers,
// that doesn't use some mutation trick.
const res = color;
return res;
}
};
pub fn main() !void {
comptime var builder = ComptimePrng.comptimeInit();
const colors = comptime blk: {
var c: [10][3]u8 = undefined;
for (&c) |*d| d.* = builder.getRandomColor();
const res = c;
break :blk res;
};
std.debug.print("-----\n", .{});
for (colors) |c| std.debug.print("color {any}\n", .{c});
std.debug.print("-----\n", .{});
const more_colors = comptime blk: {
var c: [5][3]u8 = undefined;
for (&c) |*d| d.* = builder.getRandomColor();
const res = c;
break :blk res;
};
std.debug.print("-----\n", .{});
for (more_colors) |c| std.debug.print("color {any}\n", .{c});
std.debug.print("-----\n", .{});
}
This produces deterministic results, but like @pierrelgol has already shown it would be easy to change the seed value of the Pseudo Random Number Generator via a build option, to get results that look random, but aren’t.
-----
color { 223, 35, 11 }
color { 7, 213, 128 }
color { 252, 123, 154 }
color { 26, 94, 190 }
color { 234, 94, 74 }
color { 154, 141, 240 }
color { 110, 2, 181 }
color { 89, 201, 75 }
color { 255, 240, 137 }
color { 22, 76, 66 }
-----
-----
color { 104, 153, 193 }
color { 252, 77, 170 }
color { 193, 12, 150 }
color { 32, 231, 250 }
color { 250, 16, 172 }
-----
Personally I think that Pseudo Random Number are great because they give you most of what is needed from random numbers, but also allow you to specify the seed to get reproducible and thus debug-able code.
Classical computers don’t produce true non-determinism by design (at least not directly), if it appears it is either because some kind of non-deterministic signal like from some sensor got introduced, or because your program runs multiple threads and is basically observing the non-determinism that gets created by the OS doing its own complicated scheduling things (which either may be pseudo random or are influenced by external signals which are considered sources of actual randomness).
But in general build options / the build system is a good way to add things into your program, that aren’t directly accessible from comptime.
I think this locally scoped mutable state is limited and good, because it still can be understood easily and it doesn’t allow you to create functions that just magically return different results without an explanation. But it still allows limited forms of things that have side effects, they are just contained in a scope and still deterministic.