Nix中的随机数生成器和余弦相似度
RNG and Cosine in Nix

原始链接: https://unnamed.website/posts/rng-cosine-nix/

本文以幽默的笔触探讨了在 NixOS 纯函数式环境中实现随机数生成和余弦函数的挑战。NixOS 作为一个声明式的 Linux 发行版,使用 Nix 语言进行系统配置,目标是实现可重复性。 作者努力克服 Nix 的一些限制,例如由于其纯函数特性而缺乏内置的随机数生成器。文中展示了绕过此限制的尝试,并面临诸如缓存和上下文相关的字符串等障碍。最终,他们使用 `pkgs.runCommandLocal` 和路径存在性检查实现了变通方案,证明了即使在有限制的情况下也能保持持久性。 类似地,实现余弦函数也需要克服 Nix 在惰性列表、lambda 语法和浮点数表示方面的特性,展示了将熟悉的编程概念转换为 Nix 的复杂性。作者利用无限列表来定义函数并演示了它们的用法。 尽管令人沮丧,但这篇文章深入了解了 Nix 的独特行为以及在它的函数式范式中完成看似简单任务所需的创造性解决方案。它强调了尽管 Nix 可能具有挑战性,但它为系统配置提供了强大的功能。

Hacker News 最新 | 往期 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Nix 中的 RNG 和余弦 (unnamed.website) 11 分,来自 todsacerdoti,2 小时前 | 隐藏 | 往期 | 收藏 | 讨论 加入我们 6 月 16-17 日在旧金山举办的 AI 初创公司学校! 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

NixOS is an immutable, atomic Linux distribution with a declarative and reproducible configuration and packaging system using the purely functional, lazily evaluated, dynamically typed Nix programming language.

Kublai: AAAAAAHHH too many buzzwords, what does that all mean???

Glad you asked! Basically, in NixOS, you can configure your entire system using a configuration.nix file and NixOS will magically (using a bunch of Bash scripts) figure out what needs to be installed and how to do it. For instance, if you want to enable Firefox, you’d add programs.firefox.enable = true; to your configuration.nix. Easy as that.

Kublai: Sure that sounds cool, but what if I want to enable Firefox with only 50% probability?

Great question! The easiest solution would be something like randomNumber = 4 (from this xkcd), but what about doing the RNG using the Nix language so that it’s declarative?

RNG

Sadly, Nix doesn’t have built-in RNG since it’s purely functional, but I found a project called rand-nix with this code:

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: Whoa, that’s a lot of code! What does it do?

Basically, it reads random data from /proc/sys/kernel/random/uuid, SHA256es it, and parses the hex string using builtins.fromTOML and does a bunch of other things that you don’t need to worry about. Anyways, let’s run it with nix eval --file main.nix and see what it prints out:

{ a = 3106154414; b = ""; }
SHL: This post uses some experimental Nix features, so if you get any errors, add nix.settings.experimental-features = ["nix-command" "flakes" "pipe-operators"]; to your configuration.nix. The new nix CLI is much better anyways.

Let’s try it again:

{ a = 3106154414; b = ""; }

Huh, that doesn’t look right. It’s printing out the same random number each time! And it seems like we can’t read anything from the /proc/sys/kernel/random/uuid file!

One solution to this is to use pkgs.runCommandLocal to get an environment where we can access /proc/sys/kernel/random/uuid or /dev/random which I’ll use here for simplicity:

seed = pkgs.runCommandLocal "myCommand" {} "od -A n -t d -N 4 /dev/random > $out" |>
         import |>
         builtins.toString;

Yay, now we can access a file with some randomness in it! Sadly&mldr; this doesn’t exactly work either. Now the program prints out 1661889355 for me every time I run it!

This is because pkgs.runCommandLocal creates a Nix derivation which gets cached when we run it. For future runs, Nix will just use the cached value since it thinks this is a pure function! However, the caching depends on the name that we gave the derivation, in this case “myCommand”, so we just need to specify a random value each time we run this program. Easy as that&mldr; wait, where do we get that random value from? That’s what we’re trying to do in the first place!

Kublai: I know, let’s use the current time!

Good thinking, Kublai. In Nix, we can obtain that using builtins.currentTime. However, this only gives us accuracy down to a second so if we run our program twice quickly, it’ll output the same thing. Also, this function isn’t available in pure evaluation mode. We’ll need some more creativity.

Caching&mldr; so troublesome&mldr; what if we could detect if we’re using a cached value or not? Let’s give that idea a try in 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

Huh? What’s going on?

SHL: In Nix, strings aren’t just a bunch of characters but also have a context. The string from .outPath has an additional context that causes the derivation to be evaluated while the manually copied string doesn’t.

Oh, that’s&mldr; interesting. And Nix says that two strings with different contexts are still equal! Insanity! Anyways, we can check a string’s context like this:

nix-repl> builtins.getContext (pkgs.runCommandLocal "meow3" {} "od -A n -t d -N 4 /dev/random > $out").outPath
{
  "/nix/store/37psx5dfcn2hnni4hp7jmaql6ynbq8b7-meow3.drv" = { ... };
}

Since we don’t care about the context at all, let’s just throw it away. (Minor annoyance: I think it would be cleaner to put the |> at the beginning of each line but the Nix REPL complains and insists I put them at the end of the previous line instead.)

nix-repl> (pkgs.runCommandLocal "meow4" {} "od -A n -t d -N 4 /dev/random > $out").outPath |>
            builtins.unsafeDiscardStringContext |>
            builtins.pathExists
false

Now for our RNG, we’ll just keep making derivations until we get one that’s not cached! Easy as that. (Note that once we use builtins.unsafeDiscardStringContext, we can no longer import the derivation normally for some reason and have to use builtins.readFile instead.)

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

Tada! Now every time you run this program, it’ll print out a different number, no matter if you run it twice really quickly, if you GC the Nix store, if you enable pure eval mode, whatever. Doesn’t matter. (EDIT: Turns out this trick doesn’t work in pure eval mode since in that mode, you can only access a path using a string with context for that path, but that causes Nix to materialize that path. I’ll come up with a solution, maybe using timing instead?) And that’s how you generate high-quality random numbers in a purely functional language! (Although I’m curious if it’s possible to do this without IFD which I used here.)

If you thought that wasn’t cursed enough, let’s do some more Nix crimes!

Cosine

Kublai: Hey, I also want to use the cosine function in my configuration.nix! If Nix is a real programming language surely it has cosine, right?

Um&mldr; why?

Kublai: I’d like to uh&mldr; eh&mldr; I’d like to make my computer sinusoidal! That’s it!

OK, whatever, I won’t question that. Let’s implement cosine anyways, using the trick from my lazy infinite lists post. There are much easier ways to implement cosine in Nix but let’s do it with infinite lists for the fun of it.

As a warmup, let’s define an infinite list of ones in Nix:

let
  ones = [1] ++ ones;
in
builtins.elemAt ones 10

And now let’s run it!

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

Oh no, Nix isn’t very happy! Nix lists actually aren’t the lazy linked lists from Haskell that we know and love, but rather finite arrays. Ugh, arrays! Nix is supposed to be lazy, but apparently ++ computes both its arguments immediately or something? Weird.

Luckily, we can implement our own infinite linked lists as a two-element set with a head representing the first element and tail representing the rest of the list. Now we can define our ones list:

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

This prints out [ 1 1 1 1 1 1 1 1 1 1 ], yay! Next, let’s try defining 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

Let’s run it:

[ 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: Ugh&mldr; what the heck?
SHL: For lambda functions in Nix, you have to add a space after the :. Otherwise, it’s interpreted as a string.

A&mldr; string? Even though there aren’t even any quotes? What? Weird. Anyways, the correct code is tail = map (x: x+1) ints;.

Whatever. Now we can port over all the Haskell/Python code from my previous post:

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;
}

Now let’s run it with 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: Ah shoot Nix is screaming at us again!
SHL: This time the culprit is -1.0 which is interpreted as a function. We need to wrap it with parenthesis and do cosine = map (x: -x) (integrate sine (-1.0)); to tell Nix that it’s a float.

What? That makes no sense either. Anyways, let’s try running the fixed version:

{ 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 ]; }

Looks like it finally works! Hooray! As simple as that. Next time, don’t rewrite it in Rust. Rewrite it in everyone’s favorite general-purpose language, Nix. You won’t regret it!

Kublai: Welp I kinda forgot what I wanted to use cosine for in my configuration.nix in the first place&mldr;

Random cool Nix stuff

If you’d like to read some slightly less cursed Nix stuff, here are some great articles.

Oh and also Btrfs is awesome! When I switched my main laptop to NixOS, I simply installed it onto a new subvolume, no live USB needed.

And lastly, thanks Ersei for answering all my silly Nix and NixOS questions!

联系我们 contact @ memedata.com