proc lambda {vars args body} { set exvars [lmap var $vars { upvar $var localvar list $var $localvar }] lappend args {*}$exvars list apply [list $args $body] }
简单地用 apply 实现一个“伪闭包”。其中 {*} 需要 Tcl 8.5,[lmap] 需要 8.6。
所谓“伪”是因为这个函数没法真正地持有变量引用,只能复制一下。有点像是缩水版的 C++ 闭包。虽说只是复制,但还是十分好用,因为 Tcl 在核心层面的“复制”使用的是引用计数,而且若原函数退出,也不会影响到匿名函数里面的变量,毕竟 Tcl 是没有垃圾收集的,这样做凑合能算不错吧。
有个比较遗憾的限制是,因为我比较懒,用“可选参数”实现了变量复制,所以如果参数列表有 args 且使用了外部变量,就会退化到普通的变量(正如 lambda 自身的 args)。
这个函数基本用法是这样的:
set x 100 {*}[lambda x {a b} { puts "x = $x\na=$a\nb=$b" }] 10 20
自然,你也可以给 lambda 返回的“闭包”一个名字:
set x 100 set closure [lambda x {a b} { puts "x = $x\na=$a\nb=$b" }] {*}$closure 10 20
实际上,这玩意在 Tk 上最好用:
proc foo {} { set msg "使用 lambda 辅助函数十分方便吧!" ttk::button .b -text 点我 -command [lambda msg {} { tk_messageBox -message $msg }] pack .b } foo
一般情况下,Tk 直接这样写是不行的,因为 -command 是在全局命名空间里执行的:
proc foo {} { set msg "这是不行的!" ttk::button .b -text 点我 -command { tk_messageBox -message $msg } pack .b } foo
会报错找不到 msg 变量。
如果你比较熟悉 Tcl,可能想问为什么不用 subst,像下面这样:
proc foo {} { set msg "hello tcl!" ttk::button .b -text 点我 -command [subst -nocommands { tk_messageBox -message $msg }] pack .b } foo
我开始也以为可以,但遗憾的是,不行。因为 Tk 使用 eval 处理 command,在上面的例子中,eval 会把原来好好的命令曲解成这样:
tk_messageBox -message hello tcl! # 也就是 tk_messageBox -message {*}"hello tcl!"
这是因为 eval 使用了 concat,而 concat 会把列表展开一层,正如 {*} 一样:
concat {{1 2} 3} 4 # → {1 2} 3 4
eval 的“展开”特性在少数情况下有用,大部分情况下 {*} 就可以替代 eval,在此不赘述。