Order of execution

Edit
I just remembered that I made a similar topic about serial processing when using all sig variables , guess this is related.
And just found out about the amazing ctrl+shift + cursor keys or swapping lines :slight_smile:

You have to create a UGen before you use it as an input somewhere else.

You can’t flip lines around into a random order and expect them to keep working.

Dependency is the key concept here.

The Decay unit depends on whatever b is.

If b is not yet valid, then the Decay expression will fail.

So, whenever you have one expression Z that depends on one or more other expressions (say, X and Y), then X and Y must be written first. There is no way to write Z X Y and expect it to work.

(There are languages such as Haskell where calculations are described as networks of relationships and the compiler determines the order. These languages are rare. SC is not one of them. These are temporal sequences of operations and the order is your responsibility to get right.)

hjh

Makes sense , just like max msp or loomer architect which have top,bottom left right approach etc…

Who said I fliiped lines around in a random order ?
I am learning and experimenting in supercollider , adhering the rules
By flipping lines around I meant that the first (wrong example ) can easily be turned into a working example by putting my cursor on the line where decay u-gen resides , holding crl+shift and press the down arrow

Experimenting is a valid way of learning, sure. What I meant was, you actually ran into this situation before (“Sadly there is no way to mute individual lines to get to the bottom of this” @ Chains of reassignment), where you tried to use a signal as an input to something else, but the signal hadn’t been prepared. It’s the same thing. If you’re using a signal input, there must be a signal to input.

So, in the first example here, either you expected it to work, or you expected it to fail. If you expected it to fail, then you already understand the principle (and you already know the answer – so, you could start to have a bit more confidence).

If you expected it to work, then it suggests to me that you might be under-generalizing from the experimental results – seeing the earlier question and this question as distinct, separate situations when in fact they are variants of the same principle. This is a tricky thing – nobody knows, at the beginning, how much to generalize findings. From where I sit, it looks to me like you tend to view the results of your experiments as small-scale ideas when, often, they point to broader principles – IMO, you could afford to generalize a bit more.

For instance, if you use patterns later on, eventually you’ll find Pkey, which allows reuse of previously calculated values:

// this is ok
p = Pbind(
    \a, Pwhite(1, 10, inf),
    \b, Pkey(\a) * 2
);

// this will throw an error when played
p = Pbind(
    \b, Pkey(\a) * 2,
    \a, Pwhite(1, 10, inf)
);

The surface appearance is different but it’s the same principle.

hjh

hmmm it would sure be nice if Pkey could look forward… or maybe that would be a can of worms…

If you want to make changes to an Event based on its “final” state, but before its type Function is evaluated, you can use the \finish key, which takes a Function into which you can pass the Event. As with Pfunc, you can alter or remove existing keys in the Event, add new ones, etc in this Function:

(
    p = Pbind(
        \finish, {|ev| 
            ev.degree = ev.degree % 8 + [0, 2]; // alter a key
            ev.dur = 0.1; // add a key
            ev.removeAt(\legato) // remove a key
        },
        \legato, 20,
        \degree, Pseries()
    ).play
)

This is a contrived example and in practice I probably wouldn’t have \finish first in the interest of readability; just wanted to illustrate that it’s possible.

that’s super useful! thanks for the tip!

where is this documented? not finding… thx!

It’s mentioned here, in “A Practical Guide” and here, in “Understanding Streams, Patterns, and Events”. Actual implementation here, with an excellent plain english explanation in this comment.

Hope that helps! It was quite eye opening for me when I discovered it.

There’s no way to evaluate an expression containing Pkey if the referent isn’t available – that’s impossible.

But the situation you’re asking about is, what if the referent is defined in the Pbind, just, later?

Technically that would (usually) be possible if we could determine the event-key dependencies of every key-pattern pair in the Pbind, and reorder the keys such that it’s always “keys being referred to” first, then the Pkey(s).

A couple of counterexamples:

// algebraically correct, but also un-resolvable
Pbind(
    \a, Pkey(\b) + 1,
    \b, Pkey(\a) - 1
) 

// Plazy, or Pfunc for that matter: no reliable way to find dependencies
Pbind(
    \a, Plazy {
        Pkey(("a" ++ 3.rand).asSymbol)
    },
    \a0, Pwhite(0, 9, inf),
    \a1, Pwhite(10, 19, inf),
    \a2, Pwhite(20, 29, inf)
)

OTOH… hmm… I’m thinking of something now. It’s not fully formed in my mind, so I won’t go deep into it, but it might loosely look like a “LazyEvent” where Pbind would put all the streams into the event and then any at into the event would a/ if it finds a stream that hasn’t been resolved, evaluate it or b/ return a previously evaluated result. It might work, though my first counterexample (a and b being mutually dependent) would hang the interpreter under this alternate model (where it throws an error now).

hjh

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

Extension, added to LambdaEnvir:

LazyPbind : Pbind {
	embedInStream { |inevent|
		var proto = Event.new;
		var lambdaEnvir;
		var event;

		// these will become LazyResults later
		// this is OK because both streams and functions respond to 'value'
		patternpairs.pairsDo { |key, value|
			proto.put(key, value.asStream)
		};

		loop {
			if(inevent.isNil) { ^nil.yield };
			// passing 'proto' here populates all the LazyResult objects
			lambdaEnvir = LambdaEnvir(proto)
			.parent_(inevent.parent).proto_(inevent.proto);
			event = inevent.copy;
			lambdaEnvir.keysValuesDo { |key|
				var value = lambdaEnvir[key];  // this is what resolves the LazyResult
				if(value.isNil) { ^inevent };
				if(key.isSequenceableCollection) {
					if(key.size > value.size) {
						("the pattern is not providing enough values to assign to the key set:" + key).warn;
						^inevent
					};
					key.do { |key1, i|
						event.put(key1, value[i]);
					};
				} {
					event.put(key, value);
				};
			};
			inevent = event.yield;
		}
	}
}

// oh, and LambdaEnvir needs to add:
+ LambdaEnvir {
	parent { ^envir.parent }
	proto { ^envir.proto }
	parent_ { |parent| envir.parent_(parent) }
	proto_ { |proto| envir.proto_(proto) }
}

Then:

(
p = LazyPbind(
	\instrument, \default,
	\freq, Pkey(\dur).linexp(0.05, 0.25, 200, 800),
	\dur, Pwhite(0.05, 0.25, inf)
).play;
)

p.stop;

Here’s \freq depending on \dur that is defined later, and… it works.

So… are we onto something here?

hjh

2 Likes

this is very appealing I’m excited to play with it…

love the SynthDef building example - stays legible without the annoyance of a big variable declaration block at the top. Also seems like a handy way to get a modular arrangement going perhaps.

I don’t want to oversell that – LambdaEnvir doesn’t support arguments being passed to the functionally-defined values, so it wouldn’t support reusable components (or recursion, for that matter). You could declare reusable components outside the LambdaEnvir and use them inside (but you can do that in normal SynthDef style too). But it would go some distance toward relieving the need to handle calculation dependencies by hand.

hjh

My next question would be whether LazyPbind shouldn’t replace just plain Pbind - is there a downside?

I can think of one downside.

p = Pbind(
    // begin a calculation
    \a, Pwhite(0, 9, inf),

    // now get a limiting value from somewhere
    \b, Pfunc { aControlBus.getSynchronous },

    // and finish the calculation
    \a, min(Pkey(\a), Pkey(\b))
)

LazyPbind can have only one \a, so it would break compatibility with the above. I’m pretty sure I have code like that lying around somewhere.

hjh

…didn’t know you could do that! I’ve only ever repeated keys to A/B possibilities… could have “new Pbind” check for repeated keys and fall back to “legacy Pbind” if so…

Another:

p = Pbind(
    \degree, Pwhite(0, 7, inf),
    \dur, 0.5,
    \callback, { |event|
        funcToRegisterIDs.value(event[\id])
    }
).play;

In the current Pbind, a function assigned to a key is neither a pattern nor a stream (responds to asStream with itself), so the function is stored in the event as-is and not evaluated until event-play time (\callback and \finish in particular).

The LambdaEnvir used in this POC evaluates functions as lazy values, so \callback would be evaluated during Pbind processing (which is early – the IDs won’t be populated yet).

It should work to write \callback, { { /* code */ } } but that breaks compatibility with existing code.

I suppose there could be a LazyStreamResult that calls next instead of value (which, I think, I’d have to check, should evaluate streams but pass functions through).

hjh

I’m going to use this in work for a bit and see how it goes. I think it solves some long standing annoyances (for me!) Perhaps this merits its own thread?

I’m using it too :wink: It’s quite fun to be able to write e.g.:

SynthDef(\fmfb, { |out, gate = 1, freq = 440, detun = 1.008, freqlag = 0.08,
	ratio = 1, index = 1.433, fbAmt = 0,
	ffreq = 12000, rq = 1, ffreqMul = 1, fegAtk = 0.01, fegDcy = 0.1,
	acc = 0,
	atk = 0.2, dcy = 0.4, sus = 0.8, rel = 0.4,
	mAtk = 0.2, mDcy = 0.4, mSus = 1, mRel = 1.2,
	pan = 0, width = 0.6, amp = 0.1|
	var n = 5;
	var sig = LambdaEnvir((
		freqs: { Lag.kr(freq, freqlag) * ~detunes },
		detunes: { Array.fill(n, { detun ** Rand(-1, 1) }) },
		mods: {
			var sig;
			sig = SinOsc.ar(~freqs * Lag.kr(ratio, freqlag), ~feedback % 2pi) * ~modEg;
			LocalOut.ar(sig);
			sig
		},
		feedback: { LocalIn.ar(n) * fbAmt },
		cars: { SinOsc.ar(~freqs, (~mods * index) % 2pi) },
		modEg: { EnvGen.kr(Env.adsr(mAtk, mDcy, mSus, mRel), gate) },
		carEg: { EnvGen.kr(Env.adsr(atk, dcy, sus, rel), gate, doneAction: 2) },
		mix: { Splay.ar(~cars, width, center: pan.clip2(1 - abs(width))) },
		filter: { BLowPass.ar(~mix, (ffreq * ~feg).clip(20, 20000), rq) },
		feg: { EnvGen.kr(Env([1, ffreqMul * ~accent, 1], [fegAtk, fegDcy], \exp)) },
		accent: { acc + 1 },
		out: { ~filter * (~carEg * amp) }
	)).at(\out);
	Out.ar(out, sig);
}).add;

Here’s a bit hacky fix for the {}.next vs {}.value issue:

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

// basically Thunk, possibly could fold this into Thunk
LazyResult : AbstractFunction {
	var func, value;
	var originalFunc;
	var <>method;

	*new { |func, method(\value)| ^super.newCopyArgs(func, nil, func, method) }

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

	isResolved { ^func.isNil }

	reset { func = originalFunc; value = nil }
}

LambdaEnvir {
	var envir;
	var resolvingKeys;

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

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

	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 {
				this.use {
					entity = entity.value(this);
					// "LambdaEnvir resolved '%' to %\n".postf(key, entity);
				};
			} {
				resolvingKeys.remove(key);
			};
		};
		^entity
	}

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

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

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

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

	parent { ^envir.parent }
	proto { ^envir.proto }
	parent_ { |parent| envir.parent_(parent) }
	proto_ { |proto| envir.proto_(proto) }
}

LazyPbind : Pbind {
	embedInStream { |inevent|
		var proto = Event.new;
		var lambdaEnvir;
		var event;

		patternpairs.pairsDo { |key, value|
			proto.put(key, value.asStream)
		};

		loop {
			if(inevent.isNil) { ^nil.yield };
			lambdaEnvir = LambdaEnvir(proto, method: \next)
			.parent_(inevent.parent).proto_(inevent.proto);
			event = inevent.copy;
			lambdaEnvir.keysValuesDo { |key|
				var value = lambdaEnvir[key];
				if(value.isNil) { ^inevent };
				if(key.isSequenceableCollection) {
					if(key.size > value.size) {
						("the pattern is not providing enough values to assign to the key set:" + key).warn;
						^inevent
					};
					key.do { |key1, i|
						event.put(key1, value[i]);
					};
				} {
					event.put(key, value);
				};
			};
			inevent = event.yield;
		}
	}
}

+ Object {
	isResolved { ^true }
}

hjh

1 Like