被误解的Tcl (Tcl the Misunderstood) – 为什么Tcl是一门强大的语言,而不是玩具
,
by
最近有一篇链接自reddit的题为的文章,如果你读的话就会发现这篇文章说(除了一堆其他的胡言乱语):靠,Python在能想象到的各方面都比Tcl好得多了,但人们还用Tcl来当作嵌入式解释器。
好吧,整篇文章是有点……不是那么回事。但是不幸的是,虽然很多错误观念很快被知情的读者发觉了,但是这个反对Tcl的观念却被认为是理所当然的。我希望这篇文章能说服人们,Tcl还没有那么不堪。
开场白
在我的编程生涯里,我使用了很多语言去写不同的应用:用C语言写了很多免费/付费的程序,用Scheme写了一个Web CMS(内容管理系统),用Tcl写了几个网络/Web应用,用Python写了一个商店管理程序,等等。我也玩过不少其他的编程语言,例如Smalltalk,Self,FORTH,Ruby,Joy……然而,我从不怀疑,没有哪门语言像Tcl一样被误解得如此之深。
Tcl不是完美无瑕,但大多数的不足并不是在语言设计本身,而是Tcl“之父”(John Ousterhout)几年前去世了,连同他那种可以做出强势决定的专一的领导力。只要做出正确的改变,克服Tcl的大多数不足并保留其强大功能是可以的。如果你不相信Tcl异常强大,请先花点时间来阅读这篇文章。可能你读完之后还是不喜欢它,但希望你尊敬它,同时你将有足够强大的论据来反对这种“Tcl是玩具语言”的误解。这种误解如此小气,比“Lisp括号太多”更甚。
在我们开始之前,我先花点时间解释一下Tcl的工作原理。Tcl像世界上其他优秀的语言一样,拥有一些概念,这些概念组合起来,能够实现编程自由和充分的表达力。
在这个简短的介绍之后,你会了解在Tcl中,怎样使用普通过程(procedure)实现像Lisp一样的宏(macro)(比Ruby的Block强大得多),怎样重定义语言本身的几乎所有方面,怎样在编程时忽略类型。Tcl社区开发了数个OOP系统,大规模的语言重定义,宏系统,和很多其他有趣的东西,仅仅使用Tcl本身。如果你喜欢可编程的编程语言,我打赌,你肯定至少饶有兴趣地看一眼Tcl。
五分钟学会Tcl
概念1:程序由命令(Command)组成
Tcl语言的第一个观念是:命令。程序就是一系列命令。例如把变量a设成5,并输出其值:
set a 5puts $a
命令是空格分隔的单词。命令以换行或“;”结束。Tcl中一切皆是命令——正如你所见,没有赋值运算符。设置变量需要使用命令“set”,把命令的第一个参数设为第二个参数的值。
几乎所有的Tcl命令都返回一个值。例如“set”返回所赋值的值,如果set只有一个参数(即变量名),就变量的当前值。
概念2:命令替换(Command substitution)
第二个观念是命令替换。一个命令中,有些参数出现在“[]”中。如果是这样,那个参数的值就是中括号中命令的返回值。例如:
set a 5puts [set a]
第二个命令的第一个参数[set a],将会被替换为“set a”的返回值(也就是5)。在替换后,命令将由
puts [set a]
变成
puts $a
这时,命令才会被执行。
概念3:变量替换(Variable substitution)
总是使用set命令来替换变量太麻烦了,所以,即使不是绝对必要,变量替换在Tcl早期发展中被添加进来。如果一个变量名字之前有$号,就会被它的值所替换。例如可以不用写
puts [set a]
puts $a
概念4:组合(Grouping)
如果命令是由空格分隔的单词,怎样处理包含空格的变量呢?例如
puts Hello World
是不正确的,因为“Hello”和“World”是两个不用的参数。这个问题由组合来解决。在“”"”中的文本被是单个参数,所以正确的写法是:
puts "Hello World"
命令和变量替换在这种组合中仍然有效,例如我可以这样写:
set a 5set b foobarputs "Hello $a World [string length $b]"
结果是“Hello 5 World 6”。另外,转义字符(例如“\t”,“\n”)也是有效的。但是有另外一种组合,每种特殊字符都被原样看待,没有替代过程。Tcl把任何在“{}”之间的东西看成是单一的参数,没有替换。所以:
set a 5puts {Hello $a World}
将会输出“Hello $a World”。
概念1第二次:一切都是命令
概念1是:程序由一系列命令组成。实际上,这比你想象的还要正确。例如:
set a 5if $a { puts Hello!}
“if”是个命令,有两个参数。第一个是变量“a”所替换的值,第二个是字符串 “{… puts Hello! …}”。“if”命令使用一种特殊的Eval命令(后面将会讲到),运行第二个参数,然后返回结果。当然,你也可以写自己的“if”命令版本,或者其他任何控制结构。你甚至可以重定义“if”,加入一些新功能!
概念5:一切都是字符串——没有类型
以下程序能正常运行,并且结果如你所想:
set a puset b ts$a$b "Hello World"
是的,Tcl中一切发生在运行时,并且是动态的:Tcl是终极迟绑定(late binding)语言,没有类型。命令名不是特殊类型,只是一个字符串。数字也是字符串,正如Tcl代码(还记得我们传给“if”命令第二个参数一个字符串吗?)。Tcl中,字符串表示什么由处理它的命令所决定。字符串“5”将会在命令“string length 5”中被看成一个字符,而在“if $a”中看成一个布尔值。当然命令会检查它的参数值有正确的形式。如果我要把“foo”和“bar”加起来,Tcl会产生异常,因为无法讲“foo”和“bar”解析成数字。Tcl中这种检查非常严格,所以你不会遇到PHP那种荒谬的隐式类型转换。字符串可以被解释成命令想要的值时,类型转换才会发生。
那么,Tcl如此动态,你猜怎么着?它或多或少和当前的Ruby实现一样快速。Tcl实现中有个技巧:对象(不是OOP中的对象,而是代表Tcl值的C结构)会缓存最后使用的某个字符串的本地值(译者注:例如“56”这个字符串代表一个int,会被缓存起来,下次就不用再分析一遍了)。如果一个Tcl值一直被当作数字来用,只要下一个命令继续把他当作数字,字符串的表示根本不会修改。实际的实现比上述复杂,但总体结果是:程序员不用管类型,程序仍然和其他显式类型的语言一样快。
概念6:Tcl列表(list)
Tcl使用的一种更有趣的类型(更准确点……字符串格式)是列表。列表是Tcl程序的中心数据结构:一个Tcl列表永远是一个有效的Tcl命令!(最后它们都是字符串)。最简单的列表就是命令:空格分隔的单词。例如字符串“a b foo bar”是一个有四个元素的列表。有各种操作列表的命令:取一个list中的元素,添加元素,等等。当然,列表可能有含空格的元素,所以为了创建格式良好的列表,就使用list命令。例如:
set l [list a b foo "hello world"]puts [llength $l]
llength返回列表的长度,所以上述程序将输出4。lindex返回在某个位置的元素,所以“lindex $l 2”将返回“foo”。和Lisp一样,大多数Tcl程序员使用列表来模拟所有可能的概念。
概念7:Tcl数学运算
我打赌大多数Lisp黑客已经注意到Tcl是一个前缀(prefix)表达式语言,所以你可能认为像Lisp一样,Tcl数学运算就像使用命令,例如“puts [+ 1 2]”。然而,恰恰相反,为了使Tcl更加友好,有个命令接受中缀(infix)数学表达式,并计算其值。这个命令是“expr”,Tcl数学运算像这样:
set a 10set b 20puts [expr $a+$b]
“if”和“while”等命令内部使用“expr”来计算表达式,例如:
while {$a < $b} { puts Hello}
其中,“while”命令接受两个参数——第一个字符串求值,看看在每次循环时是否为真,第二个每次被分析执行。我认为数学命令不是内置是一个设计上的错误。在做复杂运算时,“expr”很酷,但是仅仅把两个数相加,还是“[+ $a $b]”更加方便一些。值得注意的是,这点已经被正式提出,作为对语言的修改。
概念8:过程(Procedures)
自然,没有什么能阻止Tcl程序员写一个过程(即用户定义命令),来把数学操作符当作命令。就像这样:
proc + {a b} { expr {$a+$b}}
“proc”命令用来创建一个过程:第一个参数是过程名,第二个是参数列表,最后一个是过程的主体。注意第二个参数,参数列表,是一个Tcl列表。正如你所见,最后一个命令的返回值是过程的返回值(除非显式使用return)。但是等一下……Tcl中一切都是命令,是吧?所以我们可以用更简单的方式创建“+、-、*、……”的过程:
set operators [list + - * /]foreach o $operators { proc $o {a b} [list expr "\$a $o \$b"]}
定义这些之后,我们就可以用“[+ 1 2] [/ 10 2]”等表达式了。当然,把这些过程创建成类似Scheme过程一样的变长参数更好一些。Tcl过程可以使用内置命令的名字,所以你可以重定义Tcl本身。例如为了写了一个我重定义了“proc”。重定义“proc”通常对编写分析器(profiler)是很有用的(Tcl分析器是使用Tcl开发的)。在重定义内置命令之前,如果你把它重命名,那么在定义之后还是可以调用原来的命令的。
概念9:Eval和Uplevel
如果你读这篇文章,说明你已经知道Eval是什么了。命令“eval {puts hello}”当然会执行传递给eval的参数,在其他语言中也很常见。而Tcl还有另一个命令uplevel,可以在调用过程的上下文中执行语句(译者注:即在当前上下文的上一层上下文),或者说,在调用者的调用者的上下文中。这就是说,Lisp中的宏,在Tcl中就是简单的过程。例如:Tcl中没有内置的命令“repeat”:
repeat 5 { puts "Hello five times"}
但是写一个实现非常容易:
proc repeat {n body} { set res "" while {$n} { incr n -1 set res [uplevel $body] } return $res}
注意,我们用心保存最后一次执行的结果,所以我们的“repeat”像其他命令一样,返回最后执行的值。一个例子:
set a 10repeat 5 {incr a} ;# Repeat will return 15
正如你猜测的,“incr”命令用来把整数变量加1(如果你忽略了第二个参数的话)。“incr a”在调用过程的上下文中执行(即前一个栈帧)。
祝贺,你已经知道了90%以上的Tcl概念!
为什么Tcl如此强大?
我不会想你展示每一个Tcl特性,但是我将给你一个直观感受,看看Tcl怎样非常漂亮地解决高级编程任务的。我想强调我认为Tcl确实有一些错误,但是大多不在语言本身的主要概念之内。我认为,在Web编程,网络编程,GUI开发,DSL,脚本语言等方面,一个继承自Tcl的编程语言有和Ruby、Lisp和Python竞争的空间。
易扩展的简洁语法
Tcl语法如此简单,你可以用数行Tcl代码写一个Tcl分析器。正如我所说,我用Tcl语言写了一个Tcl宏处理系统,这个系统能够进行足够复杂的源码级的变换实现尾部调用优化(tail call optimization)。同时,Tcl语法能够变化成Algol一样,这取决于你的编程风格。
没有类型,但有严格的格式检查
没有类型,你不需要进行转换,但是,你不大可能引进bug,因为对字符串的格式检查非常严格。更好的是,你不需要序列化(Serialization)。你有一个巨大复杂的Tcl列表,想把它通过TCP套接字发送出去?这样就行了:“puts $socket $mylist”。另一头读取:“set mylist [read $socket]”。这样就行了。
强大的、事件驱动的I/O模型
Tcl有内置的事件驱动编程,和I/O库集成在一起。只使用核心语言所提供的功能来写复杂的网络程序如此简单,甚至是有趣。例如:以下程序是一个并行TCP服务器(内部基于select(2)),它把当前时间送给每个客户端。
socket -server handler 9999proc handler {fd clientaddr clientport} { set t [clock format [clock seconds]] puts $fd "Hello $clientaddr:$clientport, current date is $t" close $fd}vwait forever
非阻塞I/O和事件处理得如此之好,你甚至可以向一个没有输出缓存的套接字写入,Tcl自动在用户态缓存,当再次有输出缓存时,在后台发送出去。
Python用户看到某个理念时就会知道它是一个好主意——Python的“Twisted”框架使用了相同的select驱动的IO概念,而这个在Tcl本身中存在好多年了。(译者注:貌似Node.js的核心理念也是这个吧,看来Tcl超前了)
多种编程范式
使用Tcl你可以混合编写面向对象代码,函数式代码,和命令式代码,或多或少像Common Lisp那样。过去很多OOP系统和函数式编程原语都被实现出来。Tcl有所有的范式,从基于原型的OOP(译者注:Javascript那样的)到类似Smalltalk的那种,很多是以Tcl本身实现的(或者一开始作为概念论证原型)。而且,因为Tcl中代码是一级类型,很容易写出函数式语言原语,并和原语言结合很好。“lmap”的一个例子:
lmap i {1 2 3 4 5} { expr $i*$i}
这将会返回平方列表“1 4 9 16 25”。你可以写类似“map”的函数,基于一个lambda版本(也是用Tcl实现的),但是Tcl已经有拥有比Lisp更自然的函数式编程特性(Lisp方式可能对它本身很好,但是对其他语言来说就不一定了)。注意当你向一个过于死板的语言中加入函数式编程的时候会发生什么:Python以及它函数式原语的无尽争论。
中心数据结构:列表
如果你是个Lisp程序员,你知道如果在程序中有列表随处可用是多么美妙,尤其是列表的直接形式在大多数情况下如同“foo bar 3 4 5 6”一样简单。
通过uplevel的可编程编程语言
通过Tcl的eval、uplevel、upvar,以及非常强大的内省能力,你可以重定义语言并发明解决问题的新方式。例如以下有趣的命令,如果把它放在函数的第一行调用,将自动使那个函数成为一个memoizing函数(译者注:一种把函数返回值缓存起来的方法,读者可以搜所Javascript的实现):
proc memoize {} { set cmd [info level -1] if {[info level] > 2 && [lindex [info level -2] 0] eq "memoize"} return if {![info exists ::Memo($cmd)]} {set ::Memo($cmd) [eval $cmd]} return -code return $::Memo($cmd)}
然后当你写一个过程的时候,这样就行了:
proc myMemoizingProcedure { ... } { memoize ... the rest of the code ...}
i18n自动支持
Tcl可能是拥有最好的国际化支持的语言了。每个字符串内部使用utf-8表示,所有的字符串是Unicode安全的,包括正则表达式引擎。基本上,在Tcl程序里,编码不是个问题,他们自动工作。
大规模语言修改=DSL
如果你定义了一个过程叫做“unknown”,这个过程将会在Tcl处理命令出错时,将会把命令的参数传递给它,并调用之。你可以在这个过程中做任何你想做的事,返回一个值,或者引发错误。如果你只是返回一个值,那么被调用的命令就像没出错一样,并用“unknown”的返回值作为它的返回值。把这一点加在uplevel和upvar之上,这门语言几乎没有语法规则了。你所得到的是一个令人印象深刻的领域特定语言的开发环境。Tcl基本没有语法,就像Lisp和FORTH,但是“没有语法”的方式不同。Tcl默认情况下就像一个配置文件:
disable sslvalidUsers jim barbara carmelohostname foobar { allow from 2:00 to 8:00}
以上是合法的Tcl程序,只要你定义了所用的命令(disable、validUsers和hostname)。
更多
不幸的是,没有太多空间来展示很多有趣的特性:大多数Tcl命令只做一件事,并且名称容易记忆。字符串操作,内省和其他特性通过拥有子命令的命令实现。例如“string length”,“string range”等等。每个需要索引的地方都支持一种“end-数字”的记号,因此取一个列表除了第一和最后一个的所有元素,你这样写就行了:
lrange $mylist 1 end-1
并且,对常见代码都有很多很好的设计和优化。另外,Tcl源代码是你所能找到的最好的C程序之一,解释器的质量令人吃惊:不管哪方面说都是商业级别的。另一个关于实现的有趣事实是,它在不同的环境下有完全相同的工作表现,从Windows到Unix,再到Mac OS X。在不同平台上没有质量差别(是的,包括Tk,Tcl主要的GUI库)。
结论
我并没有声称每个人都该喜欢Tcl。我说的是Tcl是个强大的语言而不是一个玩具,而且可能创造一个新的类似Tcl的语言,没有Tcl的确定,而且拥有它所有的强大能力。我自己试过,结果是:代码就在那里,可以正常工作,能运行大多数Tcl程序,但是我没有时间去做自由语言开发,所以这个项目或多或少已经废弃了。另一个企图开发一个类Tcl语言的项目是,正在进行中。这个语言作为Java语言的脚本语言,它的作者(David Welton)意识到Tcl核心实现很小,基于命令的设计容易作为两个语言之间的联系(这在现代动态语言中少见,但这两个特性也试用与Scheme)。我将非常高兴,如果你读了这篇文章之后,不再认为Tcl是个玩具。谢谢。Salvatore。