398 lines
9.6 KiB
Text
398 lines
9.6 KiB
Text
|
title:: Writing Classes
|
||
|
summary:: Writing SuperCollider Classes
|
||
|
categories:: Language>OOP
|
||
|
related:: Reference/Classes
|
||
|
|
||
|
For a basic tutorial on how standard object-orientated classes are composed, look elsewhere
|
||
|
link::http://www.google.com/search?q=oop+class+tutorial::
|
||
|
|
||
|
note:: Class definitions are statically compiled when you launch SuperCollider or "recompile the library." This means
|
||
|
that class definitions must be saved into a file with the extension .sc, in a disk location where SuperCollider looks
|
||
|
for classes. Saving into the main class library (SCClassLibrary) is generally not recommended. It's preferable to use
|
||
|
either the user or system extension directories.
|
||
|
|
||
|
code::
|
||
|
Platform.userExtensionDir; // Extensions available only to your user account
|
||
|
Platform.systemExtensionDir; // Extensions available to all users on the machine
|
||
|
::
|
||
|
|
||
|
It is not possible to enter a class definition into an interpreter window and execute it.
|
||
|
::
|
||
|
|
||
|
|
||
|
|
||
|
section:: Inheriting
|
||
|
|
||
|
code::
|
||
|
MyClass : SomeSuperclass {
|
||
|
|
||
|
}
|
||
|
::
|
||
|
|
||
|
Without specifying a superclass, link::Classes/Object:: is assumed as the default superclass.
|
||
|
|
||
|
code::
|
||
|
MyClass { // :Object is implied
|
||
|
|
||
|
}
|
||
|
::
|
||
|
|
||
|
section:: Methods
|
||
|
|
||
|
class methods are specified with the asterisk within the class method, the keyword code::this:: refers to the class. A
|
||
|
class in SuperCollider is itself an object. It is an instance of link::Classes/Class::. Instance methods are specified
|
||
|
within the instance method, the keyword code::this:: refers to the instance.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
*classMethod { arg argument;
|
||
|
|
||
|
}
|
||
|
|
||
|
instanceMethod { arg argument;
|
||
|
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
|
||
|
To return from the method use teletype::^:: (caret). Multiple exit points also possible. If no teletype::^:: is
|
||
|
specified, the method will return the instance (and in the case of Class methods, will return the class). There is no
|
||
|
such thing as returning void in SuperCollider.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
someMethod {
|
||
|
^returnObject
|
||
|
}
|
||
|
|
||
|
someOtherMethod { arg aBoolean;
|
||
|
if(aBoolean,{
|
||
|
^someObject
|
||
|
},{
|
||
|
^someOtherObject
|
||
|
})
|
||
|
}
|
||
|
|
||
|
}
|
||
|
::
|
||
|
|
||
|
|
||
|
section:: New Instance creation
|
||
|
|
||
|
code::Object.new:: will return to you a new object. When overriding the class method code::.new:: you must call the
|
||
|
superclass, which in turn calls its superclass, up until code::Object.new:: is called and an object is actually created
|
||
|
and its memory allocated.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
// this example adds no new functionality
|
||
|
*new {
|
||
|
^super.new
|
||
|
}
|
||
|
|
||
|
// this is a normal constructor method
|
||
|
*new1 { arg arga,argb,argc;
|
||
|
^super.new.init(arga,argb,argc)
|
||
|
}
|
||
|
init { arg arga,argb,argc;
|
||
|
// do initiation here
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
In this case note that code::super.new:: called the method new on the superclass and returned a new object. Subsequently
|
||
|
we are calling the code::.init:: method on that object, which is an instance method.
|
||
|
|
||
|
Warning:: If the superclass also happened to call super.new.init it will have expected to call the .init method defined
|
||
|
in that class (the superclass), but instead the message .init will find the implementation of the class that the object
|
||
|
actually is, which is our new subclass. So you should use a unique method name like myclassinit if this is likely to be
|
||
|
a problem.
|
||
|
::
|
||
|
|
||
|
One easy way to copy the arguments passed to the instance variables when creating a class is to use newCopyArgs. This
|
||
|
method will copy the arguments to the instance variables in the order that the variables were defined in the class,
|
||
|
starting the parent classes and working it's way down to the current class.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
var <a,b,c;
|
||
|
|
||
|
*new { arg a,b,c;
|
||
|
^super.newCopyArgs(a,b,c)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MyChildClass : MyClass{
|
||
|
var <d;
|
||
|
|
||
|
*new { arg a,b,c,d;
|
||
|
^super.newCopyArgs(a,b,c,d)
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
Over reliance on inheritance is usually a design flaw. Explore "object composition" rather than trying to obtain all
|
||
|
your powers through inheritance. Is your "subclass" really some kind of "superclass" or are you just trying to swipe all
|
||
|
of daddy's methods? Do a websearch for Design Patterns.
|
||
|
|
||
|
Class variables are accessible within class methods and in any instance methods.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
classvar myClassvar;
|
||
|
var myInstanceVar;
|
||
|
}
|
||
|
::
|
||
|
|
||
|
For class initialization check code::initClass::.
|
||
|
|
||
|
section:: Overriding Methods (Overloading)
|
||
|
|
||
|
In order to change the behaviour of the superclass, often methods are overridden. Note that an object looks always for
|
||
|
the method it has defined first and then looks in the superclass. Here code::MyClass.value(2):: will return 6, not 4:
|
||
|
|
||
|
code::
|
||
|
Superclass {
|
||
|
calculate { arg in; in * 2 }
|
||
|
value { arg in; ^this.calculate(in) }
|
||
|
}
|
||
|
|
||
|
MyClass : Superclass {
|
||
|
calculate { arg in; in * 3 }
|
||
|
}
|
||
|
::
|
||
|
|
||
|
If the method of the superclass is needed, it can be called by super.
|
||
|
|
||
|
code::
|
||
|
Superclass {
|
||
|
var x;
|
||
|
|
||
|
init {
|
||
|
x = 5;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MyClass : Superclass {
|
||
|
var y;
|
||
|
init {
|
||
|
super.init;
|
||
|
y = 6;
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
|
||
|
|
||
|
section:: Getter Setter
|
||
|
|
||
|
SuperCollider demands that variables are not accessible outside of the class or instance. A method must be added to
|
||
|
explicitly give access:
|
||
|
|
||
|
code::
|
||
|
MyClass : Superclass {
|
||
|
var myVariable;
|
||
|
|
||
|
variable {
|
||
|
^myVariable
|
||
|
}
|
||
|
|
||
|
variable_ { arg newValue;
|
||
|
myVariable = newValue;
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
These are referred to as getter and setter methods. SuperCollider allows these methods to be easily added by adding teletype::<:: or
|
||
|
teletype::>::.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
var <getMe, >setMe, <>getMeOrSetMe;
|
||
|
}
|
||
|
::
|
||
|
|
||
|
you now have the methods:
|
||
|
|
||
|
code::
|
||
|
someObject.getMe;
|
||
|
someObject.setMe_(value);
|
||
|
::
|
||
|
|
||
|
this also allows us to say:
|
||
|
|
||
|
code::
|
||
|
someObject.setMe = value;
|
||
|
|
||
|
someObject.getMeOrSetMe_(5);
|
||
|
someObject.getMeOrSetMe.postln;
|
||
|
::
|
||
|
|
||
|
|
||
|
A getter or setter method created in this fashion may be overridden in a subclass by manually writing the method setter
|
||
|
methods should take only one argument to support both ways of expression consistently. eg.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
variable_ { arg newValue;
|
||
|
variable = newValue.clip(minval,maxval);
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
A setter method should always return the receiver. This is standard OOP practice (outside of SuperCollider as well). Do
|
||
|
not return the new value from your setter.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
variable_ { arg newValue;
|
||
|
myVariable = newValue;
|
||
|
^newValue // DO NOT DO THIS
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
section:: External Method Files
|
||
|
|
||
|
Methods may be added to Classes in separate files. This is equivalent to Categories in Objective-C. By convention, the
|
||
|
file name starts with a lower case letter: the name of the method or feature that the methods are supporting.
|
||
|
|
||
|
code::
|
||
|
+ Class {
|
||
|
newMethod {
|
||
|
}
|
||
|
|
||
|
*newClassMethod {
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
|
||
|
section:: Slotted classes
|
||
|
|
||
|
Classes defined with [slot] can use the syntax myClass[i] which will call myClass.at(i). This is useful when defining
|
||
|
classes that inherit from a Collection type class.
|
||
|
|
||
|
code::
|
||
|
MyClass[slot] {
|
||
|
*new {
|
||
|
^super.new
|
||
|
}
|
||
|
|
||
|
at {|i|
|
||
|
("Index "++i).postln
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
code::
|
||
|
a = MyClass();
|
||
|
a[3];
|
||
|
::
|
||
|
|
||
|
Defining a custom array of Points:
|
||
|
|
||
|
code::
|
||
|
MyPointArray[slot] : RawArray {
|
||
|
center { ^Point(*this.asArray.flop.collect{ |item| item.sum / item.size } ) }
|
||
|
asArray{ ^this.collect{ |elem| elem.asArray } }
|
||
|
}
|
||
|
::
|
||
|
|
||
|
|
||
|
section:: Printing custom messages to post window
|
||
|
|
||
|
By default when postln is called on an class instance the name of the class is printed in a post window. When
|
||
|
code::postln:: or code::asString:: is called on a class instance, the class then calls code::printOn:: which can be
|
||
|
overridden to obtain more useful information.
|
||
|
|
||
|
code::
|
||
|
MyTestPoint {
|
||
|
var <x, <y;
|
||
|
|
||
|
*new{ |x,y| ^super.newCopyArgs(x,y) }
|
||
|
|
||
|
printOn { arg stream;
|
||
|
stream << "MyTestPoint( " << x << ", " << y << " )";
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
code::
|
||
|
a = MyTestPoint(2,3);
|
||
|
::
|
||
|
|
||
|
subsection:: Defining custom asCompileString behaviour
|
||
|
|
||
|
A call to code::asCompileString:: should return a string which when evaluated creates the exact same instance of the
|
||
|
class. To define a custom behaviour one should either override code::storeOn:: or code::storeArgs::. The method
|
||
|
code::storeOn:: should return the string that evaluated creates the instance of the current object. The method
|
||
|
code::storeArgs:: should return an array with the arguments to be passed to code::TheClass.new::. In most cases this
|
||
|
method can be used instead of code::storeOn::.
|
||
|
|
||
|
code::
|
||
|
// either
|
||
|
MyTestPoint {
|
||
|
var <x, <y;
|
||
|
|
||
|
*new{ |x,y| ^super.newCopyArgs(x,y) }
|
||
|
|
||
|
storeOn { arg stream;
|
||
|
stream << "MyTestPoint.new(" << x << ", " << y << ")";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// or
|
||
|
MyTestPoint {
|
||
|
var <x, <y;
|
||
|
|
||
|
*new{ |x,y| ^super.newCopyArgs(x,y) }
|
||
|
|
||
|
storeArgs { arg stream;
|
||
|
^[x,y]
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
code::
|
||
|
MyTestPoint(2,3).asCompileString;
|
||
|
::
|
||
|
|
||
|
section:: Catching methods that are not explicitly defined
|
||
|
|
||
|
It is possible to catch calls to methods that are not explicitly defined in a Class by overriding code::doesNotUnderstand::.
|
||
|
|
||
|
code::
|
||
|
MyClass {
|
||
|
*new { ^super.new }
|
||
|
|
||
|
doesNotUnderstand { arg selector...args;
|
||
|
(this.class++" does not understand method "++selector);
|
||
|
|
||
|
If(UGen.findRespondingMethodFor(selector).notNil){
|
||
|
"But UGen understands this method".postln
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
::
|
||
|
|
||
|
code::
|
||
|
a = MyClass();
|
||
|
a.someMethodThatDoesNotExist
|
||
|
::
|
||
|
|
||
|
section:: Tricks and Traps
|
||
|
|
||
|
definitionList::
|
||
|
|
||
|
## "Superclass not found..."
|
||
|
|
||
|
|| In one given code file, you can only put classes that inherit from each Object, each other, and one external class.
|
||
|
In other words, you can't inherit from two separate classes that are defined in separate files.
|
||
|
|
||
|
If you should happen to declare a variable in a subclass and use the same name as a variable declared in a superclass,
|
||
|
you will find that both variables exist, but only the one in the object's actual class is accessible. You should not do
|
||
|
that. This will at some point become an error worthy of compilation failure.
|
||
|
::
|