Chuck implements an object system that borrows from both C++ and Java conventions. In our case this means:
Object
class (as in Java).For the sake of clarity we will define these terms:
ChucK has a number of classes defined within the language.
Object
: base class to all ChucK objects.Event
: ChucK's basic synchronization mechanism; may be extended to
create custom Event functionality (discussed here).Shred
: basic abstraction for a non-preemptive ChucK process.UGen
: base unit generator class (discussed here.UAna
: base unit analysis class (discussed here.Let's begin with some examples. For these examples, let's assume
Foo
is a defined class.
// create a Foo object; stored in reference variable bar
Foo bar;
This code does two things:
bar
is declared. Its type is Foo
.Foo
is created, and its reference is assigned to bar
.Note that in contrast to Java, this statement both declares a reference
variable and instantiates an instance of that class and assigns the
reference to the variable. Also note that in contrast to C++, foo
is a reference, and does not represent the object itself.
To declare a reference variable that refers to nothing (also called a null reference):
// create a null reference to a Foo object
Foo @ bar;
The above code only declare a reference and initializes it to null
(random note: the above statement may be read as "Foo at bar").
We can assign a new instance to the reference variable:
// assign new instance of Foo to bar
new Foo @=> Foo @ bar;
// (this statement is equivalent to 'Foo bar', above)
The code above is exactly equivalent to Foo bar;
as shown above. The
new
operator creates an instance of a class (in this case Foo
).
The @=>
operator performs the reference assignment (described here).
It is possible to make many references to same object:
// make a Foo
Foo bar;
// reference assign to duh
bar @=> Foo @ duh;
// (now both bar and duh points to the same object)
ChucK objects are reference counted and garbage collection takes place automatically.
As stated above, classes usually contain data and behavior, in the form of
member variables and member functions, respectively. Members are
accessed by using dot notation - reference.memberdata
and
reference.>memberfunc()
. To invoke a member function of an object (assuming
class Foo
has a member function called compute
that takes two integers
and returns an integer):
// make a Foo
Foo bar;
// call compute(), store result in boo
bar.compute( 1, 2 ) => int boo;
If a class has already been defined in the ChucK virtual machine (either in the same file or as a public class in a different file) then it can be instantiated similar to primitive types.
Unless declared public
, class definitions are scoped to the shred and
will not conflict with identically named classes in other running shreds.
Classes encapsulate a set of behaviors and data. To define a new object type,
the keyword class
is used followed by the name of that class.
// define class X
class X
{
// insert code here
}
If a class is defined as public
, it is integrated into the central
namespace (instead of the local one), and can be instantiated from other
programs that are subsequently compiled. There can be at most one public
class per file.
// define public class MissPopular
public class MissPopular
{
// ...
}
// define non-public class Flarg
class Flarg
{
// ...
}
// both MissPopular and Flarg can be used in this file
// only MissPopular can be used from another file
We define member data and methods to specify the data types and functionality
required of the class. Members, or instance data and instance functions are
associated with individual instances of a class, whereas static
data
and functions are only associated with the class (and shared by the instances).
Instance data and methods are associated with an object.
// define class X
class X
{
// declare instance variable 'm_foo'
int m_foo;
// another instance variable 'm_bar'
float m_bar;
// yet another, this time an object
Event m_event;
// function that returns value of m_foo
fun int getFoo() { return m_foo; }
// function to set the value of m_foo
fun void setFoo( int value ) { value => m_foo; }
// calculate something
fun float calculate( float x, float y )
{
// insert code
}
// print some stuff
fun void print()
{
<<< m_foo, m_bar, m_event >>>;
}
}
// instantiate an X
X x;
// set the Foo
x.setFoo( 5 );
// print the Foo
<<< x.getFoo() >>>;
// call print
x.print();
In the current release, constructors aren't supported. However, we have a single pre-constructor. The code immediately inside a class definiton (and not inside any functions) is run every time an instance of that class is created.
// define class X
class X
{
// we can put any ChucK statements here as pre-constructor
// initialize an instance data
109 => int m_foo;
// loop over stuff
for( 0 => int i; i < 5; i++ )
{
// print out message how silly
<<< "part of class pre-constructor...", this, i >>>;
}
// function
fun void doit()
{
// ...
}
}
// when we instantiate X, the pre-constructor is run
X x;
// print out m_foo
<<< x.m_foo >>>;
Static data and functions are associated with a class, and are shared by all
instances of that class -- in fact,static elements can be accessed without
an instance, by using the name of the class: Classname.element
.
// define class X
class X
{
// static data
static int our_data;
// static function
fun static int doThatThing()
{
// return the data
return our_data;
}
}
// do not need an instance to access our_data
2 => X.our_data;
// print out
<<< X.our_data >>>;
// print
<<< X.doThatThing() >>>;
// create instances of X
X x1;
X x2;
// print out their static data - should be same
<<< x1.our_data, x2.our_data >>>;
// change use one
5 => x1.our_data;
// the other should be changed as well
<<< x1.our_data, x2.our_data >>>;
Inheritance in object-oriented code allows the programmer to take an existing
class and extend or alter its functionality. In doing so we can create a
taxonomy of classes that all share a specific set of behaviors, while
implementing those behaviors in different, yet well-defined, ways. We indicate
that a new class inherits from another class using the extends
keyword. The
class from which we inherit is referred to as the parent class, and the
inheriting class is the child class. The child class receives all of the
member data and functions from the parent class, although functions from the
parent class may be overridden (below). Because the children contain the
functionality of the parent class, references to instances of a child class
may be assigned to a parent class reference type.
For now, access modifiers (public, protected, private) are included but not fully implemented. Everything is public by default.
// define class X
class X
{
// define member function
fun void doThatThing()
{
<<<"Hallo">>>;
}
// define another
fun void hey()
{
<<<"Hey!!!">>>;
}
// data
int the_data;
}
// define child class Y
class Y extends X
{
// override doThatThing()
fun void doThatThing()
{
<<<"No! Get away from me!">>>;
}
}
// instantiate a Y
Y y;
// call doThatThing
y.doThatThing();
// call hey() - should use X's hey(), since we didn't override
y.hey();
// data is also inherited from X
<<< y.the_data >>>;
Inheritance provides us a way of efficiently sharing code between classes which perform similar roles. We can define a particular complex pattern of behavior, while changing the way that certain aspects of the behavior operate.
// parent class defines some basic data and methods
class Xfunc
{
int x;
fun int doSomething( int a, int b ) {
return 0;
}
}
// child class, which overrides the doSomething function with an addition operation
class Xadds extends Xfunc
{
fun int doSomething ( int a, int b )
{
return a + b ;
}
}
// child class, which overrides the doSomething function with a multiply operation
class Xmuls extends Xfunc
{
fun int doSomething ( int a, int b )
{
return a * b;
}
}
// array of references to Xfunc
Xfunc @ operators[2];
// instantiate two children and assign reference to the array
new Xadds @=> operators[0];
new Xmuls @=> operators[1];
// loop over the Xfunc
for( 0 => int i; i < operators.cap(); i++ )
{
// doSomething, potentially different for each Xfunc
<<< operators[i].doSomething( 4, 5 ) >>>;
}
Because Xmuls and Xadds each redefine doSomething( int a, int b )
with their
own code, we say that they have overridden the behavior of the parent class.
They observe the same interface, but have potentially different implementation.
This is known as polymorphism.
Function overloading in classes is similar to that of regular functions.
For more details, see function.