Tcl/Tk 8.6の新機能

バージョン8.6の新機能を、面白そうな順に試してみたいと思います。これらの情報源としては、 Changes in Tcl/Tk 8.6と、 A Busy Developer's Guide to Tcl/Tk 8.6がとても参考になります。
またマニュアルはhttp://www.tcl.tk/man/tcl8.6/です。


zlibコマンド

Zlibは、 Jean-loup Gailly さんと Mark Adler さんによって開発された、 オープンソースのデータ圧縮・展開ライブラリです。 このライブラリを使った処理がTcl処理系の標準に取り込まれました。
zlibコマンドには、3系統の圧縮・展開コマンドの対があります。 gzipとgunzip、compressとdecompress(注:uncompressではありませんよ)、 そしてdeflateとinflate(注:deflateの方が圧縮です。inflateの方が展開です。 何となく語感上、deがつかない方が圧縮のように聞こえますが反対なので要注意!)です。

proc readBinaryFile filePath {
    set fin [open $filePath r]
    chan configure $fin -encoding binary -translation binary
    set buf [chan read $fin]
    chan close $fin
    return $buf
}

proc writeBinaryFile {filePath binData} {
    set fout [open $filePath w]
    chan configure $fout -encoding binary -translation binary
    chan puts -nonewline $fout $binData
    chan close $fout
}

# infileを読み、圧縮してcompressedに出力する。
set srcb [readBinaryFile "infile.txt"]
set data [zlib compress $srcb]
writeBinaryFile "compressed.dat" $data

# compressedを読み、展開してinfile2に出力する。
set srcb [readBinaryFile "compressed.dat"]
set data [zlib decompress $srcb]
writeBinaryFile "infile2.txt" $data

上はzlib compressとzlib decompressを使った例ですが、 他の対でも基本的な使い方はほとんど同じです。 これでTcl処理系から簡単にデータ圧縮の機能を使うことができるようになりました。
また、zlib gzipで圧縮したデータはgzipコマンドと互換性があります。
また上の例のようにファイルからバイナリモードで明示的に読んでいる場合はいいのですが、 Tcl文字列をそのままzlibコマンドに渡して圧縮することはできません。 encoding converttoコマンドでバイナリに変換してから圧縮し、 展開後もencoding convertfromコマンドで復元する必要があります。

set binData [encoding convertto utf-8 $string]
set compData [zlib compress $binData]
set binData [zlib decompress $compData]
set string [encoding convertfrom utf-8 $binData]

上ではエンコーディングをutf-8にしていますが、 要はバイナリ文字列に変換できれば良いので、cp932でもeuc-jpでも構いません。

また、zlib gunzipコマンドでは、gzipヘッダ部分を読み取って、dict変数に格納することができます。 (ここでは触れませんが、逆にzlib gzipコマンドの-headerオプションでgzipヘッダを埋め込むことも可能です)

proc readBinaryFile filePath {
    set fin [open $filePath r]
    chan configure $fin -encoding binary -translation binary
    set buf [chan read $fin]
    chan close $fin
    return $buf
}

proc writeBinaryFile {filePath binData} {
    set fout [open $filePath w]
    chan configure $fout -encoding binary -translation binary
    chan puts -nonewline $fout $binData
    chan close $fout
}

proc dictget {d key} {
    if {[dict exists $d $key]} {
        return [dict get $d $key]
    } else {
        return {}
    }
}

set srcb [readBinaryFile "zlib-gunzip-example.tar.gz"]
set data [zlib gunzip $srcb -headerVar h]
puts "commect:\t[dictget $h comment]"
puts "crc:\t[dictget $h crc]"
puts "filename:\t[dictget $h filename]"
puts "os:\t[dictget $h os]"
puts "size:\t[dictget $h size]"
puts "time:\t[clock format [dictget $h time]]"
puts "type:\t[dictget $h type]"
writeBinaryFile "zlib-gunzip-example.tar" $data

ただし、gzipヘッダーにcommentなどの各項目がすべて格納されているとは限りません。 格納されていない項目をdict getコマンドで取得しようとするとエラーになるので、 上の例では、dict existsで事前に確認してからdict getしています。


TDBC(Tcl DataBase Connectivity)

世界中の有志の皆さんによって、Tclには各種のRDBMS (リレーショナルデータベース管理システム)へ接続するための拡張ライブラリが提供されています。 一例をあげると、OracleにはOratcl、 MySQLにはmysqltcl、PostgreSQLにはlibpgtcl、 SQLiteにはtclsqliteなどなどです。 しかし残念ながら、それらの拡張間でコマンドや名前空間の名前などに統一した仕様がないため、 複数のRDBMS製品を扱うTcl開発者は、 各拡張のコマンド作法などをそれぞれマニュアルを見て覚える必要がありました。
同様の問題は他のプログラミング言語にもあって、 企業情報システムで利用されることをかなり早期から想定していたJavaは、 いち早くJDBCというAPI仕様をSun自らが整備しました。 Python、Perlも、それにならってAPI仕様が既に整備されました。 遅まきながら、Tclも同様の方針となったことは大変嬉しいことです。
それでTcl 8.6には、TDBCというパッケージと、 参照実装としていくつかのRDBMS向けの実装が提供されています。 ODBC、SQLite、MySQL、PostgreSQL、そしてOracleです。 ただし私がActiveTcl 8.6.0をインストールして試したところ、 「package require tdbc::sqlite3」で 「unknown method: -columns」というエラーになって駄目でした。
そこで、Windows環境では tdbc.tcl.tk で最新のソース配布を入手して、 MinGWなどでコンパイルする必要があります。 私はActiveTcl 8.6.0、MinGW 5.1.6を使って TDBCソース配布1.0b17をコンパイルし、 Sqlite3とODBCの動作確認ができたのでその手順をご紹介します。 MySQL、PostgreSQL、Oracleは試していません。

  1. MinGW Shell(=MSYS)を起動します。
  2. TDBCソース配布のtdbcディレクトリに行きます。 MinGWでは、ドライブを/c/のように記述します。
    cd /c/install/tcltk/tdbc/tdbc
  3. コンパイル・インストールします。 --with-tclで、ActiveTclのlibディレクトリの位置を指定するのがポイントです。 (ActiveTclのホームではありません)
    ./configure --with-tcl=/c/Wintools/tcl/lib
    make
    make install
    
  4. tdbcodbcディレクトリでも同様にします。 なお、tdbcのコンパイルをせず、tdbcodbcだけ行うのは無理です。
    ./configure --with-tcl=/c/Wintools/tcl/lib
    make
    make install
    

package require tdbc::sqlite3 1.0b17
tdbc::sqlite3::connection create d D:/usr/dbms/sqlite/testdb

set st [d prepare {
    SELECT SHITEN_CODE, SHITEN_NAME, ADDRESS, EMPLOYEE_NUM
      FROM SHITEN WHERE AREA_CODE = :area_code
}]
set area_code 3
$st foreach row {  puts [dict get $row SHITEN_NAME]  }
$st close
d close

SQLite3データベースに接続し、 SELECT文を発行するだけの単純なサンプルです。 他のTcl拡張や、JDBC等他の言語のDB接続APIを使ったことがあれば、 処理の流れやコマンド名はごくごく自然なので、すぐに慣れると思います。
一応、ODBCに接続する例です。

package require tdbc::odbc
tdbc::odbc::connection create d "DSN=ODBC_DS_MYDB;UID=testuser;PWD=testpass"
# 以下同様なので省略


なお、 http://www.tcl.tk/man/tcl8.6/TdbcCmd/contents.htm に、tdbcパッケージのコマンドのマニュアルがあります。


オブジェクト指向

えっ?オブジェクト指向が「面白そうな順」でTDBCより下なのはおかしいだろうって。 そんなことありませんばい!(どこの人じゃ)
過去長きに渡って議論されては見送られてきたオブジェクト指向 (OO:Object Oriented) プログラミングの機構が、8.6でやっとTcl世界にも訪れました。 従来代表的なOO拡張であった[incr Tcl]とはかなり文法が違いますが、 Java、Python、PHP5などに慣れていれば、 大体の感じはすぐにつかめるように工夫されていると思います。 例として、メソッドを呼ぶたびに回数を記録し、 その回数を返すことができる「アクセスカウンタ」風のクラスを作ってみます。

oo::class create Counter {
    constructor {} {
        my variable count
        set count 0
    }
    method visit {} {
        my variable count
        incr count
    }
    method getCount {} {
        my variable count
        return $count
    }
}
set c [Counter new]
$c visit
$c visit
$c visit
puts "あなたは [$c getCount] 番目のお客様です。"

クラスはoo::class createコマンドで定義し、 その引数として波括弧でクラス定義全体を大きく囲みます。
コンストラクタはconstructorで定義します。 同じようにデストラクタは destructorで定義します。 この例では引数がありませんが、引数を持つコンストラクタも作れます。
インスタンスメソッドは methodコマンドで定義します。 それらの中で、インスタンス変数を参照することを my variableで定義します。 インスタンス化は [クラス名 new]で行います。
それで上の例で気になるのは、 各メソッド内でインスタンス変数の参照を逐一 my variableで定義しないといけないのは面倒そう、 というところですね。これは、改善可能です。 その点も含めて、少し拡張した例を次に示します。

oo::class create Counter {
    variable count
    constructor {} {
        set count 0
    }
    method visit {} {
        incr count
    }
    method _getCount {} {
        return $count
    }
    method getMessage {} {
        return "あなたは [my _getCount] 番目のお客様です。"
    }
}

set c [Counter new]
$c visit
$c visit
$c visit
puts [$c getMessage]

上のように、クラス定義の中でメソッドの外の位置で variable コマンドを使えば、全てのインスタンスメソッド内で、 その変数がインスタンス変数として扱われます。
そして上の例ではもうひとつ、メソッドのpublic/privateの切り替え方法を示しています。 デフォルトでは、 インスタンスメソッドの名前が英小文字で始まる場合はpublic、 それ以外ならprivateとして扱われます。 つまり上の例では、_getCountはprivateなので、外部から[$c _getCount] として呼ぶことはできずエラーになります。 また oo::defineのexport、unexport コマンドを使うことで、個別のインスタンスメソッドに対して、 public/privateを明示的に指定することもできます。

oo::class create Counter {
    variable count
    constructor {} {
        set count 0
    }
    method visit {} {
        incr count
    }
    method _getCount {} {
        return $count
    }
    method getMessage {} {
        return "あなたは [my _getCount] 番目のお客様です。"
    }
}

oo::class create ExCounter {
    superclass Counter
    method visit {} {
        puts "ようこそいらっしゃいました。"
        next
    }
}

set c [ExCounter new]
$c visit
$c visit
$c visit
puts [$c getMessage]

最後の例は派生クラス(サブクラス)の定義方法です。 superclassコマンドによって、 あるクラスの継承元を指定すれば派生クラスになります。 派生クラスではメソッドのオーバーライドが可能です。 オーバーライドメソッドから継承元の同名のメソッドを呼び出す、 つまりJavaのsuper()のようなことをしたいときは、 Tclに新しく追加された next コマンドを使います。 nextに引数を渡すことも可能です。


PNG画像フォーマットのサポート

「image create photo」コマンドなどでロードできる画像フォーマットとして、 従来のTcl/Tkは標準でPPM/PGMとGIF形式のみをサポートしており、 その他の画像フォーマットはImg拡張などの拡張を使う必要がありました。
今般、バージョン8.6でzlibのサポートと合わせてか、 zlibと縁の深いPNG(Portable Network Graphics) 形式が使えるようになりました。 使い方はGIF等の場合と全く同じです。

image create photo Image1 -file ./data/photo/image1.png
label .la -image Image1
pack .la


lmapコマンド

lmapというコマンドが追加されました。 リストに対するフィルターのような処理を、foreachループよりも少し簡潔に書けます。
以下の例は、$dirfile ディレクトリにあるファイルの内、 絶対パスが "処理済.txt" に書かれていないファイルだけを選び、 ファイル名順にソートして配列に取得する処理です。
なお::fileutil::catはStandard Tcl Library(tcllib) に含まれるコマンドで、ファイルの内容を丸ごと取得するコマンドです。

set files [glob -nocomplain "$dirname/*.*"]
set denfiles [split [fileutil::cat "./処理済.txt"] "\n"]
set files [lsort -ascii \
  [lmap a $files {expr {([lsearch $denfiles $a] >= 0) ? [continue] : $a}}] \
]

やねうらの top
(first uploaded 2011/07/08 last updated 2021/11/14, MISUMI URANO)