Consセルがメッセージを受け取って動くLispもどきをPharo3.0 Smalltalk で

引数を評価する(carをevalしていく)ときにatomとして使っているSmalltalkのデータ全てがevalメッセージを理解

http://phaendal.hatenablog.com/entry/2015/04/17/164825


名詞とか動詞とか宣言的とかは難しくてよく分からないのですが(をゐ)、このアイデアに触発されて、タイトルにあるようにとにかく Consセルとそれに EVAL や APPLY というメッセージを送ることで評価できる Lisp っぽく動くものを手軽にでっち上げることを目指しました。


ふだんなら Squeak で書くところなのですが、これまで Pharo は新しいことをいろいろ覚えるのがめんどくさくて触ったことがなかったので、Squeak から派生して先ごろついに 4.0 が出るまでに進化したこやつの成長ぶりを確認する敵情視察も兼ねて Pharo4.0 で書いてみよう!

と…思い立ったまではよかったのですが、この出たばかりの Pharo4.0 は、なんかいろんなところが壊れていることが判明したので、これよりはいくぶんかこなれているであろう一つ前の Pharo3.0 を使うことにしました。^^;


現行バージョンではないためアーカイブされている Pharo3.0 は http://files.pharo.org/platform/ から各ホストOS(maclinux、win)用のものが入手可能です。三つの OS で共通して使える Pharo3.0-portable.zip もあります。zip を展開すると、たとえば winであれば Pharo.exe をダブルクリックすると自動的に Pharo3.0.image(後述の永続化されたオブジェクトを収めたバイナリーファイル)を読み込んで Smalltalk 環境が立ち上がります。


Smalltalk ではシステムブラウザ(場合によってはクラスブラウザとも言う)を使ってクラス定義やそこにメソッドを書きます。Pharo3.0 のシステムブラウザ(デスクトップクリック→System Browser で起動)はこんな画面です。

http://squab.no-ip.com/collab/uploads/Pharo3Browser.png


おおまかには上段に左から「パッケージ名一覧」「クラス名一覧」「プロトコル名一覧」「メソッド名一覧」の枠があり、上段で選択したクラスやメソッドの定義やコードを記述するのに下段の大きな枠を使う仕組みになっています。

この中で3番目の「プロトコル」というちょっと見慣れない概念は、メソッドカテゴリーやメッセージカテゴリーとも呼ばれ、クラスに定義されたメソッドを分類するための属性です。メソッドを定義するときにあらかじめ指定しておく必要があるものですが、特に指定しなければ as yet classified になりますし、あとから変更することも可能なので比較的ゆるいです。

後の Objective-C では同名のプロトコルという言語機能が、その後継の Javaインターフェイスに相当する機構の前身に進化したりしていますが、Smalltalk の時点ではまだメソッドに定められている属性のひとつ…程度の能力しかありません(なお、実装上はクラスが有する属性です。為念)。


下段のコードを記述する枠には、当該メソッドをコールするときに送信するメッセージ記述を模したメッセージパターン(メソッド名と仮引数名の宣言を兼ねる)に続いてメソッド本体を記述する決まりになっています。

Smalltalk には(GNU Smalltalk など変わり種の処理系を除き)他の言語にあるメソッド定義のための構文のようなものは存在しませんので、システムブラウザというマルチペインのグラフィカルな UI が、そうした構文や特殊形式の肩代わりをしていると考えることもできます。


Smalltalk では、クラスはもちろん、メソッドもそれを生成するのに使った(つまり我々が書いた)ソースコードもオブジェクトとして扱われ、たとえばメソッドのソースはそのメソッドの属性のような扱いになります(メソッドに getSource というメッセージを送るとそのソースが得られるといったふうに)。環境全体が簡易なオブジェクトストア(OODB)のようなものだと考えると、他の普通の処理系向けの IDE との違いを理解しやすいと思います。


では実際に、次のような感じで動作する ConsCell を作ってみたいと思います。

ConsCell car: 1 cdr: 2   "=> (1 . 2) "

#(a (1 2 3) c d) as: ConsCell   "=> (a (1 2 3) c d) "

(#(+ 1 2 3 4) as: ConsCell) EVAL   "=> 10 "

(#(MAP factorial (LIST 1 2 3)) as: ConsCell) EVAL   "=> (1 2 6) "

(#(reduce: (asArray 1 2 3) +) as: ConsCell) EVAL   "=> 6 "


主に配列を Consセルを使ったリストに変換し、さらにそれにメッセージを送ることで評価できるように。あと、+ や factorial、reduce: といった Smalltalk に組み込みのメソッドもそのまま使えるようにします。


ファイルアウトしたコード(すなわち、ファイルイン可能なコード)は MessageReceivableCons-Lisp.st ですが、これだとチャンクの区切りやメタ情報が入ったり、順不同だったりして読みにくいので、以下では改めて

クラス名(クラスメソッドの場合は クラス名 class) > カテゴリー名 > メッセージパターン
    メソッド本体


という独自記法で抜粋して解説します。


▼ConsCell の定義とアクセッサー(accessingプロトコル)生成

ConsCell には car と cdr というインスタンス変数を用意します。システムブラウザのクラス名一覧枠を右クリック→ Add class... でシステムにクラスを追加できます。

Object subclass: #ConsCell
    instanceVariableNames: 'car cdr'
    classVariableNames: ''
    category: 'MessageReceivableCons-Lisp'


アクセッサーはインスタンス変数と同名のものを用意します。ただ Smalltalk ではコロンもメソッド名(セレクターとも言う)に含まれるため、この「同名」と言う表現には語弊があり(というか明かな間違いで)、コロンの有り(セッター)無し(ゲッター)で別の独立したメソッドとして用意する必要があります。

いちいち手で書いてもよいのですが、数が多いと大変なので、クラス名一覧枠で ConsCell を選択して右クリック→ Analyze... → Create inst var acccessors した方が楽でしょう(Pharo4.0 ではなぜかこの機能が消滅しています。これが分かった時点で今回の Pharo4.0 の使用は諦めました。^^;)。

ConsCell > accessing > car
    ^ car

ConsCell > accessing > car: aCellOrObject
    car := aCellOrObject

ConsCell > accessing > cdr
    ^ cdr

ConsCell > accessing > cdr: aCellOrObject
    cdr := aCellOrObject


これで

ConsCell new car: 1; cdr: 2; yourself

という式をどこか(文字が入力できれば原則どこでもOKですが普通は デスクトップ右クリック→ Workspace )に入力し Print it(alt + p)すると、任意の ConsCell のインスタンスが生成できるのが確認できます。


▼文字列化(printingプロトコル)と判定メソッド(testingプロトコル

ただ、今の状態ですと ConsCell インスタンスを生成しても 「a ConsCell」としか表示されず、いちいちインスペクト(Inspect it)するなどないと中身が分からず面倒です。そこで内容を反映した文字列化を適宜おこなってくれるよう printingプロトコルにある printOn: メソッドをオーバーライドします。なお、プロトコルは、上段第三枠の右クリック→ Add protocol... で追加できます。

ConsCell > printing > printOn: aStream
    | nextCellOrObject |
    aStream nextPut: $(.
    aStream nextPutAll: car asString.
    nextCellOrObject := cdr.
    [ nextCellOrObject isConsCell ]
        whileTrue: [
            aStream
                space;
                nextPutAll: nextCellOrObject car asString.
            nextCellOrObject := nextCellOrObject cdr ].
    nextCellOrObject notNil
        ifTrue: [ aStream nextPutAll: ' . ' , nextCellOrObject asString ].
    aStream nextPut: $)


コンパイル(alt + s)時、ConsCell のインスタンスか否かを判定するメソッド(testingプロトコル)である isConsCell が未定義であることを(あるいはスペルミスではないかと)とがめられますが、その場は isConsCell で間違いないことだけを伝えてコンパイルは完了してください。

もちろん、あとで忘れずに定義しておく必要はあります。

Object > *MessageReceivableCons-Lisp > isConsCell
    ^ false

ConsCell > testing > isConsCell
    ^ true


本来であれば Object>>#isConsCell も ConsCell>>#isConsCell 同様に testingプロトコルに属させたいところですが、そうしてしまうと MessageReceivableCons-Lisp パッケージに同梱できないので、やむを得ず、パッケージ名に * 付した *MessageReceivableCons-Lisp という特殊なプロトコルに属させています。ここらへんは、Montecello などで新たに導入された今どきのパッケージ管理の限界で、個人的にも気にくわないところです。

余談ですが、Smalltalk 処理系に比較的最近組み込まれるようになった分散管理が可能な VCS である Monticelloではなく、Smalltalk に古くからあるチェンジセットという古典的なソースコード管理機構を使う場合は、文字通り「(システムに加えられた)変更の集合」として黙っていても(追加、変更の別を問わず)すべて蓄積されるため、このへんの気遣いは無用です。


これで、ConsCellオブジェクトの car、cdr の中身を反映した文字列化がされるようになったはずです。Print it して試してみましょう。

ConsCell new car: 1; cdr: 2; yourself   "=> (1 . 2) "

コンストラクター(instance creationプロトコル)、配列化メソッド(convertingプロトコル

いちいち ConsCell を new して car と cdr をセットするのは面倒なので、ConsCell class(メタクラス)に car:cdr: メソッドを定義します。システムブラウザでメタクラスにメソッドを定義する(つまり、クラスメソッドを定義する)には、中央左寄りにある □ Class side と書かれたスイッチをクリックして ■ にしてから作業(プロトコル追加→メソッドソース記述、コンパイル)します。

ConsCell class > instance creation > car: carCellOrObject cdr: cdrCellOrObject
    ^self new car: carCellOrObject; cdr: cdrCellOrObject; yourself


これだけでもだいぶすっきりするのですが、

ConsCell car: 1 cdr: 2   "=> (1 . 2) "


リストを定義しようとするとやっかいなことになります。

ConsCell car: 1 cdr: (ConsCell car: 2 cdr: (ConsCell car: 3 cdr: nil))   "=> (1 2 3) "


そこで、newFrom: メソッドをオーバーライドして

ConsCell newFrom: #(1 2 3)

もしくは

#(1 2 3) as: ConsCell


という記述で配列などの順序付きコレクションを ConsCell のリストに変換できるようにしましょう。

ConsCell class > instance creation > newFrom: aCollection
    | instance elem next |
    aCollection isEmpty ifTrue: [ ^ nil ].
    elem := aCollection first.
    (elem isCollection and: [ elem isString not ]) ifTrue: [ elem := elem as: self ].
    instance := next := self new car: elem; yourself.
    aCollection allButFirstDo: [ :each |
        elem := each.
        (elem isCollection and: [ elem isString not ]) ifTrue: [ elem := each as: self ].
        next cdr: (next := self new car: elem; yourself) ].
    ^ instance


これで次の記述が可能になります。

#(a (1 2 3) b c) as: ConsCell   "=> (a (1 2 3) b c) "

▼ちょっとした細工と便利メソッド
なぜか Phato では Squeak などでは可能な、通常のブロック(一つ目の例)の代わりにシンボルを渡してメソッドを呼び出す二つ目の例のようなこと

#(1 2 3) reduce: [:sum :x | sum + x]   "=> 6 "
#(1 2 3) reduce: #+  "=> error "


がエラーになってできないため不便きわまるので、これをちょいちょいといじってできるようにしておきます。なお、ここからふたたびインスタンスメソッドの定義なので、直前にクラスメソッドの #newFrom: を定義するために切り替えたシステムブラウザの ■ Class side チェックをクリックして元の □ に戻しておくのを忘れずに!(これを読みながら逐次定義している場合)

Symbol > *MessageReceivableCons-Lisp > argumentCount
    ^ self numArgs + 1

Symbol > *MessageReceivableCons-Lisp > valueWithArguments: args
    ^ args first perform: self withArguments: args allButFirst


という二つのメソッドを Symbol に追加しておくと、期待通りの動作をしてくれるようになります。

#(1 2 3) reduce: #+  "=> 6 "


用意しておくとあとあと便利なので、Consセルで表現されたリストの要素を配列化する ConsCell>>#asArray を convertingプロトコルに、#collect: を enumeratingプロトコルに定義しておきます。

ConsCell > converting > asArray
    ^ (Array with: car), (cdr ifNil: [ #() ]) asArray

ConsCell > enumerating > collect: aBlock
    ^ self class
        car: (aBlock value: car)
        cdr: (cdr isConsCell
           ifTrue: [ cdr collect: aBlock ]
           ifFalse: [ cdr ])


試してみます。

(#(1 2 3) as: ConsCell) asArray   "=> #(1 2 3) "
(#(1 2 3) as: ConsCell) collect: #negated   "=> (-1 -2 -3) "


念のため、この asArray はリストの各要素に再帰的に適用されないので、要素が Consセルでもあそれは配列には変換されません。


▼EVAL、APPLY、MAP の定義

では本題の Lisp(っぽい)コードを記述した ConsCell に EVAL メッセージを送って評価できるように ConsCell>>#EVAL メソッドを定義しましょう。同時に Object>>#EVAL の定義も忘れずに。あと、 APPLY するときに流用する Smalltalk の二項セレクターの判定が簡潔に記述できるよう Symbol>>#isBinarySelector もあらかじめ定義しておきます。

Symbol > *MessageReceivableCons-Lisp > isBinarySelector
    ^ self allSatisfy: #isSpecial

Object > *MessageReceivableCons-Lisp > EVAL
    ^ self

ConsCell > LISP-FUNCTIONS > APPLY
    cdr isNil ifTrue: [ ^ self class perform: car ].
    car isBinarySelector ifTrue: [ ^ cdr asArray reduce: car ].
    car numArgs = 0 ifTrue: [ ^ car value: cdr ].
    ^ car valueWithArguments: cdr asArray

ConsCell > LISP-FUNCTIONS > EVAL
    | funSymbol args |
    funSymbol := car EVAL.
    args := cdr collect: #EVAL.
    ^ (self class car: funSymbol cdr: args) APPLY


評価してみます。

(#(+ 1 2 3 4) as: ConsCell) EVAL   "=> 10 "
(#(* (+ 1 2) 3 4) as: ConsCell) EVAL   "=> 36 "


よさそうですね。


最後にこのエントリーの冒頭のと、元エントリーのサンプルコードなども評価できるように、いくつか LISP-FUNCTIONSプロトコルにメソッドを追加しましょう。

ConsCell > LISP-FUNCTIONS > CONS
    ^ self class car: car cdr: cdr car

ConsCell > LISP-FUNCTIONS > CAR
    ^ car car

ConsCell > LISP-FUNCTIONS > CDR
    ^ car cdr

ConsCell > LISP-FUNCTIONS > LIST
    ^ self

ConsCell > LISP-FUNCTIONS > MAP
    ^ cdr car collect: car

以下、久しぶりに Pharo を触った感想をざっくばらんに。

テキスト編集時に again(alt + j)や again many(alt + shift + j)、exchange(alt + e)、duplicate(alt + e)を古くから愛用している Squeakスキーとしては、これらの画期的テキスト編集向け機構を Pharo に残さなかった(残せなかった、あるいは正常に動作させられない、させる気のない)開発陣には殺意すら禁じ得ませんがw、今どきの Find/Replace はまあ普通に作ってあるしこれでいいと思います。

今回調べていてちょっと驚いたのは、意外にも古典的なチェンジセットがまだかろうじて使えることと、その一方で、古典的なプロジェクト機構(Smalltalk に古来からある仮想デスクトップ機構で、チェンジセットと紐付けしてそれを視覚的に切り替えるのにも使用できる)が完全に取り払われていて、チェンジセットがほぼ使いものにならない状態だったことです。プロジェクトは Pharo のかなり早い時期に取り除かれていたようで、なんだかすごく残念でした。

クラス名やメソッド名の補完は今風だしすごく便利だと思うのですが、query symbol(alt + q 連打)で満足している身としては、望まない動作をすることもあるのでプラマイゼロ…といったところでしょうか。

月並みですが、作っている人も使っている人も、古典的な Smalltalk環境に思い入れのない向きの Smalltalk環境としてどんどん進化している感じですね。