メタプログラミング「MyFirstMVC」

for VisualWorks 7.9 on Mac OS X (macOS 10.14 Mojave or earlier) / Windows (Windows 7 or later)
for VisualWorks 8.1 on Mac OS X (macOS 10.14 Mojave or later) / Windows (Windows 7 or later)

SmalltalkプログラムをSmalltalkプログラムで作る、すなわち、Smalltalkメタプログラミングに挑戦しましょう。バンドルを作り、パッケージを作り、名前空間を作り、クラスを作り、プロトコルを作り、メソッドを作り、テストして、保存する、これら一連のSmalltalkプログラミングの過程それ自身をプログラミングしちゃおう!という試みになります。

Smalltalkはすべてがオブジェクトへのメッセージング(メッセージ送信)で成り立っているわけですから、これら一連のSmalltalkプログラミングの過程さえも、メッセージングにできるはずです。そりゃもう『目にうつる全てのことはメッセージ』ですから…。

私の初めてのモデル・ビュー・コントローラ(My First MVC)の作成

メタプログラミングの題材として「My First MVC」を取り上げます。「私の初めてのモデル・ビュー・コントローラ」のプログラム作成過程を、メタプログラムに仕立て上げてゆくプログラミングになります。

MyFirstMVC

さぁ!始めましょう。メタプログラムの実行結果をシステムブラウザで示してゆきます。

Snapshot00

これから行ってゆくことのワークブック(Workbook:Workspaceの集合体)も用意しておきました。

$ curl -O http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/VisualWorks/MetaProgrammingMyFirstMVC/Workbook_MyFirstMVC.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 12862  100 12862    0     0   131k      0 --:--:-- --:--:-- --:--:--  130k

$ unzip Workbook_MyFirstMVC.zip
Archive:  Workbook_MyFirstMVC.zip
   creating: Workbook_MyFirstMVC/
  inflating: Workbook_MyFirstMVC/Workbook_MyFirstMVC.st  
   creating: Workbook_MyFirstMVC/Workspaces/
  inflating: Workbook_MyFirstMVC/Workspaces/01_Bundle.st  
  inflating: Workbook_MyFirstMVC/Workspaces/02_Packages.st  
  inflating: Workbook_MyFirstMVC/Workspaces/03_NameSpace.st  
  inflating: Workbook_MyFirstMVC/Workspaces/04_AJ.MyFirstModel.st  
  inflating: Workbook_MyFirstMVC/Workspaces/05_AJ.MyFirstView.st  
  inflating: Workbook_MyFirstMVC/Workspaces/06_AJ.MyFirstController.st  
  inflating: Workbook_MyFirstMVC/Workspaces/07_AJ.MyFirstModel.image.st  
  inflating: Workbook_MyFirstMVC/Workspaces/08_AJ.MyFirstModel.createView.st  
  inflating: Workbook_MyFirstMVC/Workspaces/09_AJ.MyFirstModel.defaultViewClass.st  
  inflating: Workbook_MyFirstMVC/Workspaces/10_AJ.MyFirstModel.class.example.st  
  inflating: Workbook_MyFirstMVC/Workspaces/11_AJ.MyFirstModel.class.windowSpec.st  
  inflating: Workbook_MyFirstMVC/Workspaces/12_AJ.MyFirstModel.class.menuBar.st  
  inflating: Workbook_MyFirstMVC/Workspaces/13_AJ.MyFirstModel.class.popUpMenu.st  
  inflating: Workbook_MyFirstMVC/Workspaces/14_AJ.MyFirstView.preferredExtent.st  
  inflating: Workbook_MyFirstMVC/Workspaces/15_AJ.MyFirstView.defaultControllerClass.st  
  inflating: Workbook_MyFirstMVC/Workspaces/16_AJ.MyFirstView.displayOn_.st  
  inflating: Workbook_MyFirstMVC/Workspaces/17_AJ.MyFirstController.redButtonPressedEvent_.st  
  inflating: Workbook_MyFirstMVC/Workspaces/18_AJ.MyFirstController.doSomething.st  
  inflating: Workbook_MyFirstMVC/Workspaces/19_Test.st  
  inflating: Workbook_MyFirstMVC/Workspaces/20_Save.st  

Smalltalkのファイルブラウザから「Workbook_MyFirstMVC.st」をファイルイン(File In)してくださいませ。

FileBrowser

これからやってゆくことのワークブック(Workbook:Workspaceの集合体)が開きます。

Workbook

バンドル(Bundle)の作成

    | aBundle |
    aBundle := Smalltalk.Store.Registry bundleNamedOrCreate: 'AokiJuku'.
    aBundle comment: 'AokiJuku: Programming in Summer 2019'.
    ^aBundle
    ==> {AokiJuku}
Snapshot01

パッケージ(Package)の作成

    | aBundle aCollection aPackage |
    aBundle := Smalltalk.Store.Registry bundleNamed: 'AokiJuku'.
    aBundle ifNil: [^self error: 'fatal error'].
    aCollection := Smalltalk.Core.OrderedCollection new.
    aPackage := Smalltalk.Store.Registry packageNamedOrCreate: 'AJ-System'.
    aPackage comment: 'AJ-System: System in AokiJuku'.
    aCollection add: aPackage.
    (aBundle containedItems includes: aPackage)
        ifFalse: [aBundle addItem: aPackage].
    aPackage := Smalltalk.Store.Registry packageNamedOrCreate: 'AJ-MyFirstMVC'.
    aPackage
        comment: 'AJ-MyFirstMVC: My First Model-View-Controller in AokiJuku'.
    aCollection add: aPackage.
    (aBundle containedItems includes: aPackage)
        ifFalse: [aBundle addItem: aPackage].
    ^aCollection
    ==> OrderedCollection ([AJ-System] [AJ-MyFirstMVC])
Snapshot02

名前空間(NameSpace)の作成

    | aPackage aNameSpace |
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-System'.
    aPackage ifNil: [^self error: 'fatal error'].
    aNameSpace := Smalltalk at: #AJ
                ifAbsent: 
                    [aNameSpace := Smalltalk
                                defineNameSpace: #AJ
                                private: false
                                imports: 'private Core.*'
                                category: ''.
                    aNameSpace yourself].
    aNameSpace comment: 'AJ: NameSpace for AokiJuku'.
    Smalltalk.Store.XChangeSet current moveWholeObject: aNameSpace
        toPackage: aPackage.
    ^aNameSpace
    ==> AJ
Snapshot03

クラス「AJ.MyFirstModel」の作成

    | aReference aNameSpace aPackage aClass |
    aReference := #{Smalltalk.AJ}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aNameSpace := aReference value.
    (aNameSpace isKindOf: Smalltalk.Kernel.NameSpace)
        ifFalse: [^self error: 'fatal error'].
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-System'.
    aPackage ifNil: [^self error: 'fatal error'].
    aClass := aNameSpace
                defineClass: #MyFirstModel
                superclass: #{UI.ApplicationModel}
                indexedType: #none
                private: false
                instanceVariableNames: 'image'
                classInstanceVariableNames: ''
                imports: ''
                category: ''.
    aClass
        comment: 'My First Model

Instance Variables:
    image    <Image>    bind an image, default is screen captured image
'.
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-MyFirstMVC'.
    Smalltalk.Store.XChangeSet current moveWholeObject: aClass
        toPackage: aPackage.
    ^aClass
    ==> AJ.MyFirstModel
Snapshot041

クラス「AJ.MyFirstView」の作成

    | aReference aNameSpace aPackage aClass |
    aReference := #{Smalltalk.AJ}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aNameSpace := aReference value.
    (aNameSpace isKindOf: Smalltalk.Kernel.NameSpace)
        ifFalse: [^self error: 'fatal error'].
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-System'.
    aPackage ifNil: [^self error: 'fatal error'].
    aClass := aNameSpace
                defineClass: #MyFirstView
                superclass: #{UI.View}
                indexedType: #none
                private: false
                instanceVariableNames: ''
                classInstanceVariableNames: ''
                imports: ''
                category: ''.
    aClass comment: 'My First View
'.
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-MyFirstMVC'.
    Smalltalk.Store.XChangeSet current moveWholeObject: aClass
        toPackage: aPackage.
    ^aClass
    ==> AJ.MyFirstView
Snapshot042

クラス「AJ.MyFirstController」の作成

    | aReference aNameSpace aPackage aClass |
    aReference := #{Smalltalk.AJ}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aNameSpace := aReference value.
    (aNameSpace isKindOf: Smalltalk.Kernel.NameSpace)
        ifFalse: [^self error: 'fatal error'].
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-System'.
    aPackage ifNil: [^self error: 'fatal error'].
    aClass := aNameSpace
                defineClass: #MyFirstController
                superclass: #{UI.ControllerWithMenu}
                indexedType: #none
                private: false
                instanceVariableNames: ''
                classInstanceVariableNames: ''
                imports: ''
                category: ''.
    aClass comment: 'My First Controller
'.
    aPackage := Smalltalk.Store.Registry packageNamed: 'AJ-MyFirstMVC'.
    Smalltalk.Store.XChangeSet current moveWholeObject: aClass
        toPackage: aPackage.
    ^aClass
    ==> AJ.MyFirstController
Snapshot043

モデル「AJ.MyFirstModel」のインスタンスメソッド「image」をプロトコル「accessing」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'image

    image
        ifNil: 
            [image := Graphics.Screen default
                        completeContentsOfArea: Graphics.Screen default bounds].
    ^image'
                classified: #accessing
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #image
Snapshot05101

モデル「AJ.MyFirstModel」のインスタンスメソッド「createView」をプロトコル「interface opening」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'createView

    | aView aController |
    aView := self defaultViewClass model: self.
    aController := aView controller.
    aController menuHolder: [self class popUpMenu].
    ^aView'
                classified: #'interface opening'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #createView
Snapshot05102

モデル「AJ.MyFirstModel」のインスタンスメソッド「defaultViewClass」をプロトコル「view accessing」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'defaultViewClass

    ^#{AJ.MyFirstView} value'
                classified: #'view accessing'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #defaultViewClass
Snapshot05103

モデル「AJ.MyFirstModel」のクラスメソッド「example」をプロトコル「examples」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass class
                compile: 'example
    "AJ.MyFirstModel example."

    | aBuilder |
    aBuilder := self open.
    ^aBuilder'
                classified: #examples
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #example
Snapshot05104

モデル「AJ.MyFirstModel」のクラスメソッド「windowSpec」をプロトコル「interface specs」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass class
                compile: 'windowSpec
    "Tools.UIPainter new openOnClass: self andSelector: #windowSpec"

    <resource: #canvas>
    ^#(#{UI.FullSpec} 
        #window: 
        #(#{UI.WindowSpec} 
            #label: ''My First MVC'' 
            #min: #(#{Core.Point} 200 150 ) 
            #max: #(#{Core.Point} 800 600 ) 
            #bounds: #(#{Graphics.Rectangle} 520 300 920 600 ) 
            #flags: 4 
            #menu: #menuBar ) 
        #component: 
        #(#{UI.SpecCollection} 
            #collection: #(
                #(#{UI.ArbitraryComponentSpec} 
                    #layout: #(#{Graphics.LayoutFrame} 0 0 0 0 0 1 0 1 ) 
                    #name: #ViewHolder 
                    #flags: 11 
                    #component: #createView ) ) ) )'
                classified: #'interface specs'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #windowSpec
Snapshot05105

モデル「AJ.MyFirstModel」のクラスメソッド「menuBar」をプロトコル「resources」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass class
                compile: 'menuBar
    "Tools.MenuEditor new openOnClass: self andSelector: #menuBar"

    <resource: #menu>
    ^#(#{UI.Menu} #(
            #(#{UI.MenuItem} 
                #rawLabel: ''File'' 
                #submenu: #(#{UI.Menu} #(
                        #(#{UI.MenuItem} 
                            #rawLabel: ''Quit'' 
                            #value: #closeRequest ) ) #(1 ) nil ) ) 
            #(#{UI.MenuItem} 
                #rawLabel: ''Misc'' 
                #submenu: #(#{UI.Menu} #(
                        #(#{UI.MenuItem} 
                            #rawLabel: ''Inspect'' 
                            #value: #inspect ) ) #(1 ) nil ) ) ) #(2 ) nil ) decodeAsLiteralArray'
                classified: #resources
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #menuBar
Snapshot05106

モデル「AJ.MyFirstModel」のクラスメソッド「popUpMenu」をプロトコル「resources」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass class
                compile: 'popUpMenu
    "Tools.MenuEditor new openOnClass: self andSelector: #popUpMenu"

    <resource: #menu>
    ^#(#{UI.Menu} #(
            #(#{UI.MenuItem} 
                #rawLabel: ''do something ...'' 
                #value: #doSomething ) ) #(1 ) nil ) decodeAsLiteralArray'
                classified: #resources
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #popUpMenu
Snapshot05107

ビュー「AJ.MyFirstView」のインスタンスメソッド「preferredExtent」をプロトコル「bounds accessing」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstView}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'preferredExtent

    ^self model image extent'
                classified: #'bounds accessing'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #preferredExtent
Snapshot05201

ビュー「AJ.MyFirstView」のインスタンスメソッド「defaultControllerClass」をプロトコル「controller accessing」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstView}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'defaultControllerClass

    ^#{AJ.MyFirstController} value'
                classified: #'controller accessing'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #defaultControllerClass
Snapshot05202

ビュー「AJ.MyFirstView」のインスタンスメソッド「displayOn:」をプロトコル「displaying」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstView}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'displayOn: aGraphicsContext

    self model image displayOn: aGraphicsContext'
                classified: #displaying
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #displayOn:
Snapshot05203

コントローラ「AJ.MyFirstController」のインスタンスメソッド「redButtonPressedEvent:」をプロトコル「events」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstController}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'redButtonPressedEvent: event

    | aLocus aGraphicsContext aBlock |
    aLocus := OrderedCollection new.
    aGraphicsContext := (self view graphicsContext)
                capStyle: Graphics.GraphicsContext capRound;
                joinStyle: Graphics.GraphicsContext joinRound;
                paint: Graphics.ColorValue red;
                lineWidth: 3;
                yourself.
    aBlock :=
            [[self sensor redButtonPressed] whileTrue: 
                    [| aPoint |
                    aPoint := self sensor cursorPoint.
                    aLocus add: aPoint.
                    aGraphicsContext displayPolyline: aLocus.
                    Core.Processor yield.
                    0.025 seconds wait].
            self view displayOn: aGraphicsContext].
    UI.Cursor crossHair showWhile: aBlock'
                classified: #events
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #redButtonPressedEvent:
Snapshot05301

コントローラ「AJ.MyFirstController」のインスタンスメソッド「doSomething」をプロトコル「menu processing」に作成

    | aReference aClass aSelector |
    aReference := #{AJ.MyFirstController}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    aSelector := aClass
                compile: 'doSomething

    | aRectangle anImage aGraphicsContext |
    aRectangle := self view topComponent displayBox
                intersect: Graphics.Screen default bounds.
    anImage := Graphics.Screen default completeContentsOfArea: aRectangle.
    aGraphicsContext := self view graphicsContext.
    self sensor waitNoButton.
    UI.Cursor hand showWhile: 
            [UI.InputState default cursorPoint: aRectangle center.
            anImage
                follow: [self sensor cursorPoint - anImage bounds center]
                while: [self sensor noButtonPressed]
                on: aGraphicsContext.
            self view invalidateNow].
    self sensor waitNoButton'
                classified: #'menu processing'
                attributes: ((Dictionary new)
                        add: #package -> 'AJ-MyFirstMVC';
                        yourself).
    ^aSelector
    ==> #doSomething
Snapshot05302

テスト

    "AJ.MyFirstModel example."

    | aReference aClass |
    aReference := #{AJ.MyFirstModel}.
    aReference bindingOrNil ifNil: [^self error: 'fatal error'].
    aClass := aReference value.
    (aClass isKindOf: Kernel.Class) ifFalse: [^self error: 'fatal error'].
    ^aClass example
    ==> an UIBuilder
Test1MyFirstMVC
MyFirstMVC Test2MyFirstMVC
Test3MyFirstMVC Test4MyFirstMVC

保存

    | aBundle fileName |
    aBundle := Smalltalk.Store.Registry bundleNamed: 'AokiJuku'.
    aBundle ifNil: [^self error: 'fatal error'].
    fileName := Dialog
                requestNewFileName: #FileOutOnC << #packages >> 'File out on:'
                default: aBundle name , '-MyFirstMVC' , '.st'.
    fileName isEmpty ifTrue: [^nil].
    aBundle fileOutOnFileNamed: fileName.
    ^fileName
    ==> ~/Desktop/AokiJuku-MyFirstMVC.st
Save1MyFirstMVC Save2MyFirstMVC

Updated: 2019/08/26 (Created: 2018/11/11) KSU AokiHanko