Interactive program written in Nix lang

I read this article about programming a tic-tac-toe game in Nix. The concept is interesting, but writing contents to a file as input and outputing to realized derivations is far from desired. However, I noticed that Lix allows you to read input from stdin by using builtins.readFile /dev/stdin (while the vanilla Nix does not) and that builtins.trace can output things visible in the console, so I can write a decent interactive program that has input and output and does not involve derivations at all. Try this simple demo:

#!/usr/bin/env nix-shell
#!nix-shell --pure -i nix-instantiate -p lix

let
  do = statements:
    _: builtins.foldl' (_: statement: _ // statement _ _) _ statements;
  assign = attr: value:
    _: _ // { ${attr} = value; };
  print = message:
    _: builtins.trace message _;
  ifElse = condition: ifTrue: ifFalse:
    _: if condition _ then ifTrue _ else ifFalse _;
  while = condition: body:
    ifElse condition (_: do [ body (while condition body) ]) (_: do [ ]);
  main = statement: builtins.seq (statement { } { }) "";
in

main (_: do [
  (_: assign "number" 0)
  (_: print "Current value: ${toString _.number}")
  (_: print "Input an operation (+, -, *, /) followed by a number, press Enter, and then press Ctrl+D")
  (_: assign "input" (builtins.readFile /dev/stdin))
  (while (_: _.input != "") (_: do [
    (_: assign "match" (builtins.match "^([-+*/]) *([0-9]+)\n$" _.input))
    (ifElse (_: _.match == null) (_: do [
      (_: print "Invalid input")
    ]) (_: do [
      (_: assign "operator" (builtins.elemAt _.match 0))
      (_: assign "operand" (builtins.fromJSON (builtins.elemAt _.match 1)))
      (ifElse (_: _.operator == "+") (_: do [
        (_: assign "number" (_.number + _.operand))
      ]) (_: do [
        (ifElse (_: _.operator == "-") (_: do [
          (_: assign "number" (_.number - _.operand))
        ]) (_: do [
          (ifElse (_: _.operator == "*") (_: do [
            (_: assign "number" (_.number * _.operand))
          ]) (_: do [
            (ifElse (_: _.operator == "/") (_: do [
              (_: assign "number" (_.number / _.operand))
            ]) (_: do [
            ]))
          ]))
        ]))
      ]))
    ]))
    (_: print "Current value: ${toString _.number}")
    (_: print "Input an operation (+, -, *, /) followed by a number, press Enter, and then press Ctrl+D")
    (_: assign "input" (builtins.readFile /dev/stdin))
  ]))
  (_: print "Goodbye!")
])
17 Likes

One can also read stdin with this one simple trick (but please don’t use this ever or rely on this being supported):

builtins.fetchurl { url = “file:///proc/self/fd/0”; }

It will cache the input though, so you need to pass in —tarball-ttl 0 (very intuitive, right?).

10 Likes

This is all incredibly cursed, and I’m here for it. Godspeed.

1 Like

Cursed, but amusing. :slight_smile:

At first I thought you’d built a basic IO monad, but on closer inspection it’s less structured than that. Regardless, it’s obviously a serviceable abstraction for this level of IO complexity.

1 Like

Interesting that you can actually open and reopen /dev/stdin this way in Lix, though I suspect that is an accidental oversight. Too bad that strings aren’t actually lazy in any way, so this interaction is very limited since you can’t process a stream of characters before it has been completed. (Otherwise you could implement I/O like Lazy-K does.)

1 Like

Thank you for this :pray:
Just updated the beloved cursed-nix repo with my spin on the idea. I changed the syntax a little bit, hopefully making it a bit more readable. Critique welcome

1 Like