LLDN 「君ならどう書く」 Squeak 編の解説 1
あまりに時間が短かったので、ちょっと詳しめの解説をぼちぼち書いてゆきます。
事後に提出した資料(ソース、プレゼン静止画、プレゼンイメージを再現するためのプロジェクトファイル群。自由演技作品を含む)は、LLDN のサイトで公開していただいています。
MS の TrueType フォントを抜いて、プレゼンに使ったイメージをほぼ再現したイメージ(と、対応するチェンジファイルのペア)はここに置いておきました。あのプレゼンを手元で実際に操作してみたいけど、Squeak3.8 の日本語化とかソースのインストールとか、プロジェクトファイル群のロードの作業が面倒だなーと思う人はこちらが便利でしょう。
イメージとチェンジファイル以外に必要なものは、こちらなどから入手できます。
隠されたテーマ
とにかく、Squeak を含め、Smalltalk システムの起動までこぎつけたら、ぜひとも次のことをやってみてください…と。
- print it を行なう操作を探す(か、あらかじめ調べておく。Squeak では alt-/cmd- p )
- 文字を入力できる場所で、3 + 4 とタイプして(必要なら選択状態にして) print it
最初にちらと述べましたが、Squeak やその他の Smalltalk 処理系をインストールして起動するところまでいったものの、そのあと何をしていいのかわからなくて“見なかったことにして”そっと閉じてしまう…そんな経験をしたことがあるかたは多いと思います。それはちょっともったいない話なので、なにかひとつくらいは試してから終わりにしましょうよ…と思うわけです。
Smalltalk システムが通常の OS と違うところは、文字を入力できる場所ならどこでも、いわば“ネイティブ言語”とも言える Smalltalk 言語の式をコンパイルして(!!)実行できる点にあります。この環境内にあって、Smalltalk 言語はある種、究極の Lightweight Language だと言っても差し支えないでしょう。これさえ了解できていれば、もはや何も恐れるものはないはずです。文字を打てるところをさがして、どんどん式を入力して print it …の作業を繰り返し、Smalltalk を楽しんでください。
閑話休題。w
規定演技(漢数式スキャナ、漢数式パーサー、#readFrom漢数字: 、#の漢数字)
通常のインタープリタやコンパイラが含む、字句や構文を解析する機構をオブジェクトで表現します(漢数式スキャナ、漢数式パーサー)。また、漢数字を読んで整数(an Integer)にしたり、数値(a Number)を漢数字文字列に変換するメソッドを追加します(#readFrom漢数字: 、#の漢数字)。
Squeak の Smalltalk 処理系は Smalltalk 言語で記述されているので、これのエッセンスを抽出して今回の要件を満たすサブセットを作成しました。(平たく言えば「パクった」と、まあ、そういうことです。w)
Squeak の Smalltalk 処理系が使っているスキャナは次のように使うことができます。たとえば、文字列からの字句(トークン)抽出なら、
| scanner | scanner := Scanner new. ^ scanner scanTokens: '3+4*5' " => #(3 #+ 4 #* 5) "
Smalltalk では # は括弧とともに用いて配列(だたし、要素はリテラルのみ。式を記述してその返値を要素にはできない)か、# 単独でシンボル(a Symbol。クラス「Symbol」のインスタンス)のリテラル表現に用いられます。ここでは、'3+4*5' という文字列が、3 と #+ と 4 と #* と 5 という5つのオブジェクトとして解釈、抽出されたことを示します(返値として、これらは配列に収められています)。
シンボルというのは、要素(文字)を変更できず、同内容なら同一性が保証された特殊な文字列オブジェクトのことです。
'string' copy = 'string' copy " => true " " 両者は同内容 " 'string' copy == 'string' copy " => false " " 両者は別物 " 'string' at: 4 " => $i " " 要素(文字)の抽出 " 'string' at: 4 put: $O; yourself " => 'strOng' " " 要素(文字)の置き換えも可能 "
#symbol copy = #symbol copy " => true " #symbol copy == #symbol copy " => true " "同内容なら常に同一 " #symbol at: 4 " => $b " #symbol at: 4 put: $O; yourself " => Error: symbols can not be modified. " " 不許可 "
先ほどの複数の式からなるコードは、テンポラリ変数を省略して一行で書くこともできます。
Scanner new scanTokens: '3+4*5' " => #(3 #+ 4 #* 5) "
ここでは、Scanner new で a Scanner が作られ、それに scanTokens: '3+4*5' というメッセージが送られています。scanTokens: ... というメッセージは、Scanner >> #scanTokens: というメソッドを起動します。
Scanner >> scanTokens: textOrString self scan: (ReadStream on: textOrString asString). self scanLitVec. ^token
最初に起動されるのは Scanner >>#scan: ですが、名前にある“スキャン”という程のことはしていなくて、たんに、字句解析器(つまり a Scanner。この文脈では self)の初期化をしているだけです。具体的には、ストリーム化された文字列(ソース)をセットしたのち、読み進め作業を行なって内部状態を整えます。初期化終了時点では、ひとつめの字句の解析が終わった状態になっています。なお、ストリーム(a Stream)をご存じなければ、最後に参照した要素の位置を覚えている特殊な配列のようなものをイメージしていただければよいでしょう。
Scanner >> scan: inputStream source := inputStream. self step. self step. self scanToken
初期化の最後に起動される Scanner >> #scanToken が字句解析を担当するメソッドです。
Scanner >> scanToken [(tokenType := typeTable at: hereChar asciiValue ifAbsent: [#xLetter]) == #xDelimiter] whileTrue: [self step]. mark := source position - 1. (tokenType at: 1) = $x "x as first letter" ifTrue: [self perform: tokenType] ifFalse: [token := self step asSymbol]. ^ token.
最初の式(whileTrue: [self step])は、ループを回して空白などを、ステップ(文字読み進め。ソースの読み取り位置の移動)を繰り返して無視しています。二番目の式は、のちのち、ストリームの読み位置を元に戻す必要が生じたときのためのメモです。本体は第3式で、字句タイプが $x で始まるとき同名のメソッドを起動(perform: ...)しています。
たとえば、#xDigit という字句タイプの文字が現われたら、とりあえずここで Scanner >> #xDigit というメソッドを起動します。
Scanner >> xDigit tokenType := #number. (aheadChar = 30 asCharacter and: [source atEnd and: [source skip: -1. source next ~= 30 asCharacter]]) ifTrue: [source skip: -1] ifFalse: [source skip: -2]. token := [Number readFrom: source] ifError: [:err :rcvr | self offEnd: err]. self step; step
まず、字句タイプ(tokenType)が #xDigit ですからこれをこれから処理して得られるはずの #number に変えます。次に、ソースを最後まで読み切ってしまっている場合の特殊な処理を施してから、第3式目で Number class >> #readFrom: を起動しています。これは、文字列から数値オブジェクトを起こすためのメソッドです。
Number readFrom: '3.14' " => 3.14 " Number readFrom: '2r111' " => 7 " Number readFrom: '3 times' " => 3 "
Number class >> #readFrom: はパラメータ(引数)にストリームも受け付けます。
| stream number | stream := ReadStream on: '345 times'. number := Number readFrom: stream. ^ {number. stream position} " => #(345 3) "
ここでミソは、パラメータが文字列ではなく、ストリームの場合は、数値の読み取りが終了したら(あるいは、数値以外の文字が現われたときは)、そこでストリームの読み取り位置(position)も保持されるということです。これは、パラーメータとして渡されたストリームが、スキャナが解析中のソースだったとき、数値の読み取りが終わった後、次の字句解析の作業が続けられることを意味します。
Scanner >> #scanTokens: に戻ります。#scan: を起動して初期化を追えたあと、#scanLitVec を起動しています。これは、配列のリテラル記述を解析するためのメソッドです。
Scanner >> scanLitVec | stream | stream := WriteStream on: (Array new: 16). [tokenType = #rightParenthesis or: [tokenType = #doIt]] whileFalse: [ tokenType = #leftParenthesis ifTrue: [self scanToken; scanLitVec] ifFalse: [" ...省略... "]. stream nextPut: token. self scanToken]. token := stream contents
右括弧(#rightParenthesis)か、ソースを読み終える(#doIt。ダミーの字句タイプ)までループを繰り返し、左括弧(#leftParenthesis)があると、再帰して自身を呼びだして配列の入れ子を作ります。そうでなければ、解析済みの字句を出力用のストリームに収め、次の字句読み進め(#scanToken を起動)を行ないます。
まあ、おおよそこんなかんじです。この動きをコピーして簡略化したのが「漢数式スキャナ」です。#scan: は #初期化: 、#step は #文字読み進め 、#scanToken は #字句読み進め 、#xDigit は #x漢数字 、#xBinary は #x2項演算子 に相当します。
#scanLitVec 相当のことを漢数字式文字列で行なうのに、#構造スキャン というメソッドも作りました。大枠は #scanLitVec と同じです。
(続く…のかな?)