この記事は「Tcl/Tk Advent Calendar 2021」の第4日目です。
Tcl/Tk Advent Calendar 2021の第3日目「Tcl whileクイズ!」の答え合わせをやります。
考え方
Tcl以外の言語では
while (条件文) {
条件が真の場合の処理
}
条件が偽の場合の処理
という文がある場合、while
は演算子みたいなオーラがあって、条件文が偽になるまでwhile (条件文)
という部分が繰り返し評価されるイメージではないでしょうか。(私はそういうイメージでした)
そういうイメージだったので、第一問の書き方while {[incr i]} {...
でも、第二問の書き方while [incr i] {...
でも、**「ループの末尾の}
までいったらwhile
に戻って[incr i]
が評価されてiが0になったらループが終わる。{[incr i]}
か[incr i]
かは効率面だけの違いだろう」**と思っていました。
しかしTcl言語ではfor, foreach, while, ifといった制御構文は演算子的ではなく、むしろ関数(Tclのc、つまりコマンド)と考えた方がしっくりきます。と言うより、「Tclスクリプトには制御構文とか演算子とかは存在せず、あるのは関数(コマンド)の羅列のみ」と言い切ってしまった方がいいのかもしれません。
このあたりが曖昧だったせいで第二問の書き方をしてしまって、条件文が偽になるはずのタイミングでループに入ってしまうという不具合に陥ってしまいました。
Tclでは
while 条件文 {
条件が真の場合の処理
}
条件が偽の場合の処理
という文がある場合、**「これはwhile 条件文 条件が真の場合の処理
というwhile関数(whileコマンド)の呼び出しである」**と考えれば、第一問と第二問の挙動が違うのは当たり前にしか見えなくなります。
つまり、
- 条件文の評価
- 条件が真の場合の処理
はどちらもwhile関数の中で行われることであって、ループの末尾に行ったからといってwhile 条件文
が評価されるのではなく、while関数の中でexpr関数によって条件文が評価されるのです。このため、while関数に渡す引数値はただのソースコード文字列でなければならないのです。
第一問の書き方ならば[incr i]
という文字列がwhile関数の第一引数に渡され、while関数内部でexpr関数に渡されるため繰り返しincr関数の実行結果で条件判定されます。
しかし第二問の書き方では、while関数呼び出し前に[incr i]
が評価されて-1に置き換えられ、while関数の第一引数には-1という値が渡されるため、while関数内部の条件判定では常に非0つまり真となり、無限ループとして処理されるというわけです。
なぜ呼び出し側のローカル変数iを関数内で評価・変更できるのか
Tcl未経験の方は**「while関数内部で、なぜ呼び出し側のローカル変数iを評価したり変更したりできるのか?」**という疑問を持たれると思います。
ここがTclの変態的 強力なところで、while関数内から呼び出し側へ行って文を評価して値を持ってwhile関数内に戻ってくるための機能(コマンド)があるのです。この機能によって、見た目が制御構文みたいな関数を定義できてしまうわけです。
第一問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while {[incr i]} {
puts $i
}
答え: ✖
[incr i]
は事前に展開されず、ちゃんとループのたびに評価されるので、2回目のインクリメントでiが0になってループが終了します。
第二問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while [incr i] {
puts $i
}
答え: 〇
[incr i]
は事前に-1に展開されてしまい、常に真(非ゼロ)になるため無限ループになります。
第三問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while "[incr i]" {
puts $i
}
答え: 〇
[incr i]
は文字列としてwhileに渡されているかに見えますが、"..."
内部は評価されるので事前に"-1"
になってしまい常に真(非ゼロ)になるため無限ループになります。
第四問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while \[incr i] {
puts $i
}
答え: エラー
[incr i]
が評価されないよう\
で[
をエスケープしたわけですが、そうすると\[incr
が第一引数、i]
が第二引数、{puts $i}
が第三引数になってしまうため、
wrong # args: should be "while test command"
という、Tclでは見慣れたエラーになります。(「なんでシャープ?」というツッコミは恥をかくだけなので控えましょう。#はナンバーという記号ですw)
ちなみにwhile \[incr\ i] {puts $i}
なら第一問と同じ結果になります。
第五問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while {\[incr i]} {
puts $i
}
答え: エラー
invalid character "\"
in expression "\[incr i]"
というエラーになります。ちなみにexpr {\[incr i]}
の評価結果もこのエラーになります。
第六問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
while "\[incr i]" {
puts $i
}
答え: ✖
第一問と同じ実行結果になります。
"
で囲んで文字列とした上で[
をエスケープして評価を抑制して{[incr i]}
と同じく文字列としてwhileに渡せているからです。
第七問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
set c {[incr i]}
while $c {
puts $i
}
答え: ✖
第一問と同じ実行結果になります。
whileに渡す前に$c
は[incr i]
という文字列に置き換えられますが評価まではされず、文字列のままwhileに渡されます。
第八問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
set c "[incr i]"
while $c {
puts $i
}
答え: 〇
set c "[incr i]"
でcに代入されるまえに"-1"になってしまい、whileには-1が渡されてしまいます。
第九問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
set c {\[incr i]}
while $c {
puts $i
}
答え: エラー
第五問と同じ状況です。
第十問: 以下の記述は無限ループである。〇か?✖か?エラーか?
set i -2
set c "\[incr i]"
while $c {
puts $i
}
答え: ✖
第六問と同じ状況です。