Thomas Royal

Composer | Pianist | Technologist

Writings


Making and Calling Functions in Supercollider

May 30, 2014

I stopped using SuperCollider for a while because the IDE was too brittle and the language was too limiting in its insistence that all classes be precompiled. Rather, I used Python as a replacement for sclang, using scsynth as a synthesis engine.

The new IDE made me reconsider. I love using Python with Vim and Eclipse, but the need to rewrite functions that already existed in Supercollider created a productivity bottleneck. The new IDE, with its syntax highlighting, separation from sclang, and integrated, functional help documentation, made Supercollider very usable.

My Problems With Calling Supercollider Functions

One thing that prevented me from freely adopting Supercollider was a dissatisfaction with how functions are implemented in that language. First, is the way functions are called:

(
var func = {
  |f_arg|
  f_arg.postln;
};

// because func() would be too easy
// let us get call func by asking for its value
// whether we want the value or not
func.value("ugh!");
)

Typing .value(arg) is much more cumbersome than simply typing (arg) after typing the function name. Also, the calling convention assumes that one calling a function is always interested in the value returned from the function. Maybe this issue would be improved if .eval was used instead of .value.

These are minor issues, of course. The biggest problem I had with calling Supercollider functions is that I had deep misconceptions about how variables are passed into Supercollider functions.

Passing-By-Value vs. Passing-By-Reference

One issue I had with calling functions was that I often wanted a function to alter the values of the variables I was passing in. For example:

(
var increment_arg = {
  |x|
  x = x + 1;
  x.postln;
};
var x = 1;

// attempting to increment x
increment_arg.value(x); // prints 2
x.postln; // prints 1
)

The idea is that increment_arg takes in an argument and then alters it by adding the number one.

You would not typically expect this to work. In C, for example, in order to do something similar, you have to explicitly stipulate that you would like to pass the variable in a way that allows it to be altered. To be specific, you pass to the function the variable's location in memory. This convention is also known as "pass by reference" or "pass by pointer."

I tried very hard to find a way to pass by reference in Supercollider. Neither the mailing lists nor the help documentation helped me with this. One dead end worth mentioning was my struggles with the Ref class, which I though allowed one to work with pointers, but really didn't do much for me at all.

One might wonder whether it is not simpler to call the function and then set its output to the argument. It is indeed a better approach for the toy problem:

(
var increment_arg = {
  |x|
  x = x + 1;
  x.postln;
};
var x = 1;

// here the difference is the 'x = ' at the beginning
// that assigns the output of the function to the
// original argument

x = increment_arg.value(x); // prints 2 
x.postln; // prints 2, yawn
)

There are, however, reasons for wanting a mechanism for passing by reference. For example, one might want to create a function that plays a routine that alters the value referred to by a particular variable. The below demonstrates an example of attempting to do this, which fails.

(

var key = 0;

// every second, there is a 30% chance of modulating up one half step
var key_change_routine = {
  |key|
  Routine({
    while(true,{
      if(0.3.coin,{
        key = key + 1;
      });
      "key_change_routine: ".post;
      key.postln;
      1.0.wait;
    });
  }).play;
};

// every second, print the value of key
var print_current_key_every_second = {
  |key|
  Routine({
    while(true,{ 
      "print_current_key_every_second: ".post;
      key.postln; 
      1.0.wait 
    });
  }).play;
};


key_change_routine.value(key);
print_current_key_every_second.value(key);

)

/*

Result: 

key_change_routine: 1
print_current_key_every_second: 0
key_change_routine: 2
print_current_key_every_second: 0
key_change_routine: 2
print_current_key_every_second: 0
key_change_routine: 2
print_current_key_every_second: 0
key_change_routine: 2
print_current_key_every_second: 0
key_change_routine: 3
print_current_key_every_second: 0
*/

One could of course comment out the argument names (i.e. \\|key|), and the Routine's inside the functions will use the variable key declared outside of the function scope. The problem with this is that the functions are hard-coded to only use the key variable; you cannot tell the function what value you want to alter.

One kludgey solution is to change the above code so that key is an array containing one value:

(
// this has been changed to an array
var key = [0]; 

// every second, there is a 30% chance of modulating up one half step
var key_change_routine = {
  |key|
  Routine({
    while(true,{
      if(0.3.coin,{
        \\ this has been changed to handle arrays
        key[0] = key[0] + 1; 
      });
      "key_change_routine: ".post;
      key.postln;
      1.0.wait;
    });
  }).play;
};

// every second, print the value of key
var print_current_key_every_second = {
  |key|
  Routine({
    while(true,{ 
      "print_current_key_every_second: ".post;
      key.postln; 
      1.0.wait 
    });
  }).play;
};


key_change_routine.value(key);
print_current_key_every_second.value(key);

)

/*

Result: 

key_change_routine: [ 0 ]
print_current_key_every_second: [ 0 ]
key_change_routine: [ 1 ]
print_current_key_every_second: [ 1 ]
key_change_routine: [ 1 ]
print_current_key_every_second: [ 1 ]
key_change_routine: [ 2 ]
print_current_key_every_second: [ 2 ]
key_change_routine: [ 3 ]
print_current_key_every_second: [ 3 ]
*/

If it works, it works. The problem I have with this, however, is that it seems to abuse the array, which is supposed to be a collection of elements rather than a container that allows us to cheat.

What is important about the above example, however, is that it demonstrates a subtlety pertaining to the way Supercollider handles data and how data is passed to functions.

In Supercollider, Everything is an Object: The Full Story

The first thing you learn about Supercollider is that everything is an object. This is what enables one to do awesome things like call methods on numbers like this:

440.postln; // oooo!
440.cpsmidi; // OOOOOOOOOO!

This capability is very convenient for an environment that is meant to allow one to compose music. (I was taught that if you want to be a composer, "you've got to learn to spit.")

So what happens when you pass a variable to a function? The object that is referenced by the variable, not the variable, is passed to the function. Yet this doesn't explain why a function will alter an array object but not an integer or float object. If the number object is passed to a function, shouldn't alterations to the number be reflected by references to the original variable?

The fact is, clearly not all objects are created equal. Specifically, there are two kinds of objects in Supercollider: mutable and immutable. How do you tell which one is which? You ask, of course:

1.mutable; // false
[1].mutable; //true

So arrays are mutable, and integers are not.

What does it mean to say and object is mutable? To say that an object is mutable means that it can be changed. Specifically, methods associated with a mutable object might be capable of altering the object. Methods on immutable objects that appear to change the object actually return a new object with the desired value.

This can be demonstrated by the following code (which requires that one know the difference between == and ===):

(

// create a new object and set a to reference it
var a = [1]; 
// assign b to reference the object referenced in a
var b = a;

// to the object referenced by the variable b (which is also
// the object referenced by a), change the first element to the object '2'
b.put(0,2); 
a.postln; // [ 2 ] 
b.postln; // [ 2 ] 

// == : tests for value equality
// === : tests for object identity

// true (because a and b refer to the same object)
(a===b).postln; 

// create a new object [2] and set b to reference it
b = [2]; 

// false, because a new object was created on the previous line
(a===b).postln; 
// true, because a and b refer to objects having the same value
// as defined by the Array class
(a==b).postln; 
)


(

// create a new object '1' and set a to reference it
var a = 1; 

// assign b to reference the object referenced in a
var b = a; 

// true because a and b reference the same '1' object
(a===b).postln; 

// call .log(2) on the '1' object and return a new '0' object
b = a.log(2); 

// false, because b points to a new object
(a===b).postln; 

// assign b to the '1' object
b = 1; 

// true, because for each immutable objects, there may be only one of 
// each, making a and b point to the same object.
(a===b).postln; 

)

Something interesting to note is that there can only be a singe 1 object (or 2 or 3 or \three object) in memory. Independently assigning two different variables to the value 1 means assigning both variables the reference of the same 1 object:

(
var a = 1;
var b = 1;
(a===b).postln; // true
)

We can now see why sending an array to a function allows for it to be altered. Take the following example:

(
// create an array with a '1' at position 0
var x = [1];

// 'x[0] = value' is equivalent to 
// 'x.put(0,value)'
// 'x[0] + 1' is equivalent to
// 'x.get(0) + 1'

var inc = { |x| x[0] = x[0]+1 };

// assign y to reference the value referenced by x
var y = x;

// call the above function using the object referenced at x
inc.value(x);

x.postln; // [2]
y.postln; // [2]
(x===y).postln; // true
)

In the function called inc, the array brackets are being used as a shorthand for calling the put method defined by the Array class. x inside the function contains the same reference to x (and y) outside the function. Calling methods on x inside the function is the same as calling these methods outside the function.

Finally, it is important to note that assigning a value to an argument inside the function, rather than altering the object, removes the reference to the original object and creates a reference to a new object:

(

var outside_a = [1];

var test_func = {
  |inside_a|

  // the argument 'inside_a' refers to the
  // same object that the outer variable
  // 'outside_a' refers to
  (inside_a === outside_a).postln;

  // assign 'inside_a' to refer to a newly created object
  inside_a = [1];

  // false, because now both variables refer to different object
  (inside_a === outside_a).postln;

  // true because both objects have the same value
  (inside_a == outside_a).postln;
};

test_func.value(outside_a);

)

Admittedly all of this is subtle and complex, but with time, one can get a clear picture of how variables, objects, and function arguments work in Supercollider.

Why Does Supercollider have Different Kinds of Objects?

I have no idea why Supercollider (or other languages) have mutable in immutable objects. The research I have done into this question suggests that immutable objects are more thread safe, more secure, and less error prone. I don't know if any of this is relevant for the use case of composition.

One advantage that the differentiation between mutable and immutable has is that it allows for a consistent passing convention that makes assumptions about how one wants to handle certain kinds of data. Generally, in languages like C, ints are passed by value, and arrays are passed by reference. What is a common practice in C is solidified as part of the language in Supercollider.

There is also a certain philosophical advantage to calling numbers immutable. In nature, there is no way to change the concept of one. Adding an apple to a basket doesn't change the existence of singular entities. Rather, the collection of apples is better described by a different concept: the concept of two.

Otherwise, I don't know. I am not a language designer.

The Need for Classes

The irony is that one of the reasons that I abandoned SCLang for Python is that I was unaware that the passing conventions and object-orientedness between the two languages are quite similar. The reason it took me so long to figure out was that in Python, it is very easy to make your own class, which are mutable by default. When passing integers gives you lemons, you make the object-lemonade.

The really nice thing about Python classes is that you do not have to reconfigure the entirety of your programming environment in order to make one class that you use for only one project. This is not the case in Supercollider, which seems to regard classes as precomposition enabling the improvisation of Supercollider's functional environment. Whatever; sounds to me like the old distinction in Cage's writings between structure and form.

Really, Supercollider needs a mechanism for the rapid construction of ad hoc classes. I wonder when an extraordinarily clever, good-looking Supercollider user will figure out how to do that and share his or her knowledge with the world.