The difference between defaults arguments with equals sign and brackets `arg a = 1` vs `arg a (1)`

This has been a very large discussion over here sclang: redefine that passing nil as arg means reset to default by JordanHendersonMusic · Pull Request #7413 · supercollider/supercollider · GitHub and I think it is good idea to summarise as many people including long time sc users (myself included) didn’t know what the difference was… so here goes!

TLDR: don’t use |a = 1|, instead use |a (1)|, as the former means sometimes assign depending on the call sight and you probably don’t want that.

Pipes | and arg are irrelevant, this is about the equals sign vs the brackets

Consider these two functions…

f = { arg a1 = 1; a1 };
g = { arg a1 (1); a1 };

… and the output of the 4 calls…

f.();
g.();
f.(nil);
g.(nil);

f.() & g.() both return 1, as expected!
g.(nil) also returns 1, however f.(nil) returns nil.

f.(); // 1
g.(); /// 1
f.(nil); // nil
g.(nil); // 1

This is somewhat of a useful feature as the it is quite clear the user is explicitly passing nil, so obviously they don’t want to the default argument applied… however, this also works when nil is passed implicitly.

Here are two confusing cases:

Events, Dictionary, and Globals

f = { arg a1 = 1; a1 };
f.(~foo)

In this case, if ~foo doesn’t exist, meaning it returns nil, then the result of this call is nil, not 1. This is probably not intended as the value doesn’t exist, therefore the default should be applied.

Wrapping functions and arguments

f = { arg a1 = 1; a1 };
h = { |b1| f.(b1)};
h.(); // nil

Here, there is no difference between h.() and h.(nil), this is because there is no way to check whether the call sight set the argument in supercollider.

What does this mean?

It is common practice to see code that copies the default arguments everywhere. This is bad practice and will lead to fragile code. Instead use a(1) once where the argument is used, and don’t declare any defaults.

Code that uses a = 1 should expect to receive nil, therefore the code ought to handle nil. If it doesn’t work with nil, then use a (1). For example, a boolean predicate argument should almost always be defaulted with a (1), consider this code.

f = { |isRed = true|
   if(isRed) { ... } { ... }
};
g = { |isRed| f.(isRef) };

g.(); // ERROR: Not Boolean in If statement

Unfortunately, a large amount of the class library uses a = 1 where nil has no meaning, a good example is the patterns where a repeat of nil makes no sense and the default should be applied.

How to fix this?

Well… to make this feature actually work we need another type of nil, perhaps undefined like javascript, but making this change would involve editing/checking almost every part of the interpreter and could drastically break existing code. The other option is to remove this feature, breaking any code the relies on it.

Neither are great options, so instead I’m trying to document and educate about the difference!

Why does this confusion exist in the first place?

What is nil? Is it an object, or the lack of one?

In C when you write…

void foo (){}

The return type void isn’t ‘returned’, instead is means ‘I did not write to memory’ , it isn’t a special value that indicates the absence of one, it is a compile time flag that means don’t perform a read. In supercollider, everything must return a value, so we need something to indicate the absence of a write.

This is different from a sentinel value, like NULL or nullptr. Supercollider has ‘mushed’ these two meanings together and this creates confusion.

Advice

Write code that looks like this…

f = { |a1(true)| ... };
g = { |a1| f.(a1) };

… this way things will behave like they do in other languages and you don’t need to copy default argument values around everywhere.

Only use a = 1 when the value of nil actually has meaning in the context of the function. Don’t do a null check an reassign the default as this is literally the same as a(1).

f = { |a = 1| a = a ?? {1}; ... }; // don't do this,
f = { | a(1) | ... } // do this as it is the same thing.
4 Likes

wow I suspect that this has caused me hours adding nil checks after unexpected failures. …we will have to go through all the docs I suspect!

But, in SynthDefs, if you prefer arg syntax for normal single-value kr inputs (as I do), note that freq(440) will not work in a SynthDef. (Noting for general readers.)

The issue covered in this thread concerns functions that you call yourself, where sometimes the function call might pass in nil (whether accidentally or intentionally).

In a SynthDef, the SynthDef builder calls the function, and it will always provide an OutputProxy (or array of them) for every function argument (nil will never be passed into a synth function), and the synth control takes its default from the arg-default-as-literal syntax |freq = 440|. Making that work with expression defaults (|freq(440)|) would require big interpreter changes, which I don’t see happening.

For regular functions, the advice here is solid.

hjh

works over here!

 b= {|freq(999)| var sig = SinOsc.ar(freq); Out.ar(0,sig * 0.1)}
 SynthDef(\foo, b).add;
 Synth(\foo); //999
 Synth(\foo, [freq:444]) //444
//  b= {arg freq(999); var sig = SinOsc.ar(freq); Out.ar(0,sig)} also works


…although in a Synth if you pass nil in you do get 0 not the default! should this change?

Good to know – I had thought that the compiler would put the default either into the functiondef’s prototypeFrame (needed for SynthDefs) or generate bytecodes to check for nil, but not both. In fact, for literal defaults in parentheses, it does do both – nice!

It’s not impossible.

Note that the mechanisms are totally different. This thread is about functions that you .value in the language. For a Synth, b does not get evaluated with the arg array, so there’s no opportunity for the upcoming change to intervene. Instead, an OSC message is created, containing the array’s contents, and this is sent to the server. The server has to obey what is in the message.

Currently, nil in an OSC message becomes a 0 integer.

[\abc, 0].asRawOSC
-> Int8Array[47, 97, 98, 99, 0, 0, 0, 0, 44, 105, 0, 0, 0, 0, 0, 0]

[\abc, nil].asRawOSC
-> Int8Array[47, 97, 98, 99, 0, 0, 0, 0, 44, 105, 0, 0, 0, 0, 0, 0]

Currently SC doesn’t use the osc “non-standard” type tag N (nil). That could be changed. Then the synth (server side) could look up the default value in the SynthDef.

(The other way would be to scan the argument list and find the default in the SynthDesc, but that would be an O(n) lookup where n = the SynthDef’s number of controls. O(n) is generally considered not well performing, so I wouldn’t recommend going that way.)

hjh

Yes this isn’t an expression, it’s a literal, although we do have to put expressions in brackets.

I skipped over the wholemeal expression Vs literal because it is something the user should not need to know and I’m going to be changing it when I get the chance. It’s a compiler detail and something that should never change the behaviour of the code.

The other thing I didn’t mention is that the behaviour disappears if you do a = (1+1)… I think I’m going to try makeing this a compiler error and force the user to change it to a(1+1) which would be consistent, and allow us to turn it straight into a 2.

It is impossible to get the nil behaviour with an expression… Edit: actually it’s possible, but really nasty and involves a new type in the slot and new bytecodes, edit2: actually that would break the synthdef, so it’s not a good idea.

No… But the fact the synthdef decides to not respect this shows how unknown this behaviour is.

As stated earlier, the mechanism for passing argument values to a synth on the server is totally different from that of evaluating a function. I think this comment is not a correct analysis (unless I’m misunderstanding your intent, which is possible… I’m not totally clear what you mean by “this behavior” here).

I suggested a way to address it on the server side, involving a change to the way we produce and consume OSC messages.

hjh

1 Like

I’m talking about the meaning of the code as you read it, not how it technically behaves, a = 1 means that nil is valid in the function - that’s the only reason to use that syntax. Of course the synthdef compiler is doing some reflection magic and changing the meaning of the default here so it is quite confusing.

I think your suggestion would work, but, it would have to turn all defaults into a(1) as it is impossible to write the code needed to get a = 1 to work (sorry, here I mean the behaviour…). This is because you’d need this second type of Nil. Alternatively, the compiler could store this info in the function def.

side note: in future, I’m going to rewrite the compiler/grammar to have separate ast nodes for these types of arguments which will make this easier. Might even be able to look ahead into the code an emit warnings if the user attempts to pass it to if or something…

I actually didn’t know that the defaults weren’t applied in osc messages, never thought about it to be honest, but having nil default to zero seems wrong - I don’t know how serious it is though?

I feel like we’re talking past each other…? I’m talking about transmission of synth args in OSC messages, because that’s what semiquaver asked about. Synth arg defaults are part of the binary SynthDef sent to the server, so if we have true Nil in OSC messages, the server can simply substitute the default in the def.

Function arg logic and the distinction between |freq = 440| and |freq(440)| doesn’t come into that at all. If the function arg default is in the prototypeFrame, it will end up in the server, and can be used server side.

I think the problem of server side arg defaults is simpler than you’re making it out to be.

hjh

Yes so the two will behave the same (as they both put the default in the proto frame). My point was that you can’t emulated the nil preserving behaviour of a=1, therefore, you’d be disregarding that the user specifically used a=1 rather than a(1).

It’s a question of what should the values of a be here…

SynthDef(\1, { |a=1| ... }).add;
SynthDef(\2, { |a(1)| ... }).add;

Synth(\1);
Synth(\1, [\a, foo]);
Synth(\2);
Synth(\2, [\a, foo]);

When foo may or may not be nil.

If the feature work consistently, \1 should see a set to nil or perhaps zero when foo is nil, whereas \2 should be 1.

The server doesn’t know how they were declared.

SynthDef(\b, { |freq(999)| freq.poll }).add;
Synth(\b); // 999
Synth(\b, [freq:444]) // 444
Synth(\b, [freq:nil]) // 0 // incorrect should be 999

SynthDef(\a, { |freq = 999| freq.poll }).add;
Synth(\a); // 999
Synth(\a, [freq:444]) // 444
Synth(\a, [freq:nil]) // 0 // correct

So a=1 is the correct syntax here and a(1) behaves incorrectly. To make both work correctly, you need the compiler to store how the argument was declared somewhere, we don’t do that right now.

With all due respect, but this is overthinking.

There’s no such thing as nil in the server, and there shouldn’t be. It’s conceptually nonsensical to consider extending this language-side behavior to the server. A non-starter.

hjh

It’s the number zero.

That’s valid and something someone might be depending on. The behaviour currently does a=1, meaning when you do Synth(\1, [\a, nil]), you get the ‘nil’ value, aka zero, not the default value as specified in the function.

So it already does exactly that.

Under “How to fix this?” there is also the Common Lisp approach?

https://gigamonkeys.com/book/functions

I.e. " Occasionally, it’s useful to know whether the value of an optional argument was supplied by the caller or is the default value. Rather than writing code to check whether the value of the parameter is the default (which doesn’t work anyway, if the caller happens to explicitly pass the default value), you can add another variable name to the parameter specifier after the default-value expression. This variable will be bound to true if the caller actually supplied an argument for this parameter and NIL otherwise. By convention, these variables are usually named the same as the actual parameter with a “-supplied-p” on the end."

1 Like

Hm, I thought the general consensus on the github thread was that it’s dodgy or unreliable to count on nil overriding an argument default :wink: – and, several contributors quickly came up with cases where it might be valuable to pass nil (e.g., messaging latency), and the outcome was to change the behavior anyway. But here, nobody has a concrete use case for using nil as 0, so there should be even less objection to changing it.

BTW I’m not advocating one way or the other. One user asked a question about the behavior of sending arguments to synths, and I suggested a way that we could handle it differently. Though I disagree with the current behavior, my earlier message should be taken as somewhat short of a proposal.

FWIW the fact that SC replaces nil with 0 in outgoing OSC messages has caused data loss in the past:

	free { arg completionMessage;
		if(bufnum.isNil) {
			"Cannot call free on a Buffer that has been freed".warn;
			^nil
		} {
			server.listSendMsg(this.freeMsg(completionMessage))
		}
	}
  1. Read a bunch of buffers.
  2. Later, free one of them.
  3. And accidentally free it again. The buffer’s bufnum has been set to nil, so sclang would send [\b_free, nil], which changes to [\b_free, 0] during encoding, causing the wrong buffer to be freed.

^^ that is certainly bad (the reason why we put in a check for it is that this actually happened to somebody).

hjh

1 Like

Nice! I’d never seen this before - should really get over my fear of parenthesis…

I don’t know how this could work with the existing grammar, but I’ll have a think about it, thanks!


I’d argue the opposite, in sclang functions, unexpectedly getting nil often results in a doesnotunderstand error, here, it’s just another number (0) so things still (at the type level) make sense and work. It might not be expected, but it’s not semantically wrong, nor does it produce an error - but could easily produce nans.

I also disagree, the point I was failing to make, was that:

  1. the behaviour is consistent when doing a=1, it only isn’t consistent when doing a(1) - given nil is zero.
  2. currently there isn’t a way to tell if the argument used = or (). This means you can either do the behaviour of = or () but not both - which would be a breaking change.

I would not be in favour of applying the default when the argument used =, but I would be if it used () as this would be consistent with the rest of the language. Even if this causes some breaking changes. I think consistency is important here and worth it.

It’s kinda like nullptr/NULL and having a resource live at memory address zero. Another option would have been to make all buffers start at 1.

Ah yes, I think after the dev meeting we decided against this. Not being able to detect this behaviour at compile time was the killer, doing it at runtime is expensive and means once the warning is disabled, code that once did work, ceases to without warning. And since an alternative already exists () it’s not worth it.

I think the outcome might be different if there wasn’t an alternative or there was some other benefit.

For my code, this is a shame because I’ve only ever used it incorrectly (and the class library is also full of this), but although I’ve never seen example of it being used correctly, it seems likely that it does exist somewhere.

Damn phone sent that message too early.

My position is that, because the mechanism for arg value transmission over OSC is 100% completely different from the mechanism for passing arg values into methods/functions, it isn’t really logical to extrapolate from the latter toward the former.

I’m not going to press the point any further, but I have 0% agreement with your premise here.

hjh

1 Like

Ah now I think I understand you properly! Yeah, we definitely disagree on this, my view is one of maintaining consistency with how default args behave and since the code ‘looks’ like a function it should follow the rules of one, because there is a semantic difference between which one you use. For me it’s not about the underlying mechanics, but about the meaning of the syntax. But I can completely see the other side of the arguement here!

I agree with James that UGen graph functions don’t need to differentiate between default argument types because they are never called directly.

IMO sclang should not silently converted nil to 0 in the first place. Instead, passing nil as an OSC argument where it’s not expected should be an error. But that’s probably not something we can change without breaking stuff…

At least the Class Library could be more strict about argument checking before sending OSC messages. James brought up a good example in The difference between defaults arguments with equals sign and brackets `arg a = 1` vs `arg a (1)` - #14 by jamshark70.

Synth controls are fundamentally different from function arguments.

James McCartney himself said once that he wasn’t sure it was a good idea, after all, to build synth controls from the arguments of a synthesis function – citation lost to time, but I know I’m not making it up. One conclusion to be drawn from that is that “function args as synth controls” is a historical accident rather than a Design Principle To Be Upheld, and that the syntax of function args really doesn’t have any bearing on the functionality.

“Then we should discourage the use of function arguments in SynthDefs” – there would be conceptual benefits to that. In practice, for single-value kr inputs, function-arg syntax is still the most concise and clear way to write them IMO.

  • Function args → controls: conceptual confusion, as evidenced in this thread.
  • NamedControls: If you want a self-documenting SynthDef with all controls at the top, you endure redundancy in the syntax by writing var freq = \freq.kr(440) (nah, I’ll pass), or you have to scan through the code for \xyz.kr in arbitrary locations, possibly with mismatched defaults (I’ve tried to read this and… I know some people like this style very much, but, personal opinion, I just can’t – I lose track of the inputs).
  • My synth arg preprocessor quark provides a bespoke syntax which actually covers all the cases – avoiding redundancy, direct support for rates, arrayed args, expression defaults etc – but it will never catch on unless it’s integrated into the sclang compiler.

So we only have “least objectionable” solutions for individuals’ use cases. We don’t have a good way to write synth controls. The fact that we don’t have a good way, and press function args into service to write them, does not hint toward a functional spec, even one to be written. I know I said I was going to drop it, but I think it’s a fundamental mistake to infer anything about synth input behavior from function arg syntax.

Tbh I think we should take that chance. It doesn’t make sense to “rely on” nil turning into 0.

hjh

1 Like

I agree with everything you’ve both said… but I don’t think we can change any of those things. I think we can change the behaviour in the case of () to make it consistent, and since most people use = the impact will be smaller.

Changing the way nil behaves seems likely to cause issues for works that currently function fine.

My arguement isn’t about what ought the design be, it’s what is the least painful way to reapply the default to the control.

At some point I hope to really rework the compiler and let you do macros so we might be able to have stuff like…

Synth(#!{
    in = freq(440, kr), amp([0.2, 0.4], ar);
    
    SinOsc.ar(freq) * amp
})

Or whatever, as long as the the tokens are valid. Like rust macros.