您好,登錄后才能下訂單哦!
今天小編給大家分享一下Lisp實例分析的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
Lisp有豐富的內置數據類型, 其中的整數和字符串和其他語言沒什么分別。像71或者”hello”這樣的值, 含義也和C++或者Java這樣的語言大體相同。真正有意思的三種類型是符號(symbol), 表和函數。這一章的剩余部分, 我都會用來介紹這幾種類型, 還要介紹Lisp環境是怎樣編譯和運行源碼的。這個過程用Lisp的術語來說通常叫做求值。通讀這一節內容, 對于透徹理解元編程的真正潛力, 以及代碼和數據的同一性, 和面向領域語言的觀念, 都極其重要。萬勿等閑視之。我會盡量講得生動有趣一些, 也希望你能獲得一些啟發。那好, 我們先講符號。
大體上, 符號相當于C++或Java語言中的標志符, 它的名字可以用來訪問變量值(例如currentTime, arrayCount, n, 等等), 差別在于, Lisp中的符號更加基本。在C++或Java里面, 變量名只能用字母和下劃線的組合, 而Lisp的符號則非常有包容性, 比如, 加號(+)就是一個合法的符號, 其他的像-, =, hello-world, *等等都可以是符號名。符號名的命名規則可以在網上查到。你可以給這些符號任意賦值, 我們這里先用偽碼來說明這一點。假定函數set是給變量賦值(就像等號=在C++和Java里的作用), 下面是我們的例子:
set
(test,
5
)
/
/
符號test的值為
5
set
(
=
,
5
)
/
/
符號
=
的值為
5
set
(test,
"hello"
)
/
/
符號test的值為字符串
"hello"
set
(test,
=
)
/
/
此時符號
=
的值為
5
, 所以test的也為
5
set
(
*
,
"hello"
)
/
/
符號
*
的值為
"hello"
好像有什么不對的地方? 假定我們對*賦給整數或者字符串值, 那做乘法時怎么辦? 不管怎么說, *總是乘法呀? 答案簡單極了。Lisp中函數的角色十分特殊, 函數也是一種數據類型, 就像整數和字符串一樣, 因此可以把它賦值給符號。乘法函數Lisp的內置函數, 默認賦給*, 你可以把其他函數賦值給*, 那樣*就不代表乘法了。你也可以把這函數的值存到另外的變量里。我們再用偽碼來說明一下:
3
,
4
)
/
/
3
乘
4
, 結果是
12
set
(temp,
*
)
/
/
把
*
的值, 也就是乘法函數, 賦值給temp
set
(
*
,
3
)
/
/
把
3
賦予
*
*
(
3
,
4
)
/
/
錯誤的表達式,
*
不再是乘法, 而是數值
3
temp(
3
,
4
)
/
/
temp是乘法函數, 所以此表達式的值為
3
乘
4
等于
12
set
(
*
, temp)
/
/
再次把乘法函數賦予
*
*
(
3
,
4
)
/
/
3
乘
4
等于
12
再古怪一點, 把減號的值賦給加號:
set
(
+
,
-
)
/
/
減號(
-
)是內置的減法函數
+
(
5
,
4
)
/
/
加號(
+
)現在是代表減法函數, 結果是
5
減
4
等于
1
這只是舉例子, 我還沒有詳細講函數。Lisp中的函數是一種數據類型, 和整數, 字符串,符號等等一樣。一個函數并不必然有一個名字, 這和C++或者Java語言的情形很不相同。在這里函數自己代表自己。事實上它是一個指向代碼塊的指針, 附帶有一些其他信息(例如一組參數變量)。只有在把函數賦予其他符號時, 它才具有了名字, 就像把一個數值或字符串賦予變量一樣的道理。你可以用一個內置的專門用于創建函數的函數來創建函數,然后把它賦值給符號fn, 用偽碼來表示就是:
fn [a]
{
return
*(a,
2
);
}
這段代碼返回一個具有一個參數的函數, 函數的功能是計算參數乘2的結果。這個函數還沒有名字, 你可以把此函數賦值給別的符號:
set
(times
-
two, fn [a] {
return
*
(a,
2
)})
我們現在可以這樣調用這個函數:
time-two(
5
)
// 返回10
我們先跳過符號和函數, 講一講表。什么是表? 你也許已經聽過好多相關的說法。表, 一言以蔽之, 就是把類似XML那樣的數據塊, 用s表達式來表示。表用一對括號括住, 表中元素以空格分隔, 表可以嵌套。例如(這回我們用真正的Lisp語法, 注意用分號表示注釋):
() ; 空表
(
1
) ; 含一個元素的表
(
1
"test"
) ; 兩元素表, 一個元素是整數
1
, 另一個是字符串
(test
"hello"
) ; 兩元素表, 一個元素是符號, 另一個是字符串
(test (
1
2
)
"hello"
) ; 三元素表, 一個符號test, 一個含有兩個元素
1
和
2
的
; 表, 最后一個元素是字符串
當Lisp系統遇到這樣的表時, 它所做的, 和Ant處理XML數據所做的, 非常相似, 那就是試圖執行它們。其實, Lisp源碼就是特定的一種表, 好比Ant源碼是一種特定的XML一樣。Lisp執行表的順序是這樣的, 表的第一個元素當作函數, 其他元素當作函數的參數。如果其中某個參數也是表, 那就按照同樣的原則對這個表求值, 結果再傳遞給最初的函數作為參數。這就是基本原則。我們看一下真正的代碼:
(
*
3
4
) ; 相當于前面列舉過的偽碼
*
(
3
,
4
), 即計算
3
乘
4
(times
-
two
5
) ; 返回
10
, times
-
two按照前面的定義是求參數的
2
倍
(
3
4
) ; 錯誤,
3
不是函數
(time
-
two) ; 錯誤, times
-
two要求一個參數
(times
-
two
3
4
) ; 錯誤, times
-
two只要求一個參數
(
set
+
-
) ; 把減法函數賦予符號
+
(
+
5
4
) ; 依據上一句的結果, 此時
+
表示減法, 所以返回
1
(
*
3
(
+
2
2
)) ;
2
+
2
的結果是
4
, 再乘
3
, 結果是
12
上述的例子中, 所有的表都是當作代碼來處理的。怎樣把表當作數據來處理呢? 同樣的,設想一下, Ant是把XML數據當作自己的參數。在Lisp中, 我們給表加一個前綴’來表示數據。
(
set
test '(
1
2
)) ; test的值為兩元素表
(
set
test (
1
2
)) ; 錯誤,
1
不是函數
(
set
test '(
*
3
4
)) ; test的值是三元素表, 三個元素分別是
*
,
3
,
4
我們可以用一個內置的函數head來返回表的第一個元素, tail函數來返回剩余元素組成的表。
(head '(
*
3
4
)) ; 返回符號
*
(tail '(
*
3
4
)) ; 返回表(
3
4
)
(head (tal '(
*
3
4
))) ; 返回
3
(head test) ; 返回
*
你可以把Lisp的內置函數想像成Ant的任務。差別在于, 我們不用在另外的語言中擴展Lisp(雖然完全可以做得到), 我們可以用Lisp自己來擴展自己, 就像上面舉的times-two函數的例子。Lisp的內置函數集十分精簡, 只包含了十分必要的部分。剩下的函數都是作為標準庫來實現的。
Lisp宏
我們已經看到, 元編程在一個類似jsp的模板引擎方面的應用。我們通過簡單的字符串處理來生成代碼。但是我們可以做的更好。我們先提一個問題, 怎樣寫一個工具, 通過查找目錄結構中的源文件來自動生成Ant腳本。
用字符串處理的方式生成Ant腳本是一種簡單的方式。當然, 還有一種更加抽象, 表達能力更強, 擴展性更好的方式, 就是利用XML庫在內存中直接生成XML節點, 這樣的話內存中的節點就可以自動序列化成為字符串。不僅如此, 我們的工具還可以分析這些節點, 對已有的XML文件做變換。通過直接處理XML節點。我們可以超越字符串處理, 使用更高層次的概念, 因此我們的工作就會做的更快更好。
我們當然可以直接用Ant自身來處理XML變換和制作代碼生成工具。或者我們也可以用Lisp來做這項工作。正像我們以前所知的, 表是Lisp內置的數據結構, Lisp含有大量的工具來快速有效的操作表(head和tail是最簡單的兩個)。而且, Lisp沒有語義約束, 你可以構造任何數據結構, 只要你原意。
Lisp通過宏(macro)來做元編程。我們寫一組宏來把任務列表(to-do list)轉換為專用領域語言。
回想一下上面to-do list的例子, 其XML的數據格式是這樣的:
<
todo
name
=
"housework"
>
<
item
priority
=
"high"
>Clean the hose</
item
>
<
item
priority
=
"medium"
>Wash the dishes</
item
>
<
item
priority
=
"medium"
>Buy more soap</
item
>
</
todo
>
相應的s表達式是這樣的:
(todo
"housework"
(item (priority high)
"Clean the house"
)
(item (priority medium)
"Wash the dishes"
)
(item (priority medium)
"Buy more soap"
))
假設我們要寫一個任務表的管理程序, 把任務表數據存到一組文件里, 當程序啟動時, 從文件讀取這些數據并顯示給用戶。在別的語言里(比如說Java), 這個任務該怎么做? 我們會解析XML文件, 從中得出任務表數據, 然后寫代碼遍歷XML樹, 再轉換為Java的數據結構(老實講, 在Java里解析XML真不是件輕松的事情), 最后再把數據展示給用戶。現在如果用Lisp, 該怎么做?
假定要用同樣思路的化, 我們大概會用Lisp庫來解析XML。XML對我們來說就是一個Lisp的表(s表達式), 我們可以遍歷這個表, 然后把相關數據提交給用戶。可是, 既然我們用Lisp, 就根本沒有必要再用XML格式保存數據, 直接用s表達式就好了, 這樣就沒有必要做轉換了。我們也用不著專門的解析庫, Lisp可以直接在內存里處理s表達式。注意, Lisp編譯器和.net編譯器一樣, 對Lisp程序來說, 在運行時總是隨時可用的。
但是還有更好的辦法。我們甚至不用寫表達式來存儲數據, 我們可以寫宏, 把數據當作代碼來處理。那該怎么做呢? 真的簡單。回想一下, Lisp的函數調用格式:
(function
-
name arg1 arg2 arg3)
其中每個參數都是s表達式, 求值以后, 傳遞給函數。如果我們用(+ 4 5)來代替arg1,那么, 程序會先求出結果, 就是9, 然后把9傳遞給函數。宏的工作方式和函數類似。主要的差別是, 宏的參數在代入時不求值。
(macro
-
name (
+
4
5
))
這里, (+ 4 5)作為一個表傳遞給宏, 然后宏就可以任意處理這個表, 當然也可以對它求值。宏的返回值是一個表, 然后有程序作為代碼來執行。宏所占的位置, 就被替換為這個結果代碼。我們可以定義一個宏把數據替換為任意代碼, 比方說, 替換為顯示數據給用戶的代碼。
這和元編程, 以及我們要做的任務表程序有什么關系呢? 實際上, 編譯器會替我們工作,調用相應的宏。我們所要做的, 僅僅是創建一個把數據轉換為適當代碼的宏。
例如, 上面曾經將過的C的求三次方的宏, 用Lisp來寫是這樣子:
(defmacro triple (x)
`(
+
~x ~x ~x))
(譯注: 在Common Lisp中, 此處的單引號應當是反單引號, 意思是對表不求值, 但可以對表中某元素求值, 記號~表示對元素x求值, 這個求值記號在Common Lisp中應當是逗號。反單引號和單引號的區別是, 單引號標識的表, 其中的元素都不求值。這里作者所用的記號是自己發明的一種Lisp方言Blaise, 和common lisp略有不同, 事實上, 發明方言是lisp高手獨有的樂趣, 很多狂熱分子都熱衷這樣做。比如Paul Graham就發明了ARC, 許多記號比傳統的Lisp簡潔得多, 顯得比較現代)
單引號的用處是禁止對表求值。每次程序中出現triple的時候,
(triple
4
)
都會被替換成:
(
+
4
4
4
)
我們可以為任務表程序寫一個宏, 把任務數據轉換為可執行碼, 然后執行。假定我們的輸出是在控制臺:
(defmacro item (priority note)
`(block
(
stdout tab
"Prority: "
~(head (tail priority)) endl)
(
stdout tab
"Note: "
~note endl endl)))
我們創造了一個非常小的有限的語言來管理嵌在Lisp中的任務表。這個語言只用來解決特定領域的問題, 通常稱之為DSLs(特定領域語言, 或專用領域語言)。
以上就是“Lisp實例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。