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 相关内容,这里有一些很棒的文章。

哦,还有 Btrfs 太棒了! 当我将我的主笔记本电脑切换到 NixOS 时,我只需将其安装到一个新的子卷中,无需 Live USB。

最后,感谢 Ersei 回答了我所有关于 Nix 和 NixOS 的愚蠢问题!

“错误:您已超过每日免费报价限制。 请明天再试。”

CC BY-SA 4.0