Lisp一瞥:增強(qiáng)型變量Symbol
Lisp一瞥:增強(qiáng)型變量Symbol
Author: | Kevin Lynx |
---|---|
Date: | 3.21.2011 |
Contact: | kevinlynx at gmail dot com |
Note
本文描述的Lisp主要指Lisp的方言Common Lisp。
變量,是所有編程語言里都有的語法概念。在C/C++中,變量用于標(biāo)示一個(gè)內(nèi)存地址,而變 量名則在語法層面上代表這個(gè)地址。當(dāng)鏈接器最終鏈接我們的程序時(shí),就將這些名字替換 為實(shí)際的地址。在其他語言中,變量雖然或多或少有其他不同的含義,但也大致如此。
Lisp中的變量也差不多這樣,但若將variable和Lisp中的 symbol 放在一起,則多少會(huì) 帶來些困惑。
Lisp中的“變量"
很多教授Lisp的書中,大概會(huì)簡單地告訴我們可以使用如下的方式定義一個(gè)全局變量 [1].
(defparameter *var* 1)
如上代碼,我們便定義了一個(gè)全局變量 *var*[2] ,它被初始化為數(shù)值1。同樣,我們 還可以使用另一種基本相同的方式:
(defvar *var* 1)
除了全局變量,我們還可以定義局部變量。但局部變量的定義稍顯麻煩(卻可能是另一種 設(shè)計(jì)考慮)。定義局部變量需要使用一些宏,或者特殊運(yùn)算符,例如:
(let ((var 1)) (format t "~a" var))
好了,就這些了。Lisp中關(guān)于變量的細(xì)節(jié),也就這些。你甚至能用你在C/C++中的經(jīng)驗(yàn)來窺 探一切。但是,我們很快就看到了很多困惑的地方。
我遇到的第一個(gè)困惑的地方來源于函數(shù),那么等我講講函數(shù)再來分享下坎坷。
Lisp中的函數(shù)
Lisp中的函數(shù)絕對不復(fù)雜,你絕對不用擔(dān)心我在忽悠你 [3] 。作為一門函數(shù)式語言,其首要 任務(wù)就是加強(qiáng)函數(shù)這個(gè)東西在整個(gè)語言里的功能。如果你喜歡廣閱各種與你工作不相干的 技術(shù),你肯定已經(jīng)對很多函數(shù)式語言世界中的概念略有耳聞。例如閉包,以及first class type [4] 。
Lisp中的函數(shù)就是first class type。這什么意思呢?直白來說, Lisp中的函數(shù)和變量 沒什么區(qū)別,享有同等待遇 。進(jìn)一步來說,變量fn的值可以是數(shù)值1,也可以是字符串 "hello",甚至是某個(gè)函數(shù)。這其實(shí)就是C++程序員說的functor。
Lisp中定義函數(shù)非常簡單:
(defun add2 (x) (+ 2 x))
這樣,我們就定義了一個(gè)名為add2,有1個(gè)參數(shù),1個(gè)返回值的函數(shù)。要調(diào)用該函數(shù)時(shí),只需 要 (add2 2) 即可。這簡直和我們在Lisp中完成一個(gè)加法一模一樣:(+ 2 3)
Lisp作為一門函數(shù)式語言,其函數(shù)也能作為另一個(gè)函數(shù)的參數(shù)和返回值 [5]
(defun apply-fn (fn x) (funcall fn x))
apply-fn函數(shù)第一個(gè)參數(shù)是一個(gè)函數(shù),它使用funcall函數(shù)間接地調(diào)用fn指向的函數(shù)。作為 一個(gè)C++程序員,這簡直太好理解了,這完全就是一個(gè)函數(shù)指針的語法糖嘛。于是,假設(shè)我 們要使用apply-fn來間接調(diào)用add2函數(shù):
(apply-fn add2 2) ;; wrong
可是這是不對的。我們需要通過另一個(gè)特殊操作符來完成這件事:
(apply-fn #'add2 2) ;; right
#'操作符用于將add2對應(yīng)的函數(shù)取出來,這么說當(dāng)然不大準(zhǔn)確。Again,作為一個(gè)C++程序員 ,這簡直就是個(gè)取地址操作符&的語法糖嘛。好吧,這么理解起來似乎沒問題了。
Lisp中能甚至能在任何地方定義一個(gè)函數(shù),例如我們創(chuàng)建一個(gè)函數(shù),該函數(shù)返回創(chuàng)建出來的 函數(shù),這是一個(gè)典型的講解什么是 閉包 的例子:
(defun get-add-n (n) #' (lambda (x) (+ x n)))
無論如何,get-add-n函數(shù)返回一個(gè)函數(shù),該函數(shù)是add2函數(shù)的泛型實(shí)現(xiàn)。它可以將你傳入 的參數(shù)加上n。這些代碼里使用了lambda表達(dá)式。lambda表達(dá)式直白來說,就是創(chuàng)建一個(gè)字 面上的函數(shù)。這又是什么意思呢?就像我們在代碼中寫出2,寫出"hello"一樣,2就是個(gè)字 面上的數(shù)字,"hello"就是個(gè)字面上的字符串 [6] 。
那么,總而言之,通過lambda創(chuàng)建一個(gè)函數(shù)體,然后通過#'操作符即可得到一個(gè)函數(shù),雖然 沒有名字。有了以上知識后,Again and again,作為一個(gè)C++程序員,很快我們就能得到一 個(gè)程序:定義變量,用變量去保存一個(gè)函數(shù),然后通過這個(gè)變量來調(diào)用這個(gè)函數(shù)。這是多么 天經(jīng)地義的事,就像之前那個(gè)通過參數(shù)調(diào)用其指向的函數(shù)一樣:
;; wrong (defvar fn #' (lambda (x) (+ x 2))) (fn 3)
這樣的代碼是不對的,錯(cuò)誤發(fā)生于第二行,無論你使用的Lisp實(shí)現(xiàn)是哪種,大概會(huì)得到如下 的錯(cuò)誤信息:
"The function FN is undefined."
老實(shí)說,這已經(jīng)算是多么有跡可循的錯(cuò)誤提示了啊。將以上代碼和之前的apply-fn對比,是 多么得神似啊,可惜就是錯(cuò)的。這是我們遇到的第一個(gè)理解偏差導(dǎo)致的問題。如果你還不深 入探究,你將會(huì)在這一塊遇到更多麻煩。及時(shí)地拿出你的勇氣,披荊斬棘,刨根究底,絕對 是學(xué)習(xí)編程的好品質(zhì)。
“萬惡之源“:SYMBOL
上文中提到的變量函數(shù)之類,之所以會(huì)在某些時(shí)候與我們的理解發(fā)生偏差,并且總是存在些 神秘的地方無法解釋。這完全是因?yàn)槲覀兝斫獾锰鎸?dǎo)致。Lisp中的Symbol可以說就是某 個(gè)變量,或者某個(gè)函數(shù),但這太片面。Lisp中的Symbol擁有更豐富的含義。
Symbol的名字
就像很多語言的變量、函數(shù)名一樣,Lisp中的Symbol比其他語言在命名方面更自由: 只 要位于'|'字符之間的字符串,就表示一個(gè)合法的Symbol名。 我們可以使用函數(shù) symbol-name來獲取一個(gè)Symbol的名字,例如:
(symbol-name '|this is a symbol name|) 輸出:"this is a symbol name"
'(quote)操作符告訴Lisp不要對其修飾的東西進(jìn)行求值(evaluate)。但假如沒有這個(gè)操作符 會(huì)怎樣呢?后面我們將看到會(huì)怎樣。
Symbol本質(zhì)
<ANSI Common Lisp>一書中有句話真正地揭示了Symbol的本質(zhì): Symbols are real objects 。是的,Symbols是對象,這個(gè)對象就像我們理解的C++中的對象一樣,它是一個(gè) 復(fù)合的數(shù)據(jù)結(jié)構(gòu)。該數(shù)據(jù)結(jié)構(gòu)里包含若干域,或者通俗而言:數(shù)據(jù)成員。借用<ANSI Common Lisp>中的一圖:
通過這幅圖,可以揭開所有謎底。一個(gè)Symbol包含至少圖中的幾個(gè)域,例如Name、Value、 Function等。在Lisp中有很多函數(shù)來訪問這些域,例如上文中使用到的symbol-name,這個(gè) 函數(shù)本質(zhì)上就是取出一個(gè)Symbol的Name域。
Symbol與Variable和Function的聯(lián)系
自然而然地,翻閱Lisp文檔,我們會(huì)發(fā)現(xiàn)果然還有其他函數(shù)來訪問Symbol的其他域,例如:
symbol-function symbol-value symbol-package symbol-plist
但是這些又與上文提到的變量和函數(shù)有什么聯(lián)系呢?真相只有一個(gè), 變量、函數(shù)粗略來 說就是Symbol的一個(gè)域,一個(gè)成員。變量對應(yīng)Value域,函數(shù)對應(yīng)Function域。一個(gè)Symbol 這些域有數(shù)據(jù)了,我們說它們發(fā)生了綁定(bind)。 而恰好,我們有幾個(gè)函數(shù)可以用于判 定這些域是否被綁定了值:
boundp ;判定Value域是否被綁定 fboundp;判定Function域是否被綁定
通過一些代碼來回味以上結(jié)論:
(defvar *var* 1) (boundp '*var*) ; 返回真 (fboundp '*var*) ; 返回假 (defun *var* (x) x) ; 定義一個(gè)名為*var*的函數(shù),返回值即為參數(shù) (fboundp '*var*) ; 返回真
上面的代碼簡直揭秘了若干驚天地泣鬼神的真相。首先,我們使用我們熟知的defvar定義了 一個(gè)名為 *var* 的變量,初值為1,然后使用boundp去判定 *var* 的Value域是否 發(fā)生了綁定。這其實(shí)是說: 原來定義變量就是定義了一個(gè)Symbol,給變量賦值,原來就 是給Symbol的Value域賦值!
其實(shí),Lisp中所有這些符號,都是Symbol。 什么變量,什么函數(shù),都是浮云。上面的 例子中,緊接著用fboundp判斷Symbol *var* 的Function域是否綁定,這個(gè)時(shí)候?yàn)榧佟? 然后我們定義了一個(gè)名為 *var* 的函數(shù),之后再判斷,則已然為真。這也是為什么, 在Lisp中某個(gè)函數(shù)可以和某個(gè)變量同名的原因所在。 從這段代碼中我們也可以看出 defvar/defun這些操作符、宏所做事情的本質(zhì)。
More More More
事情就這樣結(jié)束了?Of course not。還有很多上文提到的疑惑沒有解決。首先,Symbol是 如此復(fù)雜,那么Lisp如何決定它在不同環(huán)境下的含義呢?Symbol雖然是個(gè)對象,但它并不像 C++中的對象一樣,它出現(xiàn)時(shí)并不指代自己!不同應(yīng)用環(huán)境下,它指代的東西也不一樣。這 些指代主要包括變量和函數(shù),意思是說: Symbol出現(xiàn)時(shí),要么指的是它的Value,要么是 它的Function。 這種背地里干的事情,也算是造成迷惑的一個(gè)原因。
當(dāng)一個(gè)Symbol出現(xiàn)在一個(gè)List的第一個(gè)元素時(shí),它被處理為函數(shù)。這么說有點(diǎn)迷惑人,因?yàn)? 它帶進(jìn)了Lisp中代碼和數(shù)據(jù)之間的模糊邊界特性。簡單來說,就是當(dāng)Symbol出現(xiàn)在一個(gè)括號 表達(dá)式(s-expression)中第一個(gè)位置時(shí),算是個(gè)函數(shù),例如:
(add2 3) ; add2位于第一個(gè)位置,被當(dāng)作函數(shù)處理 (*var* 3) ; 這里*var*被當(dāng)作函數(shù)調(diào)用,返回3
除此之外,我能想到的其他大部分情況,一個(gè)Symbol都被指代為它的Value域,也就是被當(dāng) 作變量,例如:
(*var* *var*) ; 這是正確的語句,返回1
這看起來是多么古怪的代碼。但是運(yùn)用我們上面說的結(jié)論,便可輕易解釋:表達(dá)式中第一個(gè) *var* 被當(dāng)作函數(shù)處理,它需要一個(gè)參數(shù);表達(dá)式第二部分的 *var* 被當(dāng)作變量 處理,它的值為1,然后將其作為參數(shù)傳入。
再來說說'(quote)操作符,這個(gè)操作符用于防止其操作數(shù)被求值。而當(dāng)一個(gè)Symbol出現(xiàn)時(shí), 它總是會(huì)被求值,所以,我們可以分析以下代碼:
(symbol-value *var*) ; wrong
這個(gè)代碼并不正確,因?yàn)?*var* 總是會(huì)被求值,就像 (*var* *var*) 一樣,第二 個(gè) *var* 被求值,得到數(shù)字1。這里也會(huì)發(fā)生這種事情,那么最終就等同于:
(symbol-value 1) ; wrong
我們試圖去取數(shù)字1的Value域,而數(shù)字1并不是一個(gè)Symbol。所以,我們需要quote運(yùn)算符:
(symbol-value '*var*) ; right
這句代碼是說,取Symbol *var* 本身的Value域!而不是其他什么地方。至此,我們 便可以分析以下復(fù)雜情況:
(defvar *name* "kevin lynx") (defvar *ref* '*name*) ; *ref*的Value保存的是另一個(gè)Symbol (symbol-value *ref*) ; 取*ref*的Value,得到*name*,再取*name*的Value
現(xiàn)在,我們甚至能解釋上文留下的一個(gè)問題:
;; wrong (defvar fn #' (lambda (x) (+ x 2))) (fn 3)
給fn的Value賦值一個(gè)函數(shù), (fn 3) 當(dāng)一個(gè)Symbol作為函數(shù)使用時(shí),也就是取其 Function域來做調(diào)用。但其Function域什么也沒有,我們試圖將一個(gè)Symbol的Value域當(dāng)作 Function來使用。如何解決這個(gè)問題?想想,symbol-function可以取到一個(gè)Symbol的 Function域:
(setf (symbol-function 'fn) #' (lambda (x) (+ x 2))) (fn 3)
通過顯示地給fn的Function域賦值,而不是通過defvar隱式地對其Value域賦值,就可以使 (fn 3) 調(diào)用正確。還有另一個(gè)問題也能輕易解釋:
(apply-fn add2 2) ; wrong
本意是想傳入add2這個(gè)Symbol的function域,但是直接這樣寫的話,傳入的其實(shí)是add2的 Value域 [7] ,這當(dāng)然是不正確的。對比正確的寫法,我們甚至能猜測#'運(yùn)算符就是一個(gè) 取Symbol的Function域的運(yùn)算符。進(jìn)一步,我們還可以給出另一種寫法:
(apply-fn (symbol-function 'add2) 2)
深入理解事情的背后,你會(huì)發(fā)現(xiàn)你能寫出多么靈活的代碼。
END
關(guān)于Symbol的內(nèi)容還有更多,例如Package。正確理解這些內(nèi)容以及他們之間的關(guān)系,有助 于更深刻地理解Lisp。
posted on 2011-03-22 11:33 Kevin Lynx 閱讀(5236) 評論(8) 編輯 收藏 引用 所屬分類: lisp