JITLib — nil.buildForProxy not implemented?

I developed a small object (GitHub - tai-studio/NPModules: registry of small reusable module functions for NodeProxy) to allow to reuse predefined snippets in NodeProxy instances using the mechanism described in NodeProxy roles:

AbstractPlayControl.proxyControlClasses.put(\module, SynthDefControl);
AbstractPlayControl.buildMethods.put(\module, {
    // ...
    <something>.buildForProxy(proxy, channelOffset, index);
})

Everything works quite nicely, however, there is something I’d like to do that is is not possible, it seems:

AFAICS, the buildMethod needs to return something that is buildForProxy which sensible objects can be converted to by calling buildForProxy on them with appropriate parameters.

However, for some reason (well, to create a default behaviour), I would like to be able to remove the object altogether from within the definition, i.e. e.g.

Ndef(\a)[10] = \module -> nil

should remove the slot 10 of the Ndef.
Now, buildForProxy is not implemented on nil, therefore I am not able to do that…

Any help much appreciated :slight_smile:

for testing purposes, here is a simplified version of the intended behavior not depending on my code:

AbstractPlayControl.proxyControlClasses.put(\nilTest, SynthDefControl);
AbstractPlayControl.buildMethods.put(\nilTest, #{ | func, proxy, channelOffset = 0, index |
	func.buildForProxy(proxy, channelOffset, index);
});


// works as expected e.g.. for a function
Ndef(\a, \nilTest -> {SinOsc.ar})

Ndef(\a).sources
// -> there is something in the slot


Ndef(\a).clear; //clear everything in \a

// breaks for nil
Ndef(\a)[0] = \nilTest -> nil
// -> 'prepareForProxySynthDef' should have been implemented by Nil.

// would like it to mimick this behaviour
Ndef(\a)[0] = nil


// this works
Ndef(\a).clear; //clear everything in \a
Ndef(\a)[0] = \nilTest -> {nil}

// however, there is some source still in the slot:
Ndef(\a).sources

I guess, at func.buildForProxy, you could check if(func.isNil) and in that case, return something else that would have the desired effect downstream. But I never looked very closely at the code for NodeProxy slots, so I’m not sure what that would be. Maybe even just nil (since proxy.put(index, nil) already works, maybe that’s supported…? at least, look into the code path when nil is put into a slot).

hjh

I tried returning nil but this did not work either… I am unsure how to safely return from an unsupported entry… maybe @julian is able to help?

Another option would be to provide a different interface (method) and not use Ndef(\x)[10] = \testNil -> nil at all – the custom function or method would trap nil and use the known working approach to delete the synth Ndef(\x)[10] = nil, and pass other objects with the right proxy role. That doesn’t slip into the existing syntax as neatly, but a translator interface is often a way to get the desired behavior without requiring class library changes.

IMO I think it probably would be a useful class library change, though it might need some design. It would be easy for JITLib to catch \role -> nil and always delete, but maybe a user-defined role would like to do something different for nil. That’s definitely going to need Julian or Alberto’s input.

hjh

1 Like

thanks, James, for your valued thoughts.
Thing is that I’d like to not only support

\module -> nil

but also create a default behaviour for not (yet) defined modules.

The extension I am developing (see above) allows to define modules to be added into NodeProx slots, i.e.

// Add custom module
n = NPModules(); // nil means the default Ndef space
​
n.put(\simpleSound, {|dict| 
    var f = dict[\freq] ? 200; 
    { LFTri.ar(f) } // returns a UGen function
});

n.put(\simpleFilter, {
    filter -> {|in| HPF.ar(in) }
});
Ndef(\a)[0] = \module -> \simpleSound // defined
Ndef(\a)[1] = \module -> \simpleFilter // defined
Ndef(\a)[2] = \module -> \complexFilter // not yet defined, should be transparent

if one now changes the definition of the modules, the changes are reflected in the synth sounds

// changing \simpleFilter replaces all simpleFilter instances 
n.put(\simpleFilter, {
    filter -> {|in| LPF.ar(in) }
});

// defining \complexFilter introduces it for all defined instances
n.put(\complexFilter, {
    filter -> {|in| HPF.ar(LPF.ar(in)) }
});

this I can implement with a default behaviour, however it feels not as lightweight as I’d like tit to be…
I’ll investigate further :slight_smile:

1 Like

@LFSaw I’ve just read this. Have you found a solution?

no, I have not found a solution to free the slot with

\module -> nil 

as of now…

So you would need something that allows you to use \module -> nil instead of nil itself? So that proxy[3] = \module -> nil is the same as proxy[3] = nil?

This means we would have to implement this in NodeProxy.put, where we remove the old object when the new object is nil, inthe first line.

put { | index, obj, channelOffset = 0, extraArgs, now = true |
		var container, bundle, oldBus = bus;

		if(obj.isNil) { this.removeAt(index); ^this };
		if(index.isSequenceableCollection) {
			^this.putAll(obj.asArray, index, channelOffset)
		};

		bundle = MixedBundle.new;
		container = obj.makeProxyControl(channelOffset, this);
		container.build(this, index ? 0); // bus allocation happens here

...

This would require to add an interface to Object, Association, and Nil.

One could of course add a hacky solution, where if the container is nil, the old object is removed. But this isn’t entirely beautiful.

I don’t understand why we would want Ndef(\a)[10] = \module -> nil to clear the slot, rather than simply writing Ndef(\a)[10] = nil.

1 Like

Just an idea. Why not create a singleton ‘none’. which would mean something different from nil.

‘none’ (with minimal changes, just a few lines of code) could work like Nothing (from the Maybe monad)

For Maybe monad, Nothing is not the same as null/nil - it’s a value that represents absence. We miss this feature with nil.


EDIT:

I know that this is not a trivial enhancement (even if easy to write), but it can be a bit controversial.

Just to draft the “skeleton” that would already deliver the basic conceptual and a working implementation of none. Also, not the only way; I am not saying this is the way to go now. My point is that implementation is easy, but we need some brainwork before. I believe this change in design will not break anything (unless the word is taken), but it can be added without stress or trauma. Right?

None {
    classvar <instance;
    
    *new { ^instance }
    
    *initClass {
        instance = super.new;
    }
    
    buildForProxy { | proxy, channelOffset = 0, index = 0 |
        proxy.removeAt(index);
        ^nil
    }
    
    makeProxyControl { | channelOffset, proxy |
        ^nil 
    }
    printOn { | stream |
        stream << "none";
    }

    isNone { ^true }
    isNil { ^false }
}

tl;dr

  • It mirrors SC’s existing design (like Rest for patterns)
  • @LFSaw buildMethod stays clean and doesn’t need nil-checking
  • It smoothly handles the “undefined module” (an example of a use case)
  • The semantics are clear: none = “this slot should be empty”

Personally, I think this syntax is kind of incoherent:

Ndef(\a)[10] = \module -> nil

What meaning does ‘module’ have in this context? No disambiguation is necessary as there can only be one object in that slot regardless of role.

This seems a bit more clear

Ndef(\a)[10] =  nil

likewise…

Ndef(\a).removeAt(10)

In languages with proper Maybe/Option types, there’s a real distinction between:

** No value provided*
** Absence of object*

meaning:

  • \module -> nil

“I explicitly want to clear this module”

  • \module -> none

“This module isn’t defined yet, treat as transparent”

This mirrors patterns like Rest in SC patterns, or Haskell Nothing.

May just wait for @LFSaw

Could the default action simply be to substitute { DC.ar(0) } (or .kr) for the nil target?

I know it’s not ideally lightweight, but Out.ar is very CPU-cheap, negligible compared to actual DSP ops.

That is, one could argue about it, design interesting support classes, and modify JITLib behavior (with nonzero risk of breaking something), and not have a functioning module until all of that is done – or one could accept the addition of a neutral synth node into the graph and move forward. Viewed from that perspective, the cost of silent nodes may turn out to be less than the development cost of absolutely minimizing the number of UGens.

hjh

I tried to stay out of it, but I always end up in one more controversy. I did not even diss nil.

There is none problem! I just wondered if there is a simple solution in the system. James suggested one, maybe there is another one even.

1 Like

I’d like to create a default behavior foir the case that the user provides an undefined module. To give some context:

I developed a small object (NPModules) to allow to reuse predefined snippets in NodeProxy instances, i.e. somethiong like this:

(
NPModules()[\quelle] = {|dict|
    {
		\amp.kr(0.1) * SinOsc.ar(\cFreq.kr(100) + (SinOsc.kr(\mFreq.kr(200)) * \mIdx.kr(100)))
    }
};
)

Ndef(\a).play; // create an Ndef

// add a defined module
Ndef(\a)[0] = \module -> \quelle;


// replace the module with an undefined module
Ndef(\a)[0] = \module -> \quelle2;

// -> creates a default module (nil), 

For anyone curious, the definition of the default module is here in the source code, whereas the fallback to the default method is implemented here.

Now that look at it again, I realise that, although it feels a little weird to have a default nil object in the slot, it allows to define the functionality after the fact like so:

NPModules()[\quelle] = {|dict|
    {
				SinOsc.ar(\cFreq.kr(100))
	}
};

hope this helps clearing up my intentions :slight_smile:

A dummy synth would also allow the functionality to be defined after the fact, and would be lower risk than requiring JITLib to change, and immediately implement-able.

hjh

Maybe, yes :slight_smile: – am I right to suppose that it is actually useful that there is a node in place, so there is no need to change anything in the class library?

yepp :slight_smile: thanks for helping me think, though :slight_smile:

1 Like