This page gives an overview of the conditional expressions in SuperCollider and the many ways to write them.
| if | if(expr) { trueFuncBody } { falseFuncBody } |
| case | case { testFuncBody1 } { trueFuncBody1 } ... { testFuncBodyN } { trueFuncBodyN } |
| switch | switch(value) { testValue1 } { trueFuncBody1 } ... { testValueN } { trueFuncBodyN } |
| while | while { testFuncBody } { loopFuncBody } |
More syntax variants exist, see below.
someFuncBody is distinct from someFunc (e.g., trueFunc vs. trueFuncBody, testFunc vs. testFuncBody, etc.). In distinction from someFunc, someFuncBody does not stand for an already instantiated Function object (a lambda function), but rather for an expression that can serve as a Function definition.
In other words, if { "doThis".postln } is your someFunc, then "doThis".postln;---without the curly Brackets---is your someFuncBody.
Consequently, do not write if([false,true].choose) { { "doThis".postln; } },
but do write if([false,true].choose) { "doThis".postln; }.
For other control structures, see loop / repeat and Exception.
falseFunc == { falseFuncBody }.
For instance, when falseFunc == { "aaa" }, then falseFuncBody == "aaa".
The .if method is called on a receiver expr which must return a Boolean value. (In other words, if is a method of Boolean.)
In addition the method call takes two arguments: a Function trueFunc to execute (call .value on) if the expression is true, and another optional Function falseFunc to execute (call .value on) if expr is false.
The .if method returns the value of the function which is executed (i.e., trueFunc.value if receiver is true, falseFunc.value if receiver is false).
If falseFunc is not present and expr is false, then the .if method returns nil.
Function also implements a case method which allows for conditional evaluation with multiple cases. The receiver here but can be thought of as simply the zeroth argument, representing the first of multiple "cases". Thus, the arguments including the receiver can be written as pairs of testFuncs and corresponding trueFuncs. if a testFunc returns true, its corresponding trueFunc is evaluated and its .value returned. If no testFunc returns true, either nil is returned, or the .value of a defaultFunc supplied as the final argument. Case is inlined (under the conditions outlined below), and is therefore just as efficient as nested if statements.
or
Object implements a switch method which allows for conditional evaluation with multiple cases. Each case is represented by a pair of two arguments: a testValue followed by a trueFunc. In distinction to case, the receiver is not the first test function, but rather an object that other testValues are all compared to. The receiver is compared against the testValues, and if the comparison returns true, the corresponding trueFunc will be evaluated and its value returned. If it is false, the next testValue is compared, and so on. The return value of switch in case of no matching testValue is dependent on whether the number of arguments passed to it is even or odd. If the number of arguments is even, switch will return nil in such cases, whereas if it is odd, the last argument should be a defaultFunction whose .value will be returned.
The comparison function used is equality, i.e., ==, unless the switch statement is inlined (see also below), in which case switch compares by identity, i.e., ===. (See Identity and Equality in SuperCollider)
The switch statement will automatically be inlined if two conditions are met:
First, the test objects are all values with unique representations (Floats, Integers, Symbols, Chars, nil, false, true) and second, functions used in the switch statement have no variable or argument declarations. The inlined switch uses a hash lookup (which is faster than nested if statements), so it should be very fast and scale to any number of clauses.
(2/3) == (1 - (1/3)) returns false.or:
The following code will inline, but will compare by identity:
The identity comparison 1 === 1.0 returns false. While 1.0 and 1 represent the same numeric value, one is a Float and the other is an Integer, so they cannot be identical. On the other hand, if we prevent inlining by declaring a variable within one of the functions, the code will compare by equality: 1 == 1.0 returns true.
The while method implements conditional execution of a loop; it is a method of Function. If the testFunc answers true when evaluated, then the loopFunc is evaluated and the process is repeated. Once the testFunc returns false, the loop terminates.
Note the distinction to if: testFunc is a Function (which returns itself), e.g., {x < 0}, whereas expr above was an expression, e.g., x < 0, which returned a Boolean.
while expressions are also optimized by the compiler if they do not contain variable declarations in the testFunc and the loopFunc.
Conditional expressions are a type of control structures; another useful type are iterations such as .do and .for. These are discussed separately in the page on iteration (also see Collection: Iteration). Finally, the methods Function: -try and Function: -protect are technically conditional expressions, but their intended use is in the handling of exceptions, which is why their are discussed in Exception rather than here.
if, while, switch, and case expressions are optimized (i.e., inlined) by the compiler if they do not contain variable declarations in the functions. We can see this if we dump the bytecodes of the receiver function definition. (Bytecodes are a lower-level representation of the code sent to the interpreter.) The optimization does not use function calls within the bytecodes and instead uses a jump statement, which is faster.
This returns the following warning,1 and below it we can see what the bytecodes look like without inlining:
WARNING: FunctionDef contains variable declarations and so will not be inlined.
in file 'selected text'
line 4 char 14:
var notHere;
"hello".postln;
-----------------------------------
BYTECODES: (13)
0 2C 06 PushInt 6
2 2C 09 PushInt 9
4 E6 SendSpecialBinaryArithMsg '=='
5 04 00 PushLiteralX instance of FunctionDef - closed
7 04 01 PushLiteralX instance of FunctionDef - closed
9 B0 TailCallReturnFromFunction
10 C3 0B SendSpecialMsg 'if'
12 F2 BlockReturn
-> < closed FunctionDef >
That is, entries 5 and 7 are proper function calls, which are costly (i.e., slow/inefficient).
Here is the opposite example, where inlining has taken place:
The bytecodes read as follows:
BYTECODES: (20) 0 2C 06 PushInt 6 2 2C 09 PushInt 9 4 E6 SendSpecialBinaryArithMsg '==' 5 F8 00 07 JumpIfFalse 7 (15) 8 41 PushLiteral "hello" 9 B0 TailCallReturnFromFunction 10 C1 3A SendSpecialMsg 'postln' 12 FC 00 04 JumpFwd 4 (19) 15 40 PushLiteral "world" 16 B0 TailCallReturnFromFunction 17 C1 3A SendSpecialMsg 'postln' 19 F2 BlockReturn -> < closed FunctionDef >
Entries 5 and 12 are jump statements, instructing to jump to entries 15 and 19 respectively. In between those lines, we can recognize the contents of the two Functions, but they are now no longer implemented as function calls, but as direct instructions, which saves resources.
The control structures discussed here is intended for use in sclang (i.e., language/client), not in scsynth (i.e., server). Use of .if in signal processing contexts may appear to work in simple cases but is not advised. Intead, use Select or SelectX and related classes. Also see User FAQ: SynthDef Issues for more details.
The following code runs (does not produce an error), but is not good usage, because it is not very obvious what is going on:
Here, the receiver LFTri is a UGen. The UGen class in turn implements an UGen: -if method by translating it into a linear crossfade using binary operators:
The regular Boolean: -if discussed above does not crossfade. Hence, if the crossfade is desired, it is better practice to make this explicit by using LinSelectX:
On the other hand, in the case where not a crossfade but a simple either/or analogous to language-side Boolean.if is desired, Select is preferable:
In SuperCollider, control structures implemented as methods to be called on a receiver and which return a value. This is distinct from many other languages where, e.g., if is a reserved identifier (keyword) independent of a particular receiver, and does not return a value. For instance, in C or in python, you cannot create a variable called if, whereas in SuperCollider, you can (which does not mean that you should...). In SuperCollider, it is also possible to implement an .if method for a particular class that does something rather different (see below); despite these differences, SuperCollider's flexible syntax can make it look as if if were just such a reserved identifier, whereas it is in fact a method of Boolean.
Thus, SuperCollider's flexibility with respect to syntax is a potentially confusing factor when it comes to conditional expressions. For instance, there are four equivalent ways to write a simple if-expression:
| Receiver notation | Function call notation | |
| Regular argument notation | condition.if(trueFunc, falseFunc) | if(condition, trueFunc, falseFunc) |
| Argument(s) as trailing blocks | condition.if { trueFuncBody } { falseFuncBody } | if(condition) { trueFuncBody } { falseFuncBody } |
While the receiver notation reflects the implementation in SuperCollider most closely, it is hard to read and rarely used in practice. The two function call notations are the most commonly used. The version with trailing argument blocks superficially resembles conditional syntax familiar from C or javascript:
//if-statement in C or javascript:
if (condition) {
trueFuncBody
} else {
falseFuncBody
}The above C-like syntax is enabled in SuperCollider by a syntactic mechanism called "trailing argument blocks". A block is any expression enclosed in curly brackets, e.g. { trueFuncBody }. The syntax receiver.method { expr1 } { expr2 } is equivalent to receiver.method({ expr1 }, { expr2 }). That is, the round brackets and comma are omitted. This can be combined with the function call syntax such that we can write method(receiver) { expr1 } { expr2 }; this in turn gives the above form of the if-statement in function call notation with trailing argument block.
However, even if the curly brackets now indicate where one argument ends and where the next one starts, they also retain their usual role of showing that the enclosed expression should serve as the definition of a Function instance.
This means, first, that the use of trailing argument syntax implies that expr1 and expr2 define Functions; second, that expr1 and expr2 should not already be Functions in their own right (unless you intend them to be).
In examples:
If the receiver is itself an expression surrounded by curly brackets, a special version of the trailing argument block syntax can be used: Here, not only the arguments can be written as trailing {}-enclosed blocks, but also the receiver itself. For instance, { funcBody }.fork is often written as fork { funcBody }.
When the method takes arguments, these arguments must also be written as {}-enclosed blocks, and follow the receiver:
method { receiverFuncBody } { arg1FuncBody } { arg2FuncBody }
Among the conditional expressions discussed here, this applies to .while and .case (because they are methods of Function.)
Example:
Unlike other common programming languages, conditional expressions (.if, .case, and .switch) in SuperCollider have return values by default; these in turn can be assigned to variables. For instance, in python, the following will produce an error:
a = if True: "gobbledeegook"
and for conditional assignment, one has to write
if True: a = "gobbledeegook"
Whereas in SuperCollider, both analogous cases work:
However, this does not change the syntactic requirement that the assignment statement, when it is an argument to a conditional expression, must be a Function. In other words, the following is wrong:
This is because a Function is expected after the comma in if(expr, arg).