GNU Smalltalk チュートリアル(その1)

なんだか、GNU Smalltalk の情報を get しようと googleったら、この blog が上位に引っかかります。きっとはてなのお陰なのですが、いあいあ何にも書いてないのに、ちょっと気が引けまする。

というわけで、簡単な入門記事でも書いてみます。


* * *

はじめに

GNU Smalltalk は、Smalltalk の変り種で、普通の Smalltalk とは 文法も見た目も世界観も全部違う、かなりの捻くれ Smalltalk です。というのも、Smalltalkユーザランドも含めたOS のようなものをまるまる含んだものですが、GNU Smalltalk はその中の言語だけを引っぺがして強引に GNU (=UNIXのようなもの) とくっつけてしまった、そんな力技の Smalltalk だったりします。いっそ GNU/Smalltalk の方が体を表しているかも。

完成度もまだまだで、正直 GNU Smalltalk を使うために GNU Smalltalk を使うような状態です。(例えば何のオプションも付けずに実行しても、ガベージコレクションしたよ!・・とかパッケージをロードしたよ!とか、標準出力にダラダラ吐きます)

そんなわけで、「引き返すなら今だ!」「Smalltalk したいなら、素直に SqueakVisualWorks をつかっとけ!」と初めに言っておきまする。へにゃへにゃ。

8Queen

というわけで、早速 GNU Smalltalk でプログラミング。お手軽な例として、Timothy Budd 先生の「オブジェクト指向プログラミング入門」

オブジェクト指向プログラミング入門

オブジェクト指向プログラミング入門

  • 作者: ティモシイ・A.バッド,Timothy A. Budd,羽部正義
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2002/12
  • メディア: 単行本
  • 購入: 3人 クリック: 195回
  • この商品を含むブログ (42件) を見る

から、8Queen を実装してみます。(それにしても、この本絶版なのですね/中古で1万円越えなんて...orz)

  • nqueen.st
#!/usr/bin/env gst

Object subclass: Queen [
    | row column neighbor |

    setColumn: aNumber neighbor: aQueen [
        column := aNumber.
        neighbor := aQueen.
        row := 1.
    ]

    canAttack: testRow column: testColumn [
        | columnDifference |

        columnDifference := testColumn - column.
        (((row = testRow) or:
             [row + columnDifference = testRow]) or:
                 [row - columnDifference = testRow])
            ifTrue: [^true].

        ^neighbor canAttack: testRow column: testColumn
    ]

    advance [
        (row < 8) ifTrue: [
            row := 1 + row.
            ^self findSolution ].

        (neighbor advance) ifFalse: [^false].

        row := 1 .
        ^self findSolution
    ]

    findSolution [
        [ neighbor canAttack: row column: column ]
            whileTrue: [ self advance ifFalse: [^false] ].
        ^true
    ]

    result [
        ^(neighbor result) addLast: row; yourself
    ]

]

Object subclass: SentinelQueen [

    advance [
        ^false
    ]

    canAttack:row column: column [
        ^false
    ]

    result [
        ^OrderedCollection new
    ]
]


Eval [
    | lastQueen |
    lastQueen := SentinelQueen new.

    1 to: 8 do: [:i | 
        lastQueen := (Queen new)
            setColumn: i neighbor: lastQueen.
        lastQueen findSolution.
    ].

    lastQueen result printNl.
]

先頭の #! は UNIX っぽいスクリプト言語になってしまったのだなぁと実感させますが、Smalltalk で # は一行コメントでもなんでもないので しみじみ気持ち悪いというか、頑張ってキメラってるなぁ、と実感です。

で、ご察しのとおり、

$ gst nqueen.st

あるいは

$ ./nqueen.st

で実行します。

Smalltalk では、「ファイル?なにそれ」「すべてがオブジェクト」な世界なのですが、ファイル世界に例えるならば、メソッド毎にファイルが作られるような感じで、クラスの範囲を区切るブラケットとか、メソッドの範囲を区切るブラケットとか、そういうレキシカルスコープを表現するような構文はありません。・・・が、これが UNIX 世界のスクリプト言語としてはそれは大変な問題で、GNU Smalltalk では そこら辺の文法の独自に改造をしているわけです。なので構文の見た目的にはもう別の言語といっても良いくらい。

ザザッとさらうとこんな感じ。

namespace Foo [
    Object subclass: BarClass [
        | instanceFieldA instansFieldB |
        BarClass class >> buzClassMethod [
            "クラスメソッドのコードを書く"
        ]
        xyzzyMethod [
            "メソッドのコードを書く"
        ]
    ]
]

ここら辺は、今時のOOPL に馴染んだ方なら予想を裏切らない構造になっています。(Smalltalk 的には腑抜けたとも言えます)

ただ、よく分からないのが一番最後の Eval [ .. ] です。

Eval ブロック(?)

Eval [..] も GNU Smalltalk 独自です。一見おとなしい見た目をしていますが、特別な構文を用意しない Smalltalk 的にこの Eval というキーワードの特別扱いはとても気持ち悪いです*1

実は Eval [..] がなくても何の問題もなく実行されます。字面の期待を裏切り Eval は「ここを実行するよ」という意味ではないのです。

Eval [..]で囲むのと囲まないので、何が違うのかというと、

  • Evalブロック 毎に 名前空間が途切れる
  • GNU Smalltalk 独自の構文が (たとえば class 定義とか)Evalブロックの中では通らなくなる(たぶん)

実はわたし、動きから意味を想像しているだけでよく分かっていません。そんな状態で説明するのもおこがましいのですが、前者はこんな感じです。

  • hello.st
#!/usr/bin/env gst

Eval [
  hoge := 'hello world'.
  hoge  printNl
]

Eval [
  hoge printNl
]

実行

$ ./hello.st 
'hello world'
nil

余談ですが、GNU Smalltalk では Squeak の Workspace よろしく存在しない名前にアクセスすると、勝手に変数を作ってくれます。まぁ、ちょっとお節介だけど、代入で勝手に変数をつくるのは今時です。ただ、GNU Smalltalk は 無い名前を参照したとき nilが取れてしまうので、正直かなりデバッグしにくいです。タイプミスが怖いですよー。

というわけで、件の 8Queen は実行すると、

$ gst nqueen.st 
OrderedCollection (1 5 8 6 3 7 2 4 )

という結果が帰って参ります。

スクリプトのファイル分割

一応これでも、動いているのは分かるのですが、なんとも侘しさ倍増ですので、コンソールに画を書いてみます。ついでに 8-Queen 固定から N-Queen 対応してしまいましょう。

まずは、先ほど nqueen.st を小改造。

  • nqueen.st
Object subclass: Queen [
    | row column neighbor maxRow |

    setColumn: aNumber neighbor: aQueen maxRow: aMaxRow [
        column := aNumber.
        neighbor := aQueen.
        row := 1.
        maxRow := aMaxRow
    ]

    canAttack: testRow column: testColumn [
        | columnDifference |

        columnDifference := testColumn - column.
        (((row = testRow) or:
             [row + columnDifference = testRow]) or:
                 [row - columnDifference = testRow])
            ifTrue: [^true].

        ^neighbor canAttack: testRow column: testColumn
    ]

    advance [
        (row < maxRow) ifTrue: [
            row := 1 + row.
            ^self findSolution ].

        (neighbor advance) ifFalse: [^false].

        row := 1 .
        ^self findSolution
    ]

    findSolution [
        [ neighbor canAttack: row column: column ]
            whileTrue: [ self advance ifFalse: [^false] ].
        ^true
    ]

    result [
        ^(neighbor result) addLast: row; yourself
    ]

]

Object subclass: SentinelQueen [

    advance [
        ^false
    ]

    canAttack:row column: column [
        ^false
    ]

    result [
        ^OrderedCollection new
    ]
]

Object subclass: NQueenPuzzle [
    | queenCnt |
    NQueenPuzzle class >> new: aNumber [
        ^(self new) initialize: aNumber; yourself
    ]

    initialize: aNumber [
        queenCnt := aNumber
    ]

    solvePuzzle [
        | lastQueen |
        lastQueen := SentinelQueen new.

        1 to: queenCnt do: [:i | 
            lastQueen := (Queen new)
                setColumn: i neighbor: lastQueen maxRow: queenCnt.
            lastQueen findSolution.
        ].

        ^lastQueen result
    ]
]

予告どおりの N-Queen 対応と、前回 Eval だった部分を NQueenPuzzle クラスに括ってみました。

お次は 盤面ビュワー。result である OrderedCollection を渡されると、それを元に盤面を出力します。

  • nqueenViewer.st
Object subclass: NQueenViewer [
    | result buf |

    show: aResult [
        buf := ''.
        result := aResult.

        1 to: result size do: [:l| self writeLine: l ].
        self writeSeparator.

        self print
    ]

    writeLine: line [
        self writeSeparator; write: '|'.

        1 to: result size do: [:col |
            ((result at: col) = line) 
                ifTrue: [self write: ' Q ']
                ifFalse:[self write: '   '].
            self write: '|'.
        ].

        self cr.
    ]

    writeSeparator [
        1 to: result size do: [:x| self write: '+---'].
        self write: '+' ; cr
    ]

    write: str [
        buf := buf, str
    ]

    cr [
        buf := buf, Character nl asString
    ]

    print [
        Transcript show: buf
    ]
]

適当に作ったのであまりよいコードでないですが(文字列をガシガチしてたり..orz)、こまけぇこたぁいいんだよっ!、です。お願いです、そういうことにしといてください*2

最後にこれらを使って N-Queen パズルを説いて表示する run.st を作ります。

  • run.st
#!/usr/bin/env gst
Eval [
    | result viewer |

    FileStream fileIn: 'nqueen.st'.
    FileStream fileIn: 'nqueenViewer.st'.

    result := (NQueenPuzzle new: 10) solvePuzzle.
    result printNl.

    viewer := NQueenViewer new.
    viewer show: result.
]

他のファイルのスクリプトを利用するには、 FileStream >> fileIn: を使います。Python の import とか Ruby の require 風ですが、Smalltalk 的に伝統と格式の File in そのままなのはホッとします。

実行するとこんな感じです。



* * *

というわけで、チュートリアルを始めてみたのですが、なんていうか、チュートリアルじゃなくて、単なるわたしの感想文ですね。それと例題が「例」じゃなくって本気課題になりつつあって、さっぱりお手軽な例じゃなくなってきました(^^;

なんだか本末転倒?な感じですが、そんなこんなで つづきはまた今度、というわけで。

*1:大文字で始まる語は Smalltalk 辞書のキーワード..じゃない、というのがなんとも

*2:最初は直に Transcript show: してたのですが、途中でガベージコレクションなメッセージが混じったりしたので、急遽一括書き出しに。