Self
Self は、「プロトタイプ」の概念に基づいたオブジェクト指向プログラミング言語である。1980年代から1990年代にかけて言語設計の実験的システムとして使われていたが、2018年、Self の開発は続けられており、Self言語自身で書かれた Selfバーチャルマシンを構築する Klein プロジェクトが進められ、2006年7月にバージョン 4.3 がリリースされた。
1986年、パロアルト研究所で働いていたデビッド・アンガーとRandall SmithがSelfを設計した。Smalltalk-80が一般にリリースされて産業界から真剣に受け止められ始めていることから、オブジェクト指向プログラミング言語の研究をさらに進めることを目的として行われた。彼らはスタンフォード大学に移り、Selfの作業を進め、1987年に最初のコンパイラを完成させた。そして、言語だけではなくSelfのシステム全体を構築することに注力することになった。
一般への最初のリリースは1990年であり、翌年には彼らチームはサン・マイクロシステムズに移り、さらに Selfに関する作業を続けた。その後、何回かのリリースが行われ、1995年のバージョン 4.0リリースで長い活動休止状態に入った。最近のバージョン 4.2は2004年にリリースされ、Mac OS XとSolaris上で動作した。
Selfはいくつかの言語に概念的な影響を与えた。特筆すべきものとしては、Apple NewtonのNewtonScriptと、動的ウェブページ構築に使われるJavaScriptがある。他に、Io言語、Cel言語、Agoraなどがある。
本来のオブジェクト指向言語は以下の双対関係に基づいている:
クラスはオブジェクトの基本的な機能と振る舞いを定義する。
オブジェクト・インスタンスはあるクラスの個別の具現である。
例えば、Vehicle クラスのオブジェクトが「名前」を持ち、「運転」とか「建材を運ぶ」といった機能を持つとする。Porsche 911 が Vehicle クラスの1つのオブジェクト(インスタンス)で、「ポルシェ 911」という「名前」を持つとする。理論的には Porsche 911 に対して「建材を運べ」というメッセージを送ることができる。
この例はオブジェクト指向のはらんでいる問題を示している。ポルシェはどう考えても建材を運ぶのには適さないが、モデル化された Vehicle にはその機能が与えられている。より適したモデルを生成するには Vehicle に特殊化を施したサブクラスを使えばよい。例えば、Sports Car とか Flatbed Truck である。この場合、Flatbed Truck クラスのオブジェクトだけが「建材を運ぶ」という機能を持てばよい。一方、Sports Car にその機能を与えるのは間違いであり、単に高速に運転できればよい。
このような問題がプロトタイプという考え方を生む要因となった。クラスやオブジェクトが将来どのような機能を持つようになるか確実に予測できないと、クラス階層を正しく設計することはできないのである。Smalltalkのような初期のオブジェクト指向言語では、この手の問題が頻繁に生じた。システムがある程度まで成長すると、全体として硬直化が起きて、特に基本的なクラス群は相互の関連に縛られ、機能追加が困難となる。
Smalltalk のような動的言語では、クラスの変更によって容易にオブジェクトの振る舞いを変えられるという特徴でこれに対処できる。しかし、このような変更は注意深く行われるべきである。さもなくば、変更されたクラスに所属する、挙動が変更されるべきでないオブジェクトは「誤った」挙動を示してしまう。この問題は脆弱な基底クラス問題の一つである。一方で、C++のように、基底クラスと派生クラスを別々にコンパイルできる言語では、事前にコンパイルした派生クラスのメソッドに、基底クラスでの変更が派生しない。この問題は脆弱な基底クラス問題のもう一つの形式であり、脆弱なバイナリ・インターフェース問題の一つでもある。
Self や他のプロトタイプベース言語では、クラスとオブジェクトの双対関係が排除されている。
何らかの「クラス」に基づくオブジェクトの「インスタンス」を作るのではなく、Self では既存のオブジェクトをコピーし、それに修正を加える。従って、Porsche 911 を作るには、他の Vehicle オブジェクトをコピーし、「高速運転」メソッドを追加すればよい。コピー元となるオブジェクトを「プロトタイプ」と呼ぶ。この技法によりダイナミズムが劇的に単純化されると言われている。既存のオブジェクトがモデルとして不適切であった場合、プログラマは単にオブジェクトに修正を加えて正しい振る舞いをするようにして、それを新たなプロトタイプとして使えばよい。既存のオブジェクトを使っているコードはそのまま使うことができる。
Self のオブジェクトは「スロット」の集まりである。スロットとは、値を返すメッセージであり、スロット名の後にコロンをつければ値をセットするメッセージになる。例えば、"name" というスロットがあるとする。
myPerson name
これは name の値を返す。
myPerson name:'gizifa'
これは値をセットする。
Self は Smalltalk と同様「ブロック」を使って処理の流れを制御する。メッセージを受け取るメソッドはスロット群以外にコードを持つオブジェクトであり、任意のスロットの値としてメソッドを格納できる。メソッドの持つスロットは引数や一時変数として使われる。いずれの場合も文法的には同じである。
Self ではフィールドとメソッドに区別はなく、どちらもスロットである。メッセージによるスロットアクセスで Self の文法の大部分が説明されるため、自分自身(self)へのメッセージも多い。そのため "self" は省略できる(また、これが言語名の由来)。
スロットアクセスの構文は Smalltalk に似ている。3種類のメッセージを利用可能である:
単項 : receiver slot_name
二項 : receiver + argument
キーワード : receiver keyword: arg1 With: arg2
どのメッセージも値を返すので、receiver や argument もメッセージ形式をとることが可能である。メッセージの後ろにピリオドをつけると、リターン値を捨てることを意味する。例えば、
'Hello, World!' print.
これは Self によるHello worldプログラムである。'(シングルクォート)はリテラル文字列オブジェクトを意味する。その他のリテラル(即値)として、数、ブロック、汎用オブジェクトがある。
グループ化は括弧を使って明示される。明示的なグループ化をしない場合、優先順位は単項メッセージが最も高く、次に二項メッセージ(左から右にグループ化)、キーワードメッセージは最も優先順位が低い。代入にキーワードを使うとき、式にもキーワードが含まれている場合に追加の括弧が必要となる。それによって最初のキーワードメッセージセレクタを小文字から開始したり、次の部分を大文字で開始したりといった必要がなくなる。
valid: base bottom between: ligature bottom + height And: base top / scale factor.
この一文は曖昧さがなく、次のものと同じに解釈される:
valid: ((base bottom) between: ((ligature bottom) + height) And: ((base top) / (scale factor))).
Smalltalk-80 では、同じ式が次のように記述される:
valid := self base bottom between: self ligature bottom + self height and: self base top / self scale factor.
もう少し複雑な例として次の記述を示す:
labelWidget copy label: 'Hello, World!'.
"labelWidget" オブジェクトへの copy メッセージでコピーを作り、そのコピーの "label" スロットに "Hello, World" メッセージを格納すべくメッセージを送っている。これを使ってみると次のようになる。
(desktop activeWindow) draw: (labelWidget copy label: 'Hello, World!').
この場合、(desktop activeWindow) が最初に評価され、desktop オブジェクトが知っているウィンドウのリストからアクティブウィンドウを表すオブジェクトが返される。次に(内側から外側へ、左から右へという順で)前掲のコードが評価され labelWidget が返される。最後にそのウィジェットがアクティブウィンドウの draw スロットに送られる。
理論上、全ての Self オブジェクトはスタンドアロンな実体である。Self にはクラスもメタクラスもない。あるオブジェクトを変更しても他には影響がないが、影響があったほうがよい場合もある。通常、オブジェクトは自身のローカルなスロットへのメッセージしか認識しないが、「親」オブジェクトを指定するスロットを持つことによって、そのオブジェクト自身が解釈できないメッセージを親オブジェクトに委譲することができる。スロット名の後ろにアスタリスクがあるものは親へのポインタとなる。このような手法で、クラスベースの言語で継承機能が担っていることを実現する。委譲によって名前空間やスコープといった機能も実装できる。
例えば、「銀行口座」というオブジェクトを定義し、口座の残高を管理するとしよう。一般にこのようなオブジェクトには「入金」と「出金」のメソッドが作られ、それに必要なデータスロットも作られる。これはプロトタイプであるが、そのまま汎用の口座を表すオブジェクトとしても使える。
このオブジェクトのクローン「ボブの口座」を作ることで、上記オブジェクトがプロトタイプとして使われたことになる。このとき、そのオブジェクトが持つ全てのメソッドやデータがスロットとしてコピーされる。しかし、より典型的な手法は、もっと単純な traits object(特徴オブジェクト)と呼ばれるオブジェクトをつくり、そこにクラスに関連するものを含める。
この例では、「銀行口座」オブジェクトに入金や出金メソッドを備えさせるのではなく、その親オブジェクトに持たせる。このようにすると、多数の銀行口座オブジェクトのコピーを作っても、親オブジェクトの修正によって全銀行口座オブジェクトの動作を更新できる。
これはいわゆるクラスと何が違うのだろうか? 次の意味を考えてみよう:
myObject parent: someOtherObject.
これは、'parent*' スロットに適当なオブジェクトを代入することで myObject の「クラス」を実行時に動的に変更できることを意味する(アスタリスクはスロット名の一部だが、メッセージには表記されない)。継承やスコープとは違い、委譲オブジェクトは実行時に変更可能である。
Self のオブジェクトにはスロットを追加可能である。グラフィカルなプログラミング環境でもできるし、'_AddSlots:' プリミティブでも可能である。プリミティブは通常のキーワードメッセージと同様の構文だが、その名前は常にアンダースコアで始まる。_AddSlots プリミティブ自身は古い実装の名残りであるため、使うべきでないとされている。しかし、これを使うとコードを短縮できるため、以下ではあえて解説する。
Vehicle という単純なクラスのリファクタリングで乗用車とトラックで振る舞いを変えることを上の例で示した。Self ではこれを次のように実現できる:
_AddSlots: (| vehicle <- (|parent* = traits clonable|) |).
'_AddSlots' プリミティブの受信者オブジェクトが明示されていないので、これは "self" に対するものである。これを対話型のプロンプトで入力すると、"self" オブジェクトに相当するのは "lobby" と呼ばれるオブジェクトである。'_AddSlots' の引数はオブジェクトであり、そのスロットが受信者オブジェクトにコピーされる。この例では、それは1つのスロットだけを持つリテラルオブジェクトである。スロット名は 'vehicle' で、その値が別のリテラルオブジェクトとなっている。"<-" という記号は、第一のスロットの値を変更するのに使われる 'vehicle:' という第二のスロットを暗に示している。
"=" は定数スロットを意味するので、'parent*' には対応する 'parent:' が無い。このリテラルオブジェクトは 'vehicle' の初期値であり、クローン作成に関するメッセージを理解できるスロットを1つもっている。完全に空のオブジェクトは、(| |) あるいは単に () で示され、メッセージを全く受け付けられない。
vehicle _AddSlots: (| name <- 'automobile'|).
これも、前と同じオブジェクトが受信者であり、'parent*' に加えて新たに 'name' と 'name:' スロットが追加されている。
_AddSlots: (| sportsCar <- vehicle copy |).
sportsCar _AddSlots: (| driveToWork = (何かのコード、これがメソッドになる) |).
以前の 'vehicle' と 'sportsCar' はほとんど同じだが、ここでは後者にオリジナルが持っていなかったメソッドを伴うスロットが含まれている。メソッドを持つことができるスロットは定数スロットだけである。
_AddSlots: (| porsche911 <- sportsCar copy |).
porsche911 name:'Bobs Porsche'.
新たなオブジェクト 'porsche911' は 'sportsCar' とほぼ同じだが、最後のメッセージでその 'name' スロットの値が変更されている。これらはスロットの値は異なるものの、持っているスロットは同じであることに注意されたい。
Self の特徴の1つとして、Smalltalk システムが使っていた仮想機械と同様の仕組みに基づいている点が挙げられる。つまり、Self のプログラムはC言語などとは異なり、それ単独では機能しない。常に実行環境が必要となる。しかし、このようになっているため、Self 環境は強力なデバッグツールを提供できる。プログラムを任意の時点で停止させ、コードや値を変更し、実行を再開させるといったことが可能である。
さらに、Self 環境はオブジェクトを素早くかつ継続的に変更することを考慮している。「クラス」設計のリファクタリングは、単に既存のメソッドを新しいオブジェクトに引っ張ってくればよいだけである。メソッドの評価のような単純な作業は、コピーを作って、メソッドをコピーに引っ張ってきて、そこで修正すればよい。他のシステムとは異なり、その新たなオブジェクトだけが新しいコードを持っており、テストするにも他に影響が発生しない。そのメソッドがうまく動いたら、それを元のオブジェクトに戻せばよい。
Self の VM(仮想機械)は C言語と比較して(一部のベンチマークで)約半分程度の性能を達成している。
これはジャストインタイムコンパイル(JIT)方式によるもので、特に研究が進んでいる部分である。特に、起動当初はインタプリタとして実行し、よく送信されるメッセージや繰り返し実行されるコードの検出(プロファイリング)を行い、そのようなコードのみをコンパイルするadaptive compilationという技術は最初Selfの処理系で実装され、後にJavaのHotSpotで採用された。
Self のガベージコレクションは世代型であり、オブジェクトを世代で管理する。メモリ管理システムを使ってページへの書き込みを記録し、ライトバリアを保つ。この手法は性能がよいが、ある期間実行を行っていると全体ガベージコレクションが発生し、無視できない時間を取られてしまう。
実行時環境は選択的に呼び出し構造を平坦化することで性能向上を図っている。そのためには、型情報や異なる型に応じた複数の実行コードをキャッシュしておく必要があるが、時間の掛かるメソッド探索の回数を減らしたり、メソッド探索を条件分岐やハードコードされた呼出しで置き換えたりすることができる。これにより言語としての一般性を保ち、ガベージコレクションを備えつつ、C言語に近い性能が達成されているのである。