在 Nix 中实现 RNG 和 Cosine
RNG and Cosine in Nix
如何在你的 NixOS 配置中实现随机数生成和余弦函数。
12 分钟阅读 | 2025年4月12日
NixOS 是一个不可变的、原子化的 Linux 发行版,它使用纯函数式、惰性求值、动态类型的 Nix 编程语言进行声明式和可复现的配置和打包。
Kublai : AAAAAAHHH 太多术语了,这到底是什么意思???
很高兴你问了! 基本上,在 NixOS 中,你可以使用 configuration.nix
文件配置整个系统,然后 NixOS 会神奇地(使用一堆 Bash 脚本)找出需要安装什么以及如何安装。 例如,如果你想启用 Firefox,你可以在 configuration.nix
中添加 programs.firefox.enable = true;
。 就这么简单。
Kublai : 听起来不错,但如果我想仅以 50% 的概率启用 Firefox 怎么办?
好问题! 最简单的解决方案是类似 randomNumber = 4
(来自这个 xkcd),但如果使用 Nix 语言进行 RNG,使其具有 d e c l a r a t i v e 会怎么样?
RNG
遗憾的是,Nix 没有内置的 RNG,因为它本质上是纯函数式的,但我找到了一个名为 rand-nix 的项目,其中包含以下代码:
let
pkgs = import <nixpkgs> { };
seed = builtins.replaceStrings [ "-" "\n" ] [ "" "" ]
(builtins.readFile /proc/sys/kernel/random/uuid);
hash = builtins.hashString "sha256";
rng = seed: pkgs.lib.fix (self: {
int = (builtins.fromTOML "x=0x${builtins.substring 28 8 (hash seed)}").x;
intBetween = x: y: x + pkgs.lib.mod self.int (y - x);
next = rng (hash seed);
skip = pkgs.lib.flip pkgs.lib.pipe [
(pkgs.lib.flip pkgs.lib.replicate (x: x.next))
(pkgs.lib.pipe self)
];
take = builtins.genList self.skip;
});
in
{
a = (rng seed).int;
b = builtins.readFile /proc/sys/kernel/random/uuid;
}
Kublai : 哇,好多代码! 它有什么作用?
基本上,它从 /proc/sys/kernel/random/uuid
读取随机数据,对其进行 SHA256 哈希,并使用 builtins.fromTOML
解析十六进制字符串,并进行许多其他你不需要担心的事情。 无论如何,让我们使用 nix eval --file main.nix
运行它,看看它会输出什么:
{ a = 3106154414; b = ""; }
SHL : 这篇文章使用了一些实验性的 Nix 功能,因此如果收到任何错误,请将
nix.settings.experimental-features = ["nix-command" "flakes" "pipe-operators"];
添加到你的configuration.nix
中。 无论如何,新的nix
CLI 更好。
让我们再试一次:
{ a = 3106154414; b = ""; }
嗯,这看起来不太对劲。 每次都打印出相同的随机数! 似乎我们无法从 /proc/sys/kernel/random/uuid
文件中读取任何内容!
一种解决方案是使用 pkgs.runCommandLocal
来获取一个可以访问 /proc/sys/kernel/random/uuid
或 /dev/random
的环境,为了简单起见,我将使用后者:
seed = pkgs.runCommandLocal "myCommand" {} "od -A n -t d -N 4 /dev/random > $out" |>
import |>
builtins.toString;
耶,现在我们可以访问一个包含一些随机性的文件了! 可悲的是……这也不完全有效。 现在,每次运行该程序时,它都会为我打印出 1661889355
!
这是因为 pkgs.runCommandLocal
创建了一个 Nix derivation,当我们运行它时,它会被缓存。 对于将来的运行,Nix 将仅使用缓存的值,因为它认为这是一个纯函数! 但是,缓存取决于我们给 derivation 的名称,在这种情况下为“myCommand”,因此我们只需要在每次运行此程序时指定一个随机值。 就这么简单……等等,我们从哪里获得那个随机值? 这正是我们首先要做的事情!
Kublai : 我知道了,让我们使用当前时间!
好主意,Kublai。 在 Nix 中,我们可以使用 builtins.currentTime
获取该时间。 但是,这仅能提供精确到一秒的精度,因此如果我们快速运行两次程序,它将输出相同的内容。 此外,此函数在纯求值模式下不可用。 我们需要更多的创造力。
缓存……真是麻烦……如果我们能够检测到我们是否正在使用缓存的值会怎么样? 让我们在 nix repl
中尝试一下这个想法。
nix-repl> builtins.pathExists (pkgs.runCommandLocal "meow" {} "od -A n -t d -N 4 /dev/random > $out").outPath
true
nix-repl> builtins.pathExists (pkgs.runCommandLocal "meow2" {} "od -A n -t d -N 4 /dev/random > $out").outPath
true
nix-repl> (pkgs.runCommandLocal "meow3" {} "od -A n -t d -N 4 /dev/random > $out").outPath
"/nix/store/hajf5106d42xh7h1amx8f32g6fl017gd-meow3"
nix-repl> builtins.pathExists "/nix/store/hajf5106d42xh7h1amx8f32g6fl017gd-meow3"
false
nix-repl> (pkgs.runCommandLocal "meow3" {} "od -A n -t d -N 4 /dev/random > $out").outPath == "/nix/store/hajf5106d42xh7h1amx8f32g6fl017gd-meow3"
true
嗯? 发生了什么?
SHL : 在 Nix 中,字符串不仅仅是一堆字符,而且还具有上下文。 来自
.outPath
的字符串具有额外的上下文,该上下文会导致 derivation 被求值,而手动复制的字符串则没有。
哦,这……很有趣。 Nix 说具有不同上下文的两个字符串仍然相等! 太疯狂了! 无论如何,我们可以像这样检查字符串的上下文:
nix-repl> builtins.getContext (pkgs.runCommandLocal "meow3" {} "od -A n -t d -N 4 /dev/random > $out").outPath
{
"/nix/store/37psx5dfcn2hnni4hp7jmaql6ynbq8b7-meow3.drv" = { ... };
}
因为我们根本不在乎上下文,所以让我们直接丢弃它。 (一个小烦恼:我认为将 |>
放在每行的开头会更简洁,但是 Nix REPL 会抱怨并坚持让我将它们放在上一行的末尾。)
nix-repl> (pkgs.runCommandLocal "meow4" {} "od -A n -t d -N 4 /dev/random > $out").outPath |>
builtins.unsafeDiscardStringContext |>
builtins.pathExists
false
现在,对于我们的 RNG,我们将继续创建 derivations,直到我们得到一个未被缓存的 derivation! 就这么简单。 (请注意,一旦我们使用 builtins.unsafeDiscardStringContext
,由于某种原因,我们无法再正常地 import
derivation,而必须改用 builtins.readFile
。)
let
pkgs = import <nixpkgs> { };
genSeed =
seed:
let
p = pkgs.runCommandLocal seed { } "od -A n -t d -N 4 /dev/random > $out";
in
if p.outPath |> builtins.unsafeDiscardStringContext |> builtins.pathExists then
genSeed (builtins.readFile p)
else
builtins.readFile p;
seed = genSeed "why did I do this";
hash = builtins.hashString "sha256";
rng =
seed:
pkgs.lib.fix (self: {
int = (builtins.fromTOML "x=0x${builtins.substring 28 8 (hash seed)}").x;
intBetween = x: y: x + pkgs.lib.mod self.int (y - x);
next = rng (hash seed);
skip = pkgs.lib.flip pkgs.lib.pipe [
(pkgs.lib.flip pkgs.lib.replicate (x: x.next))
(pkgs.lib.pipe self)
];
take = builtins.genList self.skip;
});
in
(rng seed).int
哒哒! 现在,每次你运行这个程序时,它都会打印出不同的数字,无论你是否真的很快地运行两次,是否对 Nix store 进行 GC,是否启用纯求值模式,都无关紧要。(编辑:事实证明,此技巧在纯求值模式下不起作用,因为在该模式下,你只能使用具有该路径上下文的字符串来访问路径,但这会导致 Nix 具体化该路径。我会想出一个解决方案,也许可以使用计时代替?)这就是你在纯函数式语言中生成高质量随机数的方式!(尽管我很好奇是否可以在没有IFD的情况下做到这一点,我在这里使用了它。)
如果你认为这还不够糟糕,让我们再做一些 Nix 罪行!
Cosine
Kublai : 嘿,我也想在我的
configuration.nix
中使用余弦函数! 如果 Nix 是一种真正的编程语言,那么它肯定有余弦函数,对吗?
嗯……为什么?
Kublai : 我想……我想让我的电脑呈正弦曲线! 就是这样!
好的,随便吧,我不会质疑的。 无论如何,让我们使用我的惰性无限列表文章中的技巧来实现余弦函数。 有很多更简单的方法可以在 Nix 中实现余弦函数,但让我们为了好玩而使用无限列表来实现它。
作为热身,让我们在 Nix 中定义一个无限的一列表:
let
ones = [1] ++ ones;
in
builtins.elemAt ones 10
现在让我们运行它!
error:
… while calling the 'elemAt' builtin
at «string»:4:3:
3| in
4| builtins.elemAt ones 10
| ^
error: infinite recursion encountered
at «string»:2:17:
1| let
2| ones = [1] ++ ones;
| ^
3| in
哦,不,Nix 不是很开心! 实际上,Nix 列表不是我们知道和喜欢的 Haskell 中的惰性链接列表,而是有限数组。 哎呀,数组! Nix 应该是惰性的,但显然 ++
会立即计算它的两个参数,还是其他什么? 真奇怪。
幸运的是,我们可以实现我们自己的无限链接列表,作为一个包含 head
(表示第一个元素)和 tail
(表示列表的其余部分)的二元素集合。 现在我们可以定义我们的 ones
列表:
let
take = n: s: if n == 0 then [ ] else [ s.head ] ++ take (n - 1) s.tail;
ones = {
head = 1;
tail = ones;
};
in
take 10 ones
这会打印出 [ 1 1 1 1 1 1 1 1 1 1 ]
,太棒了! 接下来,让我们尝试定义 ints
:
let
take = n: s: if n == 0 then [ ] else [ s.head ] ++ take (n - 1) s.tail;
map = f: s: {
head = f s.head;
tail = map f s.tail;
};
ints = {
head = 1;
tail = map (x:x+1) ints;
};
in
take 10 ints
让我们运行它:
[ 1 «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» «error: attempt to call something which is not a function but a string: "x:x+1"» ]
Kublai : 呃……到底是怎么回事?
SHL : 对于 Nix 中的 lambda 函数,你必须在
:
之后添加一个空格。 否则,它会被解释为字符串。
一个……字符串? 即使没有引号? 什么? 真奇怪。 无论如何,正确的代码是 tail = map (x: x+1) ints;
。
随便吧。 现在我们可以移植我之前文章中的所有 Haskell/Python 代码:
let
pkgs = import <nixpkgs> { };
take = n: s: if n == 0 then [ ] else [ s.head ] ++ take (n - 1) s.tail;
map = f: s: {
head = f s.head;
tail = map f s.tail;
};
zipWith = f: s1: s2: {
head = f s1.head s2.head;
tail = zipWith f s1.tail s2.tail;
};
ints = {
head = 1;
tail = map (x: x + 1) ints;
};
integrate = s: c: {
head = c;
tail = zipWith (a: b: a / b) s ints;
};
expSeries = integrate expSeries 1.0;
sine = integrate cosine 0.0;
cosine = map (x: -x) (integrate sine -1.0);
evalAt =
n: s: x:
pkgs.lib.lists.foldr (a: acc: a + acc * x) 0 (take n s);
in
{
intsExample = take 10 ints;
expSeriesExample = take 10 expSeries;
sineExample = take 10 sine;
cosineExample = take 10 cosine;
expAt2 = evalAt 100 expSeries 2;
cosineAt2 = evalAt 100 cosine 2;
}
现在让我们使用 nix eval --file main.nix
运行它:
{ cosineAt2 = «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»»; cosineExample = [ «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» ]; expAt2 = 7.38906; expSeriesExample = [ 1 1 0.5 0.166667 0.0416667 0.00833333 0.00138889 0.000198413 2.48016e-05 2.75573e-06 ]; intsExample = [ 1 2 3 4 5 6 7 8 9 10 ]; sineExample = [ 0 «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» «error: expected a float but found a function: «lambda integrate @ /home/a/main.nix:23:18»» ]; }
Kublai : 啊,该死,Nix 又在对我们尖叫了!
SHL : 这次罪魁祸首是
-1.0
,它被解释为一个函数。 我们需要用括号将其括起来,并执行cosine = map (x: -x) (integrate sine (-1.0));
以告诉 Nix 这是一个浮点数。
什么? 这也没道理。 无论如何,让我们尝试运行修复后的版本:
{ cosineAt2 = -0.416147; cosineExample = [ 1 0 -0.5 0 0.0416667 0 -0.00138889 0 2.48016e-05 0 ]; expAt2 = 7.38906; expSeriesExample = [ 1 1 0.5 0.166667 0.0416667 0.00833333 0.00138889 0.000198413 2.48016e-05 2.75573e-06 ]; intsExample = [ 1 2 3 4 5 6 7 8 9 10 ]; sineExample = [ 0 1 0 -0.166667 0 0.00833333 0 -0.000198413 0 2.75573e-06 ]; }
看起来它终于奏效了! 万岁! 就这么简单。 下次,不要用 Rust 重写它。 用每个人最喜欢的通用语言 Nix 重写它。 你不会后悔的!
Kublai : 哎呀,我完全忘记了最初想在我的
configuration.nix
中用余弦做什么了……
随机的酷 Nix 相关内容
如果你想阅读一些不那么糟糕的 Nix 相关内容,这里有一些很棒的文章。
- Overlays 使用定点:https://nixcademy.com/posts/what-you-need-to-know-about-laziness/ https://blog.layus.be/posts/2020-06-12-nix-overlays.html
- Nix store 采用输入寻址而非内容寻址似乎很奇怪,但显然内容寻址真的很难:https://www.tweag.io/blog/2020-09-10-nix-cas/ https://www.tweag.io/blog/2020-11-18-nix-cas-self-references/ https://www.tweag.io/blog/2021-02-17-derivation-outputs-and-output-paths/ https://www.tweag.io/blog/2021-12-02-nix-cas-4/
哦,还有 Btrfs 太棒了! 当我将我的主笔记本电脑切换到 NixOS 时,我只需将其安装到一个新的子卷中,无需 Live USB。
最后,感谢 Ersei 回答了我所有关于 Nix 和 NixOS 的愚蠢问题!
“错误:您已超过每日免费报价限制。 请明天再试。”
CC BY-SA 4.0