Tcl/Tk 8.5の新機能 (themed Tk=ttkは除く)

バージョン8.5の新機能のうち、比較的早くから登場し、面白そうなものをご紹介します。
この他にも細かい機能拡張がたくさんあります。
また、8.5で追加されたthemed Tk=ttkは、特設ページでの別途ご紹介となっております。

なお、このページはまだ書きかけであります。
8.5の新機能で書ききれていない大きなものとしては、 Teapotパッケージ機構が残っています。

lassignコマンド
lrepeatコマンド
lsearchコマンド
dictコマンド
applyコマンド
chanコマンド


exprコマンドの拡張

exprコマンドに、累乗の「**」演算子、カッコ内の引数のうち最小・最大の値を返す「min」「max」演算子、 リストの中に指定した要素が含まれるかどうかを返す「in」 及びその否定の「ni」演算子が追加されました。
**は「2.0 * pow($x, 3)」が「2.0 * ($x ** 3)」とも書けるようになったというだけですし、 業務処理では累乗を活用することはほとんどないので、 私としてもふーんという程度ですが、in演算子の方はそうはいきますまい。 私が普段なじんでいるOracle PL/SQLにもある演算子なので、大変助かりまんがな。 同じことはlsearchコマンドを使っても書けますが、それよりとても短く書けます。

set x {apple melon orange grape}

puts [expr {"apple" in $x}]
puts [expr {"apple" ni $x}]
puts [expr {"kiwi" in $x}]
puts [expr {"kiwi" ni $x}]

評価結果は1か0で返ります。上の例を実行すると、1,0,0,1の4行が表示されます。 よくある例はこんな使い方でしょうか。

if {"apple" in $x} { puts 1 } else { puts 0 }

「ni」は「in」の逆の処理をします。つまり、リストに指定した要素が存在しなければ1、 そうでなければ(つまり存在すれば)0と評価されます。 Oracle PL/SQLではINの逆をする演算子は「NOT IN」です。 普通そういう名前をつけかたをするもんだと思うのですが。 「ni」なんてヤバくない?むしろ有り得なくない?有り得なくなくなくなくなくなくなくない?(黙れ) と思うのですが、「eq」と「ne」のアナロゲンから、「ni」に落ち着いたのだそうです。 そう言われてみるとふがふふ。


{*}文法

変数の値にリストをセットしたとき、これを「$変数名」の形でコマンド中に置くと、 それはリストの形はしていますが、コマンドから見るとあくまで単一の引数として扱われます。 しかし、"{*}"を引数の、つまり「$変数名」の前につけると、 変数展開の後、展開されたリストをコマンドの中に個々の引数として展開します。 次の例で説明します。

set scoords {10 10}
set ecoords {60 40}
canvas .can -bg white
.can create rectangle $scoords $ecoords -fill "#800040" -outline white
pack .can

上の例は長方形の左上と右下の座標を2つの引数として与えようとしていますが、 これはエラーになります。canvas create rectangleさぶさぶコマンドは、 座標の与え方として、「4つの引数」か、または「4つの要素をもつ1つのリスト」 のどちらかしか許可しないからです。ところが、

set scoords {10 10}
set ecoords {60 40}
canvas .can -bg white
.can create rectangle {*}$scoords {*}$ecoords -fill "#800040" -outline white
pack .can

とすると、$scoords、$ecoordsの展開結果、つまり各々2要素のリストが、 さらにコマンドライン中に展開されて、

.can create rectangle 10 10 60 40 -fill "#800040" -outline white

と解釈されるので思った通りの処理になります。ちなみに、

set opts {
    -fill "#800040" -outline white
}
canvas .can -bg white
.can create rectangle 10 10 60 40 {*}$opts
pack .can

こんな使い方もできますとも。汎用的な図形描画アプリケーションに使えそうです。 むろん、{*}は普通のTclコマンドにも使えます。 例えばformatコマンドで次のように引数へ展開することができます。

set colorRGB [Image1 get $x $y]
set colorSPEC [format "#%02x%02x%02x" {*}$colorRGB]

なお、{*}は、8.5がαリリースだったときには{expand}というトークンでして、 このページにも長くそのように書いていましたが、 正式リリースに伴って{*}と変更されました。


tclsh、sourceコマンドの拡張

そして私ども非英語圏の人物どもにとって待望の機能の登場であります。 なんとtclshアプリケーションと、sourceコマンドに、-encodingオプションが追加されました。 「-encoding euc-jp」「-encoding cp932」などと、 読む対象のTclスクリプトがそのエンコーディングで書かれているところのエンコーディングを指定すると、 そのエンコーディングで読んでくれます。 (←文章が微妙に日本語になっていない人)
これは私のように、OSのエンコーディングが日本語EUC、 RDBMSのエンコーディングがシフトJIS、というUNIXサーバを使っている人には大変な朗報かもしれません。 なんでUNIXなのにRDBMSのエンコーディングがシフトJISなんだ? といいますと、ひとえに基幹業務システムにおける半角カナの存在のせいです。 日本語EUCは半角カナの扱いがたいそう不得手なので、シフトJISを使うことが多いです。 (最近の製品だとUTF-8をサポートしたものが多いので、いずれ変わっていくでしょうけども)

#! /bin/sh
# ... \
exec tclsh -encoding cp932 "$0" "$@"
puts "ハローワールド"

そしてこのオプションによる重要な変化は、 (UNIX環境では上のように) スクリプトの書き手がそのスクリプトのエンコーディングを「宣言」することができるようになることで、 これによってプラットフォームによってエンコーディングを変換する手間がなくなり、 可搬性が向上することになります。


lassignコマンド

1つのリストの各要素を、別々の変数に一度に代入するにはどうしますか?従来のTclでは、

set msg "Hello World"
foreach {var1 var2} $msg {}

というジャンキーな方法があるのですが、世の中にもっと秩序を与えるため、 同じ処理をするlassignというコマンドが追加されました。

set msg "Hello World"
lassign $msg var1 var2

なお、リストの要素の数が変数の数より少ない場合には、 余った変数には空文字列がセットされ、エラーにはなりません。


lrepeatコマンド

そして、「assignがあるのにrepeatがないのはおかしい」という電話が殺到したためか、 (なんでやねん)lrepeatというコマンドも追加されました。 一度に大量の要素を持つリストを初期化することができます。

set v [lrepeat 5 a]
puts $v
set v [string repeat a 5]
puts $v
set v [lrepeat 5 {a b c}]
puts $v

上の例を実行すると

a a a a a
aaaaa
{a b c} {a b c} {a b c} {a b c} {a b c}

と出力します。lrepeatとstring repeatサブコマンドは別物ですよ。


checkbuttonとradiobuttonに第3の状態が?

labelframe .lafa -text おやつ
radiobutton .lafa.r1 -variable r -value 1 -text パン
radiobutton .lafa.r2 -variable r -value 2 -text みかん
radiobutton .lafa.r3 -variable r -value 3 -text 梅酒
checkbutton .lafa.c1 -variable c1 -onvalue 1 -offvalue 2 -tristatevalue 0 \
 -text バナナはおやつに含まない
pack {*}[winfo children .lafa] -side left
set c1  0
set r   ""
pack .lafa

さっそく覚えた{*}を上のように極道な使い方をして喜んでみるわけですが。(帰れ) それはおいといて、 checkbuttonとradiobuttonは、今まで「チェックされている」「チェックされていない」 の2つの状態しかなかったのですが、 この度、そのどちらでもない3番目の状態が追加されました。 といってもシュレーディンガーのなんとかのような状態ではなく、 「初期状態で、まだユーザの選択を受けていない」ことを表すものです。 これは、ボックスの背景色が灰色で、チェックはされている状態で表されます。 新しいオプション-tristatevalueが追加されており、 -onvalue、-offvalueと同じようなノリで-tristatevalueに値を指定しておくと、 変数がその値を持つときにこの状態になります。 radiobuttonの場合、変数に値がセットされていなかったり、 どのラジオボタンの-valueの値とも一致しない値がセットされていたりした場合に、この状態になります。


lsearchコマンド

バージョン8.4、8.5と、リスト関係のコマンドに隠れオプション(別に隠されてないけど) みたいなのが増えてきましたね。 8.5でlsearchコマンドに追加されたのは、「-index v」「-subindices」の2つのオプションです。

set banks {
  {100 札幌 1 50}
  {050 仙台 2 120}
  {120 千葉 3 100}
  {070 静岡 4 75}
}

# 【1】
puts [lsearch -index 1 $banks "仙台"]
puts [lsearch -index 2 $banks "4"]
# 【2】
puts [lsearch -index 1 -subindices $banks "仙台"]
puts [lsearch -index 2 -subindices $banks "4"]

# 【3】
puts [lsearch -index 1 -inline $banks "仙台"]
puts [lsearch -index 2 -inline -subindices $banks "4"]

-indexオプションは、対象のリストがリストからなるリストや、 リストからなるリストからなるリストや、 リストからなるリストからなるリストからなるリスト(以下略) の場合に、その何番目の要素を対象に検索を行うかを指定するものです。
【1】に例が2つありますが、-index 1とすると、 各リストのindex=1の要素、つまり全部日本語で書くとするならば2番目の要素 「札幌 仙台 千葉 静岡」から「仙台」を探すと先頭から2番目にヒットするので 1と表示されます。
同様に-index 2とすると、3番目の要素 「1 2 3 4」 から「4」を探すと4番目にヒットするので、 3と表示されます。
なお、上の例は対象がリストからなるリストだったので-indexの値は1次元ですが、 リストからなるリストからなるリストの場合は -index {1 2}のように2次元で指定することになります。
-subindicesオプションは-indexオプションと同時にしか指定できません。 なくても困らないコンビニオプションで、 「ヒットした要素のインデックス」と-indexで指定したインデックスを連結したリストを返します。 つまり【2】の1つ目の例は 「1 1」、2つめの例は 「3 2」となります。
そして、-indexと-inlineを両方使うとさらに便利。 この場合は、中の要素がヒットしたところのリスト全体が返されます。 【3】の1つ目の例は「050 仙台 2 120」と表示されます。 -inlineと-subindicesが両方指定されると?答えは-inlineが優先です。 【3】の2つ目の例は「070 静岡 4 75」となります。


lsortコマンド

それでlsortには8.4で既に-indexオプションが実装済ですが、さらに-indicesオプションが追加されました。

set a {A8 A10 A13 A101 A104}

puts [lsort -ascii $a]
puts [lsort -dictionary $a]

を実行すると、結果は以下のようになりますが (-asciiと-dictionaryの違いはTclマニュアルに書かれていますね)、

A10 A101 A104 A13 A8
A8 A10 A13 A101 A104

set a {A8 A10 A13 A101 A104}

puts [lsort -ascii -indices $a]
puts [lsort -dictionary -indices $a]

とするとこの後どーうなってしまうのか?

1 3 4 2 0
0 1 2 3 4

これすなわち、「『ソートされた結果の各要素が元々居たインデックス』のリストになって返ってくる」 ということですね。(わけわからん) つまり、上の「1 3 4 2 0」という並びを見て、

puts [lindex $a 1]
puts [lindex $a 3]
puts [lindex $a 4]
puts [lindex $a 2]
puts [lindex $a 0]

とすれば、A10、A101、A104、A13、A8と、-ascii順にソートされていることが分かります。


dictコマンド

8.5で新しく登場したdictコマンドは、Dictionary(辞書) というデータ構造を新たに設ける物です。
Tclにはarrayという連想配列(ハッシュテーブル)が既にあって、 連想配列、ハッシュテーブルに対して辞書というのはプログラミング世界ではある意味同義語ですから、 arrayと何が違うの? というのが第一印象ですよね。ずばり言うと、色々強化されていて、 今後はおおむねarrayの代用として使えるもの、ということでしょうか。
それでarrayよりも優れている2つの点のうち1つめは、 プロシージャに引数として渡したり、プロシージャから値として返したりできる点。 arrayでは変数「名」を渡してプロシージャ内でupvarコマンドで参照するという面倒さがあったのですが、 こうした面倒な記述から解放されます。
もう1点は、dictは入れ子にできる、 つまりキーを階層的に定義できるということです。 ん?arrayでも、"$score(scott,science,3)"のように複数のキーを定義できるじゃないかって。 そうなんですけど、でもarrayだと、それは単にカンマで連結して 1つの文字列にしているだけなので、 「2階層目のキーがscienceだ」とTcl処理系は理解しているわけではないですね。 dictでは、それができるということなんです。
ではまず、今までのarrayのような連想配列(ハッシュテーブル) として使ってみましょう。

# dict変数を定義し、値を設定する
dict set fruits "りんご" 240
dict set fruits "みかん" 150
dict set fruits "ぶどう" 640
dict set fruits "メロン" 3200

# あるキーに対応する値を出力する。キーがないとエラーになります
puts [dict get $fruits "ぶどう"]

# キーが存在するかどうかを確認する
if {[dict exists $fruits "バナナ"]} {
    puts "バナナはおやつに含まれる。"
} else {
    puts "バナナはおやつに含まれない。"
}

# dict変数を定義し、値を設定するもう1つの方法
set fruits2 [dict create "なし" 300 "びわ" 350 ]
puts [dict get $fruits2 "びわ"]

# キーと値の対に対してループで処理する
dict for {k v} $fruits {
    puts [format "%-8s ... %5d" $k $v]
}

# キーの数を取得する
puts "[dict size $fruits] 個のデータがあります。"

辞書変数を新たに作るには、上にあるように dict set か dict createコマンドを使います。 あるキーの要素が存在するか確かめるには dict exists、 あるキーの値を得るには dict get、 キーの数を知るには dict size と言うコマンドがあり、他にも沢山のコマンドがあります。 Tclマニュアルで確認できます。 ここまでは、arrayでもできたことでした。
では次の例です。

# dict変数を定義し、値を設定する
dict set shiten 00150 name "横浜"
dict set shiten 00150 area "関東"
dict set shiten 00150 address "横浜市西区南幸9-99-99"
dict set shiten 00150 employeeNum 40

dict set shiten 00320 name "新潟"
dict set shiten 00320 area "中部"
dict set shiten 00320 address "新潟市中央区柳島町9-99-99"
dict set shiten 00320 employeeNum 30

dict set shiten 00500 name "大阪"
dict set shiten 00500 area "近畿"
dict set shiten 00500 address "大阪市淀川区西中島9-99-99"
dict set shiten 00500 employeeNum 25

dict for {k data} $shiten {
    puts "Record #[incr i]: $k ... [dict get $data name]"

    # dataに$はつかない。大注意!
    dict with data {
        puts "  名称:     $name"
        puts "  地域:     $area"
        puts "  所在地:   $address"
        puts "  従業員数: $employeeNum"
    }
}

# 別の方法
foreach k [dict keys $shiten] {
    puts "Record #[incr j]: $k ... [dict get $shiten $k name]"
}

先の例と何が違うですか。shitenは辞書の変数名ですが、 その後ろに「00500 name "大阪"」「00500 area "近畿"」のようになっている部分、 「00500 name」の2つがキーで「大阪」が値なんです。(ちょっと違うけど分かりやすい表現) いやいや、正確には「00500」のキーの値もまた、 name, area, address, employeeNumの4つのキーを持つ辞書なんです。 (正しいけど分かりにくい表現) これが単純な連想配列に比べて良いところは、 縦横2次元のリレーショナルデータベース(RDB)のようなデータ構造を表現でき、 かつ行だけを抜く、列だけを抜く、といった操作もdictの各サブコマンドを使えば可能だということです。 1階層目の各要素、つまり各行をループしながら処理するには、 上の例のように dict for、 dict keys などのコマンドが使えます。(dict valuesもあります) また2階層目の各要素、つまり各列に対する処理は、 dict withという、 Visual BasicやJavaScriptのwith構文みたいなコマンドが便利に使えます。


applyコマンド

applyコマンドは、Tclに「無名関数」の仕組みをもたらす物です。
無名関数は、またの名をラムダ式(lambda expressions)とも言いますね。
applyコマンドは、後ろに指定した無名関数と引数で演算を行い、結果を返します。

set result [
  apply {s {return [expr ($s >= 50 ? 100 : ($s * 2))]}} 43
]
puts $result

上の例は、引数に50以上の数を渡すと100を、 50未満の値を渡すとその2倍の数を返す無名関数に、 パラメータを渡して計算しています。 applyの1番目の引数は無名関数の定義で、 それは2つのリスト、つまりパラメータのリストと関数の実体からなります。 上の例ではパラメータのリストとは「s」ですが、 複数のパラメータを持つ無名関数も定義できます。 関数の実体とは「return [expr ($s >= 50 ? 100 : ($s * 2))]」 の部分です。 その後ろの部分、上の例では「43」の部分がパラメータに対応する引数です。 複数のパラメータを持つ無名関数を渡した場合、 当然ながらその個数分の引数を渡す必要があります。
混同してはいけないのですが、applyはあくまで単一回の関数の適用です。 Perlのmap関数のように、配列の各要素に対して順番に適用するという機能はありません。 それは今のところ自分で書かねばなりません。 いや自分でというか、 Tclのapplyコマンドのマニュアルに、 applyコマンドを使ってmapのようなプロシージャを作る例があるので、 ものぐさボーイはそれを使えばよいとは思います。


chanコマンド

Tclには今までファイル入出力関係の沢山のコマンドがありました。 今般、散在を防ぐためなのか、 そのような設計上の議論があったかどうかは分からないのですが、 ファイル入出力関係のコマンドの多くが chanという1つのコマンドに集約されました。
例えば、今までの「eof」コマンドは「chan eof」、 「close」コマンドは「chan close」というように。 ただし、「open」だけはその利用頻度からか、集約の網をまぬがれたようです。
対象となったコマンドは、

fblocked、close、fconfigure、fcopy、eof、fileevent、
flush、gets、puts、read、seek、tell。
そして奥さん大変大変、それらのうち fblocked、fconfigure、fcopy、fileeventについては、 DEPRECATED(旧式扱い)にマークされ、より新しいバージョンでは廃止となる可能性があります。 今後はchanに集約されたコマンドは、chanを使うように書き換えていく必要があります。

やねうらの top
(first uploaded 2004/12/16 last updated 2011/07/08, MISUMI URANO)