Call by hash - NixOS Hashes in URLs

I just wrote a blog post about what happens when you make URLs immutable: mapping from a hash to a NixOS configuration of that hash. Turns out “nice” deployments (fast, zero-downtime, atomic) become much easier, without the complexity of something like Kubernetes. Blog post here, feedback appreciated!

12 Likes

That’s really cool!

I was having similar crazy thoughts:
If file paths are pointers, so are urls.
Why not push nix to start taming them!

PATH variable = dns suffix list (avoid)
file path = dns A record
inode number = ip address
symlink = dns CNAME record
signature = cert/pub key

centralized version of zooko triangle:
(half-baked thoughts, please be lenient)

  • human friendly name (dns records)
    friendly name CNAME record to hash of service A record
  • centralized number (ip addresses)
    with ipv6, there should be enough bits to truncate hash of service to an iid…
    (got to be aware of birthday problem though…)
  • secure identifier (public keys)
    still have to trust public keys, but signature games ease the burden.

I can’t wait to read about how persistence gets handled!

1 Like

This discussion went even further at least as early as 2021: Announcing Nomia, a general resource manager inspired by Nix

To summarise: What if any operating system resource was immutably addressable?

I very much subscribe to this view, and would go even further: What if any computation state was immutably addressable?

Racket – one of the most alive and relevant Lisps – pretty much goes in this direction with its first-class treatment of continuations.

7 Likes

There’s also https://www.unison-lang.org/ which takes this idea to the expression level. Everything is content addressed. Everything is distributed. It’s very neat

5 Likes

i couldn’t figure out nomia,
but found a paper and tried to write a CESK* machine to think about
immutably-addressable first-class undelimited continuations:

attempt
#!/usr/bin/env -S nix shell nixpkgs#rlwrap nixpkgs#swiProlog --command rlwrap swipl

:- use_module(library(crypto)).
:- use_module(library(http/json)).
:- initialization(main, main).

% this stuff is too advanced for me
% https://arxiv.org/pdf/1007.4446
% butchered from Figure 3 and Figure 12
% (Figure 8 is fun to try to make content-addressable)

% REPL
main :- write("call-by-value lambda-calculus with call/cc (and nats):\n"),
	repeat, repl.
repl :- write("λ> "), read_line_to_codes(user_input, Input),
	parser(Input,[Exp]), eval(Exp, Out),
	writef("\e[32mANSWER=>\e[0m %w\n\n", [Out]), !, fail.

% Parser
parser(String, Exps) :- phrase(exp(Exps), String).
exp([E]) --> wso, sexp(E), wso.
exp([]) --> [].

sexp(E) --> slist(E).
sexp(E) --> satom(E).

slist(E) --> "(", wso, elm(E), wso, ")".
elm([E|Es]) --> sexp(E), wsr, !, elm(Es).
elm([E]) --> sexp(E).
elm([]) --> [].

satom(A) --> symbol(Cs), { atom_codes(A,Cs)}.
satom(A) --> number(Cs), { number_codes(A,Cs)}.

symbol([C|Cs]) --> symbol_first(C), symbol_rest(Cs).
symbol_first(C) --> [C], { code_type(C, alpha) }.
symbol_rest([C|Cs]) --> [C], { code_type(C, alnum); string_codes([C],"/") }, symbol_rest(Cs).
symbol_rest([]) --> [].

number([C|Cs]) --> digit(C), number(Cs).
number([C]) --> digit(C).
digit(C) --> [C], { code_type(C, digit) }.

wsr --> ws, wsr; ws.
wso --> ws, wso; [].
ws --> [W], { code_type(W, white); code_type(W, space) }.

% Control String (code)
term(Exp) :-
	variable(Exp);
	abstraction(Exp,_,_);
	application(Exp,_,_);
	number(Exp).
value(Exp) :-
	closure(Exp, _, _, _);
	number(Exp);
	continuation(Exp).
variable(Exp) :-
	atom(Exp), \+ address(Exp).
abstraction(Exp, Var, Body) :-
	Exp = [lambda, [Var], Body],
	variable(Var), term(Body).
application(Exp, E1, E2) :-
	Exp = [E1, E2],
	term(E1), term(E2).
procedure(Exp) :-
	closure(Exp,_,_,_);
	callcc(Exp);
	continuation(Exp).
closure(Exp, Var, Body, Env) :-
	Exp = [closure, [Var], Body, Env],
	variable(Var), term(Body), environment(Env).
callcc('call/cc').
continuation(Exp) :-
	address(Exp).
address(Addr) :-
	atom(Addr), atom_concat('a-', A, Addr),
	catch(hex_bytes(A,_),_, false),
	string_length(A,64).

% Environment (Variable to Address)
environment(T) :- is_dict(T,env).
lookup(Var, Env, Address) :-
	Address = Env.get(Var),
	variable(Var), environment(Env), address(Address).
extendEnv(Var, Address, Env, Out) :-
	Out = Env.put(Var, Address),
	variable(Var), address(Address), environment(Env).

% Store (Address to Storable)
store(T) :- is_dict(T,store).
storable(S) :-
	value(S);
	kont(S).
alloc(Term, Address) :-
	term_string(Term, Str),
	crypto_data_hash(Str, Hash, [algorithm(sha256)]),
	atom_string(Addr, Hash),
	atom_concat('a-', Addr, Address).
dereference(Address, Store, Storable) :-
	Storable = Store.get(Address),
	address(Address), store(Store), storable(Storable).
extendStore(Address, Storable, Store, Out) :-
	Out = Store.put(Address, Storable),
	address(Address), storable(Storable), store(Store).

% Kontinuations (just think stack)
k_mt(Frame) :-
	Frame = [k_mt].
k_arg(Frame, Exp, Env, Kont_Address) :-
	Frame = [k_arg, Exp, Env, Kont_Address],
	term(Exp), environment(Env), address(Kont_Address).
k_fun(Frame, Fun, Kont_Address) :-
	Frame = [k_fun, Fun, Kont_Address],
	address(Kont_Address), procedure(Fun).
kont(Frame) :-
	k_mt(Frame);
	k_arg(Frame, _, _, _);
	k_fun(Frame, _, _).

% Transitions
step(Exp, Env, Store, Kont, Next) :-
	variable(Exp),
	lookup(Exp, Env, Address),
	dereference(Address, Store, Val),
	Next = [Val, Env, Store, Kont],
	printState("Variable Lookup", Next).
step(Exp, Env, Store, Kont, Next) :-
	abstraction(Exp, Var, Body),
	closure(Closure, Var, Body, Env),
	Next = [Closure, Env, Store, Kont],
	printState("Abstraction", Next).
step(Exp, Env, Store, Kont, Next) :-
	application(Exp, E1, E2),
	k_arg(Frame, E2, Env, Kont),
	alloc(Frame, KontA),
	extendStore(KontA, Frame, Store, StoreE),
	Next = [E1, Env, StoreE, KontA],
	printState("Application", Next).
step(Exp, _, Store, Kont, Next) :-
	procedure(Exp),
	dereference(Kont, Store, Frame),
	k_arg(Frame, Arg, EnvA, KontA),
	k_fun(FrameF, Exp, KontA),
	alloc(FrameF, KontF),
	extendStore(KontF, FrameF, Store, StoreE),
	Next = [Arg, EnvA, StoreE, KontF],
	printState("Continue to Argument", Next).
step(Exp, _, Store, Kont, Next) :-
	value(Exp),
	dereference(Kont, Store, Frame),
	k_fun(Frame, Closure, KontF),
	closure(Closure, Var, Body, EnvC),
	alloc(Exp, Address),
	extendStore(Address, Exp, Store, StoreE),
	extendEnv(Var, Address, EnvC, EnvE),
	Next = [Body, EnvE, StoreE, KontF],
	printState("Continue into Function", Next).
step(Exp, _, Store, Kont, Next) :-
	closure(Exp, Var, Body, EnvC),
	dereference(Kont, Store, Frame),
	k_fun(Frame, Fun, KontC),
	callcc(Fun),
	alloc(KontC, Address),
	extendStore(Address, KontC, Store, StoreE),
	extendEnv(Var, Address, EnvC, EnvE),
	Next = [Body, EnvE, StoreE, KontC],
	printState("call/cc Closure", Next).
step(Exp, Env, Store, Kont, Next) :-
	continuation(Exp),
	dereference(Kont, Store, Frame),
	k_fun(Frame, Fun, _),
	callcc(Fun),
	dif(Exp, Kont), % if same, then it inf loops...
	Next = [Kont, Env, Store, Exp],
	printState("call/cc Continuation", Next).
step(Exp, Env, Store, Kont, Next) :-
	continuation(Exp),
	dereference(Kont, Store, Frame),
	k_fun(Frame, Fun, N),
	callcc(Fun),
	Exp = Kont,
	Next = [Kont, Env, Store, N],
	% to get (call/cc (call/cc (lambda (k) k))) to work...
	% im not sure if this is correct or if i made a mistake somewhere...
	printState("call/cc (same) Continuation", Next).
step(Exp, Env, Store, Kont, Next) :-
	value(Exp),
	dereference(Kont, Store, Frame),
	k_fun(Frame, C, _),
	address(C),
	Next = [Exp, Env, Store, C],
	printState("Apply Continuation", Next).

% json for cheap dict pretty printing
printState(Name, [Exp, Env, Store, Kont]) :-
	writef("\e[31m%w====>\e[0m\n", [Name]),
	writef("\e[33m{C}:\e[0m\n%w\n", [Exp]),
	writef("\e[33m{E}:\e[0m\n", []),
	json_write(current_output,Env), write("\n"),
	writef("\e[33m{S}:\e[0m\n", []),
	json_write(current_output,Store), write("\n"),
	writef("\e[33m{K*}:\e[0m\n%w\n\n\n", [Kont]).

% machine stops when C has a value and K* points to k_mt (empty).
answer(State) :-
	State = [Exp, _, Store, Kont],
	value(Exp),
	dereference(Kont, Store, Frame),
	k_mt(Frame).
run(State, Out) :-
	answer(State),
	State = [Out, _, _, _];
	\+ answer(State),
	State = [Exp, Env, Store, Kont],
	step(Exp, Env, Store, Kont, Next),
	run(Next, Out).
inject(Exp, State) :-
	k_mt(Frame),
	alloc(Frame, KontA),
	extendStore(KontA, Frame, store{}, Store),
	State = [Exp, env{}, Store, KontA].
eval(Exp, Out) :-
	inject(Exp, State),
	printState("Init", State),
	run(State, Out).

% Escape before getting sucked into an infinite loop:
% (call/cc (lambda (esc) ((lambda (x) ((lambda (i) (i i)) (lambda (i) (i i)))) (esc 10))))

sorry if off-topic,
what would immutably-addressable first-class environments mean?

I think I missed the point a little;
is this pushing down the resolution further into the script by replacing the paths in the Nix files?

You get the hash right now via Git → Flake but this would be a standalone variant?
(I think also the same with Flake on it’s own but you have no way to reference it by content which is what Git is for)