Skip to content

Instantly share code, notes, and snippets.

@roberth
Created June 21, 2019 09:26
Show Gist options
  • Save roberth/0ea924050df924e6c7a61c86ac30a03a to your computer and use it in GitHub Desktop.
Save roberth/0ea924050df924e6c7a61c86ac30a03a to your computer and use it in GitHub Desktop.
concurrent, event sourced, side effecting Nix via bash
#!./interpret.nix
{ io, args, ... }:
let
main =
io.printLine "I'm hi, who are you?" ask;
ask =
io.readLine (name:
if name == ""
then
io.printLine
"Please enter your name."
ask
else
io.for [1 2 3 4 5 6 7 8 9 10] (i:
io.detach (
io.sleep 3 (
io.printLine "Hello, ${name} (${toString i})" (
io.exit 0
)
)
)
)
(pids:
io.for pids (pid:
io.wait pid
)
(statuses:
io.exit (builtins.length (builtins.filter (x: x != 0) statuses))
)
)
);
in
main
#!./run.nix
{args}:
let
inherit (import <nixpkgs/lib>)
escapeShellArgs
escapeShellArg
;
main =
let
fname = builtins.head args;
f = import "${toString ./.}/${fname}";
in
if builtins.getEnv "IS_CONTINUATION" == "1"
then runEvents fname { cont = a: f (a // { inherit io; }); } (builtins.tail args)
else start fname f (builtins.tail args)
;
io.printLine = str: cont: {
cont = _ret: cont;
shellCmd = prevArgs: ''
echo ${escapeShellArg str}
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} "{}"
'';
};
io.sleep = arg: cont: {
cont = _ret: cont;
shellCmd = prevArgs: ''
sleep ${escapeShellArg arg}
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} "{}"
'';
};
# This seems to be
io.wait = arg: cont: {
cont = cont;
shellCmd = prevArgs: ''
wait ${escapeShellArg arg}
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} "$?"
'';
};
io.readLine = cont: {
cont = cont;
shellCmd = prevArgs: ''
read r
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} \"''${r/\"/\\\"}\"
'';
};
io.fork = cont: {
cont = cont;
shellCmd = prevArgs: ''
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} null &
IS_CONTINUATION=1 run ./interpret.nix ${escapeShellArgs prevArgs} "$!"
'';
};
io.detach = child: cont:
io.fork (maybeChildPid:
if maybeChildPid == null then child else cont maybeChildPid
);
io.exit = status: {
shellCmd = st: ''
exit ${toString status}
'';
cont = builtins.abort "exiting";
};
io.for = as: f: cont:
if as == []
then cont []
else
let
a = builtins.head as;
as' = builtins.tail as;
in
f a (b:
io.for as' f (bs': cont ([b] ++ bs'))
);
io.for_ = as: f:
io.for as (a: g: f a (g {}));
start = fname: f: args:
let
cmd =
f { inherit args io; };
in
cmd.shellCmd [fname (builtins.toJSON { inherit args; })];
runEvents = f: st0: evs:
let
stn =
builtins.foldl' (st: ev: st // st.cont ev)
st0
(map builtins.fromJSON evs);
in
stn.shellCmd ([f] ++ evs);
in main
#!/usr/bin/env nix-shell
#!nix-shell -i bash
#!nix-shell -p jq
#!nix-shell --show-trace
set -eu -o pipefail
run() {
local a b s;
shift
s="["
for a in "$@"; do
b="${a//\\/\\\\}" # \ -> \\
s+='"'"${b//\"/\\\"}"'"'
done
eval "$(nix-instantiate "$f" --arg "args" "$s]" --eval --json --show-trace | jq -r)"
}
f="$1"
shift
run $f "$@"
@roberth
Copy link
Author

roberth commented Jun 21, 2019

So this thing does input and output with side effects, concurrently, implemented by event sourcing. It's as bad as it sounds, but surprisingly suitable for small interactive programs.

❤️ What's good about this:

  • you can do stuff

🤓 What I've learned:

  • currying without types isn't fun
  • continuations are fun in a very specific way
  • all continuations should have the same interface (mixing unary and nullary 'functions' was error prone)
  • this is another case showing that reading/input operations are largely in the same bucket with side effects (see what's bad)

😈 What's bad:

  • performance is O(#sideEffects^2 / concurrency + cumulativeInputSize)
  • technically impure functions in Nix like builtins.readFile may break the runtime (model them as commands instead!)
  • the number of side effects / inputs is limited by the limit on command line arguments
  • everything except inputs is recomputed after a side effect
  • run.nix is bash, but no one cares except the highlighter
  • everything else

🚀 Future work

  • rebuild this on HNix to avoid some of the nasty bits
  • build a scaffolding tool with this

💯 Credits

  • Alberto G. Corona's transient also uses event sourcing underneath (but is way more clever about it). I might not have considered writing this thing as an option otherwise.
  • LISP (thanks for letting me borrow the parens)
  • Eelco and the Nix community

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment