Tcl/Tk

概要

Tcl/Tk は、スクリプト言語 Tcl とGUIツールキット Tk からなる 非常に強力なGUIスクリプティング環境である。現在、各種オペレーティングシステム（UNIX、Windows、Macintosh）上で動作する。
Tcl 言語は、コマンド行のみで構造化文法をフォローしてしまう非常にシンプルな文法を特徴とする。Tk はクロスプラットフォームな GUI 環境としても有名で、Tcl 言語に限らず、Perl、Python、Ruby などの言語環境からも利用できる。
他にも ウェブブラウザ上で Tcl/Tk を動作させるプラグイン Tclet（ティクレット） がある。

背景

Tcl がカリフォルニア大学バークレー校のジョン・ケネス・オースターハウト博士により最初に開発されたのは1988年の事である。当時アプリケーションプログラムに組み込まれる拡張用スクリプト言語には標準がなく、アプリケーション毎に独自の言語が実装されていた。そのためアプリケーション使用者はツール毎に異なるスクリプト言語の習得を余儀なくされた。この非効率さを嘆いたオースターハウトは、状況を打開するために UNIX アプリケーションにおける標準となる拡張スクリプト言語をデザインしようと考えた。こうして作られたのが Tcl の始まりである。そのため Tcl はアプリケーションへの組み込みが容易であることを重視してデザインされた。具体的には処理系をライブラリとして提供することでC言語で書かれたアプリケーションに容易に組み込めることや、言語構造が簡素であり、かつ高い拡張性を持つこと、インタプリター言語であることが挙げられる。
Tk は Tcl 用に開発された、非常に簡単なコードで GUI を作成できるツールキットである。1990年代初頭に Tcl にバンドルされる形で公開された。アップルの HyperCard に触発されて開発されたと言う。当初 Tcl の有用な活用事例の一つとして紹介された Tk だが、その取り扱いやすさから Tcl 言語と共に一躍人気に火がつく。Tcl は当初の設計意図と異なり、アプリケーションの組み込み言語として使われるよりも、Tk と合わせた「Tcl/Tk」の形の GUI スクリプティング環境として人気を博した。特に Tk の人気は高く、Tcl にとどまらず Perl（Perl/Tk）、Python （Tkinter）、Ruby （Ruby/Tk）など、他の言語でも標準的な GUI キットとして Tk が利用された。
オースターハウトがサン・マイクロシステムズに勤めていた1994-1998年は Tcl/Tk は同社で開発が進められた。このころのサンは WWW クライアント環境の制覇に向け邁進していた時期であり、Tcl/Tk もその流れの上、その対象領域をウェブに広げていく。ウェブブラウザ上で Tk GUI を動作させるプラグイン「Tclet（ティクレット）」や、ブラウザのスクリプト言語としての Tcl の組み込み、国際化対応（内部処理の UNICODE 化）、インタプリタからバイトコンパイラへの変更による実行速度の大幅な向上など、Tcl/Tk はこの時期機能的にもっとも大きな進歩を果たした。しかし他技術との競合やブラウザでのサポートの薄さなどもあり、WWWを「第二の Tk」として人気を拡大することは出来なかった。
オースターハウトの退職に伴い、Tcl/Tk の開発はサンの手を離れた。2000年からは Tcl/Tk の開発はオープンソースにその場を移し、精力的に開発が続けられている。
2005年現在 Tcl 言語は「Tcl/Tk」の知名度とは裏腹に利用者数は少なく、Perl や Python、Ruby に比べ劣勢と言わざるを得ない。特に日本国内での利用者数は少ない。ただし、EDAツールにおいては標準的なスクリプト言語として広く利用されているほか、電子国土Webシステムにおいても一部で Tcl 言語が使われている。一方 Tk は、後発の GTK+、Qt と並び、軽量プログラミング言語における事実上の標準 GUI ツールキットの一つとなっており、広く利用されている。
なお、Tcl 言語の名前は「ツールコマンド言語」を意味する英語「tool command language」に由来し、Tk の名前は「ツールキット」を意味する英語「toolkit」に由来する。

リスト構造

リストは文字列である。リストを構成する要素はブランクかタブで区切られる。ブランクやタブを要素に含めたい場合には、その要素をブレス（{、}）で挟めば良い。以下の文字列は三つの要素から成るリストである。
 This is {a pen}

リストの抽出

Tcl パーサーがソースコードからコマンド行としてリストを取り出すとき、ひとつのリストの終端は改行コードかセミコロン（;）で判断する。しかし改行コードやセミコロンがブレスの内側にあれば、それをリストの終端記号とは見なさない。したがって、以下の3行の文字列は3つの要素から成るひとつのリストである。改行コードは3個あるが、最後の改行コードだけがリストの終端記号の役割を果たす。
 if {$a<0} {
   set a 0
 }
上記リストは Tcl の if コマンドでありC言語の if 文に似ている。しかしブレスがリスト記述記号として採用されているので似ているだけである。上記の if コマンドを以下のように改行の位置を変えると、ifコマンドの引数エラーになる。
 if {$a<0}
 {
   set a 0
 }
Tcl パーサーは上記4行の文字列を2つのリストとして認識する。したがって先頭行のifコマンド行は引数がひとつだけしか与えられていないことになり、引数エラーが発生する。
ここで重要なのは、エラーを返したのは if コマンドであり、Tcl パーサーが検出した「文法エラー」ではないということである。Tcl には制御文などの「文」はない。分岐や繰り返しなどの実行制御も単にコマンドによって実現されているだけである。「Tcl には細かなルールが無い」のであり、そこにはリスト構造と、以下に解説する「特殊記号」しかない。

ブラケット記号（コマンド置換）

Tcl パーサーはリストの先頭要素を常にコマンド名として認識する。それ以外の要素はコマンドに渡すべき引数として認識する。しかし、その引数要素がブラケット（[、]）で挟まれていると、その中身をコマンド行と認識し、それを実行してから本来の引数値を求めてくれる。これが「コマンド置換」である。
 set a [expr 100*2]

$記号（変数置換）

Tcl パーサーは変数機能の提供によりコマンド間でのデータの受け渡しも扱ってくれる。変数はsetコマンドにより生成される。そして、$記号が先頭に付いた要素を変数名とみなし、その値に要素全体を置換する。これが「変数置換」である。
 set a {This is a pen.}
 puts $a
変数置換はコマンド行の先頭要素に対しても機能する。つまりコマンド名を変数で与えることも可能である。
変数置換は1回しか実行されない。変数置換結果に文字「$」が含まれていても再置換が試みられることはない。
また、変数置換で得られた文字列にブランクが含まれていても、2つの引数としてではなく、ブランクが含まれた1つの引数として渡される。引数の分離（リストの認識）は変数置換前に行われるからである。もし置換結果から改めて引数分離を行わせたいなら eval コマンドを利用する。

ダブルクォーテーション記号

リスト構造で解説したように、ブレス（{、}）は、改行コードなどの特殊文字の機能を無効化する。この法則はコマンド置換子であるブラケットや、変数置換子である$記号に対しても貫かれる。そのため、下記のコードではコマンド置換も変数置換も行われない。
 puts {[expr 100*$num] 円}
そのため、実行結果として出力される文字列は以下のものになる。
[expr 100*$num] 円
コマンド置換も変数置換も機能させ、かつブランクを含む文字列をひとつの引数として puts コマンドに渡すには、ブレスの代わりにダブルクォーテーション（"）で挟めば良い。
 puts "[expr 100*$num] 円"
num変数に3がセットされていればコマンドの実行結果として
300 円
が出力される。
このように、ダブルクォーテーションの機能はブレス機能とほぼ同等であるが、コマンド置換と変数置換をTclパーサーに許すところが異なる。ダブルクォーテーションの中でのコマンド置換、変数置換は Tcl の特長的な機能である。なお、ダブルクォーテーションとブレスの機能は、それらがリスト要素の先頭と末尾に記述された場合にのみ有効である。下の例では「"」は文字として扱われる。
 set text 石を投げたら"ゴツン"と音がした

セミコロン記号（マルチコマンド）

複数のコマンドを1行に記述したい場合はコマンド行をセミコロン（;）で区切れば良い。
 set a 100 ; set b 200; puts [expr $a * $b]

#記号（注釈行）

コマンドの位置に「#」を記述すると行末までコメントと見なされる。「コマンドの位置に」ということが重要であり、以下の最後のコードは「#」がコマンドの位置にないので誤りである。
 #初期値セット
 # set a 0
 set b 0 ; # 初期値
 set c 0   # 初期値

バックスラッシュ記号

ひとつのコマンドを複数行で記述したい場合は、行の末尾にバックスラッシュを付ける。
 command $arg1 $arg2 \
          $arg3 $arg4
$文字の前に\を置くと$置換子の機能を抑制して単なる文字として扱わせることができる。
 puts "金額＝\$100"
コマンド置換子（[、]）、ブレス（{、}）の前に置いた場合も同様である。
基本的にバックスラッシュにはC言語とほぼ同じ機能がある。例えば改行コードは「\n」と書ける。

補足：引数をブレスで挟むことのもうひとつの意味

Tcl パーサーから渡された引数がコマンド内部で評価されるか否かは重要である。ここでの評価とはコマンド置換や変数置換のことである。例えば算術演算を行う expr コマンドは引数を内部で評価する。従って以下の2つのコマンドは同じ結果を返す。
 expr $a*$b
 expr {$a*$b}
Tcl パーサーが行ってくれずとも、exprコマンドが内部で変数置換しているので同じ結果が得られるのである。ifコマンドやwhileコマンドは第1引数として与えられた文字列を、コマンド内部で expr によって評価し、その結果を使う。
 set val true
 if $val {～処理～}
 while $val {～処理～}
Tcl パーサは第一引数の「$val」を評価して「true」（文字列）に書き換える。同様に第二引数「{～処理～}」を評価して、文字列リテラルなのでそのまま同じ値「{～処理～}」に書き換え、以下のようにする。
 if true {～処理～}
 while true {～処理～}
Tcl パーサはその後、true と {～処理～} を if、while コマンドに渡し、if、while コマンド側は受け取った文字列を expr コマンドに「[expr true]」として渡し、その結果をもって処理を続ける。気をつけなければならないのは while コマンドは {～処理～} をひと通り評価し終えた後、再度、第一引数を expr で評価するが、上記のコードでは与えられている値が true なので {～処理～} の中に「set val false」のような文があったとしても、第一引数で与えられた値そのものは変化しないので、ループを無限に繰り返すことになる（{～処理～} の中に break コマンドが含まれていない場合）。
 set val true
 if {$val} {～処理～}
 while {$val} {～処理～}
この場合、Tcl パーサは引数を順に評価し、第一引数として「{$val}」という4文字の文字列リテラルを、第二引数として「{～処理～}」を、if、while コマンドに渡す。if、while コマンド側は受け取った文字列を expr コマンドに「[expr {$val}]」として渡し、その結果をもって処理を続ける。while コマンドは {～処理～} をひと通り評価し終えた後、再度、第一引数を expr で評価するが、上記のコードでは与えられている値が {$val} なので {～処理～} の中で「set val false」のような文があれば、その評価値も false になって、その時点で繰り返し処理を終了する。
一方、switchコマンドの第1引数は内部では評価されない。したがって下記の2番目の記述は期待する結果が得られない。
 switch $val {...}
 switch {$val} {...}
もし引数をコマンド内部で評価してくれるならば、その引数はブレス({})で挟んで渡した方が効率が良い。Tclパーサーとコマンドの両方による多重評価処理を回避できるからである。このような理由により、ifコマンドやwhileコマンドやforコマンドの引数は常にブレスで挟むべきである。特にループの条件式は必ずブレスで挟む必要がある。ブレスで挟まないと変数置換されてからループコマンドに渡されてしまうので、定数を並べた条件式になってしまい、無限ループをもたらす。

コマンドの拡張

コマンドには、Tcl パーサーにあらかじめ実装されているビルトインコマンドと、ユーザーにより作成された拡張コマンドがある。ユーザーによる拡張コマンドの実装は簡単である。まず、C言語などで「コマンド本体関数」と「登録用関数」を記述し、ダイナミックリンクライブラリファイルに格納する。そして組み込みコマンドの load を用いて拡張コマンドを登録する。
 load hello.dll hello
load コマンドの引数には、「ライブラリファイル名」と「登録用コマンド名」を与える。load コマンドは、登録用コマンド名から「登録用関数」名を求め、これを実行してくれる。例えば hello コマンドであれば Hello_Init を実行してくれる。この「登録用関数」の中に本当の登録処理を記述しておく。従って、例えば load コマンドに渡したコマンド名にライブラリ名の意味を持たせ、「登録用関数」の中で複数のコマンドを登録してしまうことも可能である。
「本当の」コマンド登録はC言語のインターフェース用の関数の Tcl_CreateCommand や Tcl_CreateObjCommand を用いて行う。拡張コマンドの実体となる「コマンド本体関数」を、決められた型と引数に従って先に定義しておき、そのコマンド関数アドレスと公開コマンド名を引数に与えて実行すれば登録される。
コマンド登録関数
int Hello_Init (Tcl_Interp *interp) 
  {
  Tcl_CreateObjCommand (interp, "hello", Tcl_HelloCmd, NULL, NULL) ;
  return TCL_OK ;
  }
コマンド本体関数
int Tcl_HelloCmd (ClientData dummy, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv []) 
  {
  ...
  return TCL_OK ;
  }
コマンド本体関数は、Tcl パーサーからの引数を、引数の数（argc）と引数文字列配列（argv）で受け取る。ただし、引数を文字列で受け取るこの方式は Tcl_CreateCommand 関数で登録する場合であり、引数を Tcl オブジェクトで受け取りたい場合には、上例のように Tcl_CreateObjCommand 関数で登録する。

内部構造（Tcl オブジェクト）

Tcl パーサーは、スクリプト文字列を受け取り、常に処理結果を文字列で返すように見える。これでは文字列の解析や文字コードとバイナリ値への変換が頻繁に行われていることになり、いかにも効率が悪く思える。しかし決してそのような単純なものではなく、内部では可能な限りバイナリ値を維持している。Tcl スクリプトでバイナリ値も扱えるのはこのおかげである。
例えば下記のように変数に数値を与え、その計算結果を変数に格納するとき、おのおの変数の型は文字列と double の二つの型を持つ。文字列として参照されるときは文字列型として振る舞い、double として参照されるとき（計算式の中などで）は double 型として振る舞うことができる。
 set a 100.0
 set b 200.0
 set c [expr $a * $b]
Tcl パーサー内部において変数は Tcl オブジェクトとして存在している。Tcl オブジェクトは char *、int、double の共用体を持った構造体であり、文字列への変換が要求されない限り、int 値は int 値のまま、double 値は double 値のまま維持される。スクリプトでは変数の型宣言を行えないが、変数への値セットで型が仮定され、Tcl オブジェクト間のデータ移動で無駄な型変換が行われないように配慮されている、ということである。
リストも同様に内部ではリスト型の Tcl オブジェクトとして存在している。このような仕組みになっているので、リストを作成する時は list コマンドを用いるべきであることが分かる。また、巨大なリストを文字列として全体参照するのも効率を悪くするので慎重にすべきである。下記の例では変数 listA には文字列として格納されるが、listB には int 値のリストとして格納される。この後、これらの変数にリスト処理コマンドでアクセスすると、listA に対しては要素分解処理が行われるが、listB に対しては不要となる。
 set a 100
 set b 200
 set listA "$a $b"
 set listB [list $a $b]
前項で解説したコマンド登録関数の後者（Tcl_CreateObjCommand）は、Tcl パーサーからの引数を、無駄に文字列変換することなく、Tcl オブジェクトのままで受け取るコマンド関数を登録するためのものである。
返値も Tcl オブジェクトを通じて返すことができる。下記は long 値をそのまま返す例である。
 /* long値を返す例 */
 Tcl_SetLongObj (Tcl_GetObjResult (interp), longVal) ;
返値のみならず、Tcl オブジェクトは不要になったとき自動的に削除される仕組みがある。それは Tcl オブジェクトが持つ「参照カウンタ」による制御である。アプリケーションで Tcl オブジェクトを作成し、それを存続したければ参照カウンタを増加させておき（Tcl_IncrRefCount）、必要なしとなったところで減ずる（Tcl_DecrRefCount）ようにする。参照カウンタが減らされて 0 になったときにのみ、そのオブジェクトは削除される。つまり、存続と廃棄の要求を自分だけの都合で出しておくだけで、削除のタイミングがコントロールされるという仕組みである（スマートポインタ）。
変数のみならず、スクリプト自体も Tcl オブジェクトとして存在している。