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.