Tip Of The Day#1: Syntactic shortcuts for if statements (syntactic sugar)

I am starting a little series on tips and tricks especially geared towards the beginner but possibly useful to more experienced users as well.

#1 Syntactic shortcuts for if statements

Instead of if (someVal.notNil) { do something } or if (someVal.isNil) { do something2 } we can use the syntactic shortcuts someVal !? { do something if the value is not nil } and someVal ?? { do something2 if the value is nil}. These are described in the Object help file. There is no syntactic shortcut for if (someval.notNil) { do something } { else do something2 } like eg. in JavaScript: someVal ? do something : do something2. Still the syntactic shortcuts are extremely helpful and add clarity to the code as I will try to demonstrate below:

// An event of events of events.... A very common and useful data structure which could easily be deeper than this. More on this in a later post.
// This is just a silly example
p = (sub: (sub2: (sub3: (val1: 6, val2: 8, val3: 7))))

// Now wanting to manipulate some values, but only if the sub-event is not nil and the values exist
// We can do this because the values exist
p.sub.sub2.sub3.newVal = p.sub.sub2.sub3.val1 * p.sub.sub2.sub3.val2 * p.sub.sub2.sub3.val3

// but this will throw an error because sub4 is not defined
p.sub.sub2.sub4.newVal = p.sub.sub2.sub4.val1 * p.sub.sub2.sub4.val2 * p.sub.sub2.sub4.val3

// So we need to safe-guard, first going for one-liners

if (p.sub.sub2.sub3.notNil) { p.sub.sub2.sub3.newVal = p.sub.sub2.sub3.val1 * p.sub.sub2.sub3.val2 * p.sub.sub2.sub3.val3 };
if (p.sub.sub2.sub4.notNil) { p.sub.sub2.sub4.newVal = p.sub.sub2.sub4.val1 * p.sub.sub2.sub4.val2 * p.sub.sub2.sub4.val3 };

/// pretty clumsy, so a 2 liner is better

(
var ev = p.sub.sub2.sub3;
if (ev.notNil) { ev.newVal = ev.val1 * ev.val2 * ev.val3 }
)

(
var ev = p.sub.sub2.sub4;
if (ev.notNil) { ev.newVal = ev.val1 * ev.val2 * ev.val3 }
)

// same result, p.sub.sub2.sub3.newVal = 336, p.sub.sub2.sub4 = nil;

// NOW USING SYNTACTIC SHORTCUTS we can get the best of both worlds: a 1 liner with the clarity of the 2 liner

p.sub.sub2.sub3 !? {|ev| ev.newVal = ev.val1 * ev.val2 * ev.val3 }
p.sub.sub2.sub4 !? {|ev| ev.newVal = ev.val1 * ev.val2 * ev.val3 }

// notice that the event, if not nil, is the argument to the function above unlike a normal if statement which has no arguments:

if (p.sub.sub2.sub3.notNil) {|ev| ev.debug(\ev) }
p.sub.sub2.sub3 !? {|ev| ev.debug(\ev) }

// If the values in the sub event are not guaranteed to exist we can again apply syntactic sugar for the nil check

p.sub.sub2.sub3 !? {|ev| ev.newVal = ev.val1 * ev.val2 * ev.val3 * (ev.val4 ? 1) } 
// val4 is undefined so mulitply by 1 = no change

// if later we now define val4, 
p = (sub: (sub2: (sub3: (val1: 6, val2: 8, val3: 7)))) // reset to original state
p.sub.sub2.sub3.val4 = 5;
p.sub.sub2.sub3 !? {|ev| ev.newVal = ev.val1 * ev.val2 * ev.val3 * (ev.val4 ? 1) }
// val4 is defined so multiply with val4 = 5;
2 Likes

I’m a big fan of this syntax x !? {|y| ...use y...}, however it is quite slow because it can’t be inlined.

It is best to omit the |y| if you need speed.

1 Like

Yes, good point. Can you elaborate a bit on this?

TLDR: turns out this doesn’t matter with events and it is actually okay, I didn’t know that; it really matters with arrays. @julian that might interest you as we recently had a conversation about function inline-ability in the class library.

So like all benchmarks, it is really complicated…

I’m gonna write an explanation of why it might be slower first. Then I’ll do two sets of benchmarks.

This will be the example code.

e = (\a: 10)

1 e.a !? { |value| value + 1 }

BYTECODES: (9)
  0   16       PushInstVar 'e'
  1   A1 00    SendMsg 'a'
  3   04 01    PushLiteralX instance of FunctionDef - closed
  5   B0       TailCallReturnFromFunction
  6   C2 48    SendSpecialMsg '!?'
  8   F2       BlockReturn

TailCallReturnFromFunction is slow because it has to do a bunch of setup and tear-down.

2 e.a !? { e.a + 1 }

BYTECODES: (12)
  0   16       PushInstVar 'e'
  1   A1 00    SendMsg 'a'
  3   8F 1B 00 04 ControlOpcode 4  (10)
  7   16       PushInstVar 'e'
  8   A1 00    SendMsg 'a'
 10   6B       PushOneAndAdd
 11   F2       BlockReturn

Performs the lookup twice, is this faster than version one?

See how the PushOneAndAdd instruction is directly written into the top function definition, this means the instruction is right there, no setup or tear-down.

8F 1B 00 04 ControlOpcode 4 This is the instruction that does the test. The ‘4’ corresponds to the size of the bytecodes generated from { e.a + 1 }, this way, if it is nil, it can skip these instructions. On the far left shows you the instructions index in the total array, so 7 16 PushInstVar 'e' is at position 7, add 4 gives you 11 F2 BlockReturn skipping the add one, and returning the nil that was already there.

3 var r = e.a; r !? { r + 1 }

BYTECODES: (13)
  0   16       PushInstVar 'e'
  1   A1 00    SendMsg 'a'
  3   80 00    StoreTempVar 'r'
  5   30		 PushTempZeroVar 'r'
  6   8F 1B 00 02 ControlOpcode 2  (11)
 10   30		 PushTempZeroVar 'r'
 11   6B       PushOneAndAdd
 12   F2       BlockReturn

Same as before, but the skipped instructions only have a size of 2.

benchmarks

I ran each version 1,000,000 times.

e = (\a: 10)

#1 
e.a !? { |value| value + 1 }
// time to run: 0.45990054399999 seconds.
#2
e.a !? { e.a + 1 }
time to run: 0.5949357180001 seconds.
#3
var r = e.a; r !? { r + 1 }
time to run: 0.30478602100004 seconds.

Version two is slightly slower than 1 as lookup is slow, but version 3 is much faster.

e = [0, 1];

#1 
e[0] !? { |value| value + 1 }
// time to run: 0.096922036000024 seconds.
#2
e[0] !? { e[0] + 1 }
time to run: 0.05078639300018 seconds.
#3
var r = e[0]; r !? { r + 1 }
time to run: 0.055378582999992 seconds.

In this case, the lookup is faster than looking up a variable, and twice as fast as the function call, that’s a big improvement.

2 Likes

Thanks for the explanation and testing. Good to know. I am constantly chasing optimizations of code - partly because I need it since I am dealing in realtimesness and partly as a game (yeah I know the sayings about the pitfalls of fixating on optimizing code…but still).