|
|
||
オブジェクト指向の“モヤッと”の正体を知りたくなったら、id:sumim:20080415:p1 や id:sumim:20040525:p1 がお役に立つかも…。
読者のみなさんにもgotoのある言語で試すことをオススメします。
再帰呼び出しを再帰呼び出しなしで実現 - 西尾泰和のはてなダイアリー
ということだったので goto も使える Squeak Smalltalk で書いてみました(違うw。
| VERBOSE stack functionResult showAny showMany total3 | VERBOSE := true. stack := OrderedCollection new. functionResult := 0. showMany := nil. "for compiler" showAny := [:x | (x isKindOf: Integer) ifTrue: [ Transcript show: x ] ifFalse: [ showMany value: x ] ]. showMany := [:xs | Transcript show: '['. (1 to: xs size) do: [:i | showAny value: (xs at: i). ] separatedBy: [Transcript show: ', ']. Transcript show: ']' ]. total3 := [:xs | | ENTRYPOINT result i LOOP x RETURNPOINT frame | ENTRYPOINT := thisContext previousPc. result := 0. i := 0. LOOP := thisContext previousPc. i := i + 1. x := xs at: i. VERBOSE ifTrue: [ Transcript show: 'xs:'. showMany value: xs. Transcript show: ', x:'. showAny value: x. Transcript show: ', i:'; show: i; show: ', result:'; show: result; cr; endEntry. ]. (x isKindOf: Integer) ifTrue: [ result := result + x. ] ifFalse: [ stack addFirst: {xs. i. x. result}. xs := x. RETURNPOINT := thisContext previousPc + 10. thisContext pc: ENTRYPOINT. RETURNPOINT. frame := stack removeFirst. xs := frame first. i := frame second. x := frame third. result := frame last. result := result + functionResult ]. i < xs size ifTrue: [thisContext pc: LOOP]. stack isEmpty ifFalse: [ functionResult := result. thisContext pc: RETURNPOINT. ]. result ]. VERBOSE ifTrue: [World findATranscript: nil]. total3 value: #(1 (2 3) 4) "=> 10 "
xs:[1, [2, 3], 4], x:1, i:1, result:0 xs:[1, [2, 3], 4], x:[2, 3], i:2, result:1 xs:[2, 3], x:2, i:1, result:0 xs:[2, 3], x:3, i:2, result:2 xs:[1, [2, 3], 4], x:4, i:3, result:6
goto の動作は簡単のため、戻ってきたい場所のプログラムカウンター(pc)をラベルに模した変数に保持し(LABEL := thisContext previousPc)、必要なときにその pc に飛ぶ(thisContext pc: LABEL)ことで実現しています。
しかしこの方法だと RETURNPOINT の pc は未通過のため保持できないので RETURNPOINT への代入については ENTRYPOINT に戻る前にオフセット値をハードコードして保持しなければならなかった点がちょっと心残りですね。^^;
Smalltalk で「セレクタ−」というのは、0個以上の引数と組み合わせて「メッセージ」を構成する要素で、端的には通常の言語におけるメソッド名のようなものです。たとえば、arr at: 1 put: #something という Smalltalk の式があったとき、arr がレシーバー、at: 1 put: #something がメッセージ、1 と #something が引数で at:put: がセレクターです(セレクターの実体はシンボルなので #at:put: と書くこともあります)。at: 1 put: #something というメッセージを arr に送ることで、通常であれば #at:put: という名前の(Smalltalk ではコロンもメソッド名に含まれます)メソッドがコールされます。よくある文法で書くと arr.at:put:(1, #something) と同じことで、そんなに難しいことはありません。
@koropicot とある Smalltalker は、プログラム中の上位50のメッセージ、変数、定数、語彙をしゃべれないようでは Smalltalker とは言えない、と言ってますね。その上位50が操れて中級、とも
— でこれきさん (@dico_leque) 2013年5月15日
ということで、このツイートでは「メッセージ」と表現されている「セレクター」、すなわちメソッド名のうち、Squeak Smalltalk のシステム(処理系)内でよく使われている上位50位をカウントして抽出してみました。
| selectors counts | selectors := Set new. SystemNavigation default allBehaviorsDo: [:class | selectors addAll: class selectors]. counts := OrderedCollection new. selectors do: [:sel | counts add: (SystemNavigation default allCallsOn: sel) size -> sel]. ^(counts sort: #>) first: 50
短いコードですが、それなりに時間のかかる処理なので実際にお試しの際には注意してください。また、この手のリフレクション操作は処理系依存的なコードになるのが常なので、他の Smalltalk 処理系で同じことを試すには、似たような方針でその処理系が提供している API やユーティリティクラスを使ったコードに書き直す必要がある点にもご留意ください。
結果を以下に示します。
6999->#== 5509->#= 4774->#new 4270->#+ 3058->#size 3014->#- 2536->#at: 2357->#do: 2298->#, 2051->#assert: 2019->#@ 1762->#<= 1743->#class 1723->#> 1504->#first 1458->#at:put: 1354->#* 1232->#< 1148->#name 1074->#add: 1059->#nextPutAll: 1003->#new: 969->#isEmpty 941->#translated 938->#default 937->#error: 933->#asString 927->#includes: 907->#nextPut: 882->#not 861->#on: 840->#collect: 799->#ifTrue: 792->#extent: 782->#value: 737->#cr 735->#// 732->#printString 711->#notNil 682->#isNil 668->#contents 663->#value 651->#last 640->#/ 630->#at:ifAbsent: 615->#extent 594->#color: 581->#isKindOf: 567->#>= 562->#current
調べるまでもなく、なじみのメソッド名ばかりですね。^^;
なお、前述コードは記述を簡単にするためにソース中でメソッドがコールされる回数そのものではなく、そのメソッドをコールしているメソッドの数をカウントしているので、メソッド内で複数回使われることの多いセレクターの順位は前後するかもしれません。またカウントに使ったイメージは 4.3-ja にさらに手を入れた普段使っているイメージなので公式イメージや、他のバージョンでは結果が異なることがあります。あしからず。
| m a b c | m := 9. c := 3. b := (Float pi / c) tan. a := (Float pi / c) sin. World findATranscript: nil. 0 to: m do: [:n | | d | d := c * (1 << n). Transcript cr; show: d -> ({a. b} * d). b := a * b / (a + b). a := (a * b / 2) sqrt]
3->#(2.598076211353316 5.19615242270663) 6->#(3.0 3.464101615137753) 12->#(3.105828541230248 3.215390309173472) 24->#(3.132628613281237 3.1596599420975) 48->#(3.139350203046866 3.146086215131434) 96->#(3.14103195089051 3.142714599645368) 192->#(3.14145247228546 3.141873049979823) 384->#(3.141557607911857 3.141662747056848) 768->#(3.141583892148318 3.14161017660469) 1536->#(3.14159046322805 3.141597034321526)
| m a b c | m := 9. c := 4. b := (Float pi / c) tan. a := (Float pi / c) sin. World findATranscript: nil. 0 to: m do: [:n | | d | d := c * (1 << n). Transcript cr; show: d -> ({a. b} * d). b := a * b / (a + b). a := (a * b / 2) sqrt]
4->#(2.82842712474619 4.0) 8->#(3.061467458920718 3.31370849898476) 16->#(3.121445152258052 3.182597878074528) 32->#(3.13654849054594 3.151724907429256) 64->#(3.140331156954753 3.144118385245904) 128->#(3.141277250932773 3.142223629942457) 256->#(3.1415138011443 3.141750369168967) 512->#(3.141572940367092 3.141632080703182) 1024->#(3.14158772527716 3.14160251025681) 2048->#(3.1415914215112 3.14159511774959)
a、b は、それぞれ円に内接、外接する正多角形の一辺の長さ(の半分)。辺の数が倍になったときの an+1、bn+1 を、ひとつ前の an、bn で表わすには…
△ACD ≡ △RFD, △OAP ∽ △OBR, △OCP ∽ △OFR, an : bn = an - bn+1 : bn+1 ∴ bn+1 = an bn / (an + bn)
△OEQ ≡ △ORD, △APR ∽ △ADC, an : 2an+1 = an+1 : bn+1 ∴ an+1 = √(an bn+1 / 2)
変化しにくいインフォメーションと変化しがちなプレゼンテーションとの分離。メンタルモデルの記述としてのMVCは失敗(続きは DCI, Data - Context - Interaction で)。Smalltalk-76, 1978。
Controller を追加。Editor は特殊な Controller として位置付け。Smalltalk-80, 1979-。
Model から View - Controller 寄りのロジックを分離して独立させた ApplicationModel を追加。VisualWorks Smalltalk, 1992。
Smalltalk での実装を分解して、デザインパターンの組み合わせ例(Observer、Strategy、Composite、Factory Method、Decorator)として解釈・紹介。C++, 1994。
モダンな GUI コンポーネントにおいて冗長な Controller の役割を View に吸収させ、View と Model の仲介者として Presenter を追加 Controller をアプリケーションレベルに引き上げ、View と Model との仲介者として位置づけ直し Presenter と改名。C++/Java, 1996。
Potel MVP の Smalltalk 実装。 をヒントに、VisualWorks の Application Model における ApplicationModel の役割を UI 寄りの実装の Presenter に担わせ、モダンな GUI コンポーネントにおいて冗長な Controller の役割を View に吸収させて改めて View とすることで、Model - Presenter - View という三連構造を新たに提唱。頑張る Controller を Presenter に昇格させただけの Potel の MVP とはぶっちゃけ別物。Dolphin Smalltalk, 1996。
Java(Webアプリ), 1999。
VisualWorks の Application Model のこと? 2004。
Fowler Presentation Model を WPF 向けに特殊化した実装。C#, 2005。
CocoaのMVCは、たんなる「コントローラーが頑張るMVC」程度のものだと思っていたのだけれども、実は独自の工夫があるのか、普通にApplication Modelとかの流れなのかについて整理がついていないのでこれから勉強する。
参考文献:
タイトルは釣りです。
まずおおざっぱに用語の整理をさせていただくと、ここで「トレイト」は、シェルリ(Nathanael Schärli)らが2002年頃に発表したTraitsやそれ用のエンティティ(trait)を指し、「ミクスイン(Mixin, mixin)」は従来からある実装の多重継承方法のひとつ、具体的には継承機構を使ってメソッドを定義したクラス様エンティティ(クラスでも構わない)を継承パスに差し込むことで対象となるクラスにメソッドを追加する機構(特別な機構を要しないときは単なるクラスの運用方法)、そのときに用いるクラスあるいはクラス様エンティティ(例えばRubyならモジュールとか)を指すことにします。
トレイトやその機構について説明すべきことはいろいろありそうですが、詳しくはシェルリらの論文(Traits: Composable Units of Behaviour など)を読んでいただくとして、ここでは実装の多重継承のしくみとして考えた場合のミクスイン機構との違いを、少々乱暴ですが単純に、ミクスインの「リニア化」かトレイトの「フラット化」か、に絞ることが可能だと考えることにします。
例えば、複数のミクスインM1、M2(おのおのがさらにM0を継承)を同時に継承した場合、ミクスイン機構では何らかのルールに従ってミクスインを順番に列べ(リニア化し)て、その列びを継承パスに挿入します。Pythonで書くとこんな感じでしょうか。
class B(object): def m(self): print "B", class C(B): def m0(self): print "C", self.m() C().m0() #=> C B
class M0(object): def m(self): print "M0", super(M0, self).m() class M1(M0): def m(self): print "M1", super(M1, self).m() class M2(M0): def m(self): print "M2", super(M2, self).m() class C(M1, M2, B): def m0(self): print "C", self.m() C().m0() #=> C M1 M2 M0 B
こうしたミクスインの挙動に対し、トレイト機構では同時に use するトレイトはまず「フラット化」されます。トレイトは、とりあえずメソッドを束ねたもの(集合)だと思ってください。同時に use される複数のトレイトは、いったんばらしてひとつの仮想的なトレイトにまとめられて、その後、改めてクラスに use される(あるいは「注入」される)と考えると少しわかりやすいです。その際、メソッド名の衝突が見つかれば実行時にエラーになったりコンパイルに失敗します。つまり、先の Python のミクスインの例のようなコードはトレイトではこのままでは動きません。
実際にどうなるか Squeak Smalltalk で試してみましょう。
次のコードは、先のコードのミクスインのところをトレイトに置き換えて Squeak Smalltalk 向けに書き直したものです。Squeak Smalltalk の IDE にあまり詳しくなくてもただ起動してワークスペースなどに貼り付け、空行で隔てられた式ひとつひとつを順に選択してから do it (alt/cmd + d)するか、fileIn (全体を選択してから alt/cmd + shift + g。もしくはこのコードを example.st に保存しておき (FileStream fileNamed: 'example.st') fileIn を do it する等々)して実行できるコードとして記述してあります。クラスブラウザを使わないため、通常の Smalltalk のコードの書き方や実行のしかたからは外れますが、その点ご留意ください。
なお、同様のことはドットインストールの「Smalltalk入門」で話題の Pharo Smalltalk でも試せますが、Pharo 1.4 には #uses: が無いので自分で別途定義しておくか、該当メソッドを使用している式をあらかじめ書き換える必要があります(ちょっと長いですが B subclass: #C uses: T1-{#m}+T2 instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Traits-Examples'. などとして試してみてください)。
Object subclass: #B instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Traits-Examples'. B compile: 'm Transcript space; show: #B; cr; endEntry'. B subclass: #C instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Traits-Examples'. C compile: 'm0 Transcript space; show: #C. self m'. Transcript open. C new m0. "=> C B "
Trait named: #T0 uses: {} category: 'Traits-Examples'. T0 compile: 'm Transcript space; show: #T0. super m'. Trait named: #T1 uses: T0 category: 'Traits-Examples'. T1 compile: 'm Transcript space; show: #T1. super m'. Trait named: #T2 uses: T0 category: 'Traits-Examples'. T2 compile: 'm Transcript space; show: #T2. super m'. C uses: T1 + T2. Transcript open. C new m0 "=> Error: A class or trait does not properly resolve a conflict between multiple traits it uses. "
このように衝突の検出を告げるエラーになってそのままでは最後の出力ができません。
検出された衝突を回避するには、原因となっているメソッドのうちどれをどのトレイトから排除するかを use するときに明示的にしてやる必要があります。たとえば、エラーを出す最後の式を含む三つの式を次のような式に置き換えると正常に動作させられます(この場合は、トレイトT1 のメソッドm を排除することで、結果的にトレイトT2 の m を残しています)。
C uses: T1 - {#m} + T2.
Transcript clear.
C new m0 "=> C T2 B "
出力結果もミクスインの場合とは違っていることに気付かれたでしょうか。このように出力されるとまるで T0 や T1 ごと排除されて機能していないように見えますが、実際にはそんなことはありません。あくまでメソッド m についてのみ、フラット化の結果、T2 由来の m が使われているというだけの話です。T0 や T1 も引き続き use されていますので、例えば、T0 にメソッドmT0、T1 に mT1 を追加で定義してやれば、ふつうにそれぞれを C のインスタンスからコールできます。
T0 compile: 'mT0 Transcript space; show: #mT0'. T1 compile: 'mT1 Transcript space; show: #mT1'. C new mT0; mT1. "=> mT0 mT1 "
これが、本来、トレイトに期待される挙動です。
では、Scala のトレイトはどうかというと、話の流れからもう予想は容易につくと思いますが、こんなふうになります。
trait TA { def m() : Unit } trait T0 extends TA { abstract override def m = { print("T0 ") super.m } } trait T1 extends T0 { abstract override def m = { print("T1 ") super.m } } trait T2 extends T0 { abstract override def m = { print("T2 ") super.m } } class B { def m = println("B") } class C1 extends B { def m0 = { print("C1 ") m } } class C2 extends B with T1 with T2 { override def m = super.m def m0 = { print("C2 ") m } } object Example extends App { (new C1).m0 //=> C1 B (new C2).m0 //=> C2 T2 T1 T0 B }
Python と同じミクスインの挙動ですね。あと、あいにく宣言時に何も extends していない T0 の m からは super.m がコールできないため、トレイトを使った後者の出力では B が抜けています。
追記:B の m が T0 の m から super.m でコール出来ない件は abstract override 修飾子と抽象トレイトを一段噛ますことで実現できるようです。
@kmizu 教えていただいたabstract overrideで、件のT0の m から、将来継承パスの上流に来るBのmをsuper.mとして呼べるかなと思ったのですが、やはり(当然ですが)AnyRefにはmが無いと叱られます。Bを予め継承する以外で何か実現方法はありますか?
— sumim (@sumim) 2013年3月7日
@sumim traitT{ def m; } と定義して、 trait T0 extends T { abstract override def m = { println("T0"); super.m } で書けるのではないかと思います(確かめてません。すいません)。
— Kota Mizushimaさん (@kmizu) 2013年3月8日
REPL の :power モードで確認すると、Python のときと同様に、C2 の継承パスの T0 のすぐ上流に B がきちんと挿入されていることが分かります(なぜか先の実行の結果と T1 T2 が入れかわってしまっていますが???)。
追記: この件、@kmizu さんから次のようなご指摘をいただきました。ありがとうございます。
@sumim 件のトレイトの記事ですが、他の部分はおいといて、REPLの:powerモードで確認してるのは、sub-type関係であって、class linearlizationの結果ではないので、あれで当然かと。
— Kota Mizushimaさん (@kmizu) 2013年3月7日
ScalaのClass Linerizationの仕様については、 http://www.scala-lang.org/docu/files/ScalaReference.pdf の p.56 に記載があるので、そっちを参照されるのが良いかと。REPLのpower modeではClass Linearizationの結果は(簡単には)取れないと思います。
— Kota Mizushimaさん (@kmizu) 2013年3月7日
scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'. **
** scala.tools.nsc._ has been imported **
** global._, definitions._ also imported **
** Try :help, :vals, power.<tab> **
scala> intp.types("C2").tpe.baseTypeSeq.toList
res1: List[$r.intp.global.Type] = List(C2, T1, T2, T0, B, Object, Any)
だから何だ?なんか文句があるのか?とかつっかかられたりすると困りますが、Scala のトレイトはトレイトじゃないからシェルリらのトレイトについてほとんど知らなくても問題ないよ、あと Scala のトレイトの挙動を前提にシェルリらのトレイトの説明を読むと混乱するから気をつけて、といった老婆心から書いてみました。
追記:
PHP も 5.4.0 からシェルリらのトレイトをサポートしていますので、同様のサンプルコードを書いてみました。当たり前ですが Squeak Smalltalk のトレイトと同じ挙動をします。
<?php trait T0 { public function m() { echo "T0 "; parent::m(); } } trait T1 { use T0; public function m() { echo "T1 "; parent::m(); } } trait T2 { use T0; public function m() { echo "T2 "; parent::m(); } } class B { public function m() { echo "B\n"; } } class C0 extends B { public function m0() { echo "C0 "; $this->m(); } } /* class C1 extends B { use T1, T2; //=> Fatal error: Trait method m has not been applied, because there are collisions with other trait methods public function m0() { echo "C1 "; $this->m(); } } // */ class C2 extends B { use T1, T2 { T2::m insteadof T1; } public function m0() { echo "C2 "; $this->m(); } } (new C0())->m0(); //=> C0 B (new C2())->m0(); //=> C2 T2 B
最近のコメント
最近のトラックバック