Squeak Smalltalk で小町算


演算子に優先順位を持たせていない Smalltalk の場合、ただ式を作って評価しただけでは電卓モードの答えしか出せません。しかし、括弧を付ける細工をするのは個人的に苦手(^_^;)なので、パーサーをちょこっといじって(a ModifiedParser)通常の言語と同様に乗除を優先させるなんちゃってコンパイラ(a ModifiedCompiler)をこしらえてみました。これなら同じ式文字列に対してコンパイラを切り替えて評価することで両方の答えを確かめることができます。


まず、パーサー。いじるのは、スキャナから継承した #xBinary と #messagePart:repeat: の二つなので、ModifiedParser を Parser のサブクラスとして定義して、これら二つのメソッドを再定義します。後者は長いですが、Parser>>#messagePart:repeat: からコードをコピペして、赤字部分を追加しただけです。やっていることは簡単で、字句解析の際に #multiplicative という字句タイプを新設し、乗除にこれを当てはめます。あとは、#messagePart:repeat: で #word(単項メッセージ)の次の優先順位で #multiplicative をパースさせる…というカラクリです。

Parser subclass: #ModifiedParser

ModifiedParser >> xBinary
    super xBinary.
    (#(* /) includes: token) ifTrue: [tokenType := #multiplicative]

ModifiedParser >> messagePart: level repeat: repeat
    | start receiver selector args precedence words keywordStart |
    [receiver := parseNode.
    (hereType == #keyword
            and: [level >= 3])
        ifTrue: [start := self startOfNextToken.
            selector := WriteStream
                        on: (String new: 32).
            args := OrderedCollection new.
            words := OrderedCollection new.
            [hereType == #keyword]
                whileTrue: [keywordStart := self startOfNextToken + requestorOffset.
                    selector nextPutAll: self advance.
                    words
                        addLast: (keywordStart to: self endOfLastToken + requestorOffset).
                    self primaryExpression
                        ifFalse: [^ self expected: 'Argument'].
                    self messagePart: 2 repeat: true.
                    args addLast: parseNode].
            (Symbol
                    hasInterned: selector contents
                    ifTrue: [:sym | selector := sym])
                ifFalse: [selector := self
                                correctSelector: selector contents
                                wordIntervals: words
                                exprInterval: (start to: self endOfLastToken)
                                ifAbort: [^ self fail]].
            precedence := 3]
        ifFalse: [((hereType == #binary
                        or: [hereType == #verticalBar])
                    and: [level >= 2])
                ifTrue: [start := self startOfNextToken.
                    selector := self advance asOctetString asSymbol.
                    self primaryExpression
                        ifFalse: [^ self expected: 'Argument'].
                    self messagePart: 1 repeat: true.
                    args := Array with: parseNode.
                    precedence := 2]
                ifFalse: [(hereType == #multiplicative
                            and: [level >= 1])
                        ifTrue: [start := self startOfNextToken.
                            selector := self advance asOctetString asSymbol.
                            self primaryExpression
                                ifFalse: [^ self expected: 'Argument'].
                            self messagePart: 0 repeat: true.
                            args := Array with: parseNode.
                            precedence := 2]
                        ifFalse: [hereType == #word
                                ifTrue: [start := self startOfNextToken.
                                    selector := self advance.
                                    args := #().
                                    words := OrderedCollection
                                                with: (start + requestorOffset to: self endOfLastToken + requestorOffset).
                                    (Symbol
                                            hasInterned: selector
                                            ifTrue: [:sym | selector := sym])
                                        ifFalse: [selector := self
                                                        correctSelector: selector
                                                        wordIntervals: words
                                                        exprInterval: (start to: self endOfLastToken)
                                                        ifAbort: [^ self fail]].
                                    precedence := 1]
                                ifFalse: [^ args notNil]]]].
    parseNode := MessageNode new
                receiver: receiver
                selector: selector
                arguments: args
                precedence: precedence
                from: encoder
                sourceRange: (start to: self endOfLastToken).
    repeat] whileTrue.
    ^ true


コンパイラは新しく作ったパーサーを使うように指示するだけなので簡単。

Compiler subclass: ModifiedCompiler

ModifiedCompiler class >> parserClass
    ^ModifiedParser


念のため動作をチェックしてみましょう。通常のコンパイラは電卓で逐次計算をしたときと同様の挙動をします。

Compiler evaluate: '1 + 2 * 3'   "=> 9 "


新しく作ったコンパイラでは、乗除が優先して評価されます。

ModifiedCompiler evaluate: '1 + 2 * 3'   "=> 7"


大丈夫みたいですね。


さて、コンパイラは出来たのでこれに評価させる組み合わせを作りましょう。演算子穴埋め用の重複を許す組み合わせの列挙には #asDigitsToPower:do: を用います。

| index |
index := 0.
#('+' '-' '*' '/' '') asDigitsToPower: 8 do: [:signs | index := index + 1].
^index   "=> 390625 "


ただ、手元の古いマシンでは、ちょっと時間がかかりそうなので、待ち時間の見当をつけられるように(少々長たらしくなりますが…)プログレスバーを表示させることにしました。

| index |
'searching...'
    displayProgressAt: Display center
    from: (index := 0) to: (5 raisedTo: 8)
    during: [:bar |
        #('+' '-' '*' '/' '') asDigitsToPower: 8 do: [:signs | bar value: (index := index + 1)]].
^index   "=> 390625 "


準備が整ったところで、実際の処理を記述。

| results1 results2 index |
results1 := OrderedCollection new.
results2 := OrderedCollection new.
'searching...'
    displayProgressAt: Display center
    from: (index := 0) to: (5 raisedTo: 8)
    during: [:bar |
        #('+' '-' '*' '/' '') asDigitsToPower: 8 do: [:signs |
            | expression |
            bar value: (index := index + 1).
            expression := '1{1}2{2}3{3}4{4}5{5}6{6}7{7}8{8}9' format: signs.
            (ModifiedCompiler evaluate: expression) = 100 ifTrue: [results1 add: expression].
            (Compiler evaluate: expression) = 100 ifTrue: [results2 add: expression]]].
^{results1 inspect; size. results2 inspect; size}


結果はこちら。

=> #(101 68)


以上のファイルイン用チェンジセット