Async lang behaviour - how to this could be made easier for new users

Orginal impl

Realised the original implementation isn’t here. Its pretty hairy at the moment, but it is essentially an auto-promise wrapper. It works with all classes (except name args don’t work, which was why I started this thread) and just calls wait if needed. Its definitely a work in progress.

Impl
ObjectPreCaller  {
	var <>impl_underlyingObject;
	var <>impl_preFunc;

	// DO NOT WAIT ON THIS METHOD AS THE INTERPRETER USES IT TO PRINT
	asString { |limit| ^impl_underlyingObject.asString(limit)  }
	class { ^impl_underlyingObject.class() }

	dump { impl_preFunc.(); ^impl_underlyingObject.dump() }
	post { impl_preFunc.(); ^impl_underlyingObject.post()}
	postln { impl_preFunc.(); ^impl_underlyingObject.postln()}
	postc { impl_preFunc.(); ^impl_underlyingObject.postc()}
	postcln { impl_preFunc.(); ^impl_underlyingObject.postcln()}
	postcs { impl_preFunc.(); ^impl_underlyingObject.postcs()}
	totalFree { impl_preFunc.(); ^impl_underlyingObject.totalFree() }
	largestFreeBlock { impl_preFunc.(); ^impl_underlyingObject.largestFreeBlock() }
	gcDumpGrey { impl_preFunc.(); ^impl_underlyingObject.gcDumpGrey() }
	gcDumpSet { impl_preFunc.(); ^impl_underlyingObject.gcDumpSet() }
	gcInfo { impl_preFunc.(); ^impl_underlyingObject.gcInfo() }
	gcSanity { impl_preFunc.(); ^impl_underlyingObject.gcSanity() }
	canCallOS { impl_preFunc.(); ^impl_underlyingObject.canCallOS() }
	size { impl_preFunc.(); ^impl_underlyingObject.size()}
	indexedSize { impl_preFunc.(); ^impl_underlyingObject.indexedSize()}
	flatSize { impl_preFunc.(); ^impl_underlyingObject.flatSize()}
	functionPerformList { impl_preFunc.(); ^impl_underlyingObject.functionPerformList() }
	copy { impl_preFunc.(); ^impl_underlyingObject.copy()}
	contentsCopy { impl_preFunc.(); ^impl_underlyingObject.contentsCopy()}
	shallowCopy { impl_preFunc.(); ^impl_underlyingObject.shallowCopy()}
	copyImmutable { impl_preFunc.(); ^impl_underlyingObject.copyImmutable() }
	deepCopy { impl_preFunc.(); ^impl_underlyingObject.deepCopy() }
	poll { impl_preFunc.(); ^impl_underlyingObject.poll()}
	value { impl_preFunc.(); ^impl_underlyingObject.value()}
	valueArray { impl_preFunc.(); ^impl_underlyingObject.valueArray()}
	valueEnvir { impl_preFunc.(); ^impl_underlyingObject.valueEnvir()}
	valueArrayEnvir { impl_preFunc.(); ^impl_underlyingObject.valueArrayEnvir()}
	basicHash { impl_preFunc.(); ^impl_underlyingObject.basicHash()}
	hash { impl_preFunc.(); ^impl_underlyingObject.hash()}
	identityHash { impl_preFunc.(); ^impl_underlyingObject.identityHash()}
	next { impl_preFunc.(); ^impl_underlyingObject.next()}
	reset { impl_preFunc.(); ^impl_underlyingObject.reset()}
	iter { impl_preFunc.(); ^impl_underlyingObject.iter()}
	stop { impl_preFunc.(); ^impl_underlyingObject.stop()}
	free { impl_preFunc.(); ^impl_underlyingObject.free()}
	clear { impl_preFunc.(); ^impl_underlyingObject.clear()}
	removedFromScheduler { impl_preFunc.(); ^impl_underlyingObject.removedFromScheduler()}
	isPlaying { impl_preFunc.(); ^impl_underlyingObject.isPlaying()}
	embedInStream { impl_preFunc.(); ^impl_underlyingObject.embedInStream()}
	loop { impl_preFunc.(); ^impl_underlyingObject.loop()}
	asStream { impl_preFunc.(); ^impl_underlyingObject.asStream()}
	eventAt { impl_preFunc.(); ^impl_underlyingObject.eventAt()}
	finishEvent { impl_preFunc.(); ^impl_underlyingObject.finishEvent()}
	atLimit { impl_preFunc.(); ^impl_underlyingObject.atLimit()}
	isRest { impl_preFunc.(); ^impl_underlyingObject.isRest()}
	threadPlayer { impl_preFunc.(); ^impl_underlyingObject.threadPlayer()}
	threadPlayer_ { impl_preFunc.(); ^impl_underlyingObject.threadPlayer_()}
	isNil { impl_preFunc.(); ^impl_underlyingObject.isNil()}
	notNil { impl_preFunc.(); ^impl_underlyingObject.notNil()}
	isNumber { impl_preFunc.(); ^impl_underlyingObject.isNumber()}
	isInteger { impl_preFunc.(); ^impl_underlyingObject.isInteger()}
	isFloat { impl_preFunc.(); ^impl_underlyingObject.isFloat()}
	isSequenceableCollection { impl_preFunc.(); ^impl_underlyingObject.isSequenceableCollection()}
	isCollection { impl_preFunc.(); ^impl_underlyingObject.isCollection()}
	isArray { impl_preFunc.(); ^impl_underlyingObject.isArray()}
	isString { impl_preFunc.(); ^impl_underlyingObject.isString()}
	containsSeqColl { impl_preFunc.(); ^impl_underlyingObject.containsSeqColl()}
	isValidUGenInput { impl_preFunc.(); ^impl_underlyingObject.isValidUGenInput()}
	isException { impl_preFunc.(); ^impl_underlyingObject.isException()}
	isFunction { impl_preFunc.(); ^impl_underlyingObject.isFunction()}
	trueAt { impl_preFunc.(); ^impl_underlyingObject.trueAt()}
	mutable { impl_preFunc.(); ^impl_underlyingObject.mutable()}
	frozen { impl_preFunc.(); ^impl_underlyingObject.frozen()}
	halt { impl_preFunc.(); ^impl_underlyingObject.halt() }
	prHalt { impl_preFunc.(); ^impl_underlyingObject.prHalt() }
	primitiveFailed { impl_preFunc.(); ^impl_underlyingObject.primitiveFailed() }
	reportError { impl_preFunc.(); ^impl_underlyingObject.reportError() }
	mustBeBoolean { impl_preFunc.(); ^impl_underlyingObject.mustBeBoolean()}
	notYetImplemented { impl_preFunc.(); ^impl_underlyingObject.notYetImplemented()}
	dumpBackTrace { impl_preFunc.(); ^impl_underlyingObject.dumpBackTrace() }
	getBackTrace { impl_preFunc.(); ^impl_underlyingObject.getBackTrace() }
	throw { impl_preFunc.(); ^impl_underlyingObject.throw() }
	species { impl_preFunc.(); ^impl_underlyingObject.species()}
	asCollection { impl_preFunc.(); ^impl_underlyingObject.asCollection()}
	asSymbol { impl_preFunc.(); ^impl_underlyingObject.asSymbol()}
	asCompileString { impl_preFunc.(); ^impl_underlyingObject.asCompileString() }
	cs { impl_preFunc.(); ^impl_underlyingObject.cs()}
	storeArgs { impl_preFunc.(); ^impl_underlyingObject.storeArgs()}
	dereference { impl_preFunc.(); ^impl_underlyingObject.dereference()}
	reference { impl_preFunc.(); ^impl_underlyingObject.reference()}
	asRef { impl_preFunc.(); ^impl_underlyingObject.asRef()}
	dereferenceOperand { impl_preFunc.(); ^impl_underlyingObject.dereferenceOperand()}
	asArray { impl_preFunc.(); ^impl_underlyingObject.asArray()}
	asSequenceableCollection { impl_preFunc.(); ^impl_underlyingObject.asSequenceableCollection()}
	rank { impl_preFunc.(); ^impl_underlyingObject.rank()}
	slice { impl_preFunc.(); ^impl_underlyingObject.slice()}
	shape { impl_preFunc.(); ^impl_underlyingObject.shape()}
	unbubble { impl_preFunc.(); ^impl_underlyingObject.unbubble()}
	yield { impl_preFunc.(); ^impl_underlyingObject.yield() }
	alwaysYield { impl_preFunc.(); ^impl_underlyingObject.alwaysYield() }
	dependants { impl_preFunc.(); ^impl_underlyingObject.dependants() }
	release { impl_preFunc.(); ^impl_underlyingObject.release() }
	releaseDependants { impl_preFunc.(); ^impl_underlyingObject.releaseDependants() }
	removeUniqueMethods { impl_preFunc.(); ^impl_underlyingObject.removeUniqueMethods() }
	inspect { impl_preFunc.(); ^impl_underlyingObject.inspect()}
	inspectorClass { impl_preFunc.(); ^impl_underlyingObject.inspectorClass()}
	inspector { impl_preFunc.(); ^impl_underlyingObject.inspector() }
	crash { impl_preFunc.(); ^impl_underlyingObject.crash() }
	stackDepth { impl_preFunc.(); ^impl_underlyingObject.stackDepth() }
	dumpStack { impl_preFunc.(); ^impl_underlyingObject.dumpStack() }
	dumpDetailedBackTrace { impl_preFunc.(); ^impl_underlyingObject.dumpDetailedBackTrace() }
	freeze { impl_preFunc.(); ^impl_underlyingObject.freeze() }
	beats_ { impl_preFunc.(); ^impl_underlyingObject.beats_()  }
	isUGen { impl_preFunc.(); ^impl_underlyingObject.isUGen()}
	numChannels { impl_preFunc.(); ^impl_underlyingObject.numChannels()}
	clock_ { impl_preFunc.(); ^impl_underlyingObject.clock_()  }
	asTextArchive { impl_preFunc.(); ^impl_underlyingObject.asTextArchive() }
	asBinaryArchive { impl_preFunc.(); ^impl_underlyingObject.asBinaryArchive() }
	help { impl_preFunc.(); ^impl_underlyingObject.help()}
	asArchive { impl_preFunc.(); ^impl_underlyingObject.asArchive() }
	initFromArchive { impl_preFunc.(); ^impl_underlyingObject.initFromArchive()}
	archiveAsCompileString { impl_preFunc.(); ^impl_underlyingObject.archiveAsCompileString()}
	archiveAsObject { impl_preFunc.(); ^impl_underlyingObject.archiveAsObject()}
	checkCanArchive { impl_preFunc.(); ^impl_underlyingObject.checkCanArchive()}
	isInputUGen { impl_preFunc.(); ^impl_underlyingObject.isInputUGen()}
	isOutputUGen { impl_preFunc.(); ^impl_underlyingObject.isOutputUGen()}
	isControlUGen { impl_preFunc.(); ^impl_underlyingObject.isControlUGen()}
	source { impl_preFunc.(); ^impl_underlyingObject.source()}
	asUGenInput { impl_preFunc.(); ^impl_underlyingObject.asUGenInput()}
	asControlInput { impl_preFunc.(); ^impl_underlyingObject.asControlInput()}
	asAudioRateInput { impl_preFunc.(); ^impl_underlyingObject.asAudioRateInput()}
	slotSize { impl_preFunc.(); ^impl_underlyingObject.slotSize() }
	getSlots { impl_preFunc.(); ^impl_underlyingObject.getSlots() }
	instVarSize { impl_preFunc.(); ^impl_underlyingObject.instVarSize()}

	do { arg function; impl_preFunc.(); ^impl_underlyingObject.do( function ); }
	generate { arg function, state; impl_preFunc.(); ^impl_underlyingObject.generate( function, state ); }
	isKindOf { arg aClass; impl_preFunc.(); ^impl_underlyingObject.isKindOf( aClass );  }
	isMemberOf { arg aClass; impl_preFunc.(); ^impl_underlyingObject.isMemberOf( aClass ); }
	respondsTo { arg aSymbol; impl_preFunc.(); ^impl_underlyingObject.respondsTo( aSymbol ); }
	performMsg { arg msg; impl_preFunc.(); ^impl_underlyingObject.performMsg( msg );  }
	perform { arg selector ... args; impl_preFunc.(); ^impl_underlyingObject.perform( selector, *args );  }
	performList { arg selector, arglist; impl_preFunc.(); ^impl_underlyingObject.performList( selector, arglist );  }
	superPerform { arg selector ... args; impl_preFunc.(); ^impl_underlyingObject.superPerform( selector, *args );  }
	superPerformList { arg selector, arglist; impl_preFunc.(); ^impl_underlyingObject.superPerformList( selector, arglist );  }
	tryPerform { arg selector ... args; impl_preFunc.(); ^impl_underlyingObject.tryPerform( selector, *args );  }
	multiChannelPerform { arg selector ... args; impl_preFunc.(); ^impl_underlyingObject.multiChannelPerform( selector, *args );  }
	performWithEnvir { arg selector, envir; impl_preFunc.(); ^impl_underlyingObject.performWithEnvir( selector, envir );  }
	performKeyValuePairs { arg selector, pairs; impl_preFunc.(); ^impl_underlyingObject.performKeyValuePairs( selector, pairs );  }
	dup { arg n ; impl_preFunc.(); ^impl_underlyingObject.dup( n );  }
	! { arg n; impl_preFunc.(); ^(impl_underlyingObject !  n);  }
	== { arg obj; impl_preFunc.(); ^(impl_underlyingObject ==  obj);  }
	!= { arg obj; impl_preFunc.(); ^(impl_underlyingObject !=  obj);  }
	=== { arg obj; impl_preFunc.(); ^(impl_underlyingObject ===  obj); }
	!== { arg obj; impl_preFunc.(); ^(impl_underlyingObject !==  obj); }
	equals { arg that, properties; impl_preFunc.(); ^impl_underlyingObject.equals( that, properties );  }
	compareObject { arg that, instVarNames; impl_preFunc.(); ^impl_underlyingObject.compareObject( that, instVarNames );  }
	instVarHash { arg instVarNames; impl_preFunc.(); ^impl_underlyingObject.instVarHash( instVarNames );  }
	|==| { arg that; impl_preFunc.(); ^(impl_underlyingObject |==|  that);  }
	|!=| { arg that; impl_preFunc.(); ^(impl_underlyingObject |!=|  that);  }
	prReverseLazyEquals { arg that; impl_preFunc.(); ^impl_underlyingObject.prReverseLazyEquals( that );  }
	-> { arg obj; impl_preFunc.(); ^(impl_underlyingObject ->  obj);  }
	first { arg inval; impl_preFunc.(); ^impl_underlyingObject.first( inval ); }
	cyc { arg n; impl_preFunc.(); ^impl_underlyingObject.cyc( n );  }
	fin { arg n; impl_preFunc.(); ^impl_underlyingObject.fin( n );  }
	repeat { arg repeats; impl_preFunc.(); ^impl_underlyingObject.repeat( repeats ); }
	nextN { arg n, inval; impl_preFunc.(); ^impl_underlyingObject.nextN( n, inval );  }
	streamArg { arg embed; impl_preFunc.(); ^impl_underlyingObject.streamArg( embed );  }
	composeEvents { arg event; impl_preFunc.(); ^impl_underlyingObject.composeEvents( event ); }
	? { arg obj; impl_preFunc.(); ^(impl_underlyingObject ?  obj); }
	?? { arg obj; impl_preFunc.(); ^(impl_underlyingObject ??  obj); }
	!? { arg obj; impl_preFunc.(); ^(impl_underlyingObject !?  obj); }
	matchItem { arg item; impl_preFunc.(); ^impl_underlyingObject.matchItem( item ); }
	falseAt { arg key; impl_preFunc.(); ^impl_underlyingObject.falseAt( key );  }
	pointsTo { arg obj; impl_preFunc.(); ^impl_underlyingObject.pointsTo( obj ); }
	subclassResponsibility { arg method; impl_preFunc.(); ^impl_underlyingObject.subclassResponsibility( method );  }
	doesNotUnderstand { arg selector ... args; impl_preFunc.(); ^impl_underlyingObject.doesNotUnderstand( selector, *args );  }
	shouldNotImplement { arg method; impl_preFunc.(); ^impl_underlyingObject.shouldNotImplement( method );  }
	outOfContextReturn { arg method, result; impl_preFunc.(); ^impl_underlyingObject.outOfContextReturn( method, result );  }
	immutableError { arg value; impl_preFunc.(); ^impl_underlyingObject.immutableError( value );  }
	deprecated { arg method, alternateMethod; impl_preFunc.(); ^impl_underlyingObject.deprecated( method, alternateMethod );  }
	printClassNameOn { arg stream; impl_preFunc.(); ^impl_underlyingObject.printClassNameOn( stream );  }
	printOn { arg stream; impl_preFunc.(); ^impl_underlyingObject.printOn( stream );  }
	storeOn { arg stream; impl_preFunc.(); ^impl_underlyingObject.storeOn( stream );  }
	storeParamsOn { arg stream; impl_preFunc.(); ^impl_underlyingObject.storeParamsOn( stream );  }
	simplifyStoreArgs { arg args; impl_preFunc.(); ^impl_underlyingObject.simplifyStoreArgs( args );  }
	storeModifiersOn { arg stream; impl_preFunc.(); ^impl_underlyingObject.storeModifiersOn( stream ); }
	as { arg aSimilarClass; impl_preFunc.(); ^impl_underlyingObject.as( aSimilarClass ); }
	deepCollect { arg depth, function, index, rank ; impl_preFunc.(); ^impl_underlyingObject.deepCollect( depth, function, index , rank ); }
	deepDo { arg depth, function, index , rank ; impl_preFunc.(); ^impl_underlyingObject.deepDo( depth, function, index , rank ); }
	bubble { arg depth, levels; impl_preFunc.(); ^impl_underlyingObject.bubble( depth, levels);  }
	obtain { arg index, default; impl_preFunc.(); ^impl_underlyingObject.obtain( index, default ); }
	instill { arg index, item, default; impl_preFunc.(); ^impl_underlyingObject.instill( index, item, default );  }
	addFunc { arg ... functions; impl_preFunc.(); ^impl_underlyingObject.addFunc(*functions );  }
	removeFunc { arg function; impl_preFunc.(); ^impl_underlyingObject.removeFunc( function );  }
	replaceFunc { arg find, replace; impl_preFunc.(); ^impl_underlyingObject.replaceFunc( find, replace );  }
	addFuncTo { arg variableName ... functions; impl_preFunc.(); ^impl_underlyingObject.addFuncTo( variableName, *functions );  }
	removeFuncFrom { arg variableName, function; impl_preFunc.(); ^impl_underlyingObject.removeFuncFrom( variableName, function );  }
	while { arg body; impl_preFunc.(); ^impl_underlyingObject.while( body );  }
	switch { arg ... cases; impl_preFunc.(); ^impl_underlyingObject.switch(*cases );  }
	yieldAndReset { arg reset ; impl_preFunc.(); ^impl_underlyingObject.yieldAndReset( reset );  }
	idle { arg val; impl_preFunc.(); ^impl_underlyingObject.idle( val );  }
	changed { arg what ... moreArgs; impl_preFunc.(); ^impl_underlyingObject.changed( what, *moreArgs );  }
	addDependant { arg dependant; impl_preFunc.(); ^impl_underlyingObject.addDependant( dependant );  }
	removeDependant { arg dependant; impl_preFunc.(); ^impl_underlyingObject.removeDependant( dependant );  }
	update { arg theChanged, theChanger; impl_preFunc.(); ^impl_underlyingObject.update( theChanged, theChanger ); }
	addUniqueMethod { arg selector, function; impl_preFunc.(); ^impl_underlyingObject.addUniqueMethod( selector, function );  }
	removeUniqueMethod { arg selector; impl_preFunc.(); ^impl_underlyingObject.removeUniqueMethod( selector );  }
	& { arg that; impl_preFunc.(); ^(impl_underlyingObject &  that); }
	| { arg that; impl_preFunc.(); ^(impl_underlyingObject |  that); }
	% { arg that; impl_preFunc.(); ^(impl_underlyingObject %  that); }
	** { arg that; impl_preFunc.(); ^(impl_underlyingObject **  that); }
	<< { arg that; impl_preFunc.(); ^(impl_underlyingObject <<  that); }
	>> { arg that; impl_preFunc.(); ^(impl_underlyingObject >>  that); }
	+>> { arg that; impl_preFunc.(); ^(impl_underlyingObject +>>  that); }
	<! { arg that; impl_preFunc.(); ^(impl_underlyingObject <!  that); }
	blend { arg that, blendFrac; impl_preFunc.(); ^impl_underlyingObject.blend( that, blendFrac );  }
	blendAt { arg index, method; impl_preFunc.(); ^impl_underlyingObject.blendAt( index, method);  }
	blendPut { arg index, val, method; impl_preFunc.(); ^impl_underlyingObject.blendPut( index, val, method);  }
	fuzzyEqual { arg that, precision; impl_preFunc.(); ^impl_underlyingObject.fuzzyEqual( that, precision); }
	pair { arg that; impl_preFunc.(); ^impl_underlyingObject.pair( that ); }
	pairs { arg that; impl_preFunc.(); ^impl_underlyingObject.pairs( that );  }
	awake { arg beats, seconds, clock; impl_preFunc.(); ^impl_underlyingObject.awake( beats, seconds, clock );  }
	performBinaryOpOnSomething { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnSomething( aSelector, thing, adverb );  }
	performBinaryOpOnSimpleNumber { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnSimpleNumber( aSelector, thing, adverb );  }
	performBinaryOpOnSignal { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnSignal( aSelector, thing, adverb );  }
	performBinaryOpOnComplex { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnComplex( aSelector, thing, adverb );  }
	performBinaryOpOnSeqColl { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnSeqColl( aSelector, thing, adverb );  }
	performBinaryOpOnUGen { arg aSelector, thing, adverb; impl_preFunc.(); ^impl_underlyingObject.performBinaryOpOnUGen( aSelector, thing, adverb );  }
	writeDefFile { arg name, dir, overwrite; impl_preFunc.(); ^impl_underlyingObject.writeDefFile( name, dir, overwrite );  }
	slotAt { arg index; impl_preFunc.(); ^impl_underlyingObject.slotAt( index );  }
	slotPut { arg index, value; impl_preFunc.(); ^impl_underlyingObject.slotPut( index, value );  }
	slotKey { arg index; impl_preFunc.(); ^impl_underlyingObject.slotKey( index );  }
	slotIndex { arg key; impl_preFunc.(); ^impl_underlyingObject.slotIndex( key );  }
	slotsDo { arg function; impl_preFunc.(); ^impl_underlyingObject.slotsDo( function );  }
	slotValuesDo { arg function; impl_preFunc.(); ^impl_underlyingObject.slotValuesDo( function );  }
	setSlots { arg array; impl_preFunc.(); ^impl_underlyingObject.setSlots( array );  }
	instVarAt { arg index; impl_preFunc.(); ^impl_underlyingObject.instVarAt( index );  }
	instVarPut { arg index, item; impl_preFunc.(); ^impl_underlyingObject.instVarPut( index, item );  }
	writeArchive { arg pathname; impl_preFunc.(); ^impl_underlyingObject.writeArchive( pathname );  }
	writeTextArchive { arg pathname; impl_preFunc.(); ^impl_underlyingObject.writeTextArchive( pathname );  }
	getContainedObjects { arg objects; impl_preFunc.(); ^impl_underlyingObject.getContainedObjects( objects );  }
	writeBinaryArchive { arg pathname; impl_preFunc.(); ^impl_underlyingObject.writeBinaryArchive( pathname );  }
}



AutoPromise : ObjectPreCaller {
	var <>priv_condVar;
	var <>priv_isSafe;

	*new{
		var self = super.new()
		.priv_isSafe_(false)
		.priv_condVar_(CondVar());

		self.impl_preFunc = {
			if(self.priv_isSafe.not, {
				try
				{ self.priv_condVar.wait({ self.priv_isSafe }) }
				{ |er|
					if((er.class == PrimitiveFailedError) && (er.failedPrimitiveName == '_RoutineYield'),
						{ AutoPromise.prGenerateError(self.class.name).error.throw },
						{ er.throw } // some other error
					)
				}
			})
		};
		^self
	}

	// only call this once in normal use
	impl_addUnderlyingObject { |obj|
		impl_underlyingObject = obj
	}

	impl_markSafe {
		priv_isSafe = true;
		priv_condVar.signalAll;
	}

	// used to wrap the functions that the child class explicitly defines
	doesNotUnderstand { |selector ... args|
		this.impl_preFunc.();
		^this.impl_underlyingObject.perform(selector.asSymbol, *args)
	}

	*prGenerateError { |className|
		^className ++ "'s value has not completed,"
		+ "either use it in a Routine/Thread, or,"
		+ "literally wait until the resource has loaded and try again"
	}
}



+ Buffer {
	*readAP { |server, path, startFrame=0, numFrames=(-1), action|
		var r = AutoPromise();
		var buffer = Buffer.read(
			server: server,
			path: path,
			startFrame: startFrame,
			numFrames: numFrames,
			action: { |buf|
				r.impl_markSafe();
				action !? {action.(buf)};
			}
		);
		r.impl_addUnderlyingObject(buffer);
		^r
	}

	at {|index|
		var r = AutoPromise();
		this.get(index, action: {|v|
			r.impl_addUnderlyingObject(v);
			r.impl_markSafe();
		});
		^r
	}
}



usage

This is what it looks like to use, I’ve also made it work for Buffer.get as it can seamlessly wrap any type.

Basic forked

fork {
	~b = Buffer.readAP(s, ~path);
	~b.numFrames.postln; // waits automatically
}

Basic no fork

~b =  Buffer.readAP(s, ~path);

// wait a second

~b.numFrames; //has been updated behind the scenes, does not wait

Wrapping Buffer.get

fork {
	~b = Buffer.readAP(s, ~path);
	~result = ~b[41234]; // waits on ~b, calls Buffer.get -- returns an AutoPromise
	format("result + 1 = %", ~result + 1).postln; // waits on ~result
}
~b = Buffer.readAP(s, ~path);
// wait a second
~result = ~b[41234]; 
// wait a second
format("result + 1 = %", ~result + 1).postln; 

There is an issue here though…

fork {
	~b = Buffer.readAP(s, ~path);
	~result = ~b[41234]; // waits on ~b, calls Buffer.get -- returns an AutoPromise
	format("1 + result = %", 1 + ~result).postln; // waits on ~result
}

… doing 1 + ~result does not work as no message has been sent. I don’t know if this is a simple change or not as you might just be able to call value inside Number.add with little consequence? Ultimately the issue is that it uses a primitive here. Instead, you get an error as the current ‘value’ of ~result is nil.

Benchmarks

@shiihs
Okay turns out I was … sort of…

Basic single buffer = the same


s.waitForBoot {
	{
		var b = Buffer.readAP(s, ~path) ;
		b.numFrames.postln;
		b.free;
	}.bench // time to run: 0.12787021299999 seconds.
}

s.waitForBoot {
	{
		var b = Buffer.read(s, ~path) ;
		s.sync;
		b.numFrames.postln;
		b.free;
	}.bench // time to run: 0.12784386300001 seconds.
}

10 Buffers = the same.
Now this assumes you call sync, but it breaks on my system if you don’t.
This is the one that I thought would be faster, but for some reason it isn’t? Not a big deal as it is at least no slower.

s.waitForBoot {
	{
		~bufs = 10.collect({
			Buffer.readAP(s, ~path)
		});
		~bufs[0].numFrames.postln;
	}.bench; //time to run: 1.227028303 seconds.
	~bufs.do(_.free);
}

s.waitForBoot {
	{
		~bufs = 10.collect({
			Buffer.read(s, ~path)
		});
		s.sync;
		~bufs[0].numFrames.postln;
	}.bench; // time to run: 1.230603833 seconds.
	~bufs.do(_.free);
}

Drawbacks

  • Doesn’t work with keyword arg function calls (original purpose of this thread) — solvable, but definitively not trivial.
  • If you wrap it in normal parenthesis, (...), it will only sometimes throw an error — I’ve added a custom error message to make this more obvious. @jamshark70’s suggestion of change the interpreter to always fork fixes this, but might have a performance cost, although since this is only ever evaluated once at a time, it might be minor?
  • Hides the true nature of the server/client relationship — I don’t think this is a drawback at all, and you might as well argue that Buffer hides the fact everything is sending OSC messages.
  • One down side is that things like SynthDef do not work if you have many being defined in parallel, and AutoPromise approach can be unclear if something is run in parallel. I don’t think this is AutoPromise’s fault, it is SynthDef’s and the change should be there.
  • When passing some AutoPromise’d to a method that calls a primitive, this doesn’t count as a message, so no sync is done — it might not be possible to solve this mean the commutative property is broken in some cases (this does not apply to Buffer as it isn’t a primitive or used in any primitive calls).

I still think this is the best solution for Buffer (with always fork in interpreter).

  • User just writes the code as they would as if s.sync didn’t exist.
  • It is always safe.
  • The performance is okay.
  • Almost completely backwards compatible — we don’t have to wait for the messiah of SC4 to arise.
  • If this is applied to other classes, there would be no reason to teach the client/server split or even mention synchronous/asynchronous programming in the introduction of supercollider — that is the biggest win in my mind.

As a way to fix the primitives, there could be an extra method added to Object called impl_touch, which just does nothing and would need to be called before any _XPritimitive primitive is called… Otherwise primitive types might just have to be explicitly waited on.

Why not just call wait automatically? In supercollider (unlike many other languages) we have a very clear definition of what it means to ‘use’/‘access’ an object — its when you send a message.

I’m not a fan of your example (when applied to Buffer) as it means the user must remember to call .value, which is essentially the same as remembering to call s.sync, and having certain built in things do it automatically just confuses things. In my solution, the user has to do nothing (except make sure its being called in a forked context, or leave sufficient time, but that is true of both approaches).

JMc’s math implementation (likely cribbed from Smalltalk) should definitely support this.

1 + ~result first dispatches to Integer:+. If the primitive fails (which it will in this case), then it calls performBinaryOpOnSimpleNumber on the second operand – this method, because the first operand is known to be a simple number at this point. AutoPromise’s performBinaryOpOnSimpleNumber should then await.

I.e., a message is sent to the AutoPromise: performBinaryOpOnSimpleNumber.

There’s a suite of other performBinaryOp methods that should be included for completeness.

It does take some time to understand math ops in the class library, but it’s IMO beautiful: handles every case with minimalistic, elegant factoring. (One reason why I suggested making Promise/Future a subclass of AbstractFunction is to take advantage of the compose***Op interface.)

While I don’t have a big investment in the eventual outcome, I’d note that both lnihlen and spacechild1 have raised concerns about “hairy” implementations. Balancing user needs against forward maintainability is a difficult question.

hjh

I tend to agree with this approach. I think most are sold on the idea to make things like async easier for the new user. I’ll just point out that if the main objection is in the name of looking out for the new user, then adopting the latest and greatest in the culture should come naturally (they’re new, after all, there’s no culture shift for them).

And for experienced users (who likely share OP’s pain in learning about async processes), the latest abstraction will be a breath of fresh air. But also for experienced users, who have fully internalized this at-times-useful async behavior, this

might lead to some confusing behavior (though maybe quick to adapt to?).

Thanks @jordan for starting this discussion. It will be useful to refer back to in the future… would you consider changing the name of the thread to capture the theme of async lang behavior?

If you start to learn sclang with no prior experience, there are lots of hurdles to overcome. I can vividly remember when I learned sclang in university I didn’t know anything about object oriented programming; I didn’t even know what a method call is! Learning a complex object oriented programming language like sclang will require a large amount of effort from a novice programmer.

I agree that we should strive to keep the learning curve flat, but we should also be realistic. Unpopular opinion: a classical oboist who has no idea about programming and wants to dabble in live-electronics should probably start with Max/MSP.

never mind (a)synchronous programming.

Actually, the core idea around asynchronous programming itself is rather trivial:

  1. certain operations can take an unbounded amount of time
  2. we have to wait for such operations to complete
  3. it might be nice if we could do something else in between

The problem is rather how asynchronous programming is typically done in sclang. Just a short recapitulation:

// for a single buffer, we can pass a callback function:
~buf = Buffer.read('foo.wav', action: { ... });

// For multiple buffers we need to use s.sync instead
~bufs = ~files.collect({ |x| Buffer.read(x) });
s.sync;

// Oh, but this only works if the async operation involves a *single* Server roundtrip,
// so the following does not work:
~data = [];
~bufs.do { |b| b.getToFloatArray(action: { |data| ~data = ~data.add(data) }) };
~s.sync; // nope...

// Also, s.sync only works for asynchronous operations that involve the Server, so the following doesn't work either:
~cmds.do(_.unixCmd);
~s.sync; // nope...

// So how do we actually synchronize in the last two examples? Go figure...

// Finally, getting data asynchronously with callbacks is awkward:
~buf.getn(0, 128, action: { |data| ~data = data });
s.sync;

In a promise-based model, on the other hand, all of these operations would look the same and they would be much simpler to use:

// read a single buffer and wait for completion
~buf = Buffer.read('foo.wav').await;

// Wait for multiple buffers to load
~bufs = ~files.collect({ |x| Buffer.read(x) }).await;

// Naturally, this also works for operations with several Server roundtrips:
~data = ~bufs.collect(_.getToFloatArray }).await;

// Same for async operations that don't involve the Server:
~cmds.collect(_.unixCmd).await;

// Getting data asynchronously looks the same:
~data = ~buf.getn(0, 128).await;

I hope this illustrates the point I’m trying to make. Asynchronous programming does not have to be hard!

For them to simply playback a soundfile on the server they would need to be taught all about the server/client split, then what a promise is, and that they should always remember to call await.

You don’t really need to know much about the actual client/server-architecture. The only thing you do need to know is that some operations are asynchronous and you need to await them. IMO it is not more difficult than remembering to call SynthDef.add.

how do they know whether to await or not? That is a lot to ask of a new user.

Documentation and examples.


One important thing I forgot to mention: a promise-based model also simplifies error handling because you can use exceptions, just like with ordinary synchronous method calls!

try {
~buf = Buffer.read('foo.txt').await;
} { |error|
...
}

In general, I think there is lots of truth in the adage “explicit is better than implicit” (PEP 20 – The Zen of Python | peps.python.org).

I’m not saying that your autopromise approach is bad per se, but I don’t think such “magic” belongs to basic server abstractions like Buffer. A method like numFrames should just return a value and not do some funky stuff behind the scene, such as blocking the calling thread. Waiting for completion should be done explicitly.

Actually, you can keep your internal promise object and just let the user await it with an explicit method call:

~buf = Buffer.read(`foo.wav`).await;
~buf.numFrames;

This way we can get close to a “real” promise-based model. It is not perfect because we don’t really return a promise, but it’s probably the best we can get without breaking backwards compatibility or adding lots of new dedicated methods (which would just further bloat the Class Library).

Anyway, thanks indeed for starting this discussion!

2 Likes

Assuming we’re not changing the current behavior of Buffer, I still think something like this

could be quite useful, though a warning is probably more appropriate.
The getters for state vars depending on async resources could be guarded by a flag that is set when the Buffer (or whatever) is loaded. See Buffer:-queryDone

// called from Server when b_info is received
queryDone {
	doOnInfo.value(this);
	doOnInfo = nil;
	stateLoaded = true; // new
}

// new getter
numFrames {
	stateLoaded.not.if{
		"Tried to access Buffer before the server had finished loading it.".warn
	};
	^numFrames // still return the var
}

I’m not sure how rude we’re willing to be with warnings/errors, but in this case it’s clearly bad behavior to access these uninitialized vars.

Okay a follow up about server commands.

I’m writing a promise and making a new SmartBuffer class (for lack of a better name). I will eventually add all these to a quark, with the hope they might be added to the standard class library.

To synchronise with any server message, can a sync command be used? The documentation says,

Replies with a /synced message when all asynchronous commands received before this one have completed.

Is that because the server only has one thread executing these and therefore, it will only ever be done once all previous commands have been completed, or does it mean it will end up waiting longer than needed? This isn’t too much of a problem with SmartBuffer as other messages can be used, but something like SmartSynthDef and the \d_recv message doesn’t respond with the name of the defined synthdefs in \done.

1 Like

Yes, that’s exactly how it works.

1 Like

Coming to this thread quite late (good discussion though!), but a couple notes:

Probably it’s slightly better to do your sync via the completionMessage rather than an explicit s.sync call. It’s unlikely to be THAT important, but it removes one set of abstractions (the sync mechanism) from your implementation. The implementation I’m using with Deferred is just:

    *doRead {
        |server, path, startFrame = 0, numFrames = -1, bufnum|
        var d = Deferred();
        Buffer.read(server, path, startFrame, numFrames, d.valueCallback, bufnum);
        ^d
    }

Though it involves a lot of boilerplate, I think you could do a full Buffer implementation that’s transparently async. There are only two server objects that really require synchronization: Buffers and SynthDefs - I think it might be overkill to go too far down the route of any uber-generic solution, when you could just fix probably five or ten methods on these objects. I’ll sketch out what I’m thinking of (making use of Deferred as my promise, but could be translated to other mechanisms) - this is just a mockup / example.

AsyncBuffer : Buffer {
    var <>async;
    
    *read {
        |argpath, fileStartFrame = 0, numFrames = -1, bufStartFrame = 0, leaveOpen = false, action|
        var deferred, newBuffer;
        deferred = Deferred();
        newBuffer = super.read(argpath, fileStartFrame, numFrames, bufStartFrame, leaveOpen, deferred.valueCallback());
        deferred.then(action);
        newBuffer.async = deferred;
    }
    
    // basically: wrap this, asUGenInput, and every other read operation that depends on
    // the buffer being loaded - there should only be 5 or 6?
    asControlInput {
        if (thisThread.isKindOf(Routine)) {
            async.wait;
            ^this.bufnum
        } {
            if (async.hasValue.not) {
                Error("Buffer is waiting to be loaded").throw
            }
        }
    }
}

+Buffer {
    *new { arg server, numFrames, numChannels, bufnum;
        ^AsyncBuffer.new(server, numFrames, numChannels, bufnum)
    }
    // and other Buffer creation methods.
}

Thinking about the behavior and compatibility here:

  1. If we ARE in a Routine:
    1.1 …and our Buffer is already loaded: behavior is identical to before
    1.2 …out buffer is not loaded: we wait implicitly, and when we return behavior is identical to before. This is pretty unlikely to have negative effects, except in cases where we are e.g. reading a bufnum for a message we schedule in the future (this would have worked before, and would now have an extra pause which could be disruptive)
  2. If we’re NOT in a Routine:
    2.1. …and the buffer is not loaded: this is now an explicit error instead of undefined behavior as it was before
    2.2. …and the buffer is loaded: this is the same as before

It’s likely that any user code that’s even remotely sophisticated is already managing these server sync issues somehow, so adding this behavior by default (or without care…) is going to be somewhere between not-beneficial and a bug risk - this feels muuuuuuch better as a Quark than a core library change. If there was momentum to introduce better async API’s in the core library, we might as well just skip backwards compatibility entirely and redesign ALL the server object API’s, and then move internal uses over to the new API’s - there’s lots of good clean-up work to do anyways…

1 Like

No programmer would think that the current API design for async stuff in SuperCollider is anything other than a hot mess by contemporary standards :slight_smile: - and no musician is likely to think it’s anything other than confusing and fragile. Probably everyone in this thread can agree on that at least…

3 Likes

FWIW I agree with all the responses – I phrased my original post super poorly ^^ I was trying to say you can’t avoid the learning curve with any sufficiently expressive tool, and that point’s now been made more clearly by other posts in this thread.

1 Like

I find your proposed solution to be the most elegant one. It involves employing SynthDef and Buffer in a manner akin to how Option types in F# or Maybe types in Haskell operate.

It doesn’t seem like a good idea at first to create a wrapper for such unique and common cases as SynthDefs and Buffers if there is a cleaner way to make them handle different situations.

just my 2 cents ))

1 Like

Yes, but when it comes to delving into the old Church’s problem/idea of “program synthesis” using non-algorithmic definitions, music emerges as a particularly fertile domain. :upside_down_face:

Sorry, I’m not sure what Church thing you’re referring to or how it relates to what I said.

1 Like

Of course, it’s not the same thing, we’re talking music, not math, but it surely relates to the idea of defining high-level descriptions of ideas and tasks.

Just wanted to share a lighthearted note, my friend. :smiling_face_with_three_hearts:

For completeness, there’s also (outside of core) VSTPlugin, with two stages of initialization: plug-in loading and preset loading. A promise-based approach would greatly simplify its use!

I can’t think of others offhand, except maybe NRT (waiting for any offline process). The LADSPA UGen is long unmaintained.

hjh

3 Likes

Definitely!

Note that asynchronous programming is not limited to Server interaction. I already mentioned unixCmd in Async lang behaviour - how to this could be made easier for new users - #38 by Spacechild1.

2 Likes

One more thought on this.

Since we really only care that a server resource (a Buffer / SynthDef) is available ON THE SERVER, we really need to wait on our promise only the last stage just before sending to the server. As a result, we can remove even more of this abstraction from the core by having our read-barrier for server promises in NetAddr, the last point before sending to the server. This would look something like:

  1. Subclass our generic promise class as e.g. ServerDeferred, ServerPromise etc
  2. Add an override for asControlInput that simply returns this. This will mean that all OSC messages will still contain unresolved promises:
ServerDeferred : Deferred
{
  asControlInput { ^this }
}
  1. Create a new NetAddr implementation that resolves all promises in an OSC message before sending. This could be done on a separate thread without halting the current thread if that is desired - this would resolve problems with introducing new waits in existing threaded code AND make it work properly when executing directly from the interpreter.

There are some non-ideal things about this solution, but apart from the hack of leaving a theoretically invalid object in an OSC packet it actually may constrain the required changes pretty nicely. With this solution, you could implement e.g. an AsyncSafeServer that uses the read-barrier NetAddr - Buffer and SynthDef could actually check for the presence of this server and return promises ONLY when used on that server. But asControlInput is a pretty internal, undocumented method, and IMO makes no obvious guarantees about what it returns, other than “this can be used in an OSC message” (which, for our case, is true).

2 Likes

For fun, here’s what it looks like to just append a sync flag to the creation methods, using some of the Deferred pattern @scztt introduced, without wrappers and minimal bookkeeping. (Though the OSC intercepting method could be more broadly applicable/extensible).

This uses uses the *read creator and numFrames as the server state-dependent variable as an example.

// Showing only the modifications to Buffer
Buffer { 

	var >numFrames; // removed getter
	var <>loadState, >isSynchronous = false; // new

	// adding the sync flag
	*read { arg server, path, startFrame = 0, numFrames = -1, action, bufnum, sync = false;
		server = server ? Server.default;
		bufnum ?? { bufnum = server.nextBufferNumber(1) };
		^super.newCopyArgs(server, bufnum)
		.doOnInfo_(action).cache
		.loadState_(Condition()).isSynchronous_(sync)    // <<< new
		.allocRead(path, startFrame, numFrames, {|buf|["/b_query", buf.bufnum] }, sync)
	}

	// new getter, this is the pattern for any server state-dependent vars
	numFrames {
		if (loadState.test) {
			^numFrames
		} { 
			^this.prGetSynchronous(thisMethod) 
		}
	}

	// new dispatch method
	prGetSynchronous { |method|
		if (isSynchronous) {
			if (thisThread.isKindOf(Routine)) {
				loadState.wait;
				^this.perform(method.name) // request again
			} {
				Error("Buffer hasn't loaded - synchronous access needs to be done in a Routine.").throw
			}
		} {
			Error("Buffer hasn't loaded - use Buffer's sync arg and a Routine to ensure Buffer is loaded before accessing.").throw
		}
	}

	queryDone {
		doOnInfo.value(this);
		doOnInfo = nil;
		loadState.test_(true).signal;   // <<< new
	}
}

and now in use…

( // a collection of buffers
s.boot;
p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
p = p.dup(25); // make large enough for significant delay
)

// Try immediate access inside a routine
(
fork {
	b = Buffer.read(s, p.first, sync: true); 
	b.numFrames.postln; // ok!
}
)
b.free; // cleanup

// Try access without synchronous flag
b = Buffer.read(s, p.first); b.numFrames;
    >> ERROR: Buffer hasn't loaded - use Buffer's sync arg and a Routine to ensure Buffer is loaded before accessing.

// Try immediate access with sync=true, but outside a routine
b = Buffer.read(s, p.first, sync: true); b.numFrames;
    >> ERROR: Buffer hasn't loaded - synchronous access needs to be done in a Routine.

// one line at a time, routine or not, same as original
b = Buffer.read(s, p.first, sync: true);
// wait a moment
b.numFrames;     // ok, no error, regardless of sync arg, it's loaded anyway
b.free;          // cleaup

// Load the whole collection
// Buffers load asynchronously, only the request is delayed!
(
fork {
	var bufs, askIdx = 14; // interact with whichever buffer

	// load the bufs - can actually be done before the routine
	bufs = p.collect{ |path|
		Buffer.read(s, path, sync: true);
	};
	// access - invokes a wait
	"buffer % numframes: %\n".postf(askIdx, bufs[askIdx].numFrames);
}
)

Buffer.freeAll; // clean up

and if the loadState condition is visible, waiting for all buffers to load is straightforward:

( // a collection of buffers
s.boot;
p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
p = p.dup(50); // make large enough for significant delay
)

(
fork {
	var bufs = p.collect{ |path|
		Buffer.read(s, path, sync: true);
	};
	// you effectively wait only as long as the longest-loading buffer
	bufs.do{ |b| b.loadState.wait }; 
	"All buffers are loaded.\n".postln;
}
)

Buffer.freeAll; // clean up

So hopefully that prGetSynchronous dispatch method would do most of the boilerplate.

1 Like