set a 5 puts $a
by liding ios
最近有一篇連接自reddit的題爲Tour de Babel的文章,若是你讀的話就會發現這篇文章說(除了一堆其餘的胡言亂語):靠,Python在能想象到的各方面都比Tcl好得多了,但人們還用Tcl來看成嵌入式解釋器。shell
好吧,整篇文章是有點……不是那麼回事。可是不幸的是,雖然不少錯誤觀念很快被知情的讀者發覺了,可是這個反對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。babel
Tcl語言的第一個觀念是:命令。程序就是一系列命令。例如把變量a設成5,並輸出其值:
set a 5 puts $a
命令是空格分隔的單詞。命令以換行或「;」結束。Tcl中一切皆是命令——正如你所見,沒有賦值運算符。設置變量須要使用命令「set」,把命令的第一個參數設爲第二個參數的值。
幾乎全部的Tcl命令都返回一個值。例如「set」返回所賦值的值,若是set只有一個參數(即變量名),就變量的當前值。
第二個觀念是命令替換。一個命令中,有些參數出如今「[]」中。若是是這樣,那個參數的值就是中括號中命令的返回值。例如:
set a 5 puts [set a]
第二個命令的第一個參數[set a],將會被替換爲「set a」的返回值(也就是5)。在替換後,命令將由
puts [set a]
變成
puts $a
這時,命令纔會被執行。
老是使用set命令來替換變量太麻煩了,因此,即便不是絕對必要,變量替換在Tcl早期發展中被添加進來。若是一個變量名字以前有$號,就會被它的值所替換。例如能夠不用寫
puts [set a]
而是寫成
puts $a
若是命令是由空格分隔的單詞,怎樣處理包含空格的變量呢?例如
puts Hello World
是不正確的,由於「Hello」和「World」是兩個不用的參數。這個問題由組合來解決。在「」"」中的文本被是單個參數,因此正確的寫法是:
puts "Hello World"
命令和變量替換在這種組合中仍然有效,例如我能夠這樣寫:
set a 5 set b foobar puts "Hello $a World [string length $b]"
結果是「Hello 5 World 6」。另外,轉義字符(例如「\t」,「\n」)也是有效的。可是有另一種組合,每種特殊字符都被原樣看待,沒有替代過程。Tcl把任何在「{}」之間的東西當作是單一的參數,沒有替換。因此:
set a 5 puts {Hello $a World}
將會輸出「Hello $a World」。
概念1是:程序由一系列命令組成。實際上,這比你想象的還要正確。例如:
set a 5 if $a { puts Hello! }
「if」是個命令,有兩個參數。第一個是變量「a」所替換的值,第二個是字符串 「{… puts Hello! …}」。「if」命令使用一種特殊的Eval命令(後面將會講到),運行第二個參數,而後返回結果。固然,你也能夠寫本身的「if」命令版本,或者其餘任何控制結構。你甚至能夠重定義「if」,加入一些新功能!
如下程序能正常運行,而且結果如你所想:
set a pu set 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值一直被看成數字來用,只要下一個命令繼續把他看成數字,字符串的表示根本不會修改。實際的實現比上述複雜,但整體結果是:程序員不用管類型,程序仍然和其餘顯式類型的語言同樣快。
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程序員使用列表來模擬全部可能的概念。
我打賭大多數Lisp黑客已經注意到Tcl是一個前綴(prefix)表達式語言,因此你可能認爲像Lisp同樣,Tcl數學運算就像使用命令,例如「puts [+ 1 2]」。然而,偏偏相反,爲了使Tcl更加友好,有個命令接受中綴(infix)數學表達式,並計算其值。這個命令是「expr」,Tcl數學運算像這樣:
set a 10 set b 20 puts [expr $a+$b]
「if」和「while」等命令內部使用「expr」來計算表達式,例如:
while {$a < $b} { puts Hello }
其中,「while」命令接受兩個參數——第一個字符串求值,看看在每次循環時是否爲真,第二個每次被分析執行。我認爲數學命令不是內置是一個設計上的錯誤。在作複雜運算時,「expr」很酷,可是僅僅把兩個數相加,仍是「[+ $a $b]」更加方便一些。值得注意的是,這點已經被正式提出,做爲對語言的修改。
天然,沒有什麼能阻止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自己。例如爲了寫了一個Tcl宏處理系統我重定義了「proc」。重定義「proc」一般對編寫分析器(profiler)是頗有用的(Tcl分析器是使用Tcl開發的)。在重定義內置命令以前,若是你把它重命名,那麼在定義以後仍是能夠調用原來的命令的。
若是你讀這篇文章,說明你已經知道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 10 repeat 5 {incr a} ;# Repeat will return 15
正如你猜想的,「incr」命令用來把整數變量加1(若是你忽略了第二個參數的話)。「incr a」在調用過程的上下文中執行(即前一個棧幀)。
祝賀,你已經知道了90%以上的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]」。這樣就好了。
Tcl有內置的事件驅動編程,和I/O庫集成在一塊兒。只使用核心語言所提供的功能來寫複雜的網絡程序如此簡單,甚至是有趣。例如:如下程序是一個並行TCP服務器(內部基於select(2)),它把當前時間送給每一個客戶端。
socket -server handler 9999 proc 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」同樣簡單。
經過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 ... }
Tcl多是擁有最好的國際化支持的語言了。每一個字符串內部使用utf-8表示,全部的字符串是Unicode安全的,包括正則表達式引擎。基本上,在Tcl程序裏,編碼不是個問題,他們自動工做。
若是你定義了一個過程叫作「unknown」,這個過程將會在Tcl處理命令出錯時,將會把命令的參數傳遞給它,並調用之。你能夠在這個過程當中作任何你想作的事,返回一個值,或者引起錯誤。若是你只是返回一個值,那麼被調用的命令就像沒出錯同樣,並用「unknown」的返回值做爲它的返回值。把這一點加在uplevel和upvar之上,這門語言幾乎沒有語法規則了。你所獲得的是一個使人印象深入的領域特定語言的開發環境。Tcl基本沒有語法,就像Lisp和FORTH,可是「沒有語法」的方式不一樣。Tcl默認狀況下就像一個配置文件:
disable ssl validUsers jim barbara carmelo hostname 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的肯定,並且擁有它全部的強大能力。我本身試過,結果是Jim interpreter:代碼就在那裏,能夠正常工做,能運行大多數Tcl程序,可是我沒有時間去作自由語言開發,因此這個項目或多或少已經廢棄了。另外一個企圖開發一個類Tcl語言的項目是Hecl,正在進行中。這個語言做爲Java語言的腳本語言,它的做者(David Welton)意識到Tcl核心實現很小,基於命令的設計容易做爲兩個語言之間的聯繫(這在現代動態語言中少見,但這兩個特性也試用與Scheme)。我將很是高興,若是你讀了這篇文章以後,再也不認爲Tcl是個玩具。謝謝。Salvatore。