プログラミングのプログラミング

for VisualWorks 7.8 / 7.7 / 7.6

Smalltalkのプログラムをシステムブラウザを用いて作成してゆくことが多いと思います。そのプログラミング行為そのものをプログラムにしてみましょう。名付けて「プログラミングのプログラミング」です。パッケージを作るところから、名前空間を定義し、クラス群を生成し、メソッド群を作成し、例題プログラムをこしらえてテストを行い、ソースコードとしてセーブし、バイナリとしてセーブ(パーセル化)するところまで、これら一連のプログラムを紹介します。

ワークブック[Workbook]

Workbook.zipをダウンロードし、適当な場所に展開してください。Workbookというディレクトリができあがります。

$ curl -O http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/VisualWorks/ProgrammingOfProgramming/Workbook.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6361  100  6361    0     0  58194      0 --:--:-- --:--:-- --:--:-- 94940

$ unzip Workbook.zip
Archive:  Workbook.zip
   creating: Workbook/
  inflating: Workbook/Workbook.st    
   creating: Workbook/Workspaces/
  inflating: Workbook/Workspaces/1_Package.st  
  inflating: Workbook/Workspaces/2_NameSpace.st  
  inflating: Workbook/Workspaces/3_Classes.st  
  inflating: Workbook/Workspaces/4_Methods.st  
  inflating: Workbook/Workspaces/5_Methods.st  
  inflating: Workbook/Workspaces/6_Methods.st  
  inflating: Workbook/Workspaces/7_Testing.st  
  inflating: Workbook/Workspaces/8_Saving.st  
  inflating: Workbook/Workspaces/9_Parcel.st

そうしたら、VisualWorksを立ち上げ、ファイルブラウザを開き、Workbookのディレクトリの中を見てください。その中にWorkbook.stというファイルがありますので、それを選択してファイルイン(File In)します。

Workbook1.png

1_Packageから9_Parcelまでのタブを持つワークブックが現れます。パッケージを作るところから、名前空間を定義し、クラス群を生成し、メソッド群を作成し、テストを行い、ソースコードとしてセーブし、バイナリとしてセーブ(パーセル化)するところまで、スモールトーカー(Smalltalker)が暗記している(そらんじている)典型的なイディオム(慣用句としてのプログラム断片)を収録しています。

Workbook2.png

参考のため、上記のワークブックを出現させるプログラムを以下に紹介しておきます。(実のところ、下記のプログラムにもメタプログラミングが施されており、プログラミングのプログラミングになっています。ちょっと難しいので、どうぞ飛び越して、次へと歩を進めてくださいませ。)

| aDirectory aContext aBoolean aBlock aDialog aCollection aWorkbook aBuilder aRectangle |
aDirectory := nil.
aContext := thisContext.
aBoolean := true.
aBlock :=
        [[aContext notNil and: [aBoolean yourself]] whileTrue: 
                [(aContext selector == #fileIn and: [aContext receiver isKindOf: Stream])
                    ifTrue: 
                        [| aFilename |
                        aFilename := aContext receiver stream fileName.
                        aDirectory := aFilename head asFilename.
                        aBoolean := false].
                aContext := aContext sender]].
[aBlock value]
    on: aBlock messageNotUnderstoodSignal
    do: 
        [:exception |
        aDirectory := nil.
        exception return].
aDirectory
    ifNil: 
        [aDirectory := Filename defaultDirectory construct: 'Workbook'.
        aDirectory exists
            ifFalse: 
                [aDialog := ChooseDirectoryDialog new.
                aDialog windowTitle: 'Choose the direcotry ' , aDirectory tail printString.
                aDialog open ifFalse: [^nil].
                aDirectory := aDialog selection]].
aDirectory := aDirectory construct: 'Workspaces'.
aDirectory exists ifFalse: [^nil].
aCollection := OrderedCollection new.
aDirectory directoryContents asSortedCollection
    do:
        [:each | 
        ('*.st' match: each)
            ifTrue: [aCollection add: (aDirectory construct: each)]].
aWorkbook := Workbook new.
aCollection with: (1 to: aCollection size)
    do: 
        [:each :index |
        | aStream pageContents labelString aPage |
        aStream := each readStream.
        [pageContents := aStream contents] ensure: [aStream close].
        labelString := each tail asString.
        labelString := (Filename splitExtension: labelString) first.
        aPage := WorkspacePage labeled: labelString with: pageContents.
        index = 1
            ifTrue: [aWorkbook replaceAllTextPagesWith: aPage]
            ifFalse: [aWorkbook addPage: aPage]].
aBuilder := aWorkbook allButOpenInterface: #windowSpec.
aRectangle := (Screen default bounds center extent: 0 @ 0)
            expandedBy: 400 @ 250.
aBuilder window openIn: aRectangle.
aBuilder window displayPendingInvalidation.
aWorkbook postOpenWith: aBuilder.
^aWorkbook

パッケージ[1_Package]

まず、システムブラウザにたよらずに、プログラムでパッケージを作るところから始めましょう。1_Packageのタブをひいて得られるプログラムを全選択して実行(Do it)してください。

Package1.png

実行したプログラムを示しておきます。Store.RegistryをレシーバにしてpackageNamedOrCreate: packageNameStringというメッセージを送っているところがミソです。

| packageNameString commentString aPackage |
packageNameString := 'Forest-ProjectM'.
commentString := 'Smalltalk Studies in Kyoto'.
aPackage := Store.Registry packageNamedOrCreate: packageNameString.
aPackage comment: commentString.
^aPackage

本当にパッケージができているのかを確かめましょう。システムブラウザを開けて調べればいいのですが、そこはプログラミングのプログラミング、プログラムでシステムブラウザを開け、当該のパッケージを選択してみます。以下のようなプログラムになります。

| packageNameString aPackage aBrowser aNavigator |
packageNameString := 'Forest-ProjectM'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator selectPundle: aPackage.
^aBrowser

実行しましょう。ただし、上記のプログラムを音読して(声に出して読んで)からにしてください。(なぜ、OrCreateのキャメルがないのか、おわかりいただけますよね。リファクタリングブラウザの開き方とパッケージの選択の仕方など、後ほど時間があるときにでも、RefactoringBrowserとBrowserNavigatorの2つのクラスを調査していただけると助かります。)

Package2.png

以下ではプログラム群と画像群に物を言わせて多くを語りませんが、どのメッセージ式がミソであるのか等々、メタプログラミングの高みをめざして、私たちのプログラミング行為の背後で動いているプログラムの外在化(externalization)に心を砕いていただければ幸いです。

名前空間[2_NameSpace]

次は名前空間です。2_NameSpaceのタブをひいて得られるプログラムを全選択して実行してください。

NameSpace1.png
| nameSpaceSymbol packageNameString commentString aNameSpace aPackage |
nameSpaceSymbol := #ProjectM.
packageNameString := 'Forest-ProjectM'.
commentString := 'Smalltalk Studies in Kyoto'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aNameSpace := Smalltalk at: nameSpaceSymbol ifAbsent: [nil].
aNameSpace
    ifNil: 
        [aNameSpace := Smalltalk
                    defineNameSpace: nameSpaceSymbol
                    private: false
                    imports: 'private Smalltalk.*'
                    category: packageNameString.
        aNameSpace comment: commentString.
        aNameSpace yourself].
([aNameSpace package]
    on: aNameSpace messageNotUnderstoodSignal
    do: [:exception | Store.Registry containingPackageForNameSpace: aNameSpace])
        = aPackage
        ifFalse: 
            [Store.XChangeSet current moveWholeObject: aNameSpace toPackage: aPackage].
^aNameSpace

名前空間ができているのかを確かめます。

| nameSpaceSymbol aNameSpace aBrowser aNavigator aBinding |
nameSpaceSymbol := #ProjectM.
aNameSpace := Smalltalk at: nameSpaceSymbol ifAbsent: [^nil].
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aBinding := aNameSpace fullyQualifiedReference binding.
aNavigator setBinding: aBinding.
^aBrowser
NameSpace2.png

クラス群[3_Classes]

さぁ、簡単なMVC(モデル・ビュー・コントローラ)のためのクラス群(Model・View・Controller)を作成します。3_Classesのタブをひいて得られるプログラムを全選択して実行してください。

Classes1.png
| nameSpaceSymbol packageNameString commentString aPackage aNameSpace aCollection |
nameSpaceSymbol := #ProjectM.
packageNameString := 'Forest-ProjectM'.
commentString := 'Smalltalk Studies in Kyoto'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aNameSpace := Smalltalk at: nameSpaceSymbol ifAbsent: [^nil].
aCollection := (OrderedCollection new)
            add: (Array with: #Model with: #{UI.Model} with: 'forest picture');
            add: (Array with: #View with: #{UI.View} with: 'offset');
            add: (Array with: #Controller with: #{UI.Controller} with: '');
            yourself.
^aCollection collect: 
        [:anArray |
        | aClass |
        aClass := aNameSpace
                    defineClass: (anArray at: 1)
                    superclass: (anArray at: 2)
                    indexedType: #none
                    private: false
                    instanceVariableNames: (anArray at: 3)
                    classInstanceVariableNames: ''
                    imports: ''
                    category: packageNameString.
        aPackage addClass: aClass.
        aClass comment: commentString.
        aClass yourself]

ちゃんとクラス群が定義できたのかを確かめます。

| aCollection aBrowser aNavigator |
aCollection := Array
            with: ProjectM.Model
            with: ProjectM.View
            with: ProjectM.Controller.
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator state classesAndNameSpaces: aCollection.
aNavigator
    setState: aNavigator state;
    changed.
^aBrowser
Classes2.png

メソッド群(1)[4_Methods]

モデルのメソッド群を作ります。4_Methodsのタブをひいて得られるプログラムを全選択して実行してください。

Methods11.png
| aClass aCollection |
aClass := ProjectM.Model.
aCollection := (OrderedCollection new)
            add: (Array with: 'accessing' with: '

picture
    ^picture

                        ');
            add: (Array with: 'accessing'
                        with: '

picture: anImage
    picture := anImage

                        ');
            yourself.
^aCollection collect: 
        [:anArray |
        | aProtocol aCode aTree aSelector aMethod |
        aProtocol := (anArray at: 1) asSymbol.
        aCode := (anArray at: 2) yourself.
        aTree := Refactory.Browser.RBParser parseMethod: aCode.
        aSelector := aClass compile: aTree formattedCode classified: aProtocol.
        aMethod := aClass compiledMethodAt: aSelector.
        aMethod yourself]

モデルのインスタンスメソッド群が作られているのかを確かめます。

| aClass aBrowser aNavigator |
aClass := ProjectM.Model.
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator selectClass: aClass.
(aNavigator state)
    protocols: aClass organization categories;
    selectors: aClass selectors.
aNavigator
    setState: aNavigator state;
    changed.
^aBrowser
Methods12.png

メソッド群(2)[5_Methods]

ビューのメソッド群を作ります。5_Methodsのタブをひいて得られるプログラムを全選択して実行してください。

zzz1.png
| aClass aCollection |
aClass := ProjectM.View.
aCollection := (OrderedCollection new)
            add: (Array with: 'controller accessing'
                        with: '

defaultControllerClass
    ^Controller

                        ');
            add: (Array with: 'displaying'
                        with: '

displayOn: aGraphicsContext
    aGraphicsContext
        paint: ColorValue white;
        displayRectangle: self bounds.
    model
        ifNotNil: 
            [model picture ifNotNil: [:it | it displayOn: aGraphicsContext at: offset]]

                        ');
            add: (Array with: 'initialize-release'
                        with: '

initialize
    super initialize.
    offset := 0 @ 0

                        ');
            add: (Array with: 'scrolling'
                        with: '

scrollBy: aPoint
    self scrollTo: offset + aPoint

                        ');
            add: (Array with: 'scrolling'
                        with: '

scrollTo: aPoint
    offset := aPoint

                        ');
            add: (Array with: 'updating'
                        with: '

update: aNode
    self invalidateNow

                        ');
            yourself.
^aCollection collect: 
        [:anArray |
        | aProtocol aCode aTree aSelector aMethod |
        aProtocol := (anArray at: 1) asSymbol.
        aCode := (anArray at: 2) yourself.
        aTree := Refactory.Browser.RBParser parseMethod: aCode.
        aSelector := aClass compile: aTree formattedCode classified: aProtocol.
        aMethod := aClass compiledMethodAt: aSelector.
        aMethod yourself]

ビューのインスタンスメソッド群が作られているのかを確かめます。

| aClass aBrowser aNavigator |
aClass := ProjectM.View.
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator selectClass: aClass.
(aNavigator state)
    protocols: aClass organization categories;
    selectors: aClass selectors.
aNavigator
    setState: aNavigator state;
    changed.
^aBrowser
Methods22.png

メソッド群(3)[6_Methods]

コントローラのメソッド群を作ります。6_Methodsのタブをひいて得られるプログラムを全選択して実行してください。

Methods31.png
| aClass aCollection |
aClass := ProjectM.Controller.
aCollection := (OrderedCollection new)
            add: (Array with: 'events'
                        with: '

blueButtonPressedEvent: event
    self mouseButtonActivity.
    ^nil

                        ');
            add: (Array with: 'activities'
                        with: '

clickActivity
    Transcript
        cr;
        show: thisContext printString

                        ');
            add: (Array with: 'activities'
                        with: '

grabActivity
    | mouse previous current |
    mouse := self sensor.
    previous := current := mouse cursorPoint.
    [mouse anyButtonPressed] whileTrue: 
            [current := mouse cursorPoint.
            previous = current
                ifFalse: 
                    [(self view)
                        scrollBy: current - previous;
                        invalidateNow.
                    previous := current].
            Processor yield]

                        ');
            add: (Array with: 'activities'
                        with: '

mouseButtonActivity
    Cursor crossHair showWhile: 
            [| symbol |
            symbol := self mouseButtonChecking.
            symbol = #click ifTrue: [self clickActivity].
            symbol = #grab ifTrue: [self grabActivity]].
    self sensor waitNoButton

                        ');
            add: (Array with: 'activities'
                        with: '

mouseButtonChecking
    | mouse limit |
    mouse := self sensor.
    limit := Time millisecondClockValue + 333.
    [Time millisecondClockValue < limit] whileTrue: 
            [mouse anyButtonPressed ifFalse: [^#click].
            Processor yield].
    ^#grab

                        ');
            add: (Array with: 'events'
                        with: '

redButtonPressedEvent: event
    self mouseButtonActivity.
    ^nil

                        ');
            add: (Array with: 'events'
                        with: '

yellowButtonPressedEvent: event
    self mouseButtonActivity.
    ^nil

                        ');
            yourself.
^aCollection collect: 
        [:anArray |
        | aProtocol aCode aTree aSelector aMethod |
        aProtocol := (anArray at: 1) asSymbol.
        aCode := (anArray at: 2) yourself.
        aTree := Refactory.Browser.RBParser parseMethod: aCode.
        aSelector := aClass compile: aTree formattedCode classified: aProtocol.
        aMethod := aClass compiledMethodAt: aSelector.
        aMethod yourself]

コントローラのインスタンスメソッド群が作られているのかを確かめます。

| aClass aBrowser aNavigator |
aClass := ProjectM.Controller.
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator selectClass: aClass.
(aNavigator state)
    protocols: aClass organization categories;
    selectors: aClass selectors.
aNavigator
    setState: aNavigator state;
    changed.
^aBrowser
Methods32.png

テスト[7_Testing]

例題プログラムをモデルのクラスメソッドとして作成し、それを走らせて、テストします。7_Testingのタブをひいて得られるプログラムを全選択して実行してください。

Testing1.png
| aClass aProtocol aCode |
aClass := ProjectM.Model class.
aProtocol := 'examples' asSymbol.
aCode := '

example
    "ProjectM.Model example."

    | aModel aView aWindow aRectangle |
    aModel := Model new.
    aModel picture: (CachedImage
                on: (Screen default completeContentsOfArea: (Point zero
                                extent: (Screen default bounds extent min: 1024 @ 768)))).
    aView := View model: aModel.
    aWindow := ApplicationWindow
                model: aModel
                label: nil
                minimumSize: 400 @ 300.
    aWindow component: aView.
    aRectangle := Point zero extent: 800 @ 600.
    aRectangle := aRectangle align: aRectangle center
                with: Screen default bounds center.
    aWindow openIn: aRectangle.
    ^aModel

'.
aTree := Refactory.Browser.RBParser parseMethod: aCode.
aClass compile: aTree formattedCode classified: aProtocol.
^aClass soleInstance example

次のようなウィンドウが開いてきます。

Testing2.png

システムブラウザで例題プログラムを見てみましょう。

| aClass aProtocol aSelector aBrowser aNavigator |
aClass := ProjectM.Model class.
aProtocol := #examples.
aSelector := #example.
aBrowser := Refactory.Browser.RefactoringBrowser open.
aNavigator := aBrowser navigator.
aNavigator selectClass: aClass.
(aNavigator state)
    protocol: aProtocol;
    selector: aSelector.
aNavigator
    setState: aNavigator state;
    changed.
^aBrowser
Testing3.png

保存[8_Saving]

作ってきたものをソースコードとして(XML形式で)ファイルにセーブにします。8_Savingのタブをひいて得られるプログラムを全選択して実行してください。

Saving1.png
| packageNameString aPackage aFilename |
packageNameString := 'Forest-ProjectM'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aFilename := Filename defaultDirectory construct: packageNameString , '.st'.
aPackage fileOutOnFileNamed: aFilename.
^aPackage

ソースコードがセーブされているのかを確かめます。ファイルブラウザを開き、当該ファイルを選択するプログラムになります。

| packageNameString aPackage aFilename aFileBrowser |
packageNameString := 'Forest-ProjectM'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aFilename := Filename defaultDirectory construct: packageNameString , '.st'.
aFilename exists ifFalse: [^nil].
aFileBrowser := Tools.FileTools.FileBrowser new.
aFileBrowser open.
aFileBrowser selectFileNamed: aFilename.
^aFileBrowser
Saving2.png

小包[9_Parcel]

作ってきたものを小包(パーセル)にします。9_Parcelのタブをひいて得られるプログラムを全選択して実行してください。

Parcel1.png
| packageNameString nameSpaceSymbol commentString aPackage aNameSpace aParcel |
packageNameString := 'Forest-ProjectM'.
nameSpaceSymbol := #ProjectM.
commentString := 'Smalltalk Studies in Kyoto'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aNameSpace := Smalltalk at: nameSpaceSymbol ifAbsent: [^nil].
aParcel := Parcel parcelNamed: packageNameString.
aParcel isNil
    ifTrue: 
        [aParcel := Parcel createParcelNamed: packageNameString.
        aParcel comment: commentString].
aParcel addNameSpace: aNameSpace.
aPackage allClasses do: [:aClass | aParcel addEntiretyOfClass: aClass].
aParcel
    parcelOutOn: packageNameString , '.pcl'
    withSource: packageNameString , '.pst'
    hideOnLoad: false
    republish: false
    backup: false.
^aParcel

ちゃんとパーセルとしてセーブされて(.pclと.pstの2つのファイルができあがって)いるのかを確かめます。

| packageNameString aPackage aFilename aFileBrowser |
packageNameString := 'Forest-ProjectM'.
aPackage := Store.Registry packageNamed: packageNameString.
aPackage ifNil: [^nil].
aFilename := Filename defaultDirectory construct: packageNameString , '.pcl'.
aFilename exists ifFalse: [^nil].
aFileBrowser := Tools.FileTools.FileBrowser new.
aFileBrowser open.
aFileBrowser selectFileNamed: aFilename.
^aFileBrowser
Parcel2.png

for VisualWorks 7.8 / 7.7 / 7.6


Updated: 2015/11/08 (Created: 2010/01/01) KSU AokiHanko