Noob: don't understand usage of curly brackets in if-statement

This may look like the dumbest question of them all, but I can’t get my head around the following:

// please evaluate line by line

~wahr = {|i| ("wahr" + i).postln}; // a function

~falsch = {|i| ("falsch" + i).postln}; // a function

// EXAMPLE A
5.do({|i| if ( [false, true].choose, ~wahr, ~falsch)}); 
// works (but i is nil, as i is not handed over to functions, so this nil is expected)

// EXAMPLE B
5.do({|i| if ( [false, true].choose, ~wahr.(i), ~falsch.(i))}); 
// doesn't work, as both functions are executed (if statement is ignored), but why?

// EXAMPLE C
5.do({|i| if ( [false, true].choose, {~wahr.(i)}, {~falsch.(i)})}); 
//works, but why do I need to put the functions into curly brackets again?

In example B I don’t understand why just handing over the i value to the functions all of sudden makes the whole thing ignore the if statement.

And in example C I don’t understand why it works: Putting { } around something makes it a function, and if I evaluate ~wahr or ~falsch on its own, post window returns “a function”. So when using these functions in the if statement, why do I need to put these functions into { } again? And why are these { } not needed in A and B?

Thanks much in advance!

if expects two functions. In Example 1 and 3 you indeed pass functions. In Example 2, however, you evaluate both functions, so if actually receives the results. Of course, this is not what you wanted.

1 Like

Another key point is that SC has no flow of control structures.

It has only method calls. (Some method calls are treated specially by the compiler for speed, but if infinitely-fast processing were possible, we could do without that entirely: if could be defined by dispatching true and false separately as it does now, and while can be defined by tail recursion.)

A method has a receiver (for if, the receiver is the condition) and zero or more arguments. Note that this means, unlike the C/Java family, there is no “true block” and “false block” – in SC, if has arguments for the true or false values.

In every method call, the receiver and all arguments must be evaluated before they can be passed in. If an argument is written as a + 1 then it must do the addition before calling the method.

So what is the result of the expression ~wahr above? It’s a function: {|i| ("wahr" + i).postln}. So when you write if(something, ~wahr), the environment variable is evaluated, resolving to the function, and then the function is passed into if, where it acts like a block to be evaluated conditionally.

What is the result of the expression ~wahr.value(1)? It is the string that was posted. The value call must be executed before calling if! So it is no longer a conditional.

What is the result of { ~wahr.value(1) }? A function containing the function call – which is passed in to be used as a conditional block.

hjh

2 Likes

Well, as I recently got bitten by this. That’s not quite so. Normally for method call if, which happens when you use parentheses but not braces, both branches are evaluated, but one result discarded, as this is the normal call convention in SC, that is to evaluate arguments of function or method call before the call.

What you call “treated specially by the compiler for speed” is probably the short circuit behavior that the compiler emits code for when you use if with braces, i.e. function literals as 2nd (and possibly 3rd) arguments. That one is exactly what control structures do in C, so it’s misleading to say there are no control structures in SC. It’s just that they are almost aliased to method calls that look very similar.

x = 0; y = 4
if(true, x = x + 1, y = y + 1) // evals to 1 of course, but
[x, y] // -> [ 1, 5 ]

// whereas
x = 0; y = 4
if(true, {x = x + 1}, {y = y + 1})
[x, y] // -> [ 1, 4 ]

// above same as
x = 0; y = 4
if(true) {x = x + 1} {y = y + 1}
[x, y] // -> [ 1, 4 ]


{ if(true, x = x + 1, y = y + 1) }.def.dumpByteCodes
/*
BYTECODES: (15)
  0   6C       PushSpecialValue true
  1   01 19    PushInstVarX 'x'
  3   6B       PushOneAndAdd
  4   07 19    StoreInstVarX 'x'
  6   01 1A    PushInstVarX 'y'
  8   6B       PushOneAndAdd
  9   07 1A    StoreInstVarX 'y'
 11   B0       TailCallReturnFromFunction
 12   C3 0B    SendSpecialMsg 'if'
 14   F2       BlockReturn
-> < closed FunctionDef >
*/

{ if(true, {x = x + 1}, {y = y + 1}) }.def.dumpByteCodes
/*
BYTECODES: (18)
  0   6C       PushSpecialValue true
  1   F8 00 08 JumpIfFalse 8  (12)
  4   01 19    PushInstVarX 'x'
  6   6B       PushOneAndAdd
  7   07 19    StoreInstVarX 'x'
  9   FC 00 05 JumpFwd 5  (17)
 12   01 1A    PushInstVarX 'y'
 14   6B       PushOneAndAdd
 15   07 1A    StoreInstVarX 'y'
 17   F2       BlockReturn
-> < closed FunctionDef >
*/

At bytecode level there definitely are different things in SC for actual conditional jumps (e.g. JumpIfFalse) and method calls (SendSpecialMsg 'if').

It’s a different perspective on the same thing… is it “a method call that has been optimized to inline the functions” or is it “a control structure masquerading as a method call”?

IMO (opinion) the baseline implementation for branching and looping in SC is via method calls with function arguments. By “baseline” I mean that you could remove all of the inline-optimization from the compiler, so that every looping and branching keyword compiles to a standard method call, and SC would behave exactly the same (just, slower).

(
var x = 0, y = 4;

{
	if(true) {
		var random = rrand(1, 10);
		x = x + random;
		y = y + random;
	}
}.def.dumpByteCodes;
)

BYTECODES: (7)
  0   6C       PushSpecialValue true
  1   04 00    PushLiteralX instance of FunctionDef in closed FunctionDef
  3   B0       TailCallReturnFromFunction
  4   C2 0B    SendSpecialMsg 'if'
  6   F2       BlockReturn

Some such constructions are inlined for speed, specifically those where the functions passed as arguments 1/ are literal functions and 2/ don’t declare local variables or arguments. (In practice, this covers most uses of if, while, case and switch.) I personally don’t think it’s entirely accurate to say that SC’s if is a C-style control structure that is sometimes compiled as a method call with arguments.

But I also recognize that we are looking at the same Rubik’s Cube from different angles, and just seeing different colors.

hjh

I actually hadn’t tried these mixed cases before because it’s horrible style. The rule implemented by the compiler seems to be that any stuff that is in braces is subject to short-circuit at least for if on Booleans. So:

x = 0; y = 4
if(true, x = x + 1, {y = y + 1})
[x, y] // -> [ 1, 4 ]

x = 0; y = 4
if(true, {x = x + 1}, y = y + 1)
[x, y] // -> [ 1, 5 ]


// opposite test condition
x = 0; y = 4
if(false, x = x + 1, {y = y + 1})
[x, y] // -> [ 1, 5 ]

x = 0; y = 4
if(false, {x = x + 1}, y = y + 1)
[x, y] // -> [ 0, 5 ]

I don’t want to get into arguments like these, but in my opinion it’s not the same the same thing because semantics differ when the branches have side effects, such as in my examples.

I think there’s a misunderstanding:

if (true) { x } { y } is exactly the same as if (true, { x }, { y }) or true.if { x } { y }. It’s just syntactic sugar. (@jamshark70 please correct me if I’m wrong here!)

In many case you can simply omit parentheses when passing functions as arguments. You can also enclose the first argument in paranthesis. Let me demonstrate this with do. These are all the same:

5.do({ |x| x.postln }); // normal method call
5.do { |x| x.postln }; // omit parenthesis
do(5, { |x| x.postln }); // function call syntax
do (5) { |x| x.postln }); // can omit parenthesis when enclosing first argument in parenthesis

sclang uses something called uniform function call syntax where method calls can be written as function calls and vice versa.

Now you see how we arrive at if (true) { x } { y } which looks so familiar to C programmers.

if (true, x, y), on the other hand, does something different because you pass values, not functions. Sometimes this is ok, e. g. if you want if to return one of two values depending on the condition: if (x == 4, 1, 2). Here we don’t need to put the arguments in functions because there is really nothing to evaluate. It’s basically like the ternary operator in C.


if basically just takes the two arguments it receives and calls .value on one of them and discards the other, depending on the condition. It always short-circuits! You can look up the implementation in Boolean.sc. I think then everything becomes clear.

if(true, x = x + 1, y = y + 1) → the interpreter evaluates both expressions and passes the results to the if method. Here it would take the first argument, call .value on it (which might do nothing) and return it.

if(true, {x = x + 1}, {y = y + 1}) → the interpreter passes two functions to the if method. Here it would take the first function, call .value on it, i.e. call it, and return the result.

1 Like

Are you sure the compiler does nothing on top of that? At least some inlining? It’s literally impossible for the compiler to emit code like in my first post in this thread for if(true, {x = x + 1} {y = y + 1}) just with method calls. It emits no method calls in fact for that expression. To repeat that here:

{ if(true, {x = x + 1}, {y = y + 1}) }.def.dumpByteCodes
/*
BYTECODES: (18)
  0   6C       PushSpecialValue true
  1   F8 00 08 JumpIfFalse 8  (12)
  4   01 19    PushInstVarX 'x'
  6   6B       PushOneAndAdd
  7   07 19    StoreInstVarX 'x'
  9   FC 00 05 JumpFwd 5  (17)
 12   01 1A    PushInstVarX 'y'
 14   6B       PushOneAndAdd
 15   07 1A    StoreInstVarX 'y'
 17   F2       BlockReturn
-> < closed FunctionDef >
*/

Where is the method call?

As @jamshark70 has said, the compiler will do certain optimizations like inlining. However, it is not allowed to change the semantics of your code!

Here’s another example:

(true || { "false".postln; false }) → first argument already true, no need to evaluate the second argument (= function never called)

(true || ( "false".postln; false )) → expression in paranthesis is evaluated by the interpreter before passing it to the or method and is therefore evaluated regardless of the condition.

There is really no compiler magic about short-circuiting. It all just boils down to the difference between expressions and function blocks.

When inlining doesn’t happen, you are correct however that the Boolean.sc implementation is what happens, as you said. That is

True : Boolean {
	if { arg trueFunc, falseFunc; ^trueFunc.value }

As example when the short-circuit happens as a result of the if method call, we can look at a “horrible style” mixed example, which lacks braces on the first branch

{ if(true, x = x + 1, { y = y + 1 }) }.def.dumpByteCodes
/*
-> < closed FunctionDef >
BYTECODES: (12)
  0   6C       PushSpecialValue true
  1   01 19    PushInstVarX 'x'
  3   6B       PushOneAndAdd
  4   07 19    StoreInstVarX 'x'
  6   04 00    PushLiteralX instance of FunctionDef - closed
  8   B0       TailCallReturnFromFunction
  9   C3 0B    SendSpecialMsg 'if'
 11   F2       BlockReturn
-> < closed FunctionDef >
*/

In this case indeed a function is pushed on stack but True.if will ignore it, i.e. not call .value on it because it only does that on its first argument.

As I said, there’s a difference between expressions (paranthesis) and functions (braces).

if(true, x = x + 1, { y = y + 1 })

Here the first argument is an expression and the second argument is a function. The braces are not optional!

You can actually see it in the byte code:

  1. push true
  2. evaluate the expression x = x + 1 and push the result on the stack = ARG1
  3. push the function { y = y + 1 } on the stack (don’t evaluate it!) = ARG2
  4. call the if method on true with ARG1 and ARG2 as arguments
  5. if will evaluate ARG1 (does nothing) and return it. ARG2 is discarded (= short circuiting).

Again, the if method itself always short-circuits.

I guess the real source of confusion in SC for newbies used to C or Java is that the meaning of braces in SC is not “begin block that executes this right now” but: make a function that executes only later this when .value is sent to it.

There’s probably nothing like a true block equivalent in SC since var declarations don’t seem to work nested in parentheses more than one level

(var x = 1; x) // ok

(var x = 1; (var y = 2; y)) // err

{ var x = 1; (var y = 2; y) } // also err

{ var x = 1; { var y = 2; y }.value } // ok, but a Function

( var x = 1; { var y = 2; y }.value ) // evals to 2 right now...

But there’s no “true inner block” on the last line, only a function declared and then evaluated, although that is arguably functionally equivalent.

Yes! I also think that’s a common source of confusion.

There’s probably nothing like a true block equivalent in SC since var declarations don’t seem to work nested in parentheses more than one level

Yes, that’s confusing. The distinction between expressions and blocks is rather vague. Unlike other languages, expressions/blocks (maybe “expression blocks”?) can have several statements, but for some reasons variable declarations are only allowed on the top level. I don’t know why…

But there’s no “true inner block” on the last line, only a function declared and then evaluated, although that is arguably functionally equivalent.

Just wanted to write that { ... }.value is a way to emulate nested blocks similar to C-style languages. You beat me to it.

Interestingly, even something like

{ var x = 1; { 2 }.value }.def.dumpByteCodes

isn’t optimized by the compiler to remove the inner .value method call.

BYTECODES: (6)
  0   04 00    PushLiteralX instance of FunctionDef - closed
  2   B0       TailCallReturnFromFunction
  3   C1 06    SendSpecialMsg 'value'
  5   F2       BlockReturn
-> < closed FunctionDef >

I’ll stop digressing on blocks now :smiley:

The compiler generally doesn’t inline function definitions that contain variable declarations.

When you tick Post Inline Warnings in the preferences, the compiler will even emit a warning for every function that it couldn’t inline because of variables. This is very annoying for Quark authors because users will complain about such warnings, so you’re effectively prohibited from using local variables in if-statements… As a consequence, you see code where all local variables are declared in the top most function block - which completely goes against good coding practices (declaring variables where they are actually used). Very frustrating.

1 Like

Well, the last 16 posts or so have been about this, so that is hopefully clear by now. As for.

Your example A is defective because ~wahr expects an argument, but you’re not providing one that makes sense from the 5.do context. So i in ~wahr is going to be nil in that example A call.

~wahr = {|i| ("wahr" + i).postln};
~wahr.() // wahr nil

What {~wahr.(i)} does is to return a function that sends to ~wahr the proper argument from the 5.do context (which is also a function). So {~wahr.(i)} is not the same function as ~wahr because i in the former is already bound; a clue is that {~wahr.(i)} is a function that does not take any arguments! This is really called creating a closure.

Here’s a simpler example

~wahr = {|i| ("wahr" + i).postln}; 

z = {~wahr.(42)} // also a function
// but takes no arg and always gives the same arg to ~wahr

z.() // 42

z.(111)  // still 42

Now every time the do does a loop it creates a new function like z, but with a different constant bound inside, from the current loop iteration.

Well, that is what happens conceptually. In reality, the if there gets inlined
at line 13 below because you’ve used braces, i.e. functions on both branches of if and because the two branch functions like {~wahr.(i)} don’t declare any variables of their own! So the if practically works like in a traditional imperative language in your case, meaning no actual functions get created from {~wahr.(i)}.

{ if ( [false, true].choose, {~wahr.(i)}, {~falsch.(i)} ) }.def.dumpByteCodes

/*
BYTECODES: (34)
  0   06 19    PushSpecialClass 'Array'
  2   65       PushSpecialValue 2
  3   C2 00    SendSpecialMsg 'new'
  5   6D       PushSpecialValue false
  6   C2 08    SendSpecialMsg 'add'
  8   6C       PushSpecialValue true
  9   C2 08    SendSpecialMsg 'add'
 11   C1 24    SendSpecialMsg 'choose'
 13   F8 00 0A JumpIfFalse 10  (26)
 16   41       PushLiteral Symbol 'wahr'
 17   C1 3C    SendSpecialMsg 'envirGet'
 19   1A       PushInstVar 'i'
 20   B0       TailCallReturnFromFunction
 21   C2 06    SendSpecialMsg 'value'
 23   FC 00 07 JumpFwd 7  (33)
 26   40       PushLiteral Symbol 'falsch'
 27   C1 3C    SendSpecialMsg 'envirGet'
 29   1A       PushInstVar 'i'
 30   B0       TailCallReturnFromFunction
 31   C2 06    SendSpecialMsg 'value'
 33   F2       BlockReturn
-> < closed FunctionDef >
*/

For the hardcore geeks, the magic stuff happens in compileIfMsg. The test for inlining isAnInlineableBlock is indeed whether both branch functions in an if declare no arguments and no variables of their own.

1 Like

While reading the initial answers of @Spacechild1 and @jamshark70, the “the penny dropped” (or “der Groschen fiel”.

Hopefully.

I paraphrased what I understood, please let me know if I got it right or wrong:

  • In every method call (or message), the receiver and all arguments must be evaluated first.
  • Then the message and its arguments are sent to the receiver.
  • For the if message, the condition is the receiver, trueFunction and falseFunction are the arguments, and these arguments must be functions in order for the if message to work.
  • If the evaluation of the if message’s trueFunction and falseFunction both give a function, these trueFunction and falseFunction functions are passed on to the receiver as arguments, where one of these functions gets executed conditionally, with its result becoming the returned value of the if message.
  • But if the evaluation of either of the if message’s trueFunction and falseFunction gives a value, this value is (or these values are) passed on to the receiver as arguments, where they unconditionally become the returned values of the if message.

and these arguments must be functions in order for the if message to work.

This is a bit misleading. The arguments for if don’t have to be functions, they can be values. It really depends on what you want. The point is just that if will call .value on one of its arguments (and ignore the other) depending on the condition. If the argument is a function, .value will effectively evaluate it. If the argument is already a value, then .value just returns the value itself.

Generally, it’s probably good style to also use functions for plain values to have a consistent syntax. The compiler will inline it anyway. So you would write if (true) { 1 } { 0 } instead of if(true, 1, 0), although the latter would be perfectly fine in this case.