Order of execution

Had a bit of time at lunch, so I POC’ed something.

The “something” I was thinking of involves the difference between push and pull models of computation (which James McCartney mentioned in passing at the first SC symposium, years ago). Pbind and SynthDef building are currently “push” models – you start at the top and push values down. In this model, calculations have to be written in the right order – if B depends on A, you can’t do B if A happened to be written later.

But an alternate model is to “pull” a value from the root node of a tree of operations. If this operation depends on any values that haven’t been resolved yet, then the operation itself can call for them to be resolved – effectively, a tree traversal.

The advantage in the context of this thread is, when “pulling” from a root node, the order in which the nodes are defined doesn’t matter, because values are resolved as needed, on demand. (This is definitely inspired by Haskell.) So this could support “Pkey [to] look forward” and UGens declared later in the code than their usage.

// class definitions
// proof of concept - hjh 2020-09-11

// basically Thunk, possibly could fold this into Thunk
LazyResult : AbstractFunction {
	var func, value;
	var originalFunc;
	*new { |func| ^super.newCopyArgs(func, nil, func) }

	value { |... args|
		^if(this.isResolved) {
			value
		} {
			value = func.value(*args);
			func = nil;
			value
		}
	}

	isResolved { ^func.isNil }

	reset { func = originalFunc; value = nil }
}

LambdaEnvir {
	var envir;
	var resolvingKeys;

	*new { |env|
		^super.new.init(env);
	}

	init { |env|
		envir = Environment.new;
		env.keysValuesDo { |key, value|
			envir.put(key, LazyResult(value));
		};
	}

	at { |key|
		var entity = envir.at(key);
		if(entity.isResolved) {
			// should be an already-calculated value,
			// but we need to return it below
			entity = entity.value;
		} {
			if(resolvingKeys.isNil) {
				resolvingKeys = IdentitySet.new;
			};
			if(resolvingKeys.includes(key)) {
				Error("LambdaEnvir: Circular reference involving '%'".format(key)).throw;
			};
			resolvingKeys.add(key);
			protect {
				if(currentEnvironment === this) {
					entity = entity.value(this);
				} {
					this.use {
						entity = entity.value(this);
					};
				};
			} {
				resolvingKeys.remove(key);
			};
		};
		^entity
	}

	put { |key, value| envir.put(key, value) }

	use { |func|
		var saveEnvir = currentEnvironment;
		var result;
		protect {
			currentEnvironment = this;
			result = func.value;
		} {
			currentEnvironment = saveEnvir;
		};
		^result
	}

	keysValuesDo { |func|
		envir.keysValuesDo(func)
	}

	reset {
		this.keysValuesDo { |key, value| value.reset }
	}
}

+ Object {
	isResolved { ^true }
}

With this in your extensions directory, then:

(
l = LambdaEnvir((
	a: { 10.rand },
	b: { ~a + 1 }
));

l[\b]  // between 1 and 10
)

l[\b]  // same value again -- this LambdaEnvir is already resolved

l.reset; l[\b]  // new value

// can you crash it?
// Answer: no
(
l = LambdaEnvir((
	a: { ~b + 1 },
	b: { ~a - 1 }
));

l[\b]
)

-->
ERROR: LambdaEnvir: Circular reference involving 'b'

And even SynthDef building:

(
l = LambdaEnvir((
	out: { Out.ar(\out.kr, (~filter * (~eg * \amp.kr(0.1))).dup) },
	eg: { EnvGen.kr(Env.perc, doneAction: 2) },
	filter: { RLPF.ar(~osc, \ffreq.kr(2000), \rq.kr(1)) },
	osc: { Saw.ar(\freq.kr(440)) }
));

SynthDef(\test, { l[\out] }).add;
)

// or even
(
l = LambdaEnvir((
	synthdef: { SynthDef(\test, { ~out }) },
	out: { Out.ar(\out.kr, (~filter * (~eg * \amp.kr(0.1))).dup) },
	eg: { EnvGen.kr(Env.perc, doneAction: 2) },
	filter: { RLPF.ar(~osc, \ffreq.kr(2000), \rq.kr(1)) },
	osc: { Saw.ar(\freq.kr(440)) }
));

l[\synthdef].add;
)

It needs a little more work before it could run transparently with Pkey, but I can see in my head how to do it. Also, currently LambdaEnvir is not a drop-in replacement for environments or events – for this POC, I didn’t bother to mimic the entire class interface. (Also, for SynthDef building, this currently doesn’t deal so well with non-pure UGens like BufWr, SendReply, FreeSelf etc, which are often used without plugging their output into another unit’s input – I’ve got an idea for that, but won’t do it today.)

But I think this is promising… it’s probably fiddly in ways that I haven’t realized yet, but there’s something appealing here…

hjh