Snit

Snitは、Snit's Not Incr Tclの略です。(GNUがGNU's Not UNIXの略であることは有名です)
Snitは、[incr Tcl]等のTclのオブジェクト指向拡張とは一線を画するオブジェクト・フレームワークです。
Snitにより定義されたオブジェクト・タイプはサブコマンドを持つTclコマンドの様に見えます。
複雑な[incr Tcl]等に比べて非常にシンプルで、Tclに馴染み易いと言えるでしょう。

Snitの特徴は以下の通りです。

それでは、Snitの使い方を説明します。

タイプとインスタンス

以下はHelloという名前のタイプに変数varとメソッドprintが定義されています。
変数varとメソッドprintはglobalスコープや他のタイプで定義されたそれとは競合しません。

package require snit

::snit::type Hello {
    variable var {Hello World}

    method print {} {
        puts $var
    }
}

インスタンスの生成は以下の様にします。

# インスタンスの作成(その1)
Hello create foo
=> ::foo
# インスタンスの作成(その2)
Hello foo
=> ::foo
# インスタンスの作成(その3)
Hello foo%AUTO%
=> ::fooHello1

インスタンスとタイプの破棄は以下の様にします。

# インスタンスの生成
Hello foo
# インスタンスの破棄
foo destroy
# タイプとインスタンスの破棄
Hello destroy

変数とメソッド

タイプの変数は、その変数を参照するメソッドを作る以外にアクセスできません。
ただし、以下のようにnamespaceを使って強引にアクセスをすることはできます。

package require snit

::snit::type Hello {
    variable var {Hello World}

    method print {} {
        puts $var
    }
}
# インスタンスの作成
Hello foo

# 強引な変数のアクセス
puts $::Hello::Snit_inst1::var
=> Hello World

一方、メソッドはサブコマンドのようにアクセスします。

# インスタンスの作成
Hello foo

# メソッドのアクセス
foo print
=> Hello World

privateなメソッド

methodで定義されたメソッドはpublicなメソッドになります。
privateなメソッドを定義するには、procを使います。

package require snit

::snit::type Hello {
    variable var {Hello World}

    # privateなメソッド
    proc doprint {selfns} {
        variable var
        puts $var
    }
    # publicなメソッド
    method print {} {
        doprint $selfns
    }
}
# インスタンスの作成
Hello foo
# メソッドのアクセス
foo doprint
'::foo doprint' is not defined.
# 強引なメソッドのアクセス
::Hello::doprint ::Hello::Snit_inst1
=> Hello World
# メソッドのアクセス
foo print
=> Hello World

メソッドからメソッドを呼び出すには、呼び出すメソッドの第1引数にnamespace名を渡す要があります。
※ selfns変数はnamaspace名になります。この場合、$selfnsの内容は::Hello::Snit_inst1になります。

staticなメンバとstaticなメソッド

staticなメソッドは、タイプのインスタンスのstaticなメンバにしかアクセスできません。
staticなメンバを定義するには、typevariableを使います。
staticなメンバはタイプのすべてのインスタンスで共有されます。

staticなメソッドを定義するには、typemethodを使います。
staticなメソッドの呼び出しにインスタンス名は必要としません。
staticなメソッドは、タイプのサブコマンドとして呼び出します。

package require snit

::snit::type Count {
    # staticなメンバ
    typevariable count 0
    
    # staticなメソッド
    typemethod print {} {
        puts "$type = $count"
    }
    # publicなメソッド
    method incr {} {
        incr count
    }
}
# インスタンスの作成
Count foo1
Count foo2
# publicなメンバの呼び出し
foo1 incr
=> 1
foo2 incr
=> 2
# staticなメンバの呼び出し
Count print
=> ::Count = 2

※ type変数はタイプ名になります。この場合、$typeの内容は::Countになります。

コンストラクタとデストラクタ

コンストラクタはインスタンス生成時に、デストラクタはインスタンス破棄時に呼び出される特殊なメソッドです。
デフォルト・コンストラクタは引数を持ちませんが、この後に説明するオプション処理のためにargsで受けます。

package require snit

snit::type Hello {
    # デフォルト・コンストラクタ
    constructor {args} {
        $self configurelist $args
        puts start
    }
    # デストラクタ
    destructor {
        puts end
    }
}
# インスタンスの生成
Hello foo
=> start
=> ::foo
# インスタンスの破棄
foo destroy
=> end

引数を持つコンストラクタも定義できます。

package require snit

snit::type Hello {
    variable data
    constructor {breed color} {
        set data(breed) $breed
        set data(color) $color
    }
}
# インスタンスの生成
Hello foo labrador chocolate
=> ::foo

オプション

Snitのタイプにはオプションの概念があります。
以下の例は-langオプションで返却するメッセージを切り替えます。

package require snit

::snit::type Hello {
    variable var_english {Hello!}
    variable var_japanese {こんにちは!}
    option -lang english
	
    method print {} {
        if {$options(-lang) == "english"} {
            return $var_english
        } else {
            return $var_japanese
        }
    }
    onconfigure -lang {lang} {
        if {$lang != "english"} {
            set options(-lang) japanese
        } else {
            set options(-lang) english
        }
    }
}

# インスタンスの生成
Hello foo1
foo1 print
=> Hello!

# インスタンスの生成(オプション付き)
Hello foo2 -lang japanese
foo2 print
=> こんにちは!

options配列変数は、オプション用のメンバです。オプションの状態を保持します。
onconfigureはオプション設定時に呼び出されるオプションハンドラを定義します。
oncgetというオプション参照時に呼び出されるオプションハンドラも定義できます。
どちらのオプションハンドラも省略可能です。

スコープ

Tclは、イベントハンドラを使ってコマンドを実行することがあります。
afterコマンド、bindコマンド、button -commandコマンド等が該当します。
イベントハンドラはglobalスコープで実行される点に注意してください。
たとえば、以下の例は、afterで1秒後にコマンドが実行されますが、
コマンドが実行されるのはglobalスコープなので、変数varは未定義エラーになります。

package require snit

::snit::type Hello {
    variable var {Hello World}

    method print {} {
        after 1000 {puts $var}
    }
}
Hello foo
foo print

解決策としては、codenameコマンドでnamespaceを事前に解決したコードを生成する方法です。

package require snit

::snit::type Hello {
    variable var {Hello World}

    method print {} {
    	set cmd [list [codename doputs] $selfns]
	after 1000 $cmd
    }
    # private method
    proc doputs {selfns} {
	puts $var
    }
}
Hello foo
foo print

この時、事前に解決したコード($cmd変数の内容)は以下の様になります。
codenameコマンドは、指定されたnamespaceで引数のコマンドを実行します。

::Hello::doprint ::Hello::Snit_inst1

もし、上記doputsメソッドがpublicなメソッドの場合は、以下のように記述します。

package require snit

::snit::type Hello {
    variable var {Hello World}

    method print {} {
    	set cmd [list $self doputs]
	after 1000 $cmd
    }
    # public method
    method doputs {} {
	puts $var
    }
}
Hello foo
foo print

この時、事前に解決したコード($cmd変数の内容)は以下の様になります。
self変数は、指定されたnamespaceで引数のコマンドを実行します。

::foo doputs

MegaWidget

Snitを使うと、TkのWidget等を使って、新しいMegaWidgetを作成できます。
MegaWidgetを作るには、::snit::typeの代わりに::snit::widgetadaptorを使います。
以下の例は、Tkのchechbuttonにisonコマンドと-isonオプションを追加しています。

package require snit

::snit::widgetadaptor pushbutton {
    # オプションの追加
    option -ison 0
    # コンストラクタ
    constructor {args} {
        installhull [checkbutton $win -indicatoron no \
            -padx 5 -pady 5 -onvalue 1 -offvalue 0 \
            -variable [list $selfns]::options(-ison)]
        $self configurelist $args
    }
    # メソッドの追加
    method ison {} {
        return $options(-ison)
    }
    # メソッドとオプションの委任
    delegate method * to hull
    delegate option * to hull
}

pack [pushbutton .pb -text Push -command {
    tk_messageBox -message [list [.pb cget -ison] [.pb ison]]
}]

押すと引っ込んだままになるpushbuttonボタンができました。
ボタンの状態をisonコマンドまたは-isonオプションで確認できます。

OffOn

まとめ

Snitは、クラスの派生(継承)を使う代わりにメソッドおよびオプションの委任という
ユニークな手法を使う。結果的にクラスの派生と同じ効果を得ることができる。
従来のオブジェクト指向プログラミングに比べて非常にシンプルであると言えるでしょう。
ただし、Snitはまだバージョンが0.8と若いため、突然の仕様変更に留意したい。

参考文献


Snit