SuperColliderショートカットマスターへの道 - オブジェクト指向から始めよう -

*全然見返していないため、プレとして出します。徐々に修正追記していきます。

TKSC#2(打ち上げの飲み)から帰ってきて、そうだ、ショートカットについてまとめようと思い、ちまちま調べたり、書いたりしていましたが、Smalltalk流のオブジェクト指向から始めないと、SCの文法、ショートカット記述を理解することは難しいということが分かりました。なあなあでコードを書いていても一応動くので、始めは良いのですが、そのうち詰まる部分がでてきます。なんでこの記述でいいんだろう?とかですね。本文を書いている最中で、私がかなり誤解していることも発覚し、当ブログでもかなりの嘘を書いてきました。すみません。それは、棚に上げておいて、

あなたは、

SinOsc.ar( 440, 0, 1 )
ar ( SinOsc, 440, 0, 1 )

の引数がそれぞれ何か説明できるか!?(twitterで嘘ついてました、ほんとすみません。)

というわけで、ショートカットへの道として、SCにおけるオブジェクト指向の解説を適当にまとめてみました。変数、引数、関数に加え、なんとなくでもおkなので、オブジェクト指向、クラス、関数を知っているとすんなり理解できると思いますよ。私は、なあなあでしか分かっていませんでした。ですから、本文もかなり間違いがあるかもしれません。指摘していただければ幸いです。

じゃあ、いくよ!

1. SCにおけるオブジェクト指向 - object oriented -

オブジェクト指向言語には大まかに2種類あり、Smalltalk流とC++流です。SCはSmalltalkの流れを汲んでいます。そのため、C++に慣れている方にとっては非常に取っ付きにくいです。オブジェクト指向に関しては、webサイトで勉強しても良いのですが、どちらの概念に基づいているかということが明記されていないことが多く、混乱の原因になります。SCにおけるオブジェクト指向言語を理解するならば、Smalltalkの流れを汲んだ言語の基礎本読むのが一番手っ取り早いです。ただし、今更Smalltalkをやっても仕方がなさそうですから、SCに近いRubyをお勧めします。Rubyには日本語の本が多いですしね。もちろん、SmalltalkRuby関係の入門サイトでもちゃんと説明は書いてありますよ。また、C++Javaでのオブジェクト指向をマスターしている方は、一旦概念を横に置いておかないと、何がなんやら分からなくなり、Smalltalk死ね!SC死ね!max/msp最強!!とかなりますので、ご注意を。別にmax最強でも良いんですけどね。
ex) SinOsc.arは
smalltalk概念で読むと、SinOscオブジェクトにarメッセージを送る。
C++概念で読むと、SinOscクラスのarメンバ関数を呼び出す。
のように読めます。処理結果は同じですが、ニュアンスは全然違います。

1.1 オブジェクト object

オブジェクトとは、様々な属性値(周波数、振幅など)、振る舞い(大きい値を返す、別のものを加えるなど)を保有している”部品”を指します。(オブジェクトは部品、モノというニュアンスよりも目的というニュアンスの方がわかりやすいのかな?目的にメッセージを送って制御する、とか)属性値は、プロパティ(パラメータ)。振る舞いは、メソッドといいます。
SuperColliderでは、Smalltalkライクなオブジェクト指向言語であり、全てのもの(単なる数値含め)がオブジェクトであるという位置づけとなっています。例えば、変数xを定義していた場合、xはオブジェクトとして扱われます。10という数値もオブジェクトであり、プロパティとメソッドを保有していることになります。
SCでは、クラスとしてObjectクラスをもっています。Objectクラスは全てのクラスのスーパークラスであり、もっとも基本的なメソッドはObjectクラスから継承し、他のクラスでも使用できます。また、数値を含めてオブジェクトですから、数値もオブジェクトがもっているメソッドを実行することができます。(どのデータ型もObjectクラスを継承しているため、Objectクラスがもっているメソッドを実行可能、ということでただしいのかな?継承というか共有?)

1.2 処理
1.2.1 メソッド method

メソッドとは、オブジェクトがもっている処理(振る舞い)のことで、postウィンドウに結果を出力する(これはpostメソッド)、というような処理のことを指します。では、オブジェクトがもっているメソッドの実行方法(呼び出し方法)です。記述方法は、次のようになります。

	レシーバ.メッセージ		//レシーバがメッセージを受け取る
	//オブジェクト.メソッド	C++ライクでは、オブジェクトのメソッド(メンバ関数)を呼び出す

smalltalkの流れを汲んだオブジェクト指向言語では、メソッドの呼び出しを"レシーバにメッセージを送る"ことで実現します。この記述は、メッセージ式と呼ばれています。レシーバとは、メッセージを受け取るオブジェクトのことです。ここで混乱する箇所としては、メッセージ?メソッド?同じっすか?、かな。メッセージ式の記述では、メッセージとメソッドは別物です。メソッドは上記の通り、オブジェクトが保持している処理のことです。メッセージとは、レシーバ(オブジェクト)のメソッドを呼び出す処理のことです。メッセージには、呼び出したいメソッドの名前を示す”セレクタ”とセレクタに渡す引数に分けられます。
”レシーバ.メッセージ”は、”レシーバ.メソッド名(引数)” or ”レシーバ.セレクタ(引数)”と読み替えることができ、レシーバはメッセージから、実行すべきメソッドとそれに渡す引数を受け取り、自身のメソッドを実行する。という解釈まで落とし込めます。
C++に慣れているとメッセージと、メソッドを同じに捉えてしまいますが、違いますよ。強引に、メソッド?ああ、メンバ関数ね、とか進めていくとSCのショートカット記述で詰まる気がします。

1.2.2 メッセージ message

メソッド項目で書いちゃってます!オブジェクトにメソッドを実行させるための、命令のことです。

1.3 クラスとインスタンス class & instance

前項でのオブジェクトとメソッドの基礎はよろしいでしょうか。"レシーバ.メッセージ"ですよ!前項まででクラス、クラス、おい、クラスってなんだよ?と。クラスとは、構造を定義し、インスタンスとしてオブジェクト郡の実装をするものです。要はクラスをもとに、インスタンスと呼ばれるオブジェクトを作成します。ここでの作成とはメモリに割り当てられることを示します。
クラスには継承という概念があり、親クラスが保有しているメソッドは子クラスに継承されます。親クラスをスーバークラス。子クラスをサブクラスと呼びます。あるクラスが保有しているメソッド、さらにスーパークラスから継承しているメソッドを確認したい場合は、クラスオブジェクトにdumpAllMethodsメッセージを送ります。インスタンスとオブジェクトはごっちゃになりやすいです。インスタンス=オブジェクト だ。とか書いてるサイトもありますが、違います。クラスについては、こんなもんで。(memo:要追記、修正部分)
クラスの書き方とかやらねーの?と言われそうですが、理解出来ていない部分があるため、すっとばします。インスタンスの作成は次章で。

1.4 SCでのオブジェクト、インスタンスの使用方法

前項までで、オブジェクトやクラスの概念は理解してもらえたでしょうか?え、無理?すみません。

1.4.1 基本

クラスという設計書からインスタンスという色々いじくり倒せるオブジェクトを作成します。SinOscクラスのインスタンスを作成するには

	//SinOscクラスオブジェクトにnewメッセージを送り。SinOscクラスのnewメソッドを実行させる。ここでnewメソッドは省略できるけれど、それはまた別のお話。
	a = SinOsc.new();
	//確認。aはSinOscクラスから作成されてますね。classメソッドはObjectクラスのメソッドで、_ObjectClassというプリミティブメソッドを呼び出している。プリミティブメソッドとは、大元のC++で実装されている処理のこと。
	a.class;	

これでaというSinOscクラスのインスタンスが作成されました、当然、aはオブジェクトです。スーパークラスである、Ugenのメソッドも継承しているのでメッセージで呼び出して使用可能です。

	//SinOscクラスのスーパークラスはUGenクラスです
	SinOsc.superclass.postln;
	//UGenのメソッドも呼び出せる	
	a.init;
	a.copy;
	a.madd;	
	//UGenのスーパークラスはAbstractFunctionですね
	UGen.superclass.postln;	
	//AbstractFunctionのメソッドも呼び出せる	
	a.neg;
	a.reciprocal;	

ここで、クラスブラウザやOsc.scをみて、arメソッドもあるじゃん!とかいって

	//arメッセージをaオブジェクトに送り、arメソッドを実行させたい
	a.ar();

とかやっても無駄です。SinOscクラスのarメソッドをみると、頭に*がくっついています。これは、クラスメソッドを意味します。(ここらへんよくわかっていないのですが)クラスメソッドとは、クラスに直接属しているメソッドでコンストラクタ的な意味をもち、インスタンスでは実行できません。*がついていないメソッドはインスタンスメソッドでありインスタンスで実行可能です。インスタンスメソッドは、実体をもっていないクラスからは実行できません。

	b = LFPulse();
	//arはクラスメソッド
	LFPulse.ar();
	b.ar(440);	
	//signalRangeはインスタンスメソッド
	b.signalRange;
	LFPulse.signalRange;

実行できないものは、ポストウィンドウにエラーが返ってきますが、これらは間違いというわけではありません。bというオブジェクトにarメソッドを実行しろ、というメッセージを送っているだけですからね。例えば、2行目実行後のポストウィンドウのエラーメッセージを見てください。ERROR: Message 'ar' not understood.とあるかと。arメソッドを保有していないので、そんなメッセージ理解できねーよ。と文句言っているわけですね。その下に、レシーバはLFPulseのインスタンスだよ、ごにょごにょ。引数はIntの440。さらに下の方にselectorはarだよ。とかありますね。エラーメッセージは、基礎文法を理解していると、読めるようになります!(私は、これを書いている最中に初めてちょっと読めましたw)

1.4.2 謎の記述方法

もうちょいメッセージ使ったメソッドの呼び出しをみてみましょうか。Arrayクラスにpostlnメソッドは定義されていませんが、Array.new.add(1).postln; はpostlnメソッドがちゃんと実行されます。変数x、数値も同様で 2.postln; x.postln; もpostlnメソッドが実行可能です。全てのスーパークラスであるObjectクラスのメソッドを継承しているからですね。というようなことを前項まででやってきました。もちろん単なる数値もObjectクラスのメソッドで呼び出し可能です。
次の2行を見て下さい。

	//レシーバ.メッセージ のメッセージ式	
	2.do( { arg x; x.postln; });
	//?
	do(2, {arg x; x.postln; });	

どちらも2回繰り返す処理を行います。上のdo(引数)というメッセージは、2というIntegerクラスのdoメソッドを実行させます。Integer.scには下のように定義されています。

	do { arg function;
		// iterates function from 0 to this-1
		// special byte codes inserted by compiler for this method
		var i = 0;
		while ({ i < this }, { function.value(i, i); i = i + 1; });
	}

どうやら、doメソッドの引数は関数をくれくれ、らしいので、{ }で囲ったものをdoセレクタ後方の引数( )内に{ }で関数を記述しています。一方、下の記述方法は何かというと、どうやらメッセージ式においてレシーバを省略すると、メッセージ内の第1引数がレシーバと認識されるようです。以下を実行して、エラーを確認してみましょう。エラーメッセージ読めると非常に捗ります。

	1.ar(440, 0, 1);
	ar(1,440, 0, 1);

上は、メッセージarがわかんね。と返され、レシーバはInt 1と解釈されていますね。下も実行してみましょう。なんと!同じエラーが返ってきました。ここで注目すべき箇所は、上下どちらもdoesNotUnderstandの引数が*3と解釈されている部分でしょう。ar( )の引数は4つではなく、3つだそうですよ。やはり、レシーバを省略したときはメッセージの第1引数がレシーバと解釈されるようです。うーむ。

1.4.3 関係ない話(情報求む)

バイナリオペレータのメソッドが意味不明、よくわからないんだけれど。Intクラスにはmaxメソッドなどの定義はないので1個上のSimpleNumberクラスをみてみると、

	max { arg aNumber=0.0, adverb; _Max; ^aNumber.performBinaryOpOnSimpleNumber('max', this, adverb) }

などとある。thisはperformBinaryOpOnSimpleNumberメソッドに渡されるらしい。^はリターンだ。

	1.max(10);	//結果は10
	-1.max();	//引数を省略すると結果は0、これはaNumberの初期値が0だからだと理解できる。
	max(1, 10);	//結果は10
	max(-1);	//結果は0、結果にaNumberの初期値が返ってきている。

thisがないため、レシーバの値は_Maxに直接渡されているのだろうか?

1.performBinaryOpOnSimpleNumber(\max, this, 10);

これの結果はMath operation failed.と返ってくる。performBinaryOpOnSimpleNumberメソッドの定義を見てみると以下だ。

		performBinaryOpOnSimpleNumber { arg aSelector, aNumber; ^error("Math operation failed.\n") }

aSelector,aNumberがなんだろうと、このエラーを返すらしい。ということは、_Maxのプリミティブメソッドでおkなら、perform-メソッドは実行されていないことがわかる。_Maxはどんな処理をしているのかが、どうやら鍵らしい。PryPrimitive.cppをみてみるとdefinePrimitive関数でopMax、と_Maxをひもづけているんだろう。opMaxを検索してみる、SetRawとかsc_maxがよくわからない。むり。たぶん、レシーバの値と第1引数の値だけプリミティブメソッドに渡され、ごにょごにょして返ってくるのだろう。第2引数のadverbは謎。(helpのadverbsにごちゃごちゃ書いてありますね。そのうち読んでみます!)

1.5 まとめ
	SinOsc.ar( 440, 0, 1 );

ふふん、SinOscクラスオブジェクトにメッセージとして、セレクタar、引数440,0,1送っているのさ。arメソッドはクラスメソッドだから、SinOscクラスオブジェクトから実行可能なんだぜ!

	ar( SinOsc, 440, 0, 1 );

しっ、見ちゃだめです!(えっ?

以上でオブジェクト指向を終わります。ちょっとは理解深まりましたか?
・SCでは、全てがオブジェクト。
Smalltalk流のオブジェクト指向
・エラーメッセージを読み解くことで、理解できることもある。
・やっぱり謎
用語
オブジェクト:プロパティ、メソッドを保有した部品のこと
クラス:構造を定義し、インスタンスとして実体化される設計書のこと
インスタンス:クラスをもとにメモリ上に領域を確保し、いじれるようにしたオブジェクトのこと
プロパティ:変数によって決められているオブジェクトの値
メソッド:オブジェクトが保有している、処理のこと。クラスメソッドとインスタンスメソッドがあります
メッセージ:オブジェクトに命令すること
レシーバ:メッセージで命令されるオブジェクトのこと
メッセージ式:レシーバ.メッセージ と”ドット”でつなげ、レシーバにメッセージを送り、メソッドを実行させる記述方法

2. 関数 function

前章では、オブジェクトについて書きましたので、おまけ的に関数も書いておきます。

2.1 関数の記述方法

オペレーション(演算、働き)を定義し、”value”メッセージを受け取ると動作します。関数を定義するには”{}”で括ります。引数の定義は”{”の後、変数の定義は引数の後、変数の後に処理を定義します。要は、
function = { arg 引数; var 変数; 処理 } で定義して
function.value(第1引数、第2引数) と引数に値を渡します。
これが基本ですね。変数、処理の記述は問題ないとして、引数の書き方は色々あるので注意しましょう。

	//最も基本的な書き方
	var fun; fun = { arg a,b,c; a.postln; b.postln; c }; fun.value(1,2,3);
	//引数は||ブロックで囲ってもおk。これでも書けるよ!的な書き方
	var fun; fun = { | a, b, c | a.postln; b.postln; c }; fun.value(1,2,3);
	//引数に配列を用いるには ”...”
	var fun; fun = { arg ... a; a }; fun.value(1,2,3,4,5);
	var fun; fun = { arg a,b ... c; a.postln; b.postln; c }; fun.value(1,2,3,4,5,6,7);

関数はこんなとこでしょう。

3. もうちょい具体的に

前章までで、オブジェクト指向の基本、おまけに関数について記載しました。でも、具体例やらんと身に付かないですよねー。ひとつひとつ、解読していくと理解が深まるんじゃね?しかし、これがまた難しい。

3.1 解読

SCらしく、SynthDefでの一般的なシンセ定義を解読してみましょうか。”レシーバ.メッセージ”のルールに従って読んでみます。
tksc#1の4. シーケンスを書いてみる、より

	(
		SynthDef.new( 
			'melo',
			 {
				arg frq = 785, amp = 0.5, atk = 0.005, rel = 0.2;
				var env, snd;
				env = EnvGen.kr( Env.perc( atk, rel ), doneAction: 2 );
				snd = SinOsc.ar( frq , 0, amp * env );
				Out.ar( 0, Pan2.ar ( snd ) );
			}
		).store;
	)

ちょっと変更していますが、これを分析していきます。まず、一番外から見てみるとSynthDefクラスオブジェクトに送っているメッセージは2つあります。newとstoreです。まず、SynthDefクラスはnewメッセージを受け取りnewメソッドでインスタンスを作成します。このときSynthDefが受け取る引数は二つで、シンボル'melo'と{}で括られた関数です。では、newメソッドの処理内容を見てみましょうか。SynthDef.scには以下のように記述されています。

	*new { arg name, ugenGraphFunc, rates, prependArgs, variants, metadata;
		^this.prNew(name).variants_(variants).metadata_(metadata)
			.build(ugenGraphFunc, rates, prependArgs)
	}

第1引数はname、第2引数はugenGraphFuncですね。^はリターン、thisはレシーバとなります。newメソッドの処理では、まずSynthDefクラスオブジェクトに、nameを引数としてprNewメッセージを送っています、さらにugenGraphFuncはbuildメッセージの第1引数として送っていますね。variants_()とmetadata_()に関してはSynthDefのクラス変数variantsとmetadataに()内の値を代入しています。ここでは空ですね。では、prNewメソッドを見てみましょうか。

	*prNew { arg name;
		^super.new.name_(name.asString)
	}

ここでは、'melo'を引数nameに代入し、SynthDefのスーパークラスであるObjectクラスのインスタンスをnewメッセージで作成、'melo'はasStringメッセージを受けシンボルから文字列に変換するasStringメソッドを実行します。文字列として返ってきた"melo"をスーパークラスインスタンスの変数nameに代入しています。(変数nameがあやしいなあ、これはSynthDefの変数nameかな)

(参考)
	SynthDef.superclass
	'melo'.asString.class

buildメソッドはbuildUgenGraphメソッドを呼び、さらにこいつがaddControlsFromArgsOfFuncメソッドを呼んでいます。buildメソッドの処理については、@OsamuTakahashi氏が詳しく解説しています。凄い人だなあ、全然レベルが違う。さて、上のnewメソッドまで戻りますと、prNewで文字列を変数に代入したり、buildでごにょごにょしたSynthDefオブジェクトが返ってくるようです。この返ってきたSynthDefオブジェクトにstoreメッセージを送っています。storeメソッドは.scsyndefという名前でシンセ定義ファイルを作成し、さらにsendメソッドと同じ処理をします。

大枠の処理は以上となっています。大雑把ではありますが、オブジェクト指向の基礎知識があれば多少読めるかと思います。続いて、newメッセージの第2引数である関数の中身を見てみましょう。関数の基本は”{引数; 変数; 処理; }”ですよ。1行目は、引数ですね。基本的な書き方ですね。2行目は変数、これも問題ないと思いますが宣言しておかないと次の行からのインスタンスが格納できません。次の処理にいきます。変数envにはEnvGenインスタンスを入れます。EnvGenクラスはUGenをスーパークラスとし、他のUGenクラスを親とするクラスと似たようなクラスです。EnvGen.scを見てみると他のよくわからんクラスもありますが、横に置いておきましょう。arメソッドには、初期値がない引数envelopeとその他もろもろがあります。ここで解読すべきは、envelopeでしょう。まず、arメッセージの引数でEnvGenクラスオブジェクトに何を渡しているのか見てみましょう。Env.perc( atk, rel )ですね。これはいうまでもなく、Envクラスオブジェクトにpercメッセージを送っているだけですね。ということは、envelopeにはEnvオブジェクトが入ることになります。arメソッドに戻ります。envelopeはmultiNewListメッセージの引数である配列の最後の要素に使われていますね。なんか”`”が頭についています!バ、バッククォートだと!?どうせショートカット記述だろ、とsyntax-shortcutのヘルプを見てみるとcreating a Refの部分でyou can writeに”`”を見つけました!でも、Refクラスなんて知りませんよ、と。Refヘルプを見てみると a reference to a value とありますがさっぱり意味がわかりません。情報求む。

	//オブジェクトを格納するのか?
	a = Ref(Env.perc( 0.1, 0.2 ))
	a.dereference
	a.value
	a.value_(SinOsc)
	a.value	//おお、SinOscオブジェクトに変わった!だからなんだ!

というわけで、終了。ダメでした。

とりあえず、以上

実は、先にショートカット記述について書いていたのですが、どうしても理解できない部分が多く、もっと基本からやらなくちゃ!と思い、とりあえずオブジェクト指向まで戻ってみたところ、そうだったのか!と気づくことが多く、やべ、誤解していたなあ。という箇所も多々ありました。基本からやり直すことで、結構理解が深まった気がします。
恒例ですが、SuperCollider.jpにhtmlファイルをアップしたいと思います。が、それはまだまだ先のお話になりそうです。ここの修正、追記が終わったらショートカットに移り、それが完了したら、基礎文法の修正にはいります。