newLISP/print 簡介
歡迎閱讀本 newLISP 簡介!您會發現 newLISP 易於學習且功能強大,它將經典 LISP 的一些強大功能和優雅性與現代指令碼語言的功能(如正則表示式、網路功能、Unicode 支援、多工處理等)結合在一起。
本書是對該語言基礎知識的簡單直觀的描述。您應該熟悉使用文字編輯器,理想情況下,您之前應該做過一些指令碼編寫,但不需要之前的程式設計經驗或 LISP 知識。我希望這足以讓您入門,並準備好開始探索 newLISP 的真正力量。
我在 MacOS X 系統上編寫了這篇文章,但如果您使用的是 Linux、Windows 或 newLISP 支援的眾多其他平臺之一,它不應該有任何區別。這些示例旨在在新版本 10 中執行。
這是一份非官方文件 - 有關 newLISP 的官方和權威描述,請參閱與軟體一起安裝的出色參考手冊。
- 主要的 newLISP 網站,newlisp.org,提供有用的論壇、程式碼示例和文件以及最新的 newLISP 軟體
- Noodles 上的 newLISP 維基,newlisp-on-noodles.org
- 約翰·斯莫爾(John Small)在 newLISP in 21 minutes 上釋出了精彩的 21 分鐘介紹,newLISP in 21 minutes
- 精美的 newLISP 蜻蜓標誌由布萊恩·格雷勒斯(Brian Grayless)設計,由 11 對括號構成(fudnik.com)
感謝所有為本文件早期版本做出貢獻的人,他們提出了建議或發現了錯誤。繼續加油。
安裝說明可在 newlisp.org 中找到。
安裝完 newLISP 後,您可以透過多種方式執行它。有關完整詳細資訊,請參閱 newLISP 文件。最直接的方式是透過在命令列(在控制檯或終端視窗中)鍵入 newlisp 命令來執行 newLISP 直譯器。
$ newlisp newLISP v.x on OSX IPv4 UTF-8, execute 'newlisp -h' for more info. >
這對於嘗試短表示式、測試想法和除錯非常有用。您可以在此環境中透過將程式碼行括在 [cmd] 和 [/cmd] 之間來編寫多行程式碼。在新版本中,您只需在空白行上使用 回車鍵 來開始和結束多行塊。
>[cmd]
(define (fibonacci n)
(if (< n 2)
1
(+ (fibonacci (- n 1))
(fibonacci (- n 2)))))
[/cmd]
>(fibonacci 10)
89
>
newLISP 的視覺化介面 newLISP-GS 為 newLISP 應用程式提供了一個圖形工具包,並且還為您提供了用於編寫和測試程式碼的開發環境:newLISP 編輯器。在 Windows 上,它安裝為桌面圖示和“程式啟動”選單中的資料夾。在 MacOS X 上,應用程式包和圖示安裝在“應用程式”資料夾中。newLISP-GS 編輯器為您提供多個選項卡式視窗、語法著色和用於檢視執行程式碼結果的監視器區域。
您還可以從命令列執行 newLISP-GS 編輯器:您可以在 C:/Program Files/newlisp/newlisp-edit(Windows)或 /usr/bin/newlisp-edit(在 Unix 上)找到該檔案。(這是一個 newLISP 原始檔,所以您也可以檢視程式碼。)
您可以在您最喜歡的文字編輯器中編輯 newLISP 指令碼。在 MacOS X 上,您可以使用 BBEdit、TextWrangler 或 TextMate 執行 newLISP 指令碼,或者您可以使用預安裝的 Unix 文字編輯器,例如 vim 或 emacs。在 Windows 上,您可以使用 UltraEdit、EditPlus 或 NotePad++,僅舉幾例。如果您使用的是 Linux,那麼您比我更瞭解文字編輯器,並且您可能已經有了自己的偏好。
newLISP 網站在 http://newlisp.org/index.cgi?Code_Contributions 上託管了一些流行編輯器的配置檔案。
在 Unix 上,newLISP 指令碼的第一行應該是
#!/usr/local/bin/newlisp
或
#!/usr/bin/env newlisp
如果您希望在外部文字編輯器中執行 newLISP,您必須使用更多 println 函式來檢視每個函式或表示式返回的值。
通常,您使用 exit 函式結束 newLISP 指令碼或控制檯會話
(exit)
您只需要學習三條基本規則就可以用 newLISP 程式設計。以下是第一條規則
規則 1:列表是元素的序列
[edit | edit source]列表是括號中包含的一系列元素。
(1 2 3 4 5) ; a list of integers
("the" "cat" "sat") ; a list of strings
(x y z foo bar) ; a list of symbol names
(sin cos tan atan) ; a list of newLISP functions
(1 2 "stitch" x sin) ; a mixed list
(1 2 (1 2 3) 3 4 ) ; a list with a list inside it
((1 2) (3 4) (5 6)) ; a list of lists
列表是newLISP中的基本資料結構,也是編寫程式程式碼的方式。但現在先不要輸入這些示例 - 還有兩條規則需要學習!
規則 2:列表中的第一個元素很特殊
[edit | edit source]當newLISP看到列表時,它將第一個元素視為函式,然後嘗試使用剩餘的元素作為函式所需的資訊。
(+ 2 2)
這是一個包含三個元素的列表:名為 + 的函式,後面跟著兩個數字。當newLISP看到這個列表時,它會對其進行評估並返回 4 的值(當然)。請注意,第一個元素被newLISP視為函式,而其餘元素被解釋為該函式的引數 - 函式期望的數字。
以下是一些更多示例,演示了這兩條規則。
(+ 1 2 3 4 5 6 7 8 9)
返回 45。 + 函式將列表中所有數字加起來。
(max 1 1.2 12.1 12.2 1.3 1.2 12.3)
返回 12.3,列表中最大的數字。同樣,列表的長度沒有(合理的)限制:如果一個函式可以接受 137 個專案(max 和 + 都可以),那麼你可以傳遞 137 個專案給它。
(print "the sun has put his hat on")
"the sun has put his hat on"
列印字元 the sun has put his hat on 的字串。(它也返回字串,這就是為什麼,當你在控制檯中工作時,有時你會看到事物重複出現兩次)。 print 函式可以列印單個字元字串,或者你可以提供一系列元素來列印。
(print 1 2 "buckle" "my" "shoe")
12bucklemyshoe
列印兩個數字和三個字串(儘管格式不是很好,因為你還沒有遇到 format 函式)。
directory 函式
(directory "/")
生成指定目錄的列表,在本例中是根目錄 "/"
("." ".." ".DS_Store" ".hotfiles.btree" ".Spotlight-V100"
".Trashes"".vol" ".VolumeIcon.icns" "Applications"
"automount" "bin" "cores" "Desktop DB" "Desktop DF"
"Desktop Folder" "dev""Developer" "etc" "Library"
"mach" "mach.sym" "mach_kernel" "Network" "private"
"sbin" "System" "System Folder" "TheVolumeSettingsFolder"
"tmp" "User Guides And Information" "Users" "usr"
"var" "Volumes")
如果你沒有指定目錄,它將列出當前目錄。
(directory)
("." ".." "2008-calendar.lsp" "allelements.lsp" "ansi.lsp"
"binary-clock.lsp" ... )
有一個 read-file 函式可以讀取文字檔案的內容。
(read-file "/usr/share/newlisp/modules/stat.lsp")
這裡函式需要一個引數 - 檔名 - 並將檔案的內容以字串形式返回給你。
這些是newLISP程式碼構建塊的典型示例 - 一個包含函式呼叫的列表,後面可能跟著函式所需的任何額外資訊。newLISP有超過 380 個函式,你可以參考優秀的newLISP參考手冊,詳細瞭解所有函式以及如何使用它們。
你可以嘗試這些示例。如果你在終端使用newLISP,只需輸入它們即可。如果你將這些行輸入文字編輯器並將其作為指令碼執行,除非將表示式括在 println 函式中,否則你不會看到函式呼叫的結果。例如,鍵入
(println (read-file "/usr/share/newlisp/modules/stat.lsp"))
以列印 read-file 函式的結果。
每個newLISP表示式都會返回一個值。即使 println 函式也會返回一個值。你可以說列印操作實際上只是一個副作用,它的主要任務是返回一個值。你可能注意到,當你在控制檯視窗中互動使用 println 時,你會看到返回值兩次:一次是在列印時,另一次是在將值返回給呼叫函式(在本例中是最頂層)時。
在你遇到第三條規則之前,還有一件有用的事情要看看。
巢狀列表
[edit | edit source]你已經發現了一個列表巢狀在另一個列表中。這是一個例子
(* (+ 1 2) (+ 3 4))
當newLISP看到這一點時,它會這樣 思考
嗯,讓我們從第一個內部列表開始。我可以做到
(+ 1 2)
很容易。它的值為 3。我也可以做第二個列表
(+ 3 4)
很容易。它評估為 7。
所以,如果我用這些值替換這兩個內部列表,我得到
(* 3 7)
這真的很容易。我將為這個表示式返回 21 的值。
(* (+ 1 2) (+ 3 4))
(* 3 (+ 3 4))
(* 3 7)
21
看到第一行末尾的兩個右括號,緊隨其後是 4 嗎?兩者都是必不可少的:第一個括號結束 (+ 3 4 列表,第二個括號結束以 (* 開始的乘法運算。當你開始編寫更復雜的程式碼時,你會發現你將列表放在列表中,再放在列表中,再放在列表中,你可能會用六個右括號結束一些更復雜的定義。一個好的編輯器會幫助你跟蹤它們。
但你無需擔心空白、行終止符、各種標點符號或強制縮排。而且因為所有資料和程式碼都以相同的方式儲存在列表中,所以你可以隨意混合它們。稍後將詳細介紹。
有些人第一次看到LISP程式碼時,會擔心括號氾濫。其他人稱它們為 指甲剪 或者說LISP代表 大量煩人的愚蠢括號。但我更願意將括號視為包含newLISP 思想 的小 把手
當你在好的編輯器中編輯newLISP程式碼時,你可以透過 抓住把手 來輕鬆地移動或編輯一個想法,並且可以使用平衡括號命令輕鬆地選擇一個想法。你很快就會發現括號比你最初想象的更有用!
引用阻止評估
[edit | edit source]現在你可以學習使用newLISP程式設計的第三條規則。
規則 3:引用阻止評估
[edit | edit source]要阻止newLISP評估某件事,請對其進行引用。
比較這兩行
(+ 2 2)
'(+ 2 2)
第一行是一個包含函式和兩個數字的列表。在第二行中,列表被引用 - 在前面有一個單引號或撇號(')。你不需要在結束括號後面再加一個引號,因為一個就夠了。
> (+ 2 2)
4
> '(+ 2 2)
(+ 2 2)
>
對於第一個表示式,newLISP像往常一樣工作,並且熱心地評估列表,返回數字 4。但對於第二個表示式,一旦它看到引號,newLISP甚至不會考慮透過加法來評估列表;它只是返回列表,沒有評估。
這個引號在newLISP中起著與英文書寫中開合引號相同的作用 - 它們通知讀者,該詞或短語不要按正常方式解釋,而要以某種特殊方式處理:非標準或諷刺的含義,也許是另一個人說的話,或者是不應字面理解的東西。
那麼,為什麼你想要阻止newLISP評估事物呢?你很快就會遇到一些示例,其中你引用事物是為了阻止newLISP認為列表中的第一項是函式。例如,當你將資訊儲存在列表中時,你不希望newLISP以通常的方式評估它們。
(2006 1 12) ; today's year/month/date
("Arthur" "J" "Chopin") ; someone's full name
你不希望newLISP尋找名為 2006 或 "Arthur" 的函式。此外,2006 不是有效的函式名,因為它以數字開頭,函式名不能以雙引號開頭,因此無論哪種情況,你的程式都會停止並出現錯誤。因此,你需要 引用 列表,以阻止它們的第一個元素被用作函式而不是資料。
'(2006 1 12) ; evaluates to (2006 1 12)
'("Arthur" "J" "Chopin") ; evaluates to ("Arthur" "J" "Chopin")
newLISP將表示式視為資料 - 以及將資料視為表示式 - 的能力將在稍後詳細討論。
使用垂直撇號(ASCII 程式碼 39)來引用列表和符號。有時,文字編輯器或其他程式會將這些簡單的垂直撇號更改為花括號引號。這些引號不起作用,因此你必須將任何 智慧引號 更改為垂直撇號。
符號和引用
[edit | edit source]符號是newLISP 事物,它有一個名稱。你在程式碼中定義某件事併為其分配一個名稱。然後,你可以使用名稱而不是內容來引用該事物。例如,在輸入以下內容後
(set 'alphabet "abcdefghijklmnopqrstuvwxyz")
現在有一個名為 alphabet 的新符號,其值為由字母表 26 個字母組成的字串。 set 函式將從 a 到 z 的字元儲存在 alphabet 符號中。現在這個符號可以在其他地方使用,並且在使用時會評估為字母表。每當你想要使用字母表 26 個字母時,請使用這個符號,不要對其進行引用。例如,這是 upper-case 函式
(upper-case alphabet)
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
我使用符號而不引用它,因為我想要newLISP使用符號的值,而不是它的名稱。我實際上對將 alphabet 這個詞大寫並不感興趣,而是對字母表本身感興趣。newLISP在本例中沒有永久改變符號的值,因為 upper-case 總是建立並返回一個新的字串,保留儲存在符號中的那個字串不變。
符號對應於其他程式語言中的變數。事實上,newLISP 使用符號的頻率並不像其他語言使用變數那樣頻繁。部分原因是值會不斷地由表示式返回,並直接輸入到其他表示式中,而不會被儲存。例如,在以下程式碼中,每個函式都將結果直接傳遞給下一個**包含**函式。
(println (first (upper-case alphabet)))
"A"
**upper-case** 將返回值直接傳遞給 **first**,**first** 將返回值直接傳遞給 **println**,**println** 既列印該值,又將它列印的字串作為返回值提供給你。因此,減少了臨時儲存值的必要性。但是,在很多其他地方,你確實需要用到符號。
以下再舉兩個關於符號引用的示例。
(define x (+ 2 2 ))
(define y '(+ 2 2))
在第一個示例中,我沒有引用 (+ 2 2) 列表 - newLISP 將其計算為 4,然後將 4 賦值給符號 x,x 計算為 4。
x
;-> 4
在第二個示例中,我引用了該列表。這意味著符號 y 現在持有的是列表而不是數字。每當 newLISP 遇到符號 y 時,它都會返回該列表,而不是 4。(當然,除非你也先引用 y!)
y
;-> (+ 2 2)
'y
;-> y
順便說一下,在本檔案中
; the semicolon is the comment character
;-> that ";->" is my way of saying "the value is"
以及 newLISP 直譯器列印的輸出通常以以下方式顯示
like this
設定和定義符號
[edit | edit source]有幾種方法可以建立和設定符號的值。你可以使用 **define** 或 **set**,如下所示。
(set 'x (+ 2 2))
;-> 4
(define y (+ 2 2))
;-> 4
**set** 期望後面跟著一個符號,但會先計算第一個引數。因此,你應該引用符號以防止其被計算(因為它可能計算為除符號以外的值),或者提供一個計算為符號的表示式。**define** 不期望引數被引用。
你也可以使用 **setf** 和 **setq** 來設定符號的值。這些函式期望第一個引數為符號或符號引用,因此你不必引用它。
(setf y (+ 2 2))
;-> 4
(setq y (+ 2 2))
;-> 4
這兩個函式(具有相同的作用)可以設定符號(變數)、列表、陣列或字串的內容。習慣上,在設定符號時使用 **setq**,在設定列表或陣列元素時使用 **setf**。
**define** 也用於定義函式。請參閱 建立自己的函式。
破壞性函式
[edit | edit source]一些 newLISP 函式會修改其操作的符號的值,而另一些函式會建立值的副本並返回該副本。從技術上講,那些修改符號內容的函式被稱為 **破壞性** 函式 - 儘管你經常會使用它們來建立新資料。在本檔案中,我將描述諸如 **push** 和 **replace** 之類的函式為破壞性函式。這僅僅意味著它們更改了某事的值,而不是返回一個修改後的副本。
控制流
[edit | edit source]有許多不同的方法來控制程式碼的流程。如果你使用過其他指令碼語言,你可能在這裡會找到你喜歡的功能,以及更多其他功能。
所有控制流函式都遵循 newLISP 的標準規則。每個函式的一般形式通常是一個列表,其中第一個元素是關鍵字,後面跟著一個或多個要計算的表示式。
(keyword expression1 expression2 expression3 ...)
測試:if...
[edit | edit source]也許你在任何語言中能寫出的最簡單的控制結構就是一個簡單的 **if** 列表,它包含一個測試和一個動作。
(if button-pressed? (launch-missile))
第二個表示式,對 launch-missile 函式的呼叫,只有在符號 button-pressed? 計算為 **true** 時才被計算。1 是 true。0 是 true - 畢竟它是一個數字。-1 是 true。newLISP 已知的絕大多數事物都是 true。newLISP 知道只有兩件事是 false 而不是 true:**nil** 和空列表 ()。此外,newLISP 不知道值的任何事物都是 false。
(if x 1)
; if x is true, return the value 1
(if 1 (launch-missile))
; missiles are launched, because 1 is true
(if 0 (launch-missile))
; missiles are launched, because 0 is true
(if nil (launch-missile))
;-> nil, there's no launch, because nil is false
(if '() (launch-missile))
;-> (), and the missiles aren't launched
你可以使用任何計算為 true 或 false 的內容作為測試。
(if (> 4 3) (launch-missile))
;-> it's true that 4 > 3, so the missiles are launched
(if (> 4 3) (println "4 is bigger than 3"))
"4 is bigger than 3"
如果符號計算為 **nil**(也許是因為它不存在或未被賦值),newLISP 將其視為 false,並且測試將返回 **nil**(因為沒有提供替代操作)。
(if snark (launch-missile))
;-> nil ; that symbol has no value
(if boojum (launch-missile))
;-> nil ; can't find a value for that symbol
(if untrue (launch-missile))
;-> nil ; can't find a value for that symbol either
(if false (launch-missile))
;-> nil
; never heard of it, and it doesn't have a value
你可以新增第三個表示式,它是 **else** 操作。如果測試表達式計算為 **nil** 或 **()**,則計算第三個表示式,而不是第二個表示式,第二個表示式將被忽略。
(if x 1 2)
; if x is true, return 1, otherwise return 2
(if 1
(launch-missile)
(cancel-alert))
; missiles are launched
(if nil
(launch-missile)
(cancel-alert))
; alert is cancelled
(if false
(launch-missile)
(cancel-alert))
; alert is cancelled
以下是一個典型的實際三部分 **if** 函式,格式化以便儘可能清楚地顯示結構。
(if (and socket (net-confirm-request)) ; test
(net-flush) ; action when true
(finish "could not connect")) ; action when false
雖然測試後有兩個表示式 - (net-flush) 和 (finish ...) - 但只計算其中一個。
如果你不集中精力,你可能會在其他語言中找到的諸如 **then** 和 **else** 之類的熟悉 **指示詞** 的缺失會讓你措手不及!但是,你可以輕鬆地添加註釋。
你可以將 **if** 與任意數量的測試和操作一起使用。在這種情況下,**if** 列表由一系列測試-操作對組成。newLISP 會依次處理這些對,直到其中一個測試成功,然後計算該測試對應的操作。如果可以,請將列表格式化為多列,以使結構更清晰。
(if
(< x 0) (define a "impossible")
(< x 10) (define a "small")
(< x 20) (define a "medium")
(>= x 20) (define a "large")
)
如果你使用過其他 LISP 方言,你可能會認出這與 **cond**(條件函式)是一個簡單的替代方法。newLISP 也提供了傳統的 **cond** 結構。請參閱 選擇:if、cond 和 case。
你可能想知道如果測試成功或不成功,如何執行兩個或多個操作。有兩種方法可以做到這一點。你可以使用 **when**,它類似於沒有 'else' 部分的 **if**。
(when (> x 0)
(define a "positive")
(define b "not zero")
(define c "not negative"))
另一種方法是定義一個表示式塊,這些表示式構成一個單一表達式,你可以在 **if** 表示式中使用它。我將在 塊:表示式組 中簡要討論如何做到這一點。
之前,我說過當 newLISP 遇到一個列表時,它會將第一個元素視為一個函式。我還應該提到,它會在將第一個元素應用於引數之前先計算第一個元素。
(define x 1)
((if (< x 5) + *) 3 4) ; which function to use, + or *?
7 ; it added
這裡,表示式 (if (< x 5) + *) 的第一個元素,根據將 x 與 5 進行比較的測試結果,返回一個算術運算子。因此,整個表示式要麼是加法,要麼是乘法,具體取決於 x 的值。
(define x 10)
;-> 10
((if (< x 5) + *) 3 4)
12 ; it multiplied
這種技巧可以幫助你編寫簡潔的程式碼。與其寫成這樣:
(if (< x 5) (+ 3 4) (* 3 4))
你可以寫成這樣:
((if (< x 5) + *) 3 4)
計算結果如下:
((if (< x 5) + *) 3 4)
((if true + *) 3 4)
(+ 3 4)
7
注意每個表示式如何將值返回給包含函式。在 newLISP 中,**每個** 表示式都會返回某個值,即使是 **if** 表示式。
(define x (if flag 1 -1)) ; x is either 1 or -1
(define result
(if
(< x 0) "impossible"
(< x 10) "small"
(< x 20) "medium"
"large"))
x 的值取決於 **if** 表示式返回的值。現在符號 result 包含一個字串,具體取決於 x 的值。
迴圈
[edit | edit source]有時你希望重複執行一系列操作多次,在一個迴圈中迴圈執行。有多種可能性。你可能希望對
- 列表中的每個專案
- 字串中的每個專案
- 一定次數
- 直到發生某事
- 在某些條件成立的情況下
newLISP 為所有這些情況(以及更多情況)提供瞭解決方案。
遍歷列表
[edit | edit source]newLISP 程式設計師喜歡列表,因此 **dolist** 是一個非常有用的函式,它將一個區域性迴圈符號(變數)依次設定為列表中的每個專案,並在每個專案上執行一系列操作。將迴圈變數的名稱和要掃描的列表放在括號中,放在 **dolist** 之後,然後在後面新增操作。
在以下示例中,我在定義區域性迴圈變數 i 之前,還設定了另一個名為 counter 的符號,i 將儲存由 **sequence** 函式生成的數字列表中的每個值。
(define counter 1)
(dolist (i (sequence -5 5))
(println "Element " counter ": " i)
(inc counter)) ; increment counter by 1
Element 1: -5 Element 2: -4 Element 3: -3 Element 4: -2 Element 5: -1 Element 6: 0 Element 7: 1 Element 8: 2 Element 9: 3 Element 10: 4 Element 11: 5
請注意,與 **if** 不同的是,**dolist** 函式和許多其他控制詞允許你在一個接一個地編寫一系列表示式:這裡 **println** 和 **inc**(增量)函式都針對列表中的每個元素被呼叫。
有一個訪問系統維護的迴圈計數器的有用捷徑。我剛剛使用了一個計數器符號,每次迴圈都進行增量,以跟蹤我們在列表中遍歷到了什麼位置。但是,newLISP 會自動為你維護一個迴圈計數器,它在一個名為 $idx 的系統變數中,因此我可以省略計數器符號,只需每次迴圈都檢索 $idx 的值。
(dolist (i (sequence -5 5))
(println "Element " $idx ": " i))
Element 0: -5 Element 1: -4 Element 2: -3 Element 3: -2 Element 4: -1 Element 5: 0 Element 6: 1 Element 7: 2 Element 8: 3 Element 9: 4 Element 10: 5
在某些情況下,你可能更喜歡使用對映函式 **map** 來處理列表(稍後介紹 - 請參閱 應用和對映:將函式應用於列表)。**map** 可以用於將函式(現有的函式或臨時定義)應用於列表中的每個元素,而無需使用區域性變數遍歷列表。例如,讓我們使用 **map** 來生成與上述 **dolist** 函式相同的輸出。我定義了一個由兩個表示式組成的臨時 **print and increase** 函式,並將此函式應用於由 **sequence** 生成的列表中的每個元素。
(define counter 1)
(map (fn (i)
(println "Element " counter ": " i)
(inc counter))
(sequence -5 5))
經驗豐富的 LISP 程式設計師可能更熟悉 **lambda**。**fn** 是 **lambda** 的同義詞:使用哪一個都無所謂。
你可能還會發現 **flat** 對遍歷列表很有用,因為它透過複製來將包含巢狀列表的列表展平,以便更輕鬆地處理。
((1 2 3) (4 5 6))
例如,從
(1 2 3 4 5 6)
變為
例如。請參閱 flat。
要遍歷傳遞給函式的引數,可以使用 **doargs** 函式。請參閱 引數:args。
遍歷字串[edit | edit source]
(define alphabet "abcdefghijklmnopqrstuvwxyz")
(dostring (letter alphabet)
(print letter { }))
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
可以使用類似於 **dolist** 的 **dostring** 來遍歷字串中的每個字元。
一定次數
[edit | edit source]如果您想執行某個操作固定次數,請使用 dotimes 或 for。dotimes 會在列表主體中執行指定次數的操作。您應該為區域性變數提供一個名稱,就像您在 dolist 中所做的那樣。
(dotimes (c 10)
(println c " times 3 is " (* c 3)))
0 times 3 is 0 1 times 3 is 3 2 times 3 is 6 3 times 3 is 9 4 times 3 is 12 5 times 3 is 15 6 times 3 is 18 7 times 3 is 21 8 times 3 is 24 9 times 3 is 27
您必須為這些形式提供一個區域性變數。即使您沒有使用它,也必須提供一個。
請注意,計數從 0 開始,並持續到 n - 1,永遠不會真正達到指定的值。程式設計師認為這是合理的和合乎邏輯的;非程式設計師只需要習慣從 0 開始計數,並指定 10 來獲得 0 到 9。
記住這一點的一種方法是想想生日。您在完成第一年時慶祝您的第一個生日,在這段時間裡您是 0 歲。您在開始慶祝您的第 10 個生日時完成了您的前 10 年生活,這在您停止成為 9 歲時開始。newLISP 函式 first 獲取索引號為 0 的元素……
當您知道重複次數時使用 dotimes,但是當您想要 newLISP 根據開始、結束和步長值計算出應該執行多少次重複時使用 for。
(for (c 1 -1 .5)
(println c))
1 0.5 0 -0.5 -1
這裡 newLISP 足夠聰明,可以算出我想以 0.5 的步長從 1 降到 -1。
為了再次提醒您 從 0 開始計數,請比較以下兩個函式
(for (x 1 10) (println x))
1 ... 10
(dotimes (x 10) (println x))
0 ... 9
提供了一個逃生路線
[edit | edit source]for、dotimes 和 dolist 喜歡迴圈,一遍又一遍地執行一組操作。通常,重複會持續下去,直到達到限制 - 最後一個數字或列表中的最後一個專案。但是您可以在測試形式中指定一條緊急逃生路線,該路線將在下一個迴圈開始之前執行。如果此測試返回 true,則下一個迴圈不會啟動,並且表示式比平時更早結束。這為您提供了一種在正式的最後迭代之前停止的方法。
例如,假設您想要將列表中的每個數字減半,但(出於某種原因)如果其中一個數字是奇數,則想要停止。比較此 dolist 表示式的第一個和第二個版本
(define number-list '(100 300 500 701 900 1100 1300 1500))
; first version
(dolist (n number-list)
(println (/ n 2)))
50 150 250 350 450 550 650 750
; second version
(dolist (n number-list (!= (mod n 2) 0)) ; escape if true
(println (/ n 2)))
50 150 250
如果對 n 是否為奇數的測試 (!= (mod n 2) 0) 返回 true,則第二個版本將停止迴圈。
請注意這裡使用了僅限整數的除法。我在示例中使用了 / 而不是浮點除法運算子 div。如果您想要另一個運算子,請不要使用其中一個!
您也可以為 for 和 dotimes 提供逃生路線測試。
對於更復雜的流程控制,您可以使用 catch 和 throw。throw 將表示式傳遞給上一個 catch 表示式,該表示式將完成並返回表示式的值。
(catch
(for (i 0 9)
(if (= i 5) (throw (string "i was " i)))
(print i " ")))
輸出是
0 1 2 3 4
並且 catch 表示式返回 i was 5。
您也可以使用布林函式設計流程。請參閱 塊:表示式組。
直到某事發生,或當某事為真時
[edit | edit source]您可能有一個用於測試某個情況的測試,當發生有趣的事情時,該測試將返回 nil 或 (),否則將返回一個 true 值,您對此不感興趣。要重複執行一系列操作,直到測試失敗,請使用 until 或 do-until
(until (disk-full?)
(println "Adding another file")
(add-file)
(inc counter))
(do-until (disk-full?)
(println "Adding another file")
(add-file)
(inc counter))
這兩者的區別在於何時執行測試。在 until 中,首先進行測試,然後如果測試失敗,則評估主體中的操作。在 do-until 中,首先評估主體中的操作,然後進行測試,以檢視是否可以進行另一個迴圈。
這兩個程式碼片段中的哪一個正確?好吧,第一個在新增檔案之前測試磁碟的容量,但是第二個使用 do-until 的程式碼片段,直到新增檔案後才檢查可用磁碟空間,這不是很謹慎。
while 和 do-while 是 until 和 do-until 的互補對立,只要測試表達式保持為真,就重複一個塊。
(while (disk-has-space)
(println "Adding another file")
(add-file)
(inc counter))
(do-while (disk-has-space)
(println "Adding another file")
(add-file)
(inc counter))
選擇每個的 do- 變體,以便在評估測試之前執行操作塊。
塊:表示式組
[edit | edit source]許多 newLISP 控制函式允許您構建一個操作塊:一組表示式,這些表示式會一個接一個地依次評估。構造是隱式的:您無需執行任何操作,只需按正確的順序和位置寫入它們即可。檢視上面的 while 和 until 示例:每個示例都有三個將依次評估的表示式。
但是,您也可以使用 begin、or 和 and 函式顯式地建立表示式塊。
begin 在您想要顯式地將表示式組合在一起形成一個列表時很有用。每個表示式都會依次評估
(begin
(switch-on)
(engage-thrusters)
(look-in-mirror)
(press-accelerator-pedal)
(release-brake)
; ...
)
等等。您通常只在 newLISP 期望一個表示式時使用 begin。您不需要在 dotimes 或 dolist 構造中使用它,因為這些構造已經允許多個表示式。
塊中每個表示式的結果無關緊要,除非它糟糕到足以完全停止程式。返回 nil 是可以的
(begin
(println "so far, so good")
(= 1 3) ; returns nil but I don't care
(println "not sure about that last result"))
so far, so good not sure about that last result!
在 begin 的情況下,塊中每個表示式的返回值都會被丟棄;只有最後一個表示式的值會被返回,作為整個塊的值。但是對於另外兩個 block 函式 and 和 or,返回值很重要且有用。
and 和 or
[edit | edit source]and 函式遍歷表示式塊,但如果其中一個表示式返回 nil(false),則立即完成該塊。要到達 and 塊的末尾,每個表示式都必須返回一個 true 值。如果一個表示式失敗,則塊的評估將停止,newLISP 會忽略其餘表示式,返回 nil,以便您知道它沒有正常完成。
這是一個測試 disk-item 是否包含一個有用目錄的 and 示例
(and
(directory? disk-item)
(!= disk-item ".")
(!= disk-item "..")
; I get here only if all tests succeeded
(println "it looks like a directory")
)
disk-item 必須透過所有三個測試:它必須是一個目錄,它不能是 . 目錄,並且它不能是 .. 目錄(Unix 術語)。當它成功通過了這三個測試後,評估將繼續,並且訊息被打印出來。如果其中一個測試失敗,則塊將完成而不會列印訊息。
您也可以對數字表達式使用 and
(and
(< c 256)
(> c 32)
(!= c 48))
這將測試 c 是否在 33 到 255(含)之間,並且不等於 48。這將始終返回 true 或 nil,具體取決於 c 的值。
在某些情況下,and 可以產生比 if 更簡潔的程式碼。您可以使用 and 代替上一頁的這個示例
(if (number? x)
(begin
(println x " is a number ")
(inc x)))
使用 and 代替
(and
(number? x)
(println x " is a number ")
(inc x))
您也可以在這裡使用 when
(when (number? x)
(println x " is a number ")
(inc x))
or 函式比其對應的 and 函式更容易滿足。表示式序列會依次評估,直到其中一個返回 true 值。然後會忽略其餘表示式。您可以使用它來遍歷重要條件列表,其中任何一個失敗都足以放棄整個工作。或者,反過來,使用 or 來遍歷一個列表,其中任何一個成功都是繼續的充分理由。無論如何,請記住,一旦 newLISP 獲得一個非 nil 結果,or 函式就會完成。
以下程式碼設定了一系列條件,每個數字都必須避免滿足這些條件 - 只需一個 true 答案,它就不會被打印出來
(for (x -100 100)
(or
(< x 1) ; x mustn't be less than 1
(> x 50) ; or greater than 50
(> (mod x 3) 0) ; or leave a remainder when divided by 3
(> (mod x 2) 0) ; or when divided by 2
(> (mod x 7) 0) ; or when divided by 7
(println x)))
42 ; the ultimate and only answer
歧義:amb 函式
[edit | edit source]您可能會或可能不會找到 amb 的好用途 - 歧義函式。給定列表中的一系列表示式,amb 將選擇並評估其中一個表示式,但您事先不知道哪個表示式
> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
2
> (amb 1 2 3 4 5 6)
6
> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
5
> (amb 1 2 3 4 5 6)
3
>...
使用它隨機選擇替代操作
(dotimes (x 20)
(amb
(println "Will it be me?")
(println "It could be me!")
(println "Or it might be me...")))
Will it be me? It could be me! It could be me! Will it be me? It could be me! Will it be me? Or it might be me... It could be me! Will it be me? Will it be me? It could be me! It could be me! Will it be me? Or it might be me... It could be me! ...
選擇:if、cond 和 case
[edit | edit source]要測試一系列替代值,您可以使用 if、cond 或 case。case 函式允許您根據切換表示式的值執行表示式。它由一系列值/表示式對組成
(case n
(1 (println "un"))
(2 (println "deux"))
(3 (println "trois"))
(4 (println "quatre"))
(true (println "je ne sais quoi")))
newLISP 依次遍歷這些對,檢視 n 是否與 1、2、3 或 4 中的任何值匹配。一旦一個值匹配,表示式就會被評估,並且 case 函式會完成,返回表示式的值。最好將一個最終對與 true 和一個通配表示式一起放在一起,以應對完全沒有匹配的情況。如果 n 是一個數字,它將始終為真,因此將此放在最後。
潛在匹配值不會被評估。這意味著你不能寫這個
(case n
((- 2 1) (println "un"))
((+ 2 0) (println "deux"))
((- 6 3) (println "trois"))
((/ 16 4) (println "quatre"))
(true (println "je ne sais quoi")))
即使從邏輯上來說應該可以:如果 n 是 1,你會期望第一個表示式 (- 2 1) 匹配。但是,該表示式沒有被評估 - 沒有任何求和被評估。在這個例子中,true 操作 (println "je ne sais quoi") 被評估。
如果你更喜歡使用評估其引數的 case 版本,那麼在 newLISP 中很容易實現。請參閱 宏。
之前我提到過 cond 是 if 的更傳統版本。newLISP 中的 cond 語句具有以下結構
(cond
(test action1 action2 etc)
(test action1 action2 etc)
(test action1 action2 etc)
; ...
)
其中每個列表都包含一個測試,以及一個或多個表示式或操作,如果測試返回 true 則會評估這些表示式或操作。newLISP 會執行第一個測試,如果測試為 true 則執行操作,然後忽略剩餘的測試/操作迴圈。測試通常是一個列表或列表表示式,但它也可以是一個符號或一個值。
一個典型的例子如下
(cond
((< x 0) (define a "impossible") )
((< x 10) (define a "small") )
((< x 20) (define a "medium") )
((>= x 20) (define a "large") )
)
這與 if 版本基本相同,只是每對測試-操作都用括號括起來。以下是用於比較的 if 版本
(if
(< x 0) (define a "impossible")
(< x 10) (define a "small")
(< x 20) (define a "medium")
(>= x 20) (define a "large")
)
對於更簡單的函式,使用 if 可能更容易。但是,當編寫較長的程式時,cond 可能更易讀。如果你希望特定測試的操作評估多個表示式,cond 的額外括號可以使你的程式碼更短
(cond
((< x 0) (define a -1) ; if would need a begin here
(println a) ; but cond doesn't
(define b -1))
((< x 10) (define a 5))
; ...
控制函式 dolist、dotimes 和 for 涉及臨時區域性符號的定義,這些符號在表示式持續期間存在,然後消失。
類似地,使用 let 和 letn 函式,你可以定義僅在列表內部存在的變數。它們在列表之外無效,並且在列表評估完成後,它們會失去其值。
let 列表中的第一個專案是一個子列表,它包含變數(不需要引用)以及用於初始化每個變數的表示式。列表中的剩餘專案是可訪問這些變數的表示式。最好將變數/起始值對對齊
(let
(x (* 2 2)
y (* 3 3)
z (* 4 4))
; end of initialization
(println x)
(println y)
(println z))
4 9 16
此示例建立了三個區域性變數 x、y 和 z,併為每個變數分配了值。主體包含三個 println 表示式。在這些表示式完成後,x、y 和 z 的值將不再可訪問 - 儘管整個表示式返回了最後一個 println 語句返回的值 16。
如果你將 let 列表想象成在一行上,那麼它的結構很容易記住
(let ((x 1) (y 2)) (+ x y))
如果你想在第一個初始化部分中的其他地方引用區域性變數,請使用 letn 而不是 let
(letn
(x 2
y (pow x 3)
z (pow x 4))
(println x)
(println y)
(println z))
在 y 的定義中,你可以引用 x 的值,我們剛剛將其定義為 2。letn 是 let 的巢狀版本,它允許你執行此操作。
我們對區域性變數的討論引出了函式。
define 函式提供了一種方法來將表示式列表儲存在一個名稱下,適用於以後執行。你定義的函式可以使用與 newLISP 的內建函式相同的方式。函式定義的基本結構如下
(define (func1)
(expression-1)
(expression-2)
; ...
(expression-n)
)
當你不想向函式提供任何資訊時,或者當你需要提供資訊時,可以使用以下結構
(define (func2 v1 v2 v3)
(expression-1)
(expression-2)
; ...
(expression-n)
)
你可以像呼叫任何其他函式一樣呼叫新定義的函式,如果你的定義需要引數,則在列表中向其傳遞值
(func1) ; no values expected
(func2 a b c) ; 3 values expected
我說預期,但 newLISP 很靈活。你可以向 func1 提供任意數量的引數,newLISP 不會抱怨。你也可以向 func2 提供任意數量的引數 - 在這種情況下,如果提供的引數不足以定義 a、b 和 c,它們在開始時將被設定為 nil。
當函式執行時,主體中的每個表示式都會按順序進行評估。最後一個被評估的表示式的值將作為函式的值返回。例如,此函式根據 n 的值返回 true 或 nil
(define (is-3? n)
(= n 3))
> (println (is-3? 2)) nil > (println (is-3? 3)) true
有時,你可能希望透過在末尾新增一個評估為正確值的表示式來顯式指定要返回的值
(define (answerphone)
(pick-up-phone)
(say-message)
(set 'message (record-message))
(put-down-phone)
message)
末尾的 message 評估為 (record-message) 收到的並返回的訊息,並且 (answerphone) 函式返回此值。如果沒有它,函式將返回 (put-down-phone) 返回的值,這可能只是一個 true 或 false 值。
要使函式返回多個值,你可以返回一個列表。
在函式的引數列表中定義的符號對函式來說是區域性的,即使它們之前在函式之外存在
(set 'v1 999)
(define (test v1 v2)
(println "v1 is " v1)
(println "v2 is " v2)
(println "end of function"))
(test 1 2)
v1 is 1 v2 is 2 end of function > v1 999
如果符號在函式體中定義如下
(define (test v1)
(set 'x v1)
(println x))
(test 1)
1 > x 1
它也可以從函式外部訪問。這就是你為什麼要定義區域性變數的原因!請參閱 區域性變數。
newLISP 足夠智慧,不會擔心你是否提供了超過所需的資訊
(define (test)
(println "hi there"))
(test 1 2 3 4) ; 1 2 3 4 are ignored
hi there
但它不會為你填補空白
(define (test n)
(println n))
>(test) ; no n supplied, so print nil nil > (test 1) 1 > (test 1 2 3) ; 2 and 3 ignored< 1
有時,你希望函式改變程式碼中其他地方的符號的值,有時你希望函式不改變 - 或者不能改變。以下函式在執行時會更改 x 符號的值,該符號可能在程式碼中的其他地方定義,也可能沒有定義
(define (changes-symbol)
(set 'x 15)
(println "x is " x))
(set 'x 10)
;-> x is 10
(changes-symbol)
x is 15
如果你不希望發生這種情況,請使用 let 或 letn 來定義一個區域性 x,它不會影響函式外部的 x 符號
(define (does-not-change-x)
(let (x 15)
(println "my x is " x)))
(set 'x 10)
> (does-not-change-x) my x is 15 > x 10
x 在函式外部仍然是 10。函式內部的 x 與函式外部的 x 不同。當你使用 set 更改函式內部的區域性 x 的值時,它不會更改任何函式外部的 x
(define (does-not-change-x)
(let (x 15) ; this x is inside the 'let' form
(set 'x 20)))
>(set 'x 10) ; this x is outside the function 10 > x 10 > (does-not-change-x) > x 10
你可以使用 local 函式來代替 let 和 letn。它與 let 和 letn 相似,但你不需要在首次提及區域性變數時為它們提供任何值。它們只是 nil,直到你設定它們
(define (test)
(local (a b c)
(println a " " b " " c)
(set 'a 1 'b 2 'c 3)
(println a " " b " " c)))
(test)
nil nil nil 1 2 3
還有其他方法可以宣告區域性變數。當你定義自己的函式時,你可能會發現以下技術更容易編寫。注意逗號
(define (my-function x y , a b c)
; x y a b and c are local variables inside this function
; and 'shadow' any value they might have had before
; entering the functions
逗號是一個巧妙的技巧:它是一個普通的符號名稱,如 c 或 x
(set ', "this is a string")
(println ,)
"this is a string"
- 它只是不太可能被用作符號名稱,因此它在引數列表中作為視覺分隔符很有用。
在函式定義中,你可以在函式引數列表中定義的區域性變數可以分配預設值,如果你在呼叫函式時沒有指定值,將使用這些預設值。例如,這是一個具有三個命名引數 a、b 和 c 的函式
(define (foo (a 1) b (c 2))
(println a " " b " " c))
如果你在函式呼叫中沒有提供值,則符號 a 和 c 將分別取值 1 和 2,但 b 將為 nil,除非你為它提供值。
> (foo) ; there are defaults for a and c but not b
1 nil 2
(foo 2) ; no values for b or c; c has default
2 nil 2
> (foo 2 3) ; b has a value, c uses default
2 3 2
> (foo 3 2 1) ; default values not needed
3 2 1
>
你可以看到,newLISP 對函式引數採用了非常靈活的方法。你可以編寫接受任意數量引數的定義,從而為你(或你的函式的呼叫者)提供最大的靈活性。
args 函式返回傳遞給函式的任何未使用的引數
(define (test v1)
(println "the arguments were " v1 " and " (args)))
(test)
the arguments were nil and ()
(test 1)
the arguments were 1 and ()
(test 1 2 3)
the arguments were 1 and (2 3)
(test 1 2 3 4 5)
the arguments were 1 and (2 3 4 5)
請注意,v1 包含傳遞給函式的第一個引數,但任何剩餘的未使用的引數都包含在 (args) 返回的列表中。
使用 args,你可以編寫接受不同型別輸入的函式。請注意以下函式如何在沒有引數的情況下呼叫,使用字串引數呼叫,使用數字呼叫,或者使用列表呼叫
(define (flexible)
(println " arguments are " (args))
(dolist (a (args))
(println " -> argument " $idx " is " a)))
(flexible)
arguments are ()
(flexible "OK")
arguments are ("OK")
-> argument 0 is OK
(flexible 1 2 3)
arguments are (1 2 3)
-> argument 0 is 1
-> argument 1 is 2
-> argument 2 is 3
(flexible '(flexible 1 2 "buckle my shoe"))
arguments are ((flexible 1 2 "buckle my shoe"))
-> argument 0 is (flexible 1 2 "buckle my shoe")
args 允許你編寫接受任意數量引數的函式。例如,newLISP 非常樂意讓你向一個定義合適的函式傳遞一百萬個引數。我試過了
(define (sum)
(apply + (args)))
(sum 0 1 2 3 4 5 6 7 8
; ...
999997 999998 999999 1000000)
; all the numbers were there but they've been omitted here
; for obvious reasons...
;-> 500000500000
實際上,newLISP 對此感到滿意,但我的文字編輯器不滿意。
doargs 函式可以代替 dolist 用於遍歷 args 返回的引數。你可以將 flexible 函式編寫為
(define (flexible)
(println " arguments are " (args))
(doargs (a) ; instead of dolist
(println " -> argument " $idx " is " a)))
newLISP 還有更多方法可以控制程式碼執行的流程。除了 catch 和 throw(它們允許你處理和捕獲錯誤和異常)之外,還有 silent,它就像 begin 的靜默版本。
如果您想要更多,您可以使用 newLISP 宏編寫您自己的語言關鍵字,這些宏可以用與使用內建函式相同的方式使用。請參閱 宏。
考慮這個函式
(define (show)
(println "x is " x))
請注意,此函式引用了某個未指定的符號 x。此符號在定義或呼叫函式時可能存在也可能不存在,並且可能存在值也可能不存在。當此函式被評估時,newLISP 會查詢稱為 x 的最近符號,並找到它的值
(define (show)
(println "x is " x))
(show)
x is nil
(set 'x "a string")
(show)
x is a string
(for (x 1 5)
(dolist (x '(sin cos tan))
(show))
(show))
x is sin x is cos x is tan x is 1 x is sin x is cos x is tan x is 2 x is sin x is cos x is tan x is 3 x is sin x is cos x is tan x is 4 x is sin x is cos x is tan x is 5
(show)
x is a string
(define (func x)
(show))
(func 3)
x is 3
(func "hi there")
x is hi there
(show)
x is a string
您可以看到 newLISP 如何透過動態跟蹤哪個 x 是活動的來始終為您提供當前 x 的值,即使可能存在其他x潛伏在後臺。for 迴圈開始時,迴圈變數 x 接管為當前 x,但隨後該 x 立即被列表迭代變數 x 取代,該變數取幾個三角函式的值。在每組三角函式之間,迴圈變數版本的 x 會短暫地彈出。經過所有這些迭代後,字串值將再次可用。
在 func 函式中,還有一個 x 對函式是本地的。呼叫 show 時,它將列印此區域性符號。對 show 的最後一次呼叫將返回 x 最初擁有的第一個值。
雖然 newLISP 不會對所有這些不同的 x感到困惑,但您可能會感到困惑!因此,最好使用更長、更具解釋性的符號名稱,並使用區域性變數而不是全域性變數。如果您這樣做,您犯錯或在以後的日期誤讀程式碼的可能性會更小。一般來說,除非您確切地知道符號的來源以及如何確定其值,否則在函式中引用未定義的符號不是一個好主意。
這種動態跟蹤符號當前版本的程序稱為動態作用域。當您檢視上下文(上下文)時,將有更多關於此主題的資訊。它們提供了一種組織類似名稱符號的替代方法 - 詞法作用域。
列表在 newLISP 中無處不在 - LISP 代表列表處理 - 因此,存在許多用於處理列表的有用函式並不奇怪。將它們全部組織成一個邏輯的描述性敘述相當困難,但這裡介紹了其中大多數。
newLISP 的一個好處是,許多在列表上工作的函式也適用於字串,因此您將在下一章中遇到很多這些函式,它們將應用於字串。
您可以直接構建列表並將其分配給符號。引用列表以停止立即對其進行評估
(set 'vowels '("a" "e" "i" "o" "u"))
;-> ("a" "e" "i" "o" "u") ; an unevaluated list
列表通常由其他函式為您建立,但您也可以使用以下函式之一構建自己的列表
- list 根據表示式建立新列表
- append 將列表粘合在一起以形成一個新列表
- cons 將元素新增到列表的開頭或建立列表
- push 在列表中插入新成員
- dup 複製元素
使用 list 根據表示式序列構建列表
(set 'rhyme (list 1 2 "buckle my shoe"
'(3 4) "knock" "on" "the" "door"))
; rhyme is now a list with 8 items"
;-> (1 2 "buckle my shoe" '(3 4) "knock" "on" "the" "door")
請注意,(3 4) 元素本身就是一個列表,它巢狀在主列表中。
cons 接受正好兩個表示式,並且可以執行兩項工作:將第一個元素插入現有列表的開頭,或構建一個新的兩個元素列表。在這兩種情況下,它都會返回一個新的列表。newLISP 會根據第二個元素是列表還是不是自動選擇要執行的工作。
(cons 1 2) ; makes a new list
;-> (1 2)
(cons 1 '(2 3)) ; inserts an element at the start
;-> (1 2 3)
要將兩個或多個列表粘合在一起,請使用 append
(set 'odd '(1 3 5 7) 'even '(2 4 6 8))
(append odd even)
;-> (1 3 5 7 2 4 6 8)
請注意 list 和 append 在您連線兩個列表時的區別
(set 'a '(a b c) 'b '(1 2 3))
(list a b)
;-> ((a b c) (1 2 3)) ; list makes a list of lists
(append a b)
;-> (a b c 1 2 3) ; append makes a list
list 在建立新列表時保留源列表,而 append 使用每個源列表的元素建立一個新列表。
要記住這一點:List 保留源列表的 List 性,但 aPPend 會將元素挑選出來,並將它們全部打包在一起。
append 還可以將一堆字串組裝成一個新的字串。
push 是一個強大的命令,您可以使用它來建立新列表或將元素插入現有列表的任何位置。在列表的開頭推送元素會將所有內容向右移動一位,而在列表的末尾推送元素只會將其附加並建立一個新的最後一個元素。您還可以在列表的中間的任何位置插入元素。
儘管它的性質是建設性的,但從技術上講它是一個破壞性函式,因為它會永久更改目標列表,因此請謹慎使用。它返回插入的元素的值,而不是整個列表。
(set 'vowels '("e" "i" "o" "u"))
(push (char 97) vowels)
; returns "a"
; vowels is now ("a" "e" "i" "o" "u")
當您引用列表中元素的位置時,您使用的是基於零的編號,如果您是一位經驗豐富的程式設計師,您會期望這樣做
如果您沒有指定位置或索引,push 會在開頭推送新元素。使用第三個表示式來指定新元素的位置或索引。-1 表示列表的最後一個元素,1 表示從 0 開始從前面數的列表的第二個元素,依此類推
(set 'vowels '("a" "e" "i" "o"))
(push "u" vowels -1)
;-> "u"
; vowels is now ("a" "e" "i" "o" "u")
(set 'evens '(2 6 10))
(push 8 evens -2) ; goes before the 10
(push 4 evens 1) ; goes after the 2
; evens is now (2 4 6 8 10)
如果您提供的符號作為列表不存在,push 會有用地為您建立它,因此您不必先宣告它。
(for (c 1 10)
(push c number-list -1) ; doesn't fail first time!
(println number-list))
(1) (1 2) (1 2 3) (1 2 3 4) (1 2 3 4 5) (1 2 3 4 5 6) (1 2 3 4 5 6 7) (1 2 3 4 5 6 7 8) (1 2 3 4 5 6 7 8 9) (1 2 3 4 5 6 7 8 9 10)
順便說一句,還有很多其他方法可以生成一個無序數字列表。您還可以執行許多隨機交換,如下所示
(set 'l (sequence 0 99))
(dotimes (n 100)
(swap (l (rand 100)) (l (rand 100)))))
雖然使用 randomize 會更容易
(randomize (sequence 1 99))
;-> (54 38 91 18 76 71 19 30 ...
(這就是 newLISP 的好處之一 - 更優雅的解決方案只需重新編寫!)
push 有一個相反的函式 pop,它會破壞性地從列表中刪除元素,並返回刪除的元素。我們將在後面介紹 pop 和其他列表手術函式。請參閱 列表手術。
這兩個函式,與許多其他 newLISP 函式一樣,適用於字串和列表。請參閱 push 和 pop 也適用於字串。
一個稱為 dup 的有用函式允許您透過重複元素給定次數來快速構建列表
(dup 1 6) ; duplicate 1 six times
;-> (1 1 1 1 1 1)
(dup '(1 2 3) 6)
;-> ((1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3))
(dup x 6)
;-> (x x x x x x)
有一個技巧可以使 dup 返回一個字串列表。由於 dup 也可以用於將字串複製成一個較長的字串,因此您在列表的末尾提供額外的 true 值,newLISP 會建立一個字串列表,而不是一個字串的字串。
(dup "x" 6) ; a string of strings
;-> "xxxxxx"
(dup "x" 6 true) ; a list of strings
;-> ("x" "x" "x" "x" "x" "x")
獲得列表後,就可以開始處理它。首先,讓我們看一下對列表作為單位進行操作的函式。之後,我將介紹允許您執行列表手術的函式 - 對單個列表元素的操作。
dolist 遍歷列表中的每個專案
(set 'vowels '("a" "e" "i" "o" "u"))
(dolist (v vowels)
(println (apply upper-case (list v))))
A E I O U
在這個例子中,apply 函式需要一個函式和一個列表,並使用列表中的元素作為函式的引數。因此,它重複地將upper-case 函式應用於迴圈變數在v 中的值。由於upper-case 函式適用於字串,但apply 函式需要一個列表,所以我不得不使用list 函式將每次迭代中v 的當前值(一個字串)轉換為列表。
一個更好的方法是使用map 函式。
(map upper-case '("a" "e" "i" "o" "u"))
;-> ("A" "E" "I" "O" "U")
map 函式將指定的函式(本例中為upper-case 函式)依次應用於列表中的每個專案。map 函式的優點是它可以在一次遍歷中同時遍歷列表並將函式應用於列表中的每個專案。結果也是一個列表,這在後續處理中可能更有用。
關於dolist 和apply 函式的更多資訊,請參見其他地方(請參見 遍歷列表 和 Apply 和 map: 將函式應用於列表)。
reverse
[edit | edit source]reverse 函式的作用與你預期的一樣,它會反轉列表。它是一個破壞性函式,會永久改變列表。
(reverse '("A" "E" "I" "O" "U"))
;-> ("U" "O" "I" "E" "A")
sort 和 randomize
[edit | edit source]在某種程度上,randomize 函式和sort 函式是互補的,儘管sort 函式會改變原始列表,而randomize 函式會返回原始列表的無序副本。sort 函式會將列表中的元素按升序排列,根據型別和值進行組織。
以下是一個示例:建立一個字母列表和一個數字列表,將它們組合在一起,隨機排列結果,然後再次排序。
(for (c (char "a") (char "z"))
(push (char c) alphabet -1))
(for (i 1 26)
(push i numbers -1))
(set 'data (append alphabet numbers))
;-> ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p"
; "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" 1 2 3 4 5 6 7 8 9 10 11 12
; 13 14 15 16 17 18 19 20 21 22 23 24 25 26)
(randomize data)
;-> ("l" "r" "f" "k" 17 10 "u" "e" 6 "j" 11 15 "s" 2 22 "d" "q" "b"
; "m" 19 3 5 23 "v" "c" "w" 24 13 21 "a" 4 20 "i" "p" "n" "y" 14 "g"
; 25 1 8 18 12 "o" "x" "t" 7 16 "z" 9 "h" 26)
(sort data)
;-> (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
; 25 26 "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o"
; "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")
比較data 在隨機化之前和排序之後的差異。sort 命令會根據資料型別和值對列表進行排序:整數在字串之前,字串在列表之前,依此類推。
預設排序方法為<,它會將值排列成每個值都小於下一個值。
若要更改排序方法,可以提供一個 newLISP 內建的比較函式,例如>。當比較函式對相鄰的每對都為真時,認為相鄰的元素是按正確順序排序的。
(for (c (char "a") (char "z"))
(push (char c) alphabet -1))
alphabet
;-> ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n"
; "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")
(sort alphabet >)
;-> ("z" "y" "x" "w" "v" "u" "t" "s" "r" "q" "p" "o" "n"
; "m" "l" "k" "j" "i" "h" "g" "f" "e" "d" "c" "b" "a")
你可以提供一個自定義排序函式。這是一個函式,它接受兩個引數,如果它們按正確的順序(即第一個應該在第二個之前),則返回 true,否則返回 false。例如,假設你想要對一個檔名列表進行排序,以便最短的檔名出現在最前面。定義一個函式,如果第一個引數比第二個引數短,則返回 true,然後將這個自定義排序函式與sort 函式一起使用。
(define (shorter? a b) ; two arguments, a and b
(< (length a) (length b)))
(sort (directory) shorter?)
;->
("." ".." "var" "usr" "tmp" "etc" "dev" "bin" "sbin" "mach" ".vol"
"Users" "cores" "System" "Volumes" "private" "Network" "Library"
"mach.sym" ".Trashes" "Developer" "automount" ".DS_Store"
"Desktop DF" "Desktop DB" "mach_kernel" "Applications" "System Folder" ...)
經驗豐富的 newLISP 使用者通常會編寫一個無名函式,並將其直接提供給sort 函式。
(sort (directory) (fn (a b) (< (length a) (length b))))
這與前面的方法一樣,但可以節省大約 25 個字元。你可以使用fn 或lambda 定義行內函數或匿名函式。
unique
[edit | edit source]unique 函式會返回一個列表的副本,其中刪除了所有重複項。
(set 'data '( 1 1 2 2 2 2 2 2 2 3 2 4 4 4 4))
(unique data)
;-> (1 2 3 4)
還有一些用於比較列表的有用函式。請參見 使用兩個或更多個列表。
flat
[edit | edit source]flat 函式在處理巢狀列表時很有用,因為它可以顯示巢狀列表的樣子,而無需複雜的層次結構。
(set 'data '(0 (0 1 2) 1 (0 1) 0 1 (0 1 2) ((0 1) 0)))
(length data)
;-> 8
(length (flat data))
;-> 15
(flat data)
;-> (0 0 1 2 1 0 1 0 1 0 1 2 0 1 0)
幸運的是,flat 函式是非破壞性的,因此你可以使用它而無需擔心丟失巢狀列表的結構。
data
;-> (0 (0 1 2) 1 (0 1) 0 1 (0 1 2) ((0 1) 0)) ; still nested
transpose
[edit | edit source]transpose 函式設計用於處理矩陣(一種特殊的列表型別:請參見 矩陣)。它對普通的巢狀列表也有用。如果你將列表的列表看作一張表格,它會為你翻轉行和列。
(set 'a-list
'(("a" 1)
("b" 2)
("c" 3)))
(transpose a-list)
;->
(("a" "b" "c")
( 1 2 3))
(set 'table
'((A1 B1 C1 D1 E1 F1 G1 H1)
(A2 B2 C2 D2 E2 F2 G2 H2)
(A3 B3 C3 D3 E3 F3 G3 H3)))
(transpose table)
;->
((A1 A2 A3)
(B1 B2 B3)
(C1 C2 C3)
(D1 D2 D3)
(E1 E2 E3)
(F1 F2 F3)
(G1 G2 G3)
(H1 H2 H3))
以下是一個 newLISP 程式碼示例:
(set 'table '((A 1) (B 2) (C 3) (D 4) (E 5)))
;-> ((A 1) (B 2) (C 3) (D 4) (E 5))
(set 'table (transpose (rotate (transpose table))))
;-> ((1 A) (2 B) (3 C) (4 D) (5 E))
每個子列表都被反轉了。當然,你也可以這樣做:
(set 'table (map (fn (i) (rotate i)) table))
這更短,但速度略慢。
explode
[edit | edit source]explode 函式可以讓你將列表分解。
(explode (sequence 1 10))
;-> ((1) (2) (3) (4) (5) (6) (7) (8) (9) (10))
你也可以指定每個部分的大小。
(explode (sequence 1 10) 2)
;-> ((1 2) (3 4) (5 6) (7 8) (9 10))
(explode (sequence 1 10) 3)
;-> ((1 2 3) (4 5 6) (7 8 9) (10))
(explode (sequence 1 10) 4)
;-> ((1 2 3 4) (5 6 7 8) (9 10))
列表分析:測試和搜尋
[edit | edit source]通常你不知道列表中包含什麼,並且你想要一些取證工具來了解更多關於列表內部的資訊。newLISP 提供了很好的選擇。
我們已經接觸過length 函式,它可以查詢列表中元素的數量。
starts-with 函式和ends-with 函式可以測試列表的開頭和結尾。
(for (c (char "a") (char "z"))
(push (char c) alphabet -1))
;-> alphabet is ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l"
; "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")
(starts-with alphabet "a") ; list starts with item "a"?
;-> true
(starts-with (join alphabet) "abc") ; convert to string and test
;-> true
(ends-with alphabet "z") ; testing the list version
;-> true
這些函式也可以用於字串(它們接受正則表示式)。請參見 測試和比較字串。
contains 函式呢?實際上,newLISP 沒有一個單獨的函式可以完成這項工作。相反,你擁有find、match、member、ref、filter、index 和count 函式等。你使用哪個函式取決於你想對問題“這個列表是否包含這個專案?”得到什麼答案,以及列表是巢狀列表還是扁平列表。
如果你想要一個簡單的答案,並且只需要快速地進行頂層搜尋,請使用find 函式。請參見 find 函式。
如果你還想得到專案和列表的剩餘部分,請使用member 函式。請參見 member 函式。
如果你想要第一個匹配項的索引號(即使列表包含巢狀列表),你可以使用ref 函式。請參見 ref 和 ref-all 函式。
如果你想要一個包含所有與你的搜尋項匹配的元素的新列表,請使用find-all 函式。請參見 find-all 函式。
如果你想要知道列表是否包含某個元素模式,請使用match 函式。請參見 匹配列表中的模式。
你可以使用filter、clean 和index 函式找到滿足某個函式(內建函式或自定義函式)的所有列表項。請參見 過濾列表:filter、clean 和 index 函式。
exists 函式和for-all 函式檢查列表中的元素,檢視它們是否通過了測試。
如果你只是為了將列表中的元素更改為其他東西,而不是為了查詢它們,那麼不要先查詢它們,直接使用replace 函式即可。請參見 替換資訊:replace 函式。你也可以使用set-ref 函式查詢和替換列表元素。請參見 查詢和替換匹配的元素 函式。
如果你想要知道列表中某個專案的出現次數,請使用count 函式。請參見 使用兩個或更多個列表 函式。
讓我們看一些這些函式的示例。
find
[edit | edit source]find 函式會在列表中查詢一個表示式,並返回一個整數或nil。整數表示搜尋項在列表中第一次出現的索引。find 函式可能返回 0,如果列表以該項開頭,則它的索引號為 0,但這並不成問題——你可以在if 測試中使用此函式,因為 0 會被評估為 true。
(set 'sign-of-four
(parse (read-file "/Users/me/Sherlock-Holmes/sign-of-four.txt")
{\W} 0))
(if (find "Moriarty" sign-of-four) ; Moriarty anywhere?
(println "Moriarty is mentioned")
(println "No mention of Moriarty"))
No mention of Moriarty
(if (find "Lestrade" sign-of-four)
(println "Lestrade is mentioned")
(println "No mention of Lestrade"))
Lestrade is mentioned.
(find "Watson" sign-of-four)
;-> 477
這裡,我解析了亞瑟·柯南·道爾的《四簽名》(你可以從古騰堡計劃下載),並測試了生成的字串列表是否包含各種名稱。返回的整數表示該字串元素在列表中第一次出現的索引。
find 函式允許你使用正則表示式,因此你可以在列表中找到與某個字串模式匹配的任何字串元素。
(set 'loc (find "(tea|cocaine|morphine|tobacco)" sign-of-four 0))
(if loc
(println "The drug " (sign-of-four loc) " is mentioned.")
(println "No trace of drugs"))
The drug cocaine is mentioned.
這裡,我正在尋找夏洛克·福爾摩斯波西米亞式生活方式中任何化學放縱的痕跡:"(tea|cocaine|morphine|tobacco)" 表示茶、可卡因、嗎啡或菸草中的任何一種。
這種形式的find 函式允許你在列表的字串元素中查詢正則表示式模式。當我們探索字串時,你將會再次遇到正則表示式。請參見 正則表示式。
(set 'word-list '("being" "believe" "ceiling" "conceit" "conceive"
"deceive" "financier" "foreign" "neither" "receive" "science"
"sufficient" "their" "vein" "weird"))
(find {(c)(ie)(?# i before e except after c...)} word-list 0)
;-> 6 ; the first one is "financier"
這裡,我們正在查詢詞語列表中與我們的模式匹配的任何字串元素(一個c 緊跟著ie,這是古老且不準確的拼寫規則“i before e except after c”。
這裡的正則表示式模式(包含在括號中,括號是字串分隔符,與引號的功能大致相同)是 (c) 緊跟著 (ie)。然後是一個註釋,以(?# 開頭。正則表示式中的註釋在事情變得難以理解時很有用,因為它們經常會變得難以理解。
find 函式還可以接受一個比較函式。請參見 搜尋列表 函式。
find 函式只查詢列表中的第一個匹配項。若要查詢所有匹配項,你可以重複find 函式,直到它返回nil。詞語列表每次都會變短,找到的元素會被新增到另一個列表的末尾。
(set 'word-list '("scientist" "being" "believe" "ceiling" "conceit"
"conceive" "deceive" "financier" "foreign" "neither" "receive" "science"
"sufficient" "their" "vein" "weird"))
(while (set 'temp
(find {(c)(ie)(?# i before e except after c...)} word-list 0))
(push (word-list temp) results -1)
(set 'word-list ((+ temp 1) word-list)))
results
;-> ("scientist" "financier" "science" "sufficient")
但在這種情況下,使用filter 函式要容易得多。
(filter (fn (w) (find {(c)(ie)} w 0)) word-list)
;-> ("scientist" "financier" "science" "sufficient")
- 請參見 過濾列表:filter、clean 和 index 函式。
或者,你可以使用ref-all 函式(請參見 ref 和 ref-all 函式)來獲取索引列表。
如果你不使用正則表示式,你可以使用count 函式,它在給定兩個列表的情況下,會遍歷第二個列表,並計算第一個列表中的每個專案在第二個列表中出現的次數。讓我們看看主要人物的名字出現了多少次。
(count '("Sherlock" "Holmes" "Watson" "Lestrade" "Moriarty" "Moran")
sign-of-four)
;-> (34 135 24 1 0 0)
count 函式產生的結果列表顯示了第一個列表中的每個元素在第二個列表中出現的次數,因此在這個故事中,Sherlock 被提及了 34 次,Holmes 被提及了 135 次,Watson 被提及了 24 次,而可憐的萊斯特雷德警探只被提及了一次。
值得注意的是,find 函式只會對列表進行表面檢查。例如,如果列表包含巢狀列表,則應使用 ref 而不是 find,因為 ref 會檢視子列表內部。
(set 'maze
'((1 2)
(1 2 3)
(1 2 3 4)))
(find 4 maze)
;-> nil ; didn't look inside the lists
(ref 4 maze)
;-> (2 3) ; element 3 of element 2
member
[edit | edit source]member 函式返回源列表的剩餘部分,而不是索引號或計數。
(set 's (sequence 1 100 7)) ; 7 times table?
;-> (1 8 15 22 29 36 43 50 57 64 71 78 85 92 99)
(member 78 s)
;-> (78 85 92 99)
匹配列表中的模式
[edit | edit source]有一個強大的複雜函式叫做 match,它在列表中尋找模式。它接受萬用字元 *、? 和 +,用於定義元素的模式。+ 表示一個或多個元素,* 表示零個或多個元素,? 表示一個元素。例如,假設你想要在一個包含 0 到 9 之間的隨機數字列表中尋找模式。首先,生成一個包含 10000 個隨機數的列表作為源資料。
(dotimes (c 10000) (push (rand 10) data))
;-> (7 9 3 8 0 2 4 8 3 ...)
接下來,你決定要尋找以下模式
1 2 3
在列表中的任何地方,即任何東西后面跟著 1,然後是 2,然後是 3,然後是任何東西。像這樣呼叫 match
(match '(* 1 2 3 *) data)
這看起來很奇怪,但它只是一個列表中的模式規範,後面跟著源資料。列表模式
(* 1 2 3 *)
表示任何原子或表示式的序列(或空),後面跟著 1,然後是 2,然後是 3,後面跟著任意數量的原子或表示式(或空)。這個 match 函式返回的答案是另一個列表,包含兩個子列表,一個對應第一個 *,另一個對應第二個 *。
((7 9 3 8 . . . 0 4 5) (7 2 4 1 . . . 3 5 5 5))
而你所尋找的模式第一次出現在這些列表之間的間隙(事實上,它在列表後面的部分也出現了半打)。match 也可以處理巢狀列表。
要查詢模式的所有出現,而不僅僅是第一個,你可以在 while 迴圈中使用 match。例如,要查詢並刪除所有緊跟另一個 0 的 0,請對新版本的列表重複 match,直到它不再返回非空值。
(set 'number-list '(2 4 0 0 4 5 4 0 3 6 2 3 0 0 2 0 0 3 3 4 2 0 0 2))
(while (set 'temp-list (match '(* 0 0 *) number-list))
(println temp-list)
(set 'number-list (apply append temp-list)))
((2 4) (4 5 4 0 3 6 2 3 0 0 2 0 0 3 3 4 2 0 0 2)) ((2 4 4 5 4 0 3 6 2 3) (2 0 0 3 3 4 2 0 0 2)) ((2 4 4 5 4 0 3 6 2 3 2) (3 3 4 2 0 0 2)) ((2 4 4 5 4 0 3 6 2 3 2 3 3 4 2) (2)) > number-list ;-> (2 4 4 5 4 0 3 6 2 3 2 3 3 4 2 2)
你不必先查詢元素,然後才能替換它們:只需使用 replace,它在一個操作中完成查詢和替換。你還可以使用 match 作為比較函式來搜尋列表。參見 替換資訊:replace 和 搜尋列表。
find-all
[edit | edit source]find-all 是一個功能強大的函式,具有多種不同的形式,適合於搜尋列表、關聯列表和字串。對於列表搜尋,你需要提供四個引數:搜尋鍵、列表、操作表示式和函式,函式是你想要用於匹配搜尋鍵的比較函式。
(set 'food '("bread" "cheese" "onion" "pickle" "lettuce"))
(find-all "onion" food (print $0 { }) >)
;-> bread cheese lettuce
這裡,find-all 在列表 food 中搜索字串 "onion"。它使用 > 函式作為比較函式,因此它會找到 "onion" 大於的任何東西。對於字串,"大於" 表示在預設的 ASCII 排序順序中排在後面,因此 "cheese" 大於 "bread" 但小於 "onion"。注意,與其他允許你提供比較函式的函式(即 find、ref、ref-all、replace 當用於列表時、set-ref、set-ref-all 和 sort)不同,比較函式 必須 提供。使用 < 函式,結果是一個包含 "onion" 小於的事物的列表。
(find-all "onion" food (print $0 { }) <)
;-> pickle
ref 和 ref-all
[edit | edit source]ref 函式返回列表中元素第一個出現的索引。它特別適合用於巢狀列表,因為與 find 不同,它會檢視所有子列表內部,並返回元素第一個出現的 地址。例如,假設你已經使用 newLISP 的內建 XML 解析器將 XML 檔案(如你的 iTunes 庫)轉換為一個大型巢狀列表。
(xml-type-tags nil nil nil nil) ; controls XML parsing
(set 'itunes-data
(xml-parse
(read-file "/Users/me/Music/iTunes/iTunes Music Library.xml")
(+ 1 2 4 8 16)))
現在你可以查詢資料中的任何表示式,該表示式以普通 newLISP 列表的形式出現。
(ref "Brian Eno" itunes-data)
返回的列表將是該字串在列表中第一個出現的地址。
(0 2 14 528 6 1)
- 這是一組索引號,它們共同定義了一種 地址。這個例子意味著:在列表元素 0 中,查詢子列表元素 2,然後查詢該子列表的子列表元素 14,以此類推,深入到高度巢狀的基於 XML 的資料結構中。參見 使用 XML。
ref-all 做類似的工作,並返回一個地址列表。
(ref-all "Brian Eno" itunes-data)
;-> ((0 2 14 528 6 1) (0 2 16 3186 6 1) (0 2 16 3226 6 1))
這些函式也可以接受比較函式。參見 搜尋列表。
當你在巢狀列表中搜索某些東西時,使用這些函式。如果你想在找到它後替換它,請使用 set-ref 和 set-ref-all 函式。參見 查詢和替換匹配的元素。
過濾列表:filter、clean 和 index
[edit | edit source]在列表中查詢事物的另一種方法是過濾列表。就像淘金一樣,你可以建立一個過濾器,只保留你想要的東西,將不需要的東西衝走。
filter 和 index 函式具有相同的語法,但 filter 返回列表元素,而 index 返回想要元素的索引號(索引)而不是列表元素本身。(這些函式不適用於巢狀列表。)
過濾函式 filter、clean 和 index 使用另一個函式來測試元素:元素根據它是否透過測試而出現在結果列表中。你可以使用內建函式,也可以定義自己的函式。通常,newLISP 中測試並返回 true 或 false 的函式(有時稱為謂詞函式)的名稱以問號結尾。
NaN? array? atom? context? directory? empty? file? float? global? integer? lambda? legal? list? macro? nil? null? number? primitive? protected? quote? string? symbol? true? zero?
因此,例如,在列表中查詢整數(並刪除浮點數)的一種簡單方法是使用 integer? 函式和 filter。只有整數能透過這個過濾器。
(set 'data '(0 1 2 3 4.01 5 6 7 8 9.1 10))
(filter integer? data)
;-> (0 1 2 3 5 6 7 8 10)
filter 有一個名為 clean 的補充函式,它會刪除滿足測試條件的元素。
(set 'data '(0 1 2 3 4.01 5 6 7 8 9.1 10))
(clean integer? data)
;-> (4.01 9.1)
將 clean 想象成清除汙垢 - 它會清除任何透過測試的東西。將 filter 想象成淘金,保留任何透過測試的東西。
下一個過濾器找到柯南·道爾的故事 空屋 中所有包含字母 pp 的詞。過濾器是一個 lambda 表示式(一個沒有名稱的臨時函式),如果元素不包含 pp,則返回 nil。列表是由 parse 生成的字串元素列表,它根據模式將字串分解為較小的字串列表。
(set 'empty-house-text
(parse
(read-file "/Users/me/Sherlock-Holmes/the-empty-house.txt")
{,\s*|\s+} 0))
(filter (fn (s) (find "pp" s)) empty-house-text)
;->
("suppressed" "supply" "disappearance" "appealed" "appealed"
"supplemented" "appeared" "opposite" "Apparently" "Suppose"
"disappear" "happy" "appears" "gripped" "reappearance."
"gripped" "opposite" "slipped" "disappeared" "slipped"
"slipped" "unhappy" "appealed" "opportunities." "stopped"
"stepped" "opposite" "dropped" "appeared" "tapped"
"approached" "suppressed" "appeared" "snapped" "dropped"
"stepped" "dropped" "supposition" "opportunity" "appear"
"happy" "deal-topped" "slipper" "supplied" "appealing"
"appear")
你還可以使用 filter 或 clean 來清理列表,然後才能使用它們 - 例如,刪除 parse 操作產生的空字串。
什麼時候使用 index 而不是 filter 或 clean?嗯,當你想透過索引號而不是它們的價值來訪問列表元素時,可以使用 index:我們將在下一節中遇到一些函式,這些函式用於透過索引選擇列表項。例如,雖然 ref 只找到了第一個出現的索引,但你可以使用 index 返回元素所有出現的索引號。
如果你有一個謂詞函式,它用於查詢字母 c 後面跟著 ie 的字串,你就可以使用該函式來搜尋匹配字串的列表。
(set 'word-list '("agencies" "being" "believe" "ceiling"
"conceit" "conceive" "deceive" "financier" "foreign"
"neither" "receive" "science" "sufficient" "their" "vein"
"weird"))
(define (i-before-e-after-c? wd) ; a predicate function
(find {(c)(ie)(?# i before e after c...)} wd 0))
(index i-before-e-after-c? word-list)
;-> (0 7 11 12)
; agencies, financier, science, sufficient
記住,列表可以包含巢狀列表,並且某些函式不會檢視子列表內部。
(set 'maze
'((1 2.1)
(1 2 3)
(1 2 3 4)))
(filter integer? maze)
;-> () ; I was sure it had integers...
(filter list? maze)
;-> ((1 2.1) (1 2 3) (1 2 3 4)) ; ah yes, they're sublists!
(filter integer? (flat maze))
;-> (1 1 2 3 1 2 3 4) ; one way to do it...
測試列表
[edit | edit source]exists 函式和for-all 函式檢查列表中的元素,檢視它們是否通過了測試。
exists 返回列表中第一個透過測試的元素,或者如果它們都沒有透過測試則返回 nil。
(exists string? '(1 2 3 4 5 6 "hello" 7))
;-> "hello"
(exists string? '(1 2 3 4 5 6 7))
;-> nil
for-all 返回 true 或 nil。如果每個列表元素都透過測試,它將返回 true。
(for-all number? '(1 2 3 4 5 6 7))
;-> true
(for-all number? '("zero" 2 3 4 5 6 7))
;-> nil
find、ref、ref-all 和 replace 用於在列表中查詢專案。通常,您使用這些函式查詢與您要查詢的內容相等的專案。但是,相等只是預設測試:所有這些函式都可以接受一個可選的比較函式,該函式用於代替相等測試。這意味著您可以查詢滿足任何測試的列表元素。
以下示例使用 < 比較函式。find 查詢與 n 相比較有利的第一個元素,即 n 小於的第一個元素。對於值為 1002,滿足測試的第一個元素是 1003,即列表中的第 3 個元素,因此返回的值為 3。
(set 's (sequence 1000 1020))
;-> (1000 1001 1002 1003 1004 1005 1006 1007 1008 100
; 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020)
(set 'n 1002)
; find the first something that n is less than:
(find n s <)
;-> 3, the index of 1003 in s
您可以編寫自己的比較函式
(set 'a-list
'("elephant" "antelope" "giraffe" "dog" "cat" "lion" "shark" ))
(define (longer? x y)
(> (length x) (length y)))
(find "tiger" a-list longer?)
;-> 3 ; "tiger" is longer than element 3, "dog"
longer? 函式在第一個引數比第二個引數長時返回 true。因此,find 在此函式作為比較時,找到列表中使比較為真的第一個元素。因為 tiger 比 dog 長,所以該函式返回 3,即 dog 在列表中的索引。
您可以將匿名(或 lambda)函式作為 find 函式的一部分提供,而不是編寫單獨的函式
(find "tiger" a-list (fn (x y) (> (length x) (length y))))
如果您希望程式碼可讀,您可能會將較長或更復雜的比較器移到自己的單獨的 - 並且有文件的 - 函式中。
您還可以將比較函式與 ref、ref-all 和 replace 一起使用。
比較函式可以是任何函式,它接受兩個值並返回 true 或 false。例如,以下函式在 y 大於 6 且小於 x 時返回 true。因此,搜尋是在 data 列表中查詢一個元素,該元素既小於要搜尋的數字(在本例中為 15),又大於 6。
(set 'data '(31 23 -63 53 8 -6 -16 71 -124 29))
(define (my-func x y)
(and (> x y) (> y 6)))
(find 15 data my-func)
;-> 4 ; there's an 8 at index location 4
為了總結這些 contains 函式,以下是在它們起作用時的示例
(set 'data
'("this" "is" "a" "list" "of" "strings" "not" "of" "integers"))
(find "of" data) ; equality is default test
;-> 4 ; index of first occurrence
(ref "of" data) ; where is "of"?
;-> (4) ; returns a list of indexes
(ref-all "of" data)
;-> ((4) (7)) ; list of address lists
(filter (fn (x) (= "of" x)) data) ; keep every of
;-> ("of" "of")
(index (fn (x) (= "of" x)) data) ; indexes of the of's
;-> (4 7)
(match (* "of" * "of" *) data) ; three lists between the of's
;-> (("this" "is" "a" "list") ("strings" "not") ("integers"))
(member "of" data) ; and the rest
;-> ("of" "strings" "not" "of" "integers")
(count (list "of") data) ; remember to use two lists
;-> (2) ; returns list of counts
有多種函式用於獲取儲存在列表中的資訊
- first 獲取第一個元素
- rest 獲取除第一個元素之外的所有元素
- last 返回最後一個元素
- nth 獲取第 n 個元素
- select 根據索引選擇某些元素
- slice 提取子列表
first 和 rest 函式是傳統 car 和 cdr LISP 函式更合理的名稱,它們基於舊計算機硬體暫存器的名稱。
nth 獲取列表中的第 n 個元素
(set 'phrase '("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"))
(nth 1 phrase)
;-> "quick"
nth 還可以檢視巢狀列表的內部,因為它接受多個索引號
(set 'zoo
'(("ape" 3)
("bat" 47)
("lion" 4)))
(nth '(2 1) zoo) ; item 2, then subitem 1
;-> 4
如果您想從列表中挑選一組元素,您會發現 select 很有用。您可以在兩種不同的形式中使用它。第一種形式允許您提供一系列鬆散的索引號
(set 'phrase '("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"))
(select phrase 0 -2 3 4 -4 6 1 -1)
;-> ("the" "lazy" "fox" "jumped" "over" "the" "quick" "dog")
正數透過從開頭向前計數來選擇元素,負數透過從結尾向後計數來選擇元素
0 1 2 3 4 5 6 7 8
("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog")
-9 -8 -7 -6 -5 -4 -3 -2 -1
您還可以將索引號列表提供給 select。例如,您可以使用 rand 函式生成一個從 0 到 8 之間的 20 個隨機數的列表,然後使用此列表從 phrase 中隨機選擇元素
(select phrase (rand 9 20))
;-> ("jumped" "lazy" "over" "brown" "jumped" "dog" "the" "dog" "dog"
; "quick" "the" "dog" "the" "dog" "the" "brown" "lazy" "lazy" "lazy" "quick")
注意重複項。如果您改為編寫
(randomize phrase)
則不會有重複項:(randomize phrase) 打亂元素而不重複它們。
slice 允許您提取列表的部分。為它提供列表,後跟一個或兩個數字。第一個數字是起始位置。如果您省略第二個數字,則會返回列表的其餘部分。第二個數字(如果為正數)是要返回的元素數。
(slice (explode "schwarzwalderkirschtorte") 7)
;-> ("w" "a" "l" "d" "e" "r" "k" "i" "r" "s" "c" "h" "t" "o" "r" "t" "e")
(slice (explode "schwarzwalderkirschtorte") 7 6)
;-> ("w" "a" "l" "d" "e" "r")
如果為負數,則第二個數字指定切片另一端的一個元素,從列表末尾向後計數,-1 表示最後一個元素
(slice (explode "schwarzwalderkirschtorte") 19 -1)
;-> ("t" "o" "r" "t")
切刀可以延伸到 - 但不包括 - 您指定的元素。
newLISP 提供了一種更快、更高效的方式來選擇和切片列表。您可以使用索引號和列表一起使用,而不是使用函式。此技術稱為隱式定址。
作為使用 nth 的替代方法,將列表的符號和索引號放在一個列表中,如下所示
(set 'r '("the" "cat" "sat" "on" "the" "mat"))
(r 1) ; element index 1 of r
;-> "cat"
(nth 1 r) ; the equivalent using nth
;-> "cat"
(r 0)
;-> "the"
(r -1)
;-> "mat"
如果您有一個巢狀列表,您可以提供一系列索引號來標識層次結構中的列表
(set 'zoo
'(("ape" 3)
("bat" 47)
("lion" 4))) ; three sublists in a list
(zoo 2 1)
;-> 4
(nth '(2 1) zoo) ; the equivalent using nth
;-> 4
其中 '(2 1) 首先找到元素 2,("lion" 4),然後找到該子列表中的元素 1(第二個元素)。
您還可以使用隱式定址來獲取列表的切片。這次,在列表的符號之前,在一個列表中放置一個或兩個數字來定義切片
(set 'alphabet '("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k"
"l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"))
(13 alphabet) ; start at 13, get the rest
;-> ("n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")
(slice alphabet 13) ; equivalent using slice
;-> ("n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")
(3 7 alphabet) ; start at 3, get 7 elements
;-> ("d" "e" "f" "g" "h" "i" "j")
(slice alphabet 3 7) ; equivalent using slice
;-> ("d" "e" "f" "g" "h" "i" "j")
之前,我們解析了 iTunes XML 庫
(xml-type-tags nil nil nil nil)
(silent
(set 'itunes-data
(xml-parse
(read-file
"/Users/me/Music/iTunes/iTunes Music Library.xml")
(+ 1 2 4 8 16))))
讓我們使用隱式定址技術訪問結果 XML 結構的內部
(set 'eno (ref "Brian Eno" itunes-data))
;-> (0 2 14 528 6 1) ; address of Brian Eno
(0 4 eno) ; implicit slice
;-> (0 2 14 528)
(itunes-data (0 4 eno))
;->
(dict
(key "Track ID")
(int "305")
(key "Name")
(string "An Ending (Ascent)")
(key "Artist")
(string "Brian Eno") ; this was (0 2 14 528 6 1)
(key "Album")
(string "Ambient Journeys")
(key "Genre")
(string "ambient, new age, electronica")
(key "Kind")
(string "Apple Lossless audio file")
(key "Size")
(int "21858166")
; ...
)
如何記住兩種型別隱式定址之間的區別?sLice 數字放在 Lead 中,sElect 數字放在 End 中。
要縮短列表,從前面或後面刪除元素,請使用 chop 或 pop。chop 建立一個副本並從末尾開始,pop 更改原始副本並從開頭開始。
chop 透過切斷列表的末尾來返回一個新列表
(set 'vowels '("a" "e" "i" "o" "u"))
(chop vowels)
;-> ("a" "e" "i" "o")
(println vowels)
("a" "e" "i" "o" "u") ; original unchanged
chop 的可選第三個引數指定要刪除的元素數量
(chop vowels 3)
;-> ("a" "e")
(println vowels)
("a" "e" "i" "o" "u") ; original unchanged
pop(與 push 相反)永久地從列表中刪除指定的元素,並使用列表索引而不是長度
(set 'vowels '("a" "e" "i" "o" "u"))
(pop vowels) ; defaults to 0-th element
(println vowels)
("e" "i" "o" "u")
(pop vowels -1)
(println vowels)
("e" "i" "o")
您還可以使用 replace 從列表中刪除專案。
您可以輕鬆地使用以下函式更改列表中的元素
- replace 更改或刪除元素
- swap 交換兩個元素
- setf 設定元素的值
- set-ref 搜尋巢狀列表並更改元素
- set-ref-all 搜尋並更改巢狀列表中的所有元素
這些是破壞性函式,就像 push、pop、reverse 和 sort 一樣,它們會更改原始列表,因此請謹慎使用它們。
要將列表(或陣列)中的第 n 個元素設定為另一個值,請使用通用的 setf 命令
(set 'data (sequence 100 110))
;-> (100 101 102 103 104 105 106 107 108 109 110)
(setf (data 5) 0)
;-> 0
data
;-> (100 101 102 103 104 0 106 107 108 109 110)
注意 setf 函式如何返回剛剛設定的值 0,而不是更改後的列表。
此示例使用更快的隱式定址。當然,您也可以使用 nth 首先建立對第 n 個元素的引用
(set 'data (sequence 100 110))
;-> (100 101 102 103 104 105 106 107 108 109 110)
(setf (nth 5 data) 1)
;-> 1
data
;-> (100 101 102 103 104 1 106 107 108 109 110)
setf 必須用於儲存在符號中的列表或陣列或元素。您不能將原始資料傳遞給它
(setf (nth 5 (sequence 100 110)) 1)
;-> ERR: no symbol reference found
(setf (nth 5 (set 's (sequence 100 110))) 1)
; 'temporary' storage in symbol s
;-> 1
s
;-> (100 101 102 103 104 1 106 107 108 109 110)
有時,當您使用 setf 時,您希望在設定新值時引用舊值。為此,請使用系統變數 $it。在 setf 表示式期間,$it 包含舊值。因此,要將列表第一個元素的值增加 1
(set 'lst (sequence 0 9))
;-> (0 1 2 3 4 5 6 7 8 9)
(setf (lst 0) (+ $it 1))
;-> 1
lst
;-> (1 1 2 3 4 5 6 7 8 9)
您也可以對字串執行此操作。以下是“增加”字串第一個字母的方法
(set 'str "cream")
;-> "cream"
(setf (str 0) (char (inc (char $it))))
;-> "d"
str
;-> "dream"
您可以使用 replace 更改或刪除列表中的元素。指定要更改的元素和要搜尋的列表,以及(如果有)替換項。
(set 'data (sequence 1 10))
(replace 5 data) ; no replacement specified
;-> (1 2 3 4 6 7 8 9 10) ; the 5 has gone
(set 'data '(("a" 1) ("b" 2)))
(replace ("a" 1) data) ; data is now (("b" 2))
所有匹配項都將被刪除。
replace 返回已更改的列表
(set 'data (sequence 1 10))
(replace 5 data 0) ; replace 5 with 0
;-> (1 2 3 4 0 6 7 8 9 10)
替換可以是簡單值,也可以是返回值的任何表示式。
(set 'data (sequence 1 10))
(replace 5 data (sequence 0 5))
;->(1 2 3 4 (0 1 2 3 4 5) 6 7 8 9 10)
replace 使用系統變數集 $0、$1、$2,一直到 $15,以及特殊變數 $it,更新匹配資料。對於列表替換,僅使用 $0 和 $it,它們儲存找到的專案的 value,適合在替換表示式中使用。
(replace 5 data (list (dup $0 2))) ; $0 holds 5
;-> (1 2 3 4 ((5 5)) 6 7 8 9 10)
有關係統變數及其在字串替換中的使用,請參閱 系統變數。
如果您沒有提供測試函式,則使用 =
(set 'data (sequence 1 10))
(replace 5 data 0 =)
;-> (1 2 3 4 0 6 7 8 9 10)
(set 'data (sequence 1 10))
(replace 5 data 0) ; = is assumed
;-> (1 2 3 4 0 6 7 8 9 10)
您可以使 replace 查詢透過不同測試(而不是相等性)的元素。在替換值之後提供測試函式
(set 'data (randomize (sequence 1 10)))
;-> (5 10 6 1 7 4 8 3 9 2)
(replace 5 data 0 <) ; replace everything that 5 is less than
;-> (5 0 0 1 0 4 0 3 0 2)
測試可以是任何比較兩個值並返回真值或假值的函式。這可能非常強大。假設您有一個包含姓名及其分數的列表
(set 'scores '(
("adrian" 234 27 342 23 0)
("hermann" 92 0 239 47 134)
("neville" 71 2 118 0)
("eric" 10 14 58 12 )))
將所有分數包含 0 的人的數字加起來有多容易?有了 match 函式的幫助,這很容易
(replace '(* 0 *) scores (list (first $0) (apply + (rest $0))) match)
(("adrian" 626)
("hermann" 512)
("neville" 191)
("eric" 10 14 58 12))
在這裡,對於每個匹配元素,替換表示式從名稱和分數之和構建一個列表。match 用作比較函式 - 僅選擇匹配的列表元素進行彙總,因此 Eric 的分數未被彙總,因為他沒有設法獲得 0 分。
有關在字串上使用 replace 的更多資訊,請參閱 更改子字串。
還有更強大的方法可以修改列表中的元素。認識一下 set-ref 和 set-ref-all。
您可以使用這些函式定位和修改元素,這些函式旨在與巢狀列表配合使用。(另請參閱 使用 XML,瞭解一些應用程式。)
set-ref 函式允許您修改列表中的第一個匹配元素
(set 'l '((aaa 100) (bbb 200)))
;-> ((aaa 100) (bbb 200))
要將 200 更改為 300,請使用 set-ref,如下所示
(set-ref 200 l 300) ; change the first 200 to 300
;-> ((aaa 100) (bbb 300))
set-ref 在巢狀列表中找到第一個匹配元素並更改它;set-ref-all 可以替換每個匹配元素。考慮以下包含行星資料的巢狀列表
(("Mercury"
(p-name "Mercury")
(diameter 0.382)
(mass 0.06)
(radius 0.387)
(period 0.241)
(incline 7)
(eccentricity 0.206)
(rotation 58.6)
(moons 0))
("Venus"
(p-name "Venus")
(diameter 0.949)
(mass 0.82)
(radius 0.72)
(period 0.615)
(incline 3.39)
(eccentricity 0.0068)
(rotation -243)
(moons 0))
("Earth"
(p-name "Earth")
(diameter 1)
; ...
如何將所有“incline”符號更改為“inclination”?使用 set-ref-all 很容易
(set-ref-all 'incline planets 'inclination) ; key - list - replacement
這將返回一個列表,其中每個“incline”都更改為“inclination”。
與 replace 一樣,查詢匹配元素的預設測試是相等性。但是您可以提供不同的比較函式。以下是您如何檢查行星列表並將每個月球值大於 9 的條目更改為“lots”而不是實際數字的方法。
(set-ref-all '(moons ?) planets (if (> (last $0) 9) "lots" (last $0)) match)
替換表示式比較月球數量(結果的最後一個專案,儲存在 $0 中),如果大於 9,則評估為“lots”。搜尋詞使用 match 友好的萬用字元語法來匹配比較函式的選擇。
swap 函式可以交換列表中的兩個元素,或兩個符號的值。這將更改原始列表
(set 'fib '(1 2 1 3 5 8 13 21))
(swap (fib 1) (fib 2)) ; list swap
;-> (1 1 2 3 5 8 13 21)
fib
;-> (1 1 2 3 5 8 13 21) ; is 'destructive'
有用的是,swap 還可以交換兩個符號的值,而無需使用中間臨時變數。
(set 'x 1 'y 2)
(swap x y)
;-> 1
x
;-> 2
y
;-> 1
這種並行分配有時可以簡化生活,例如在這個略有不尋常的查詢斐波那契數的迭代函式版本中
(define (fibonacci n)
(let (current 1 next 0)
(dotimes (j n)
(print current " ")
(inc next current)
(swap current next))))
(fibonacci 20)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
如果您有兩個列表,您可能想問一些問題,例如 兩個列表中共有多少個專案?、哪些專案只在一個列表中?、此列表中的專案在另一個列表中出現多少次?,等等。以下是一些用於回答這些問題的有用函式
- difference 查詢兩個列表的集合差
- intersect 查詢兩個列表的交集
- count 計算一個列表中的每個元素在第二個列表中出現的次數
例如,要檢視句子中母音的數量,請將已知母音放在一個列表中,將句子放在另一個列表中(首先使用 explode 將句子轉換為字元列表)
(count '("a" "e" "i" "o" "u") (explode "the quick brown fox jumped over the lazy dog"))
;-> (1 4 1 4 2)
或者更短一點
(count (explode "aeiou") (explode "the quick brown fox jumped over the lazy dog"))
;-> (1 4 1 4 2)
結果 (1 4 1 4 2) 表示句子中有 1 個 a、4 個 e、1 個 i、4 個 o 和 2 個 u。
difference 和 intersect 是讓您想起學校裡學的那些維恩圖的函式(如果您上了教這些的學校)。在 newLISP 中,列表可以表示集合。
difference 返回一個列表,其中包含第一個列表中不在第二個列表中的那些元素。例如,您可以比較系統上的兩個目錄,以查詢一個目錄中存在但另一個目錄中不存在的檔案。您可以為此使用 directory 函式。
(set 'd1
(directory "/Users/me/Library/Application Support/BBEdit"))
(set 'd2
(directory "/Users/me/Library/Application Support/TextWrangler"))
(difference d1 d2)
;-> ("AutoSaves" "Glossary" "HTML Templates" "Stationery" "Text Factories")
(difference d2 d1)
;-> ()
您將哪個列表放在前面很重要!目錄 d1 中有五個不在目錄 d2 中的檔案或目錄,但 d2 中沒有不在 d1 中的檔案或目錄。
intersect 函式查詢兩個列表中都存在的元素。
(intersect d2 d1)
;-> ("." ".." ".DS_Store" "Language Modules" "Menu Scripts" "Plug-Ins" "Read Me.txt" "Scripts" "Unix Support")
這兩個函式都可以接受一個額外的引數,它控制是否保留或丟棄任何重複項。
您可以使用 difference 函式比較文字檔案的兩個修訂版。首先使用 parse (解析字串) 將檔案拆分為行
(set 'd1
(parse (read-file "/Users/me/f1-(2006-05-29)-1.html") "\r" 0))
(set 'd2
(parse (read-file "/Users/me/f1-(2006-05-29)-6.html") "\r" 0))
(println (difference d1 d2))
(" <p class=\"body\">You could use this function to find" ...)
newLISP 中提供了各種用於儲存資訊的技術。一種非常簡單且有效的技術是使用子列表列表,其中每個子列表的第一個元素是 key。這種結構被稱為關聯列表,但您也可以將其視為字典,因為您透過首先查詢 key 元素來查詢列表中的資訊。
您還可以使用 newLISP 的上下文來實現字典。請參閱 介紹上下文。
您可以使用基本的列表函式建立關聯列表。例如,您可以提供一個手工製作的引用列表
(set 'ascii-chart '(("a" 97) ("b" 98) ("c" 99)
; ...
))
或者您可以使用 list 和 push 等函式來構建關聯列表
(for (c (char "a") (char "z"))
(push (list (char c) c) ascii-chart -1))
ascii-chart
;-> (("a" 97) ("b" 98) ("c" 99) ... ("z" 122))
它是一個子列表列表,每個子列表都具有相同的格式。子列表的第一個元素是 key。key 可以是字串、數字或符號。key 後面可以有任意數量的資料元素。
這是一個包含太陽系行星的一些資料的關聯列表
(set 'sol-sys
'(("Mercury" 0.382 0.06 0.387 0.241 7.00 0.206 58.6 0)
("Venus" 0.949 0.82 0.72 0.615 3.39 0.0068 -243 0)
("Earth" 1.00 1.00 1.00 1.00 0.00 0.0167 1.00 1)
("Mars" 0.53 0.11 1.52 1.88 1.85 0.0934 1.03 2)
("Jupiter" 11.2 318 5.20 11.86 1.31 0.0484 0.414 63)
("Saturn" 9.41 95 9.54 29.46 2.48 0.0542 0.426 49)
("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27)
("Neptune" 3.81 17.2 30.06 164.8 1.77 0.0086 0.671 13)
("Pluto" 0.18 0.002 39.5 248.5 17.1 0.249 -6.5 3)
)
; 0: Planet name 1: Equator diameter (earth) 2: Mass (earth)
; 3: Orbital radius (AU) 4: Orbital period (years)
; 5: Orbital Incline Angle 6: Orbital Eccentricity
; 7: Rotation (days) 8: Moons
)
每個子列表都以字串(行星名稱)開頭,後面是資料元素(在本例中為數字)。行星名稱是 key。我在最後添加了一些註釋,因為我永遠不會記得元素 2 是行星的質量,以地球質量為單位。
您可以輕鬆地使用標準列表處理技術訪問此資訊,但 newLISP 提供了一些專門針對這些字典或關聯列表而設計的定製函式
- assoc 查詢關鍵字的首次出現並返回子列表。
- lookup 在子列表中查詢關鍵字的值。
assoc 和 lookup 都使用子列表的第一個元素(即鍵)從相應的子列表中檢索資料。以下是 assoc 的實際操作,它返回子列表。
(assoc "Uranus" sol-sys)
;-> ("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27)
以下是 lookup,它更進一步,從子列表中的某個元素中獲取資料,如果沒有指定元素,則獲取最後一個元素。
(lookup "Uranus" sol-sys)
;-> 27, moons - value of the final element of the sublist
(lookup "Uranus" sol-sys 2)
;-> 14.6, element 2 of the sublist is the planet's mass
這使你無需再使用 assoc 和 nth 的組合。
使用帶有長子列表的關聯列表時,你可能會遇到一個問題,即無法記住索引號代表什麼。以下是一個解決方案。
(constant 'orbital-radius 3)
(constant 'au 149598000) ; 1 au in km
(println "Neptune's orbital radius is "
(mul au (lookup "Neptune" sol-sys orbital-radius))
" kilometres")
Neptune's orbital radius is 4496915880 kilometres
這裡我們定義了 orbital-radius 和 au(天文單位)作為常量,你可以使用 orbital-radius 來引用子列表的右側列。這也有助於使程式碼更易讀。constant 函式類似於 set,但你提供的符號受保護,不會因 set 的其他使用而意外更改。你只能使用 constant 函式再次更改符號的值。
定義完這些常量後,以下是一個以公里為單位列出不同行星軌道的表示式。
(dolist (planet-data sol-sys) ; go through list
(set 'planet (first planet-data)) ; get name
(set 'orb-rad
(lookup planet sol-sys orbital-radius)) ; get radius
(println
(format "%-8s %12.2f %18.0f"
planet
orb-rad
(mul au orb-rad))))
Mercury 0.39 57894426 Venus 0.72 107710560 Earth 1.00 149598000 Mars 1.52 227388960 Jupiter 5.20 777909600 Saturn 9.54 1427164920 Uranus 19.22 2875273560 Neptune 30.06 4496915880 Pluto 39.50 5909121000
當你想要操作浮點數時,請使用浮點算術運算子 add、sub、mul、div 而不是 +、-、* 和 /,後者用於整數(並將值轉換為整數)。
替換關聯列表中的子列表
[edit | edit source]要更改儲存在關聯列表中的值,請像以前一樣使用 assoc 函式查詢匹配的子列表,然後使用 setf 在該子列表上將值更改為新的子列表。
(setf (assoc "Jupiter" sol-sys) '("Jupiter" 11.2 318 5.20 11.86 1.31 0.0484 0.414 64))
向關聯列表新增新專案
[edit | edit source]關聯列表也是普通列表,因此你可以使用所有熟悉的 newLISP 技術來處理它們。想要向我們的 sol-sys 列表中新增一個新的第十顆行星嗎?只需使用 push
(push '("Sedna" 0.093 0.00014 .0001 502 11500 0 20 0) sol-sys -1)
並使用以下命令檢查它是否已新增成功。
(assoc "Sedna" sol-sys)
;-> ("Sedna" 0.093 0.00014 0.0001 502 11500 0 20 0)
你可以使用 sort 對關聯列表進行排序。(但請記住,sort 會永久更改列表。)以下是一個按質量排序的行星列表。由於你不想按名稱對其進行排序,因此可以使用自定義排序(參見 sort and randomize)來比較每對的質量(索引 2)值。
(constant 'mass 2)
(sort sol-sys (fn (x y) (> (x mass) (y mass))))
(println sol-sys)
("Jupiter" 11.2 318 5.2 11.86 1.31 0.0484 0.414 63)
("Saturn" 9.41 95 9.54 29.46 2.48 0.0542 0.426 49)
("Neptune" 3.81 17.2 30.06 164.8 1.77 0.0086 0.671 13)
("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27)
("Earth" 1 1 1 1 0 0.0167 1 1)
("Venus" 0.949 0.82 0.72 0.615 3.39 0.0068 -243 0)
("Mars" 0.53 0.11 1.52 1.88 1.85 0.0934 1.03 2)
("Mercury" 0.382 0.06 0.387 0.241 7 0.206 58.6 0)
("Pluto" 0.18 0.002 39.5 248.5 17.1 0.249 -6.5 3)
你也可以輕鬆地將關聯列表中的資料與其他列表組合起來。
; restore to standard order - sort by orbit radius
(sort sol-sys (fn (x y) (< (x 3) (y 3))))
; define Unicode symbols for planets
(set 'unicode-symbols
'(("Mercury" 0x263F )
("Venus" 0x2640 )
("Earth" 0x2641 )
("Mars" 0x2642 )
("Jupiter" 0x2643 )
("Saturn" 0x2644 )
("Uranus" 0x2645 )
("Neptune" 0x2646 )
("Pluto" 0x2647)))
(map
(fn (planet)
(println (char (lookup (first planet) unicode-symbols))
"\t"
(first planet)))
sol-sys)
☿ (Unicode symbol for Mercury) ♀ (Unicode symbol for Venus) ♁ (Unicode symbol for Earth) ♂ (Unicode symbol for Mars) ♃ (Unicode symbol for Jupiter) ♄ (Unicode symbol for Saturn) ♅ (Unicode symbol for Uranus) ♆ (Unicode symbol for Neptune) ♇ (Unicode symbol for Pluto)
這裡我們建立了一個臨時的行內函數,map 將其應用於 sol-sys 中的每個行星 - lookup 查詢行星名稱,並從 unicode-symbols 關聯列表中檢索該行星的 Unicode 符號。
你可以使用 pop-assoc 快速從關聯列表中刪除元素。
(pop-assoc (sol-sys "Pluto"))
這將從列表中刪除冥王星元素。
newLISP 以上下文的形式提供了強大的資料儲存功能,你可以使用這些功能來構建字典、雜湊表、物件等等。你可以使用關聯列表來構建字典,並使用關聯列表函式來處理字典的內容。參見 Introducing contexts。
你也可以使用資料庫引擎 - 參見 Using a SQLite database。
find-all 和關聯列表
[edit | edit source]find-all 的另一種形式允許你在關聯列表中搜索與模式匹配的子列表。你可以使用萬用字元來指定模式。例如,以下是一個關聯列表。
(set 'symphonies
'((Beethoven 9)
(Haydn 104)
(Mozart 41)
(Mahler 10)
(Wagner 1)
(Schumann 4)
(Shostakovich 15)
(Bruckner 9)))
要查詢所有以 9 結尾的子列表,請使用匹配模式 (? 9),其中問號匹配任何單個專案。
(find-all '(? 9) symphonies)
;-> ((Beethoven 9) (Bruckner 9))
(有關匹配模式(列表的萬用字元搜尋)的更多資訊,請參見 matching patterns in lists。)
你也可以在關聯列表後面新增一個額外的操作表示式來使用此形式。
(find-all '(? 9) symphonies
(println (first $0) { wrote 9 symphonies.}))
Beethoven wrote 9 symphonies.
Bruckner wrote 9 symphonies.
這裡,操作表示式使用 $0 依次引用每個匹配的元素。
字串
[edit | edit source]字串處理工具是程式語言的重要組成部分。newLISP 擁有許多易於使用且強大的字串處理工具,如果你的特定需求沒有得到滿足,你可以輕鬆地向你的工具箱中新增更多工具。
以下是對 newLISP 的 string orchestra 的導覽。
newLISP 程式碼中的字串
[edit | edit source]你可以透過三種方式編寫字串
- 用雙引號括起來
- 用花括號括起來
- 用標記程式碼標記起來
像這樣
(set 's "this is a string")
(set 's {this is a string})
(set 's [text]this is a string[/text])
所有三種方法都可以處理最多 2048 個字元的字串。對於超過 2048 個字元的字串,請始終使用 [text] 和 [/text] 標籤來包圍字串。
如果你希望對跳脫字元(例如 \n 和 \t)或程式碼數字(\046)進行處理,請始終使用第一種方法(即引號)。
(set 's "this is a string \n with two lines")
(println s)
this is a string with two lines
(println "\110\101\119\076\073\083\080") ; decimal ASCII
newLISP
(println "\x6e\x65\x77\x4c\x49\x53\x50") ; hex ASCII
newLISP
雙引號字元必須使用反斜槓轉義,反斜槓也是如此,如果你希望它們出現在字串中。
當字串長度小於 2048 個字元且你不想處理任何跳脫字元時,請使用第二種方法(即花括號)。
(set 's {strings can be enclosed in \n"quotation marks" \n })
(println s)
strings can be enclosed in \n"quotation marks" \n
這是一種非常有用的編寫字串的方法,因為你無需擔心在每個引號字元之前新增反斜槓,或在其他反斜槓之前新增反斜槓。你可以在帶花括號的字串中巢狀花括號對,但不能有未匹配的花括號。我喜歡使用花括號來表示字串,因為它們朝向正確的方向(而簡單的 dumb 引號則沒有),並且因為你的文字編輯器可能能夠平衡和匹配它們。
第三種方法是使用 [text] 和 [/text] 標記,適用於跨越多行的較長文字字串,當 newLISP 輸出大量文字時會自動使用這種方法。同樣,你無需擔心哪些字元可以包含,哪些字元不能包含 - 你可以包含任何你喜歡的字元,但 [/text] 除外。跳脫字元(如 \n 或 \046)也不會被處理。
(set 'novel (read-file {my-latest-novel.txt}))
;->
[text]
It was a dark and "stormy" night...
...
The End.
[/text]
如果你想知道字串的長度,請使用 length
(length novel)
;-> 575196
newLISP 可以輕鬆處理數百萬個字元的字串。
與其使用 length,不如使用 utf8len 來獲取 Unicode 字串的長度。
(utf8len (char 955))
;-> 1
(length (char 955))
;-> 2
製作字串
[edit | edit source]許多函式(例如檔案讀取函式)會為你返回字串或字串列表。但如果你想從頭開始構建字串,一種方法是使用 char 函式。這會將提供的數字轉換為具有該程式碼數字的等效字元字串。它也可以反轉操作,將提供的字元字串轉換為等效的程式碼數字。)
(char 33)
;-> "!"
(char "!")
;-> 33
(char 955) ; Unicode lambda character, decimal code
;-> "\206\187"
(char 0x2643) ; Unicode symbol for Jupiter, hex code
;-> "\226\153\131"
當你執行支援 Unicode 的 newLISP 版本時,最後這兩個示例可用。由於 Unicode 偏向於十六進位制,因此你可以為 char 提供一個以 0x 開頭的十六進位制數。要檢視實際的字元,請使用列印命令
(println (char 955))
λ
;-> "\206\187"
(println (char 0x2643))
♃
;-> "\226\140\152"
(println (char (int (string "0x" "2643")))) ; equivalent
♃
;-> "\226\140\152"
帶有反斜槓的數字是 println 函式的結果,可能是 Unicode 字元的位元組值。
你可以使用 char 以其他方式構建字串。
(join (map char (sequence (char "a") (char "z"))))
;-> "abcdefghijklmnopqrstuvwxyz"
這使用 char 找出 a 和 z 的 ASCII 程式碼數字,然後使用 sequence 生成這兩個數字之間的程式碼數字列表。然後將 char 函式對映到列表的每個元素上,從而產生一個字串列表。最後,此列表透過 join 轉換為單個字串。
join 在構建字串時還可以使用分隔符。
(join (map char (sequence (char "a") (char "z"))) "-")
;-> "a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z"
與 join 相似的是 append,它直接在字串上操作。
(append "con" "cat" "e" "nation")
;-> "concatenation"
但更實用的是 string,它會將任何數字、列表和字串的集合轉換為單個字串。
(string '(sequence 1 10) { produces } (sequence 1 10) "\n")
;-> (sequence 1 10) produces (1 2 3 4 5 6 7 8 9 10)
請注意,第一個列表沒有被求值(因為它被引用),但第二個列表被求值,生成了一個數字列表,然後生成的列表(包括括號)被轉換為字串。
string 函式與花括號和標記標籤等各種字串標記相結合,是將變數的值包含在字串中的一種方法。
(set 'x 42)
(string {the value of } 'x { is } x)
;-> "the value of x is 42"
您也可以使用 format 來組合字串和符號值。參見 格式化字串。
dup 用於建立副本
(dup "spam" 10)
;-> "spamspamspamspamspamspamspamspamspamspam"
而 date 用於生成日期字串
(date)
;-> "Wed Jan 25 15:04:49 2006"
或者您可以提供自 1970 年以來的秒數進行轉換
(date 1230000000)
;-> "Tue Dec 23 02:40:00 2008"
參見 處理日期和時間。
現在您已經有了字串,有很多函式可以對它們進行操作。其中一些是 破壞性 函式 - 它們永久地改變字串,可能會永遠丟失資訊。另一些是構造性的,生成新的字串,而不會破壞舊字串。參見 破壞性函式。
reverse 是破壞性的
(set 't "a hypothetical one-dimensional subatomic particle")
(reverse t)
;-> "elcitrap cimotabus lanoisnemid-eno lacitehtopyh a"
現在 t 永遠改變了。但是,大小寫轉換函式不是破壞性的,它們會生成新的字串,而不會破壞舊字串
(set 't "a hypothetical one-dimensional subatomic particle")
(upper-case t)
;-> "A HYPOTHETICAL ONE-DIMENSIONAL SUBATOMIC PARTICLE"
(lower-case t)
;-> "a hypothetical one-dimensional subatomic particle"
(title-case t)
;-> "A hypothetical one-dimensional subatomic particle"
如果您知道要提取字串的哪個部分,請使用以下建構函式之一
(set 't "a hypothetical one-dimensional subatomic particle")
(first t)
;-> "a"
(rest t)
;-> " hypothetical one-dimensional subatomic particle"
(last t)
;-> "e"
(t 2) ; counting from 0
;-> "h"
您也可以將此技術用於列表。參見 從列表中選擇專案。
slice 為您提供現有字串的新切片,從切割點開始向前計數(正整數)或從末尾開始向後計數(負整數),對於給定的字元數或到指定的職位
(set 't "a hypothetical one-dimensional subatomic particle")
(slice t 15 13)
;-> "one-dimension"
(slice t -8 8)
;-> "particle"
(slice t 2 -9)
;-> "hypothetical one-dimensional subatomic"
(slice "schwarzwalderkirschtorte" 19 -1)
;-> "tort"
還有一個捷徑可以做到這一點。在列表中將所需的開始位置和長度放在字串之前
(15 13 t)
;-> "one-dimension"
(0 14 t)
;-> "a hypothetical"
如果您不想要連續的字元,而是想要挑選一些字元來組成新的字串,請使用 select 後跟一系列字元索引號
(set 't "a hypothetical one-dimensional subatomic particle")
(select t 3 5 24 48 21 10 44 8)
;-> "yosemite"
(select t (sequence 1 49 12)) ; every 12th char starting at 1
;-> " lime"
這對於在文字中查詢隱藏的編碼訊息很有用。
trim 和 chop 都是構造性的字串編輯函式,它們從原始字串的末尾向內工作。
chop 從末尾開始工作
(chop t) ; defaults to the last character
;-> "a hypothetical one-dimensional subatomic particl"
(chop t 9) ; chop 9 characters off
;-> "a hypothetical one-dimensional subatomic"
trim 可以從兩端刪除字元
(set 's " centred ")
(trim s) ; defaults to removing spaces
;-> "centred"
(set 's "------centred------")
(trim s "-")
;-> "centred"
(set 's "------centred******")
(trim s "-" "*") ; front and back
;-> "centred"
您已經看到 push 和 pop 在向列表新增和刪除專案。它們也可以用於字串。使用 push 向字串新增字元,使用 pop 從字串中刪除一個字元。除非您指定索引,否則字串將新增到或從字串的開頭新增或刪除。
(set 't "some ")
(push "this is " t)
(push "text " t -1)
;-> t is now "this is some text"
pop 始終返回被彈出內容,但 push 返回修改後的目標。當您想分解字串並按順序處理這些片段時,它很有用。例如,要列印 newLISP 版本號(儲存為 4 位或 5 位整數),請使用類似於以下程式碼
(set 'version-string (string (sys-info -2)))
; eg: version-string is now "10303"
(set 'dev-version (pop version-string -2 2)) ; always two digits
; dev-version is "03", version-string is "103"
(set 'point-version (pop version-string -1)) ; always one digit
; point-version is "3", version-string is now "10"
(set 'version version-string) ; one or two digits
(println version "." point-version "." dev-version)
10.3.03
從字串的右側開始工作並使用 pop 提取資訊並將其刪除在一個操作中更容易。
更改字串中的字元有兩種方法。要麼使用字元的索引號,要麼指定要查詢或更改的子字串。
要按索引號更改字元,請使用 setf,這是更改字串、列表和陣列的通用函式
(set 't "a hypothetical one-dimensional subatomic particle")
(setf (t 0) "A")
;-> "A"
t
;-> "A hypothetical one-dimensional subatomic particle"
您也可以使用 nth 和 setf 來指定位置
(set 't "a hypothetical one-dimensional subatomic particle")
;-> "a hypothetical one-dimensional subatomic particle"
(setf (nth 0 t) "A")
;-> "A"
t
;-> "A hypothetical one-dimensional subatomic particle"
以下是“遞增”字串第一個(第零個)字母的方法
(set 'wd "cream")
;-> "cream"
(setf (wd 0) (char (+ (char $it) 1)))
;-> "d"
wd
;-> "dream"
$it包含 setf 表示式第一部分找到的值,其數值將遞增以形成第二部分。
如果您不想使用或無法使用索引號或字元位置,請使用 replace,這是一個強大的破壞性函式,它對字串執行各種有用的操作。在以下形式中使用它
(replace old-string source-string replacement)
所以
(set 't "a hypothetical one-dimensional subatomic particle")
(replace "hypoth" t "theor")
;-> "a theoretical one-dimensional subatomic particle"
replace 是破壞性的,但是如果您想構造性地使用 replace 或其他破壞性函式以獲得其副作用,而無需修改原始字串,請使用 copy 函式
(set 't "a hypothetical one-dimensional subatomic particle")
(replace "hypoth" (copy t) "theor")
;-> "a theoretical one-dimensional subatomic particle"
t
;-> "a hypothetical one-dimensional subatomic particle"
副本被 replace 修改。原始字串 t 不會受到影響。
replace 是 newLISP 函式組中的一員,這些函式接受正則表示式來定義文字中的模式。對於大多數函式,您會在表示式末尾新增一個額外的數字來指定正則表示式操作的選項:0 表示基本匹配,1 表示不區分大小寫匹配,等等。
(set 't "a hypothetical one-dimensional subatomic particle")
(replace {h.*?l(?# h followed by l but not too greedy)} t {} 0)
;-> "a one-dimensional subatomic particle"
有時我會在正則表示式中添加註釋,以便幾天後閱讀程式碼時知道自己想做什麼。(?# 和後面的閉合括號之間的文字將被忽略。
如果您習慣使用與 Perl 相容的正則表示式 (PCRE),您會對 replace 及其使用正則表示式的同類函式 (find、regex、find-all、parse、starts-with、ends-with、directory 和 search) 感到滿意。完整詳細資訊請參見 newLISP 參考手冊。
您必須將模式引導到 newLISP 閱讀器和正則表示式處理器中。請記住引號括起來的字串和花括號括起來的字串之間的區別?引號允許處理跳脫字元,而花括號則不。花括號有一些優勢:它們在視覺上相互對齊,沒有聰明和愚蠢的版本來混淆您,您的文字編輯器可能會為您平衡它們,並且它們允許您在字串中使用更常見的引號字元,而無需一直轉義它們。但如果您使用引號,則必須將反斜槓加倍,以便單個反斜槓在正則表示式處理器中完好無損
(set 'str "\s")
(replace str "this is a phrase" "|" 0) ; oops, not searching for \s (white space) ...
;-> thi| i| a phra|e ; but for the letter s
(set 'str "\\s")
(replace str "this is a phrase" "|" 0)
;-> this|is|a|phrase ; ah, better!
replace 使用系統變數 $0、$1、$2 等(最多 $15)更新匹配項。這些指的是模式中的帶括號的表示式,等效於您可能熟悉的 \1、\2(如果您使用過 grep)。例如
(set 'quotation {"I cannot explain." She spoke in a low, eager voice,
with a curious lisp in her utterance. "But for God's sake do what I
ask you. Go back and never set foot upon the moor again."})
(replace {(.*?),.*?curious\s*(l.*p\W)(.*?)(moor)(.*)}
quotation
(println {$1 } $1 { $2 } $2 { $3 } $3 { $4 } $4 { $5 } $5)
4)
$1 "I cannot explain." She spoke in a low $2 lisp $3 in her utterance. "But for God's sake do what I ask you. Go back and never set foot upon the $4 moor $5 again."
在這裡,我們查找了五個模式,它們以逗號開頭,以 curious 結尾的任何字串分隔。$0 儲存匹配的表示式,$1 儲存第一個帶括號的子表示式,依此類推。
如果您希望使用引號而不是我在這裡使用的大括號,請記住某些字元必須使用反斜槓進行轉義。
前面的示例表明,replace 的一個重要特徵是替換不必只是一個簡單的字串或列表,它可以是任何 newLISP 表示式。每次找到模式時,都會評估替換表示式。您可以使用它來提供動態計算的替換值,或者您可以對找到的文字執行其他任何操作。甚至可以評估與找到的文字完全無關的表示式。
以下是一個示例:搜尋字母 t 後面是字母 h 或者任何母音,並打印出 replace 找到的組合
(set 't "a hypothetical one-dimensional subatomic particle")
(replace {t[h]|t[aeiou]} t (println $0) 0)
th ti to ti
;-> "a hypothetical one-dimensional subatomic particle"
對於找到的每個匹配文字片段,第三個表示式
(println $0)
被評估。這是檢視正則表示式引擎在函式執行時在做什麼的好方法。在這個例子中,原始字串似乎沒有改變,但實際上它確實改變了,因為(println $0)做了兩件事:它列印了字串,並將其值返回給 replace,從而用自身替換了找到的文字。無形縫補!如果替換表示式沒有返回字串,則不會進行替換。
您也可以做其他有用的事情,例如為以後處理構建匹配列表,並且您可以使用 newLISP 系統變數和任何其他函式來使用找到的任何文字。
在下一個示例中,我們查詢字母 a、e 或 c,並將每個出現項強制轉換為大寫
(replace "a|e|c" "This is a sentence" (upper-case $0) 0)
;-> "This is A sEntEnCE"
另一個示例,這是一個簡單的搜尋和替換操作,它統計在字串中找到字母 'o' 的次數,並將原始字串中的每個出現項替換為到目前為止的計數。替換是一個表示式塊,這些表示式塊被組合到單個 begin 表示式中。每次找到匹配項時都會評估此塊
(set 't "a hypothetical one-dimensional subatomic particle")
(set 'counter 0)
(replace "o" t
(begin
(inc counter)
(println {replacing "} $0 {" number } counter)
(string counter)) ; the replacement text should be a string
0)
replacing "o" number 1 replacing "o" number 2 replacing "o" number 3 replacing "o" number 4 "a hyp1thetical 2ne-dimensi3nal subat4mic particle"
println 的輸出不會出現在字串中;整個 begin 表示式的最終值是一個字串版本的計數器,因此它會被插入到字串中。
這是一個 replace 實踐的另一個例子。假設我有一個文字檔案,內容如下
1 a = 15 2 another_variable = "strings" 4 x2 = "another string" 5 c = 25 3x=9
我想編寫一個 newLISP 指令碼,以 10 為倍數重新編號行,從 10 開始,並將文字對齊,以便等號對齊,如下所示
10 a = 15 20 another_variable = "strings" 30 x2 = "another string" 40 c = 25 50 x = 9
(我不知道這是什麼語言!)
以下指令碼將執行此操作
(set 'file (open ((main-args) 2) "read"))
(set 'counter 0)
(while (read-line file)
(set 'temp
(replace {^(\d*)(\s*)(.*)} ; the numbering
(current-line)
(string (inc counter 10) " " $3)
0))
(println
(replace {(\S*)(\s*)(=)(\s*)(.*)} ; the spaces around =
temp
(string $1 (dup " " (- 20 (length $1))) $3 " " $5)
0)))
(exit)
我在 while 迴圈中使用了兩個 replace 操作,以使事情更清晰。第一個操作將一個臨時變數設定為替換操作的結果。搜尋字串({^(\d*)(\s*)(.*)}) 是一個正則表示式,它查詢一行開頭的任何數字,後面跟著一些空格,再後面跟著任何內容。替換字串((string (inc counter 10) " " $3) 0)) 由一個增量的計數器值、一個空格和第三個匹配項(即我剛剛查詢的任何內容)組成。
第二個替換操作的結果被打印出來。我在臨時變數temp中搜索更多帶有等號的字串和空格
({(\S*)(\s*)(=)(\s*)(.*)})
替換表示式是由重要的已找到元素($1、$3、$5)構建的,但它還包括對將等號移到字元 20 所需空格量的快速計算,這應該是第一項寬度與位置 20(我任意選擇為等號位置)之間的差值。
對於新手來說,正則表示式並不容易,但它們非常強大,特別是與 newLISP 的 replace 函式結合使用,因此值得學習。
測試和比較字串
[edit | edit source]您可以對字串執行各種測試。newLISP 的比較運算子透過查詢和比較字元的程式碼編號來進行比較,直到可以做出決定。
(> {Higgs Boson} {Higgs boson}) ; nil
(> {Higgs Boson} {Higgs}) ; true
(< {dollar} {euro}) ; true
(> {newLISP} {LISP}) ; true
(= {fred} {Fred}) ; nil
(= {fred} {fred}) ; true
當然,newLISP 的靈活引數處理允許您同時測試大量字串。
(< "a" "c" "d" "f" "h")
;-> true
這些比較函式也允許您使用單個引數。如果您只提供一個引數,newLISP 會非常人性化地假設您指的是 0 或 "",具體取決於第一個引數的型別。
(> 1) ; true - assumes > 0
(> "fred") ; true - assumes > ""
要檢查兩個字串是否具有共同特徵,您可以使用 starts-with 和 ends-with,也可以使用更通用的模式匹配命令 member、regex、find 和 find-all。starts-with 和 ends-with 足夠簡單。
(starts-with "newLISP" "new") ; does newLISP start with new?
;-> true
(ends-with "newLISP" "LISP")
;-> true
它們也可以接受正則表示式,使用其中一個正則表示式選項(0 是最常用的)。
(starts-with {newLISP} {[a-z][aeiou](?\#lc followed by lc vowel)} 0)
;-> true
(ends-with {newLISP} {[aeiou][A-Z](?\# lc vowel followed by UCase)} 0)
;-> false
find、find-all、member 和 regex 在字串中查詢所有內容。find 返回匹配子字串的索引。
(set 't "a hypothetical one-dimensional subatomic particle")
(find "atom" t)
;-> 34
(find "l" t)
;-> 13
(find "L" t)
;-> nil ; search is case-sensitive
member 檢查一個字串是否在另一個字串中。它返回包括搜尋字串在內的字串的剩餘部分,而不是第一個出現的索引。
(member "rest" "a good restaurant")
;-> "restaurant"
find 和 member 都允許您使用正則表示式。
(set 'quotation {"I cannot explain." She spoke in a low,
eager voice, with a curious lisp in her utterance. "But for
Gods sake do what I ask you. Go back and never set foot upon
the moor again."})
(find "lisp" quotation) ; without regex
;-> 69 ; character 69
(find {i} quotation 0) ; with regex
;-> 15 ; character 15
(find {s} quotation 1) ; case insensitive regex
;-> 20 ; character 20
(println "character "
(find {(l.*?p)} quotation 0) ": " $0) ; l followed by a p
;-> character 13: lain." She sp
find-all 的工作原理類似於 find,但返回所有匹配字串的列表,而不是第一個匹配項的索引。它始終接受正則表示式,因此您不需要在末尾新增正則表示式選項數字。
(set 'quotation {"I cannot explain." She spoke in a low,
eager voice, with a curious lisp in her utterance. "But for
Gods sake do what I ask you. Go back and never set foot upon
the moor again."})
(find-all "[aeiou]{2,}" quotation $0) ; two or more vowels
;-> ("ai" "ea" "oi" "iou" "ou" "oo" "oo" "ai")
或者您可以使用 regex。如果字串不包含模式,它將返回 nil,但如果包含模式,它將返回一個列表,其中包含匹配的字串和子字串,以及每個字串的開始位置和長度。結果可能非常複雜。
(set 'quotation
{She spoke in a low, eager voice, with a curious lisp in her utterance.})
(println (regex {(.*)(l.*)(l.*p)(.*)} quotation 0))
("She spoke in a low, eager voice, with a curious lisp in
her utterance." 0 70 "She spoke in a " 0 15 "low, eager
voice, with a curious " 15 33 "lisp" 48 4 " in her
utterance." 52 18)
此結果列表可以解釋為“第一個匹配項從字元 0 開始,持續 70 個字元,第二個匹配項從字元 0 開始,持續 15 個字元,另一個匹配項從字元 15 開始,持續 33 個字元”,等等。
匹配項也被儲存在系統變數($0、$1、...)中,您可以使用簡單的迴圈輕鬆檢查它們。
(for (x 1 4)
(println {$} x ": " ($ x)))
$1: She spoke in a $2: low, eager voice, with a curious $3: lisp $4: in her utterance.
字串到列表
[edit | edit source]有兩個函式可以將字串轉換為列表,以便使用 newLISP 強大的列表處理功能進行操作。命名為 explode 的函式可以開啟一個字串,並返回一個包含單個字元的列表。
(set 't "a hypothetical one-dimensional subatomic particle")
(explode t)
:-> ("a" " " "h" "y" "p" "o" "t" "h" "e" "t" "i" "c" "a" "l"
" " "o" "n" "e" "-" "d" "i" "m" "e" "n" "s" "i" "o" "n" "a"
"l" " " "s" "u" "b" "a" "t" "o" "m" "i" "c" " " "p" "a" "r"
"t" "i" "c" "l" "e")
可以使用 join 輕鬆反轉爆炸操作。explode 還可以接受一個整數。這定義了片段的大小。例如,要將字串分成密碼學家風格的 5 個字母組,請刪除空格並使用 explode,如下所示
(explode (replace " " t "") 5)
;-> ("ahypo" "theti" "calon" "e-dim" "ensio" "nalsu" "batom" "icpar" "ticle")
您可以使用 find-all 執行類似的操作。注意末尾部分。
(find-all ".{3}" t) ; this regex drops chars!
;-> ("a h" "ypo" "the" "tic" "al " "one" "-di" "men"
; "sio" "nal" " su" "bat" "omi" "c p" "art" "icl")
解析字串
[edit | edit source]parse 是將字串分解並返回各個部分的強大方法。單獨使用時,它會將字串分解,通常在詞語邊界處,吃掉邊界,並返回一個包含剩餘部分的列表。
(parse t) ; defaults to spaces...
;-> ("a" "hypothetical" "one-dimensional" "subatomic" "particle")
或者您可以提供一個分隔符字元,parse 就會在遇到該字元時分解字串。
(set 'pathname {/System/Library/Fonts/Courier.dfont})
(parse pathname {/})
;-> ("" "System" "Library" "Fonts" "Courier.dfont")
順便說一下,我可以透過過濾掉第一個空字串來消除列表中的第一個空字串。
(clean empty? (parse pathname {/}))
;-> ("System" "Library" "Fonts" "Courier.dfont")
您還可以指定分隔符字串而不是分隔符字元。
(set 't (dup "spam" 8))
;-> "spamspamspamspamspamspamspamspam"
(parse t {am}) ; break on "am"
;-> ("sp" "sp" "sp" "sp" "sp" "sp" "sp" "sp" "")
但最棒的是,您可以指定一個正則表示式分隔符。請確保您提供了選項標誌(0 或任何其他值),就像 newLISP 中的大多數正則表示式函式一樣。
(set 't {/System/Library/Fonts/Courier.dfont})
(parse t {[/aeiou]} 0) ; split at slashes and vowels
;-> ("" "Syst" "m" "L" "br" "ry" "F" "nts" "C" "" "r" "" "r.df" "nt")
這是一個眾所周知的快速且不太可靠的 HTML 標籤剝離器。
(set 'html (read-file "/Users/Sites/index.html"))
(println (parse html {<.*?>} 4)) ; option 4: dot matches newline
為了解析 XML 字串,newLISP 提供了 xml-parse 函式。請參閱 使用 XML。
使用 parse 處理文字時要小心。除非您準確指定需要的內容,否則它會認為您傳遞給它的是 newLISP 原始碼。這可能會產生意想不到的結果。
(set 't {Eats, shoots, and leaves ; a book by Lynn Truss})
(parse t)
;-> ("Eats" "," "shoots" "," "and" "leaves") ; she's gone!
分號在 newLISP 中被視為註釋字元,因此 parse 忽略了它以及該行上的所有後續內容。使用分隔符或正則表示式告訴它您真正想要的內容。
(set 't {Eats, shoots, and leaves ; a book by Lynn Truss})
(parse t " ")
;-> ("Eats," "shoots," "and" "leaves" ";" "a" "book" "by" "Lynn" "Truss")
或
(parse t "\\s" 0) ; white space
;-> ("Eats," "shoots," "and" "leaves" ";" "a" "book" "by" "Lynn" "Truss")
如果您想以其他方式分割字串,請考慮使用 find-all,它返回一個包含匹配模式的字串列表。如果您能夠將分割操作指定為正則表示式,那麼您就很幸運。例如,如果您想將數字分成三個數字一組,請使用此技巧。
(set 'a "1212374192387562311")
(println (find-all {\d{3}|\d{2}$|\d$} a))
;-> ("121" "237" "419" "238" "756" "231" "1")
; alternatively
(explode a 3)
;-> ("121" "237" "419" "238" "756" "231" "1")
該模式必須考慮末尾剩下 2 位或 1 位數字的情況。
parse 在分隔符完成工作後會將其吃掉 - find-all 會查詢並返回它找到的內容。
(find-all {\w+} t ) ; word characters
;-> ("Eats" "shoots" "and" "leaves" "a" "book" "by" "Lynn" "Truss")
(parse t {\w+} 0 ) ; eats and leaves delimiters
;-> ("" ", " ", " " " "; " " " " " " " " " "")
其他字串函式
[edit | edit source]還有其他函式可以處理字串。search 在磁碟上的檔案中查詢字串。
(set 'f (open {/private/var/log/system.log} {read}))
(search f {kernel})
(seek f (- (seek f) 64)) ; rewind file pointer
(dotimes (n 3)
(println (read-line f)))
(close f)
此示例在 system.log 中查詢字串kernel。如果找到,newLISP 會將檔案指標倒帶 64 個字元,然後列印三行,顯示上下文中的行。
還有用於處理 base64 編碼檔案以及加密字串的函式。
格式化字串
[edit | edit source]值得一提的是 format 函式,它允許您將 newLISP 表示式的值插入到預定義的模板字串中。使用 %s 表示模板中字串表示式的佔位符,使用其他 % 程式碼包含數字。例如,假設您想顯示一個類似這樣的檔案列表
folder: Library
file: mach
適合資料夾(目錄)的模板如下所示
"folder: %s" ; or
" file: %s"
將模板字串和產生檔案或資料夾名稱的表示式 (f) 傳遞給 format 函式
(format "folder: %s" f) ; or
(format " file: %s" f)
當它被計算時,f 的內容將被插入到字串中 %s 所在的位置。使用 directory 函式以這種格式生成目錄列表的程式碼如下所示
(dolist (f (directory))
(if (directory? f)
(println (format "folder: %s" f))
(println (format " file: %s" f))))
我使用 directory? 函式來選擇正確的模板字串。典型的列表如下所示
folder: . folder: .. file: .DS_Store file: .hotfiles.btree folder: .Spotlight-V100 folder: .Trashes folder: .vol file: .VolumeIcon.icns folder: Applications folder: Applications (Mac OS 9) folder: automount folder: bin folder: Cleanup At Startup folder: cores ...
您可以使用許多格式化程式碼來生成您想要的輸出。您可以使用數字控制字串和數字的對齊方式和精度。只要確保格式字串中的 % 結構與後面出現的表示式或符號匹配,並且每個結構的數量相同。
這裡還有另一個示例。我們將以十進位制、十六進位制和二進位制形式顯示前 400 個左右的 Unicode 字元。我們將使用 bits 函式來生成二進位制字串。我們將三個值的列表傳遞給 format,並在格式字串後面新增三個條目
(for (x 32 0x01a0)
(println (char x) ; the character, then
(format "%4d\t%4x\t%10s" ; decimal \t hex \t binary-string
(list x x (bits x)))))
32 20 100000 ! 33 21 100001 " 34 22 100010 # 35 23 100011 $ 36 24 100100 % 37 25 100101 & 38 26 100110 ' 39 27 100111 ( 40 28 101000 ) 41 29 101001 ...
讓 newLISP 思考的字串
[edit | edit source]最後,我必須提到 eval 和 eval-string。這兩個函式都允許您將 newLISP 程式碼傳遞給 newLISP 進行計算。如果它是有效的 newLISP 程式碼,您將看到計算結果。eval 接受一個表示式
(set 'expr '(+ 1 2))
(eval expr)
;-> 3
eval-string 接受一個字串
(set 'expr "(+ 1 2)")
(eval-string expr)
;-> 3
這意味著您可以使用我們已經遇到的任何函式構建 newLISP 程式碼,然後讓 newLISP 對其進行評估。eval 在定義宏(在您選擇執行之前延遲評估的函式)時特別有用。請參閱 宏。
您可以使用 eval 和 eval-string 編寫編寫程式的程式。
以下一段有趣的 newLISP 程式碼會不斷地無意識地重新排列一些字串並嘗試對其進行評估。未成功嘗試將被安全地捕獲。當它最終成為有效的 newLISP 程式碼時,它將被成功地評估,並且結果將滿足結束條件並結束迴圈。
(set 'code '(")" "set" "'valid" "true" "("))
(set 'valid nil)
(until valid
(set 'code (randomize code))
(println (join code " "))
(catch (eval-string (join code " ")) 'result))
true 'valid set ) ( ) ( set true 'valid 'valid ( set true ) set 'valid true ( ) 'valid ) ( true set set true ) ( 'valid true ) ( set 'valid 'valid ( true ) set true 'valid ( ) set 'valid ) ( true set true ( 'valid ) set set ( 'valid ) true set true 'valid ( ) ( set 'valid true )
我使用過顯然是使用這種程式設計技術編寫的程式……
應用和對映:將函式應用於列表
[edit | edit source]使函式和資料協同工作
[edit | edit source]通常,您會發現自己有一些資料儲存在一個列表中,並且想要將函式應用於它。例如,假設您從太空探測器中獲取了一些溫度讀數,它們儲存在一個名為 data 的列表中
(println data)
(0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)
您將如何將這些數字加起來,然後除以總數以找到平均值?也許您認為可以使用 add,它對浮點數列表求和,但是您不是互動式工作的,因此您無法編輯程式碼使其像這樣讀取
(add 0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)
由於我們將資料儲存在名為 data 的符號中,因此我們可以嘗試這樣做
(add data)
value expected in function add : data
但是不行,這行不通,因為 add 需要數字來加,而不是列表。當然,您可以使用困難的方法,編寫一個迴圈,遍歷列表中的每個專案,並每次增加一個執行總數
(set 'total 0)
(dolist (i data)
(inc total i))
(println total)
5.3
這可以正常工作。但是 newLISP 針對此問題和其他許多問題提供了一個更強大的解決方案:您可以將函式視為資料,將資料視為函式,因此您可以像操作資料一樣輕鬆地操作函式。您可以簡單地將 add 和資料列表“引入”到彼此,然後退後一步,讓它們自行處理。
有兩個重要的函式可以做到這一點:apply 和 map。
apply
[edit | edit source]apply 接受一個函式和一個列表,並使它們協同工作
(apply add data)
;-> 5.3
(div (apply add data) (length data))
;-> 0.5888888889
這將產生所需的結果。在這裡,我們將 add 函式視為任何其他 newLISP 列表、字串或數字,將其用作另一個函式的引數。您無需引用它(儘管您可以引用),因為 apply 已經期望函式的名稱。
map
[edit | edit source]另一個可以使函式和列表協同工作的函式是 map,它將函式逐一應用於列表的每個專案。例如,如果您想將 floor 函式應用於資料列表的每個元素(將其向下舍入到最接近的整數),您可以將 map、floor 和資料組合如下
(map floor data)
;-> (0 3 -2 1 -3 0 1 2 0)
並且 floor 函式將應用於資料的每個元素。結果將被組合並返回到一個新列表中。
更詳細地介紹 apply 和 map
[edit | edit source]apply 和 map 都允許您將函式視為資料。它們具有相同的基本形式
(apply f l)
(map f l)
其中 f 是函式的名稱,l 是列表。基本思想是您告訴 newLISP 使用您指定的函式處理列表。
apply 函式將列表中的所有元素用作函式的引數,並評估結果。
(apply reverse '("this is a string"))
;-> "gnirts a si siht"
在這裡,apply 檢視列表(在本例中,它包含一個字串),並將元素作為引數提供給函式。該字串將被反轉。請注意,您無需引用函式,但您確實需要引用列表,因為您不希望 newLISP 在指定函式有機會使用它之前對其進行評估。
另一方面,map 函式會像軍官檢查士兵佇列一樣逐個遍歷列表,並將函式依次應用於每個元素,使用該元素作為引數。但是,map 會記住每次評估的結果,將其收集起來,並將其返回到一個新列表中。
因此,map 看起來像是一個控制流詞,有點像 dolist,而 apply 是在程式中控制 newLISP 列表評估過程的一種方法,它可以在您想要呼叫函式時和您想要呼叫函式的位置呼叫函式,而不僅僅是作為正常評估過程的一部分。
如果我們為 map 調整前面的示例,它會給出類似的結果,儘管結果是一個列表,而不是一個字串
(map reverse '("this is a string"))
;-> ("gnirts a si siht")
因為我們使用了一個只有一個元素的列表,所以結果與 apply 示例幾乎相同,儘管請注意 map 返回一個列表,而在本例中,apply 返回一個字串
(apply reverse '("this is a string"))
;-> "gnirts a si siht"
該字串已從列表中提取,反轉,然後儲存在 map 建立的另一個列表中。
在下面的示例中
(map reverse '("this" "is" "a" "list" "of" "strings"))
;-> ("siht" "si" "a" "tsil" "fo" "sgnirts")
您可以清楚地看到 map 已將 reverse 依次應用於列表的每個元素,並返回了一個包含結果反轉字串的列表。
用另一個函式來寫它?
[edit | edit source]為了說明這兩個函式之間的關係,這裡嘗試用 apply 來定義 map
(define (my-map f l , r)
; declare a local variable r to hold the results
(dolist (e l)
(push (apply f (list e)) r -1)))
我們正在將將函式 f 應用於每個列表項的結果推送到臨時列表的末尾,然後依賴 push 在最後返回列表,就像 map 一樣。這可以工作,至少對於簡單的表示式
(my-map explode '("this is a string"))
;-> ("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g")
(map explode '("this is a string"))
;-> (("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g"))
這個示例說明了為什麼 map 如此有用。它是一種簡單的方法,可以轉換列表的所有元素,而無需使用 dolist 表示式逐個遍歷它們。
更多技巧
[edit | edit source]map 和 apply 都有一些額外的技巧。map 可以同時遍歷多個列表。如果您提供兩個或多個列表,newLISP 會將每個列表的元素交織在一起,從每個列表的第一個元素開始,並將它們作為引數傳遞給函式
(map append '("cats " "dogs " "birds ") '("miaow" "bark" "tweet"))
;-> ("cats miaow" "dogs bark" "birds tweet")
在這裡,每個列表的第一個元素將作為一對傳遞給 append,然後是每個列表的第二個元素,依此類推。
這種將線編織在一起的做法有點像用列表編織。或者像拉上拉鍊。
apply 也有一招。第三個引數指示函式應該使用多少個前面的列表引數。因此,如果一個函式需要兩個引數,而您提供三個或更多引數,apply 會回來並再次嘗試,使用第一次應用的結果和另一個引數。它會繼續遍歷列表,直到所有引數都用完為止。
為了實際演示這一點,讓我們先定義一個函式,它接受兩個引數並比較它們的長度
(define (longest s1 s2)
(println s1 " is longest so far, is " s2 " longer?") ; feedback
(if (>= (length s1) (length s2)) ; compare
s1
s2))
現在,您可以將此函式應用於一個字串列表,使用第三個引數告訴 apply 一次使用兩個字串的論據
(apply longest '("green" "purple" "violet" "yellow" "orange"
"black" "white" "pink" "red" "turquoise" "cerise" "scarlet"
"lilac" "grey" "blue") 2)
green is longest so far, is purple longer? purple is longest so far, is violet longer? purple is longest so far, is yellow longer? purple is longest so far, is orange longer? purple is longest so far, is black longer? purple is longest so far, is white longer? purple is longest so far, is pink longer? purple is longest so far, is red longer? purple is longest so far, is turquoise longer? turquoise is longest so far, is cerise longer? turquoise is longest so far, is scarlet longer? turquoise is longest so far, is lilac longer? turquoise is longest so far, is grey longer? turquoise is longest so far, is blue longer? turquoise
這就像在海灘上散步,發現一塊鵝卵石,並一直拿著它,直到出現一塊更好的鵝卵石。
apply 還為您提供了一種方法,可以遍歷列表並將函式應用於每一對專案
(apply (fn (x y)
(println {x is } x {, y is } y)) (sequence 0 10) 2)
x is 0, y is 1 x is 1, y is 2 x is 2, y is 3 x is 3, y is 4 x is 4, y is 5 x is 5, y is 6 x is 6, y is 7 x is 7, y is 8 x is 8, y is 9 x is 9, y is 10
這裡發生的事情是,println 函式返回的值是該對的第二個成員,它將成為下一對的第一個元素的值。
Lisp 特性
[edit | edit source]將函式的名稱像資料一樣傳遞來回,這是 newLISP 的一個非常典型的特性,而且非常有用。您會發現它有許多用途,有時會使用您認為無法與 map 一起使用的函式。例如,這裡有 set 在 map 的控制下努力工作
(map set '(a b) '(1 2))
;-> a is 1, b is 2
(map set '(a b) (list b a))
;-> a is 2, b is 1
這種結構為您提供了一種以並行方式而不是順序方式將值分配給符號的另一種方法。(您也可以使用 swap。)
map 的一些用途很簡單
(map char (explode "hi there"))
;-> (104 105 32 116 104 101 114 101)
(map (fn (h) (format "%02x" h)) (sequence 0 15))
;-> ("00" "01" "02" "03" "04" "05" "06" "07" "08" "09" "0a" "0b" "0c" "0d" "0e" "0f")
其他的可能變得相當複雜。例如,給定以這種形式儲存在符號 image-data 中的一串資料
("/Users/me/graphics/file1.jpg" " pixelHeight: 978" " pixelWidth: 1181")
可以使用以下方法提取這兩個數字
(map set '(height width) (map int (map last (map parse (rest image-data)))))
柯里化
[edit | edit source]一些內建的 newLISP 函式對其他函式執行操作。一個示例是 curry,它建立一個雙引數函式的副本,並建立一個具有預先確定第一個引數的單引數版本。因此,如果一個函式 f1 通常像這樣呼叫
(f1 arg1 arg2)
你可以使用curry建立一個新的函式f2,該函式帶有一個可直接使用的內建arg1。
(set 'f2 (curry f1 arg1))
現在你可以忘記第一個引數,只需要給f2提供第二個引數。
(f2 arg2)
為什麼這有用?考慮dup函式,它經常被用來插入多個空格。
(dup { } 10)
使用curry,你可以建立一個新函式,例如blank,它是dup的一個特殊版本,始終以空格作為字串。
(set 'blank (curry dup { }))
現在你可以使用(blank n)。
(blank 10)
;-> ; 10 spaces
curry可以用來建立帶有map的臨時或匿名函式。
(map (curry pow 2) (sequence 1 10))
;-> (2 4 8 16 32 64 128 256 512 1024)
(map (fn (x) (pow 2 x)) (sequence 1 10)) ; equivalent
;-> (2 4 8 16 32 64 128 256 512 1024)
但是避免在諸如inc之類的破壞性函式上使用curry。
(setq a-list-of-pairs (sequence 2 10 2))
;-> (2 4 6 8 10)
(map (curry inc 3) a-list-of-pairs) ;-> you would expect (5 7 9 11 13), instead you get
;-> (5 9 15 23 33)
; one proper way to get every number incremented by 3 would be
(map (curry + 3) a-list-of-pairs)
;-> (5 7 9 11 13)
; or if you insist in using inc, then provide a copy of the increment so the reference inc gets doesn't mess up things
(map (curry inc (copy 3)) a-list-of-pairs)
;-> (5 7 9 11 13)
介紹上下文
[edit | edit source]我們都喜歡將自己的東西整理到不同的區域或隔間。廚師將魚、肉和甜點區域分開,電子工程師將電源供應器與射頻和音訊階段分開,newLISP程式設計師使用上下文來組織程式碼。
什麼是上下文?
[edit | edit source]newLISP上下文為符號提供了一個命名的容器。不同上下文中的符號可以具有相同的名稱而不會發生衝突。因此,例如,在一個上下文中,我可以將名為meaning-of-life的符號定義為值為42,但在另一個上下文中,同名符號的值可以是dna-propagation,而在另一個上下文中,該符號的值可以是worship-of-deity。
除非你明確地選擇建立和/或切換上下文,否則你所有的newLISP工作都在預設上下文MAIN中進行。在本手冊中,當建立新的符號時,它們被新增到MAIN上下文中。
上下文非常靈活,你可以根據手頭的任務將它們用於字典、軟體物件或超級函式。
上下文:基本知識
[edit | edit source]context函式可用於執行許多不同的任務。
- 建立新的上下文。
- 從一個上下文切換到另一個上下文。
- 檢索上下文中的現有符號的值。
- 檢視當前所在的上下文。
- 在上下文中建立新的符號併為其分配值。
newLISP通常能夠讀懂你的想法,並根據你使用context函式的方式知道你想要做什麼。例如
(context 'Test)
建立一個名為Test的新上下文,正如你所預期的那樣。如果在互動式模式下輸入此程式碼,你會看到newLISP更改提示以告知你當前正在另一個上下文中工作。
> (context 'Test) Test Test>
你可以自由地在上下文之間切換。
> (context MAIN) MAIN > (context Test) Test Test>
單獨使用時,它只會告訴你當前所在的上下文。
> (context) MAIN >
一旦上下文存在,你不必引用名稱(但你也可以引用,如果你願意的話)。請注意,我為上下文名稱使用了大寫字母。這不是強制性的,只是一種約定。
上下文包含符號及其值。有多種方法可以建立符號併為其賦予值。
> (context 'Doyle "villain" "moriarty") "moriarty" >
這會建立一個新的上下文 - 注意引號,因為newLISP以前從未見過它 - 並建立一個名為“villain”的新符號,其值為“Moriarty”,但停留在MAIN上下文中。如果上下文已經存在,你可以省略引號。
> (context Doyle "hero" "holmes") "holmes" >
要獲取符號的值,你可以這樣做
> (context Doyle "hero") "holmes" >
或者,如果使用控制檯,你可以逐步執行此操作
> (context Doyle) Doyle Doyle> hero "holmes" Doyle>
或者,從MAIN上下文
> Doyle:hero "holmes" >
符號的完整地址是上下文名稱,後跟冒號(:),後跟符號名稱。如果你在另一個上下文中,始終使用完整地址。
要檢視上下文中的所有符號,請使用symbols生成一個列表。
(symbols Doyle) ;-> (Doyle:hero Doyle:period Doyle:villain)
或者,如果你已在Doyle上下文中
> (symbols) ;-> (hero period villain)
你可以以通常的方式使用此符號列表,例如使用dolist對其進行遍歷。
(dolist (s (symbols Doyle))
(println s))
Doyle:hero Doyle:period Doyle:villain
要檢視每個符號的值,請使用eval查詢其值,並使用term僅返回符號的名稱。
(dolist (s (symbols Doyle))
(println (term s) " is " (eval s)))
hero is Holmes period is Victorian villain is Moriarty
在上下文中迴圈遍歷符號有一種更有效(稍微快一點)的技術。使用dotree函式。
(dotree (s Doyle)
(println (term s) " is " (eval s)))
hero is Holmes period is Victorian villain is Moriarty
隱式建立上下文
[edit | edit source]除了使用context顯式建立上下文之外,你還可以讓newLISP自動為你建立上下文。例如
(define (C:greeting)
(println "greetings from context " (context)))
(C:greeting)
greetings from context C
在這裡,newLISP建立了一個新的上下文C和一個名為greeting的函式,該函式位於該上下文中。你也可以透過這種方式建立符號
(define D:greeting "this is the greeting string of context D")
(println D:greeting)
this is the greeting string of context D
在這兩個示例中,請注意你始終停留在MAIN上下文中。
以下程式碼建立了一個名為L的新上下文,其中包含一個名為ls的新列表,該列表包含字串。
(set 'L:ls '("this" "is" "a" "list" "of" "strings"))
;-> ("this" "is" "a" "list" "of" "strings")
上下文中的函式
[edit | edit source]上下文可以包含函式和符號。要在除MAIN之外的上下文中建立函式,請執行以下操作
(context Doyle) ; switch to existing context
(define (hello-world) ; define a local function
(println "Hello World"))
或者執行以下操作
(context MAIN) ; stay in MAIN
(define (Doyle:hello-world) ; define function in context
(println "Hello World"))
第二種語法允許你在上下文中建立上下文和函式,同時始終安全地停留在MAIN上下文中。
(define (Moriarty:helloworld)
(println "(evil laugh) Hello World"))
你無需在此處引用新的上下文名稱,因為我們正在使用define,而define(根據定義)並不期望現有符號的名稱。
要在另一個上下文中使用函式,請記住使用context:function語法呼叫它們。
預設函式
[edit | edit source]如果上下文中符號的名稱與上下文相同,則稱為預設函式(儘管實際上它可以是函式,也可以是包含列表或字串的符號)。例如,這是一個名為Evens的上下文,它包含一個名為Evens的符號。
(define Evens:Evens (sequence 0 30 2))
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
Evens:Evens
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
這是一個名為Double的函式,位於名為Double的上下文中。
(define (Double:Double x)
(mul x 2))
因此,Evens和Double是其上下文的預設函式。
預設函式有很多優點。如果預設函式的名稱與上下文相同,則在表示式中使用上下文名稱時,它將被執行,除非newLISP期望上下文名稱。例如,雖然你可以始終使用context函式以通常的方式切換到Evens上下文
> (context Evens)
Evens
Evens> (context MAIN)
MAIN
>
你可以將Evens用作列表(因為Evens:Evens是一個列表)
(reverse Evens)
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
你可以在不提供完整地址的情況下使用預設函式。類似地,你可以將Double函式用作普通函式,而不提供完整的冒號分隔地址
> (Double 3)
6
你仍然可以以通常的方式切換到Double上下文
> (context Double)
Double
Double>
newLISP足夠聰明,可以從你的程式碼中判斷是使用上下文的預設函式還是上下文字身。
按引用傳遞引數
[edit | edit source]當預設函式用作符號時,它們與其更普通的同類之間存在重要區別。當使用預設函式將資料傳遞給函式時,newLISP使用對資料的引用而不是副本。對於較大的列表和字串,引用對於newLISP在函式之間傳遞要快得多,因此如果你可以將資料儲存為預設函式並將上下文名稱用作引數,那麼你的程式碼將更快。
此外,作為結果,函式會更改作為引用引數傳遞的任何預設函式的內容。普通符號在作為引數傳遞時會被複制。觀察以下程式碼。我將建立兩個符號,其中一個是“預設函式”,另一個是普通符號。
(define Evens:Evens (sequence 0 30 2)) ; symbol is the default function for the Evens context
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
(define odds (sequence 1 31 2)) ; ordinary symbol
;-> (1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)
; this function reverses a list
(define (my-reverse lst)
(reverse lst))
(my-reverse Evens) ; default function as parameter
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
(my-reverse odds) ; ordinary symbol as parameter
;-> (31 29 27 25 23 21 19 17 15 13 11 9 7 5 3 1)
到目前為止,它們看起來行為相同。但現在檢查原始符號。
> Evens:Evens
(30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
> odds
(1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)
作為引用傳遞的列表 - 被修改了,而普通的列表引數被複制了,如往常一樣,沒有被修改。
具有記憶功能的函式
[edit | edit source]在以下示例中,我們建立一個名為Output的上下文,並在其中建立一個預設函式,也稱為Output。此函式列印其引數,並將計數器增加輸出字元數。由於預設函式的名稱與上下文相同,因此在表示式中使用上下文名稱時,它將被執行。
在此函式內部,如果存在counter變數(位於Output上下文中)的值,則將其增加;如果不存在,則建立並初始化。然後執行函式的主要任務 - 列印引數。counter符號記錄輸出字元數。
(define (Output:Output) ; define the default function
(unless Output:counter
(set 'Output:counter 0))
(inc Output:counter (length (string (args))))
(map print (args))
(println))
(dotimes (x 90)
(Output ; use context name as a function
"the square root of " x " is " (sqrt x)))
(Output "you used " Output:counter " characters")
the square root of 0 is 0 the square root of 1 is 1 the square root of 2 is 1.414213562 the square root of 3 is 1.732050808 the square root of 4 is 2 the square root of 5 is 2.236067977 the square root of 6 is 2.449489743 the square root of 7 is 2.645751311 the square root of 8 is 2.828427125 the square root of 9 is 3 ... the square root of 88 is 9.38083152 the square root of 89 is 9.433981132 you used 3895 characters
Output函式實際上會記住自建立以來完成的工作量。它甚至可以將該資訊追加到日誌檔案中。
想想可能性。你可以記錄所有函式的使用情況,並根據使用者的使用頻率向他們收費。
你可以覆蓋內建的println函式,以便在呼叫它時使用此程式碼。請參閱按你自己的方式。
字典和表格
[edit | edit source]上下文的一個常見用途是字典:一個有序的唯一鍵值對集合,排列方式使得您可以獲取鍵的當前值,或新增新的鍵值對。newLISP 使建立字典變得容易。為了說明,我將藉助偉大的偵探夏洛克·福爾摩斯。首先,我從 Project Gutenberg 下載了亞瑟·柯南·道爾的《四簽名》,然後我將該檔案載入為一個單詞列表。
(set 'file "/Users/me/Sherlock Holmes/sign-of-four.txt")
(set 'data (clean empty? (parse (read-file file) "\\W" 0))) ;read file and remove all white-spaces, returns a list.
接下來,我定義一個空字典
(define Doyle:Doyle)
這定義了 Doyle 上下文和預設函式,但將該預設函式保留為未初始化狀態。如果預設函式為空,則可以使用以下表達式來構建和檢查字典
- (Doyle key value) - 將鍵設定為值
- (Doyle key) - 獲取鍵的值
- (Doyle key nil) - 刪除鍵
要從單詞列表構建字典,您需要掃描所有單詞,如果單詞不在字典中,則將其新增為鍵,並將值設定為 1。但如果單詞已在字典中,則獲取該值,將其加 1,並儲存新值
(dolist (word data)
(set 'lc-word (lower-case word))
(if (set 'tally (Doyle lc-word))
(Doyle lc-word (inc tally))
(Doyle lc-word 1)))
這個更短的替代方法消除了條件語句
(dolist (word data)
(set 'lc-word (lower-case word))
(Doyle lc-word (inc (int (Doyle lc-word)))))
或者,更短一點
(dolist (word data)
(Doyle (lower-case word) (inc (Doyle (lower-case word)))))
每個單詞都被新增到字典中,並且值(出現次數)增加 1。在上下文內部,鍵的名稱已新增下劃線字首("_")。這樣做是為了避免任何人在鍵的名稱和 newLISP 保留字之間產生混淆,而許多保留字都出現在柯南·道爾的文字中。
您可以透過多種方式瀏覽字典。要檢視單個符號
(Doyle "baker")
;-> 10
(Doyle "street")
;-> 26
要檢視符號在上下文中儲存的方式,請使用 **dotree** 遍歷上下文,並評估每個符號
(dotree (wd Doyle)
(println wd { } (eval wd)))
Doyle:Doyle nil Doyle:_1 1 Doyle:_1857 1 Doyle:_1871 1 Doyle:_1878 2 Doyle:_1882 3 Doyle:_221b 1 ... Doyle:_your 107 Doyle:_yours 7 Doyle:_yourself 9 Doyle:_yourselves 2 Doyle:_youth 3 Doyle:_zigzag 1 Doyle:_zum 2
要將字典顯示為關聯列表,請單獨使用字典名稱。這將建立一個新的關聯列表:
(Doyle)
;-> (("1" 1)
("1857" 1)
("1871" 1)
("1878" 2)
("1882" 3)
("221b" 1)
...
("you" 543)
("young" 19)
("your" 107)
("yours" 7)
("yourself" 9)
("yourselves" 2)
("youth" 3)
("zigzag" 1)
("zum" 2))
這是一個標準的關聯列表,您可以使用列表章節中描述的函式訪問它(請參見關聯列表)。例如,要查找出現 20 次的所有單詞,請使用 **find-all**
(find-all '(? 20) (Doyle) (println $0))
;-> ("friends" 20)
("gone" 20)
("seemed" 20)
("those" 20)
("turned" 20)
("went" 20)
關聯列表由以下程式碼返回(Doyle)是字典中資料的臨時副本,而不是原始字典上下文。要更改資料,請不要對這個臨時列表進行操作,而是使用鍵值訪問技術對上下文的資料進行操作。
您還可以使用關聯列表形式的資料向字典新增新條目,或修改現有條目
(Doyle '(("laser" 0) ("radar" 0)))
儲存和載入上下文
[edit | edit source]如果您想再次使用字典,您可以將上下文儲存在檔案中
(save "/Users/me/Sherlock Holmes/doyle-context.lsp" 'Doyle)
這個包含在名為 Doyle 的上下文中的資料集合可以被另一個指令碼或 newLISP 會話快速載入,方法是
(load "/Users/me/Sherlock Holmes/doyle-context.lsp")
newLISP 將自動重新建立 Doyle 上下文中的所有符號,並在完成時切換回 MAIN(預設)上下文。
使用 newLISP 模組
[edit | edit source]上下文用作軟體模組的容器,因為它們提供詞法隔離的名稱空間。newLISP 安裝中提供的模組通常定義一個上下文,該上下文包含一組用於處理特定領域的任務的函式。
以下是一個示例。POP3 模組允許您檢查 POP3 電子郵件帳戶。您首先載入模組
(load "/usr/share/newlisp/modules/pop3.lsp")
模組現在已新增到 newLISP 系統中。您可以切換到上下文
(context POP3)
並呼叫上下文中的函式。例如,要檢查您的電子郵件,請使用 **get-mail-status** 函式,並提供使用者名稱、密碼和 POP3 伺服器名稱
(get-mail-status "someone@example.com" "secret" "mail.example.com")
;-> (3 197465 37)
; (totalMessages, totalBytes, lastRead)
如果您沒有切換到上下文,您仍然可以透過提供完整地址來呼叫相同的函式
(POP3:get-mail-status "someone@example.com" "secret" "mail.example.com")
作用域
[edit | edit source]您已經看到了 newLISP 如何動態查詢符號的 **當前** 版本(請參見作用域)。但是,當您使用上下文時,您可以採用不同的方法,程式設計師稱之為詞法作用域。使用詞法作用域,您可以顯式控制使用哪個符號,而不是依靠 newLISP 為您自動跟蹤名稱相似的符號。
在以下程式碼中,width 符號在 Right-just 上下文中定義。
(context 'Right-just)
(set 'width 30)
(define (Right-just:Right-just str)
(slice (string (dup " " width) str) (* width -1)))
(context MAIN)
(set 'width 0) ; this is a red herring
(dolist (w (symbols))
(println (Right-just w)))
!
!=
$
$0
$1
...
write-line
xml-error
xml-parse
xml-type-tags
zero?
|
~
第二行 (set 'width ...) 是一個無關緊要的資訊:在這裡更改它沒有任何影響,因為右對齊函式實際使用的符號位於不同的上下文中。
您仍然可以進入 Right-just 上下文來設定寬度
(set 'Right-just:width 15)
關於這兩種方法的優缺點有很多討論。無論您選擇哪種方法,請確保您知道程式碼執行時符號將從哪裡獲取值。例如
(define (f y)
(+ y x))
這裡,y 是函式的第一個引數,並且與任何其他 y 無關。但 x 呢?它是一個全域性符號,還是在剛剛呼叫了 f 的其他函式中定義了值?或者它根本沒有值!
最好避免使用這些 **free** 符號,並在可能的情況下使用區域性變數(使用 **let** 或 **local** 定義)。也許您可以採用某種約定,例如在全域性符號周圍加上星號。
物件
[edit | edit source]關於面向物件程式設計 (OOP) 的著作比您一生中所能讀到的還要多,因此本節只是對該主題的快速概覽。newLISP 足夠靈活,可以支援多種 OOP 風格,您可以在網上輕鬆找到這些風格的參考資料,以及關於每種風格的優缺點的討論。
在本介紹中,我將簡要概述其中的一種風格:**FOOP**,即函式式面向物件程式設計。
FOOP 簡介
[edit | edit source]FOOP 在 newLISP 10.2 版(2010 年初)中發生了變化,因此如果您使用的是舊版本的 newLISP,請更新它。
在 FOOP 中,每個物件都儲存為一個列表。類方法和類屬性(即適用於該類所有物件的函式和符號)儲存在一個上下文中。
物件儲存在列表中,因為列表是 newLISP 的基本元素。物件列表中的第一個專案是一個符號,用於標識物件的類;其餘專案是描述物件屬性的值。
一個類中的所有物件都共享相同的屬性,但這些屬性可以具有不同的值。類還可以具有在類中所有物件之間共享的屬性;這些是類屬性。儲存在類上下文中的函式提供了用於管理物件和處理它們所持資料的各種方法。
為了說明這些概念,請考慮以下使用時間和日期的程式碼。它建立在 newLISP 提供的基本日期和時間函式之上(請參見使用日期和時間)。一個時間點表示為一個 **時間物件**。一個物件包含兩個值:自 1970 年初開始經過的秒數,以及相對於格林威治的時區偏移量,以分鐘計(西經)。因此,表示典型時間物件的列表如下所示
(Time 1219568914 0)
其中 Time 是表示類名的符號,兩個數字是此特定時間的數值(這些數字表示 2008 年 8 月 24 日星期日上午 10 點左右的時間,在英格蘭的某個地方)。
使用 newLISP 通用 FOOP 建構函式構建此物件所需的程式碼很簡單
(new Class 'Time) ; defines Time context
(setq some-england-date (Time 1219568914 0))
但是,您可能想要定義一個不同的建構函式,例如,您可能想要為該物件賦予一些預設值。為此,您必須重新定義充當 **建構函式** 的預設函式
(define (Time:Time (t (date-value)) (zone 0))
(list Time t zone))
它是 Time 上下文的預設函式,它構建一個列表,其中第一個位置是類名,以及兩個整數,用於表示時間。當沒有提供值時,它們預設為當前時間和零偏移量。您現在可以使用建構函式,而無需提供任何引數
(set 'time-now (Time))
;-> your output *will* differ for this one but will be something like (Time 1324034009 0)
(set 'my-birthday (Time (date-value 2008 5 26)))
;-> (Time 1211760000 0)
(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)
接下來,您可以定義其他函式來檢查和管理時間物件。所有這些函式都儲存在同一個上下文中。它們可以透過使用 **self** 函式從物件中獲取秒數和時區資訊來提取它們。所以(self 1)獲取秒數,以及(self 2)從作為引數傳遞的物件中獲取時區偏移量。請注意,這些定義不需要您指定物件引數。以下是一些明顯的類函式
(define (Time:show)
(date (self 1) (self 2)))
(define (Time:days-between other)
"Return difference in days between two times."
(div (abs (- (self 1) (other 1))) (* 24 60 60)))
(define (Time:get-hours)
"Return hours."
(int (date (self 1) (self 2) {%H})))
(define (Time:get-day)
"Return day of week."
(date (self 1) (self 2) {%A}))
(define (Time:leap-year?)
(let ((year (int (date (self 1) (self 2) {%Y}))))
(and (= 0 (% year 4))
(or (!= 0 (% year 100)) (= 0 (% year 400))))))
這些函式是透過使用 **冒號運算子** 並提供我們希望函式作用於的物件來呼叫的
; notice 'show' uses 'date' which works with local time, so your output probably will differ
(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008
(:show my-birthday)
;-> Mon May 26 01:00:00 2008
注意我們如何使用冒號運算子作為函式的字首,沒有用空格隔開,這是一種風格問題,你可以使用空格或不使用空格。
(:show christmas-day) ; same as before
;-> Thu Dec 25 00:00:00 2008
(: show christmas-day) ; notice the space between colon and function
;-> Thu Dec 25 00:00:00 2008
這種技術允許 newLISP 提供面向物件程式設計師喜歡的特性:**多型性**。
讓我們新增另一個類,它處理持續時間 - 兩個時間物件之間的間隔 - 以天為單位測量。
(define (Duration:Duration (d 0))
(list Duration d))
(define (Duration:show)
(string (self 1) " days "))
有一個新的類建構函式 Duration:Duration 用於建立新的持續時間物件,以及一個簡單的 show 函式。它們可以像這樣與時間物件一起使用
; define two times
(set 'time-now (Time) 'christmas-day (Time (date-value 2008 12 25)))
; show days between them using the Time:days-between function
(:show (Duration (:days-between time-now christmas-day)))
;-> "122.1331713 days "
將該 :show 函式呼叫與上一節中的 :show 進行比較
(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008
您可以看到,newLISP 根據 :show 引數的類來選擇要評估的 show 函式的哪個版本。因為 christmas-day 是一個 Time 物件,所以 newLISP 評估 Time:show。但是當引數是 Duration 物件時,它評估 Duration:show。這個想法是,你可以在各種型別的物件上使用一個函式:你可能不需要知道你正在處理的物件的類。使用這種多型性,您可以將 show 函式應用於不同型別的物件列表,並且 newLISP 會每次選擇合適的那個。
(map (curry :show)
(list my-birthday (Duration (:days-between time-now christmas-day))))
;-> ("Mon May 26 01:00:00 2008" "123.1266898 days ")
**注意:**我們必須在這裡使用 **curry**,因為 **map** 需要同時使用冒號運算子和 **show** 函式。
我們稱這種特殊的 OOP 風格為 **FOOP**,因為它被認為是函式式的。在這裡,術語“函式式”指的是 newLISP 推崇的程式設計風格,它強調函式的評估,避免狀態和可變資料。正如您所見,許多 newLISP 函式返回列表的副本,而不是修改原始列表。但是,有一些被稱為**破壞性**的函式,這些函式被認為不那麼純粹地函式化。但 FOOP 不提供破壞性物件方法,因此可以認為更函式化。
關於 FOOP 需要注意的一點是,物件是不可變的;它們不能被類函式修改。例如,這裡有一個用於 Time 類的函式,它將給定數量的天數新增到時間物件中
(define (Time:adjust-days number-of-days)
(list Time (+ (* 24 60 60 number-of-days) (self 1)) (self 2)))
當它被呼叫時,它返回物件的修改副本;原始物件保持不變。
(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)
(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"
(:show (:adjust-days christmas-day 3))
;-> "Sun Dec 28 00:00:00 2008"
(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"
; notice it's unchanged
christmas-day 物件的原始日期沒有改變,儘管 :adjust-days 函式返回了一個修改後的副本,該副本調整了 3 天。
換句話說,要更改物件,請使用熟悉的使用函式返回的值的 newLISP 方法
(set 'christmas-day (:adjust-days christmas-day 3))
(:show christmas-day)
;-> "Sun Dec 28 00:00:00 2008"
christmas-day 現在包含修改後的日期。
您可以在 newLISP 論壇上搜索以下內容,找到對此想法的更完整的闡述timeutilities。另外,請務必閱讀參考手冊中關於 FOOP 的部分,它有一個關於巢狀物件的很好的例子,即包含其他物件的。物件。
我們已經介紹了 newLISP 的基礎知識,但還有很多強大的功能有待發現。一旦您掌握了語言的主要規則,就可以決定要新增哪些更高階的工具。您可能想要探索的一項功能是 newLISP 提供的宏。
宏是一種特殊的函式型別,您可以使用它來更改 newLISP 評估程式碼的方式。例如,您可以建立新的控制函式型別,例如您自己的 if 或 case 版本。
使用宏,您可以開始讓 newLISP 按照您想要的方式工作。
嚴格來說,newLISP 的宏是 **fexprs**,而不是宏。在 newLISP 中,fexprs 被稱為宏,部分原因是說“宏”比說“fexprs”容易得多,但主要是因為它們在其他 LISP 方言中與宏的作用類似:它們允許您定義 **特殊形式**,例如您自己的控制函式。
要理解宏,讓我們回到本介紹中的第一個示例之一。考慮這個表示式是如何評估的
(* (+ 1 2) (+ 3 4)) ;-> (* 3 7) ;-> 21
* 函式根本沒有看到 + 表示式,只看到了它們的結果。newLISP 熱心地評估了加法表示式,然後只將結果傳遞給乘法函式。這通常是您想要的,但有時您不希望立即評估所有表示式。
考慮內建函式 if 的操作
(if (<= x 0) (exit))
如果 x 大於 0,則測試返回 nil,因此(exit)函式不會被評估。現在假設您想定義您自己的 if 函式版本。這應該很容易
(define (my-if test true-action false-action)
(if test true-action false-action))
> (my-if (> 3 2) (println "yes it is" ) (exit)) yes it is $
但這不起作用。如果比較返回 true,newLISP 列印一條訊息,然後退出。即使比較返回 false,newLISP 也會在列印訊息之前退出。問題是(exit)在呼叫 my-if 函式之前被評估,即使您不希望它被評估。對於普通函式,引數中的表示式首先被評估。
宏類似於函式,但它們讓您控制何時以及是否評估引數。您使用 define-macro 函式定義宏,就像您使用 define 定義函式一樣。這兩個定義函式都允許您建立接受引數的自定義函式。重要的區別是,使用普通的 define,引數在函式執行之前被評估。但是,當您呼叫使用 define-macro 定義的宏函式時,引數以原始未評估的形式傳遞給定義。您決定何時評估引數。
my-if 函式的宏版本如下所示
(define-macro (my-if test true-action false-action)
(if (eval test) (eval true-action) (eval false-action)))
(my-if (> 3 2) (println "yes it is" ) (exit))
"yes it is"
test 和 action 引數不會立即被評估,只有當您想要評估它們時才使用 eval 評估。這意味著(exit)在進行測試之前不會被評估。
這種推遲評估的能力使您能夠編寫自己的控制結構並向語言新增強大的新形式。
newLISP 提供了許多用於構建宏的有用工具。除了 define-macro 和 eval 之外,還有 letex,它為您提供了一種在評估表示式之前將區域性符號擴充套件到表示式中的方法,以及 args,它返回傳遞給您的宏的所有引數。
在編寫宏時要注意的一個問題是宏中的符號名稱可能會與呼叫宏的程式碼中的符號名稱混淆。這是一個簡單的宏,它向語言添加了一個新的迴圈構造,它結合了 dolist 和 do-while。迴圈變數在條件為 true 時遍歷列表
(define-macro (dolist-while)
(letex (var (args 0 0) ; loop variable
lst (args 0 1) ; list
cnd (args 0 2) ; condition
body (cons 'begin (1 (args)))) ; body
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
它被這樣呼叫
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1)))
x is 19 x is 18 x is 17 x is 16 x is 15 x is 14 x is 13 x is 12 x is 11 x is 10
它似乎工作得很好。但有一個微妙的問題:您不能使用名為 y 的符號作為迴圈變數,即使您可以使用 x 或其他任何東西。在迴圈中放置一個 (println y) 語句以瞭解原因
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1))
(println {y is } y))
x is 19 y is true x is 18 y is true x is 17 y is true
如果您嘗試使用 y,它將不起作用
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is value expected in function dec : y
問題是,y 被宏用於儲存條件值,即使它在自己的 let 表示式中。它顯示為 true/nil 值,因此它不能被遞減。要解決此問題,將宏封裝在上下文中,並將宏設定為該上下文中的預設函式
(context 'dolist-while)
(define-macro (dolist-while:dolist-while)
(letex (var (args 0 0)
lst (args 0 1)
cnd (args 0 2)
body (cons 'begin (1 (args))))
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
(context MAIN)
它可以以相同的方式使用,但沒有任何問題
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is 19 y is 18 y is 17
newLISP 使用者找到了許多不同的理由使用宏。以下是我在 newLISP 使用者論壇上找到的一些宏定義。
這是一個 case 版本,稱為 ecase(evaluated-case),它確實評估了測試
(define-macro (ecase _v)
(eval (append
(list 'case _v)
(map (fn (_i) (cons (eval (_i 0)) (rest _i)))
(args)))))
(define (test n)
(ecase n
((/ 4 4) (println "n was 1"))
((- 12 10) (println "n was 2"))))
(set 'n 2)
(test n)
n was 2
您可以看到,分隔符(/ 4 4)和(- 12 10)都被評估了。使用標準版本的 case 它們不會被評估。
這是一個建立函式的宏
(define-macro (create-functions group-name)
(letex
((f1 (sym (append (term group-name) "1")))
(f2 (sym (append (term group-name) "2"))))
(define (f1 arg) (+ arg 1))
(define (f2 arg) (+ arg 2))))
(create-functions foo)
; this creates two functions starting with 'foo'
(foo1 10)
;-> 11
(foo2 10)
;-> 12
(create-functions bar)
; and this creates two functions starting with 'bar'
(bar1 12)
;-> 13
(bar2 12)
;-> 14
以下程式碼更改了 newLISP 的操作,以便使用 define 定義的每個函式在評估時,將它的名稱和它的引數的詳細資訊新增到一個日誌檔案中。當您執行指令碼時,日誌檔案將包含已評估的函式和引數的記錄。
(context 'tracer)
(define-macro (tracer:tracer farg)
(set (farg 0)
(letex (func (farg 0)
arg (rest farg)
arg-p (cons 'list (map (fn (x) (if (list? x) (first x) x))
(rest farg)))
body (cons 'begin (args)))
(lambda
arg
(append-file
(string (env "HOME") "/trace.log")
(string 'func { } arg-p "\n"))
body))))
(context MAIN)
(constant (global 'newLISP-define) define)
; redefine the built-in define:
(constant (global 'define) tracer)
要使用這個簡單的跟蹤器執行指令碼,在執行之前載入上下文
(load {tracer.lsp})
生成的日誌檔案包含已呼叫的每個函式的列表及其接收的引數
Time:Time (1211760000 0) Time:Time (1230163200 0) Time:Time (1219686599 0) show ((Time 1211760000 0)) show ((Time 1230163200 0)) get-hours ((Time 1219686599 0)) get-day ((Time 1219686599 0)) days-between ((Time 1219686599 0) (Time 1230163200 0)) leap-year? ((Time 1211760000 0)) adjust-days ((Time 1230163200 0) 3) show ((Time 1230422400 0)) Time:Time (1219686599 0) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) period-to-string ((Duration 124.256956)) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) Time:print ((Time 1211760000 0)) Time:string ((Time 1211760000 0)) Duration:print ((Duration 124.256956)) Duration:string ((Duration 124.256956))
它會使執行速度降低很多。
使用數字
[edit | edit source]如果您使用數字,您會很高興知道 newLISP 包含了您期望找到的大多數基本函式,以及更多其他函式。本節旨在幫助您最大程度地利用這些函式,並避免您可能會遇到的某些小陷阱。和往常一樣,請參閱官方文件以獲取完整詳細資訊。
整數和浮點數
[edit | edit source]newLISP 處理兩種不同的數字型別:整數和浮點數。整數是精確的,而浮點數 (floats) 精度較低。兩者各有優缺點。如果您需要使用非常大的整數,大於 9 223 372 036 854 775 807,請參閱涵蓋大型(64 位)整數和大型整數(無限大小)之間差異的部分 - 更大的數字.
算術運算子 +、-、*、/ 和 % 始終返回整數值。一個常見的錯誤是忘記這一點,並在沒有意識到它們正在執行整數運算的情況下使用 / 和 *
(/ 10 3)
;-> 3
這可能不是您所期望的!
浮點數僅保留 15 或 16 個最重要的數字(即數字在數字的左側,具有最高位值)。
浮點數的理念是 足夠接近,而不是 這就是確切值。
假設您嘗試定義一個符號 PI 來儲存 pi 的值,精確到 50 位小數
(constant 'PI 3.14159265358979323846264338327950288419716939937510)
;-> 3.141592654
(println PI)
3.141592654
看起來 newLISP 從右側截取了大約 40 位數字!實際上,大約儲存了 15 或 16 位數字,並且丟棄了 35 個不太重要的數字。
newLISP 如何儲存此數字?讓我們使用 format 函式來檢視
(format {%1.50f} PI)
;-> "3.14159265358979311599796346854418516159057617187500"
現在讓我們建立一個小的指令碼,將這兩個數字作為字串進行比較,這樣我們就不必目視 grep 差異
(setq original-pi-str "3.14159265358979323846264338327950288419716939937510")
(setq pi (float original-pi-str))
(setq saved-pi-str (format {%1.50f} pi))
(println pi " -> saved pi (float)")
(println saved-pi-str " -> saved pi formatted")
(println original-pi-str " -> original pi")
(dotimes (i (length original-pi-str) (!= (original-pi-str i) (saved-pi-str i)))
(print (original-pi-str i)))
(println " -> original and saved versions are equal up to this")
3.141592654 -> saved pi (float)
3.14159265358979311599796346854418516159057617187500 -> saved pi formatted
3.14159265358979323846264338327950288419716939937510 -> original pi
3.141592653589793 -> original and saved versions are equal up to this
請注意,該值精確到 9793,但隨後偏離了您最初提供的更精確的字串。9793 之後的數字是所有計算機儲存浮點值的典型方式 - 這不是 newLISP 對您的資料進行創意處理!
您可以在我的機器上使用的最大浮點數似乎約為 10308。但是,僅儲存了前 15 位左右的數字,因此大多數都是零,而且您實際上無法對其加 1。
這是浮點數格言的另一個例子:足夠接近!
順便說一下,以上評論適用於大多數計算機語言,而不僅僅是 newLISP。浮點數是便利性、速度和準確性之間的折衷方案。
整數和浮點數數學
[edit | edit source]當您使用浮點數時,請使用浮點算術運算子 add、sub、mul、div 和 mod,而不是 +、-、*、/ 和 %,它們的僅限整數的等效項
(mul PI 2)
;-> 6.283185307
並且,要檢視 newLISP 正在儲存的值(因為直譯器的預設輸出解析度為 9 或 10 位數字)
(format {%1.16f} (mul PI 2))
;-> "6.2831853071795862"
如果您忘記在這裡使用 mul,而是使用 *,則小數點後的數字會被丟棄
(format {%1.16f} (* PI 2))
;-> "6.0000000000000000"
這裡,pi 被轉換為 3,然後乘以 2。
您可以重新定義熟悉的算術運算子,使它們預設使用浮點例程而不是僅限整數的算術
; before
(+ 1.1 1.1)
;-> 2
(constant (global '+) add)
; after
(+ 1.1 1.1)
;-> 2.2
您可以在您的 init.lsp 檔案中新增這些定義,以便在您機器上的所有 newLISP 工作中使用它們。您會遇到的主要問題是與他人共享程式碼或使用匯入的庫。它們的程式碼可能會產生意想不到的結果,或者您的程式碼可能會產生意想不到的結果!
轉換:顯式和隱式
[edit | edit source]要將字串轉換為數字,或將一種型別的數字轉換為另一種型別的數字,請使用 int 和 float 函式。
這些函式的主要用途是將字串轉換為數字 - 整數或浮點數。例如,您可能正在使用正則表示式從較長的字串中提取數字字串
(map int (find-all {\d+} {the answer is 42, not 41}))
;-> (42 41) ; a list of integers
(map float (find-all {\d+(\.\d+)?} {the value of pi is 3.14, not 1.618}))
;-> (3.14 1.618) ; a list of floats
傳遞給 int 的第二個引數指定了一個預設值,如果轉換失敗,應使用該預設值
(int "x")
;-> nil
(int "x" 0)
;-> 0
int 是一個巧妙的函式,它還可以將表示以 10 以外的其他進製表示的數字的字串轉換為數字。例如,要將十六進位制數的字串形式轉換為十進位制數,請確保它以0x為字首,並且不要使用超出f的字母
(int (string "0x" "1F"))
;-> 31
(int (string "0x" "decaff"))
;-> 14600959
您可以透過僅以0為字首來轉換包含八進位制數的字串
(int "035")
;-> 29
可以透過以0b為字首來轉換二進位制數
(int "0b100100100101001001000000000000000000000010100100")
;-> 160881958715556
即使您從未使用過八進位制或十六進位制,瞭解這些轉換也是有價值的,因為有一天您可能會故意或意外地編寫以下程式碼
(int "08")
這將計算為 0 而不是 8 - 失敗的八進位制到十進位制轉換,而不是您可能期望的十進位制 8!因此,在對字串輸入使用 int 時,始終指定一個預設值和進位制,這是一個好主意
(int "08" 0 10) ; default to 0 and assume base 10
;-> 8
如果您使用的是大整數(大於 64 位整數的整數),請使用 bigint 而不是 int。請參閱 更大的數字.
無形轉換和舍入
[edit | edit source]某些函式會自動將浮點數轉換為整數。從 newLISP 10.2.0 版本開始,所有由字母組成的運算子都會生成浮點數,而用特殊字元編寫的運算子會生成整數。
因此,使用 ++ 將會將您的數字轉換為整數並進行舍入,而使用 inc 將會將您的數字轉換為浮點數
(setq an-integer 2)
;-> 2
(float? an-integer)
;-> nil
(inc an-integer)
;-> 3
(float? an-integer)
;-> true
(setq a-float (sqrt 2))
;-> 1.414213562
(integer? a-float)
;-> nil
(++ a-float)
;-> 2
(integer? a-float)
;-> true
要使 inc 和 dec 對列表起作用,您需要訪問特定元素或使用 map 來處理所有元素
(setq numbers '(2 6 9 12))
;-> (2 6 9 12)
(inc (numbers 0))
;-> 3
numbers
;-> (3 6 9 12)
(map inc numbers)
;-> (4 7 10 13)
; but WATCH OUT!
(map (curry inc 3) numbers) ; this one doesn't produce what you expected
;-> (6 12 21 33)
; use this instead:
(map (curry + 3) numbers)
;-> (6 9 12 15)
許多 newLISP 函式會自動將整數引數轉換為浮點值。這通常不是問題。但是,如果您將非常大的整數傳遞給轉換為浮點的函式,可能會丟失一些精度
(format {%15.15f} (add 1 922337203685477580))
;-> "922337203685477632.000000000000000"
由於 add 函式將非常大的整數轉換為浮點數,因此丟失了一小部分精度(在這種情況下大約為 52)。足夠接近嗎?如果不是,請仔細考慮您儲存和運算元字的方式。
數字測試
[edit | edit source]有時您需要測試一個數字是整數還是浮點數
(set 'PI 3.141592653589793)
;-> 3.141592654
(integer? PI)
;-> nil
(float? PI)
;-> true
(number? PI)
;-> true
(zero? PI)
;-> nil
使用 integer? 和 float?,您是在測試該數字是以整數還是浮點數形式儲存的,而不是測試該數字在數學上是整數還是浮點值。例如,此測試返回 nil,這可能會讓您感到驚訝
(integer? (div 30 3))
;-> nil
並不是答案不是 10(它是),而是答案是浮點 10,而不是整數 10,因為 div 函式始終返回浮點值。
絕對符號,從下舍入到上舍入
[edit | edit source]值得知道的是,floor 和 ceil 函式返回包含整數值的浮點數。例如,如果您使用 floor 將 pi 向下舍入到最接近的整數,則結果為 3,但它儲存為浮點數,而不是整數
(integer? (floor PI))
;-> nil
(floor PI)
;-> 3
(float? (ceil PI))
;-> true
abs 和 sgn 函式也可用於測試和轉換數字。abs 始終返回其引數的正版本,而 sgn 返回 1、0 或 -1,具體取決於引數是正、零還是負。
round 函式將數字舍入到最接近的整數,浮點數仍然是浮點數。您還可以提供一個可選的附加值,將數字舍入到特定的小數位數。負數在小數點後舍入,正數在小數點前舍入。
(set 'n 1234.6789)
(for (i -6 6)
(println (format {%4d %12.5f} i (round n i))))
-6 1234.67890 -5 1234.67890 -4 1234.67890 -3 1234.67900 -2 1234.68000 -1 1234.70000 0 1235.00000 1 1230.00000 2 1200.00000 3 1000.00000 4 0.00000 5 0.00000 6 0.00000
sgn 具有替代語法,允許您根據第一個引數是負數、零還是正數來評估最多三個不同的表示式。
(for (i -5 5)
(println i " is " (sgn i "below 0" "0" "above 0")))
-5 is below 0 -4 is below 0 -3 is below 0 -2 is below 0 -1 is below 0 0 is 0 1 is above 0 2 is above 0 3 is above 0 4 is above 0 5 is above 0
數字格式化
[edit | edit source]要將數字轉換為字串,請使用 string 和 format 函式
(reverse (string PI))
;-> "456395141.3"
string 和 println 都只使用前 10 位左右的數字,即使內部儲存了更多數字(最多 15 或 16 位)。
使用 format 以更多控制方式輸出數字
(format {%1.15f} PI)
;-> "3.141592653589793"
format 規範字符串使用廣泛採用的printf風格格式化。請記住,您還可以使用 format 函式的結果
(string "the value of pi is " (format {%1.15f} PI))
;-> "the value of pi is 3.141592653589793"
format 函式允許您將數字輸出為十六進位制字串
(format "%x" 65535)
;-> "ffff"
數字實用程式
[edit | edit source]建立數字
[edit | edit source]有一些有用的函式可以輕鬆地建立數字。
序列和級數
[edit | edit source]sequence 生成一個算術序列的數字列表。提供起始和結束數字(包含),以及步長值
(sequence 1 10 1.5)
;-> (1 2.5 4 5.5 7 8.5 10)
如果指定了步長值,則所有數字都將儲存為浮點數,即使結果為整數,否則它們將為整數
; with step value sequence gives floats
(sequence 1 10 2)
;-> (1 3 5 7 9)
(map float? (sequence 1 10 2))
;-> (true true true true true)
; without step value sequence gives integers
(sequence 1 5)
;-> (1 2 3 4 5)
> (map float? (sequence 1 5))
;-> (nil nil nil nil nil)
series 將其第一個引數乘以其第二個引數若干次。重複次數由第三個引數指定。這將生成幾何序列
(series 1 2 20)
;-> (1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288)
每個數字都儲存為浮點數。
series 的第二個引數也可以是函式。該函式應用於第一個數字,然後應用於結果,然後應用於該結果,依此類推。
(series 10 sqrt 20)
;-> (10 3.16227766 1.77827941 1.333521432 1.154781985 1.074607828 1.036632928
1.018151722 1.009035045 1.004507364 1.002251148 1.001124941 1.000562313
1.000281117 1.000140549 1.000070272 1.000035135 1.000017567 1.00000878
1.000004392)
normal 函式返回一個具有指定平均值和標準差的浮點數列表。例如,可以按如下方式生成一個平均值為 10 且標準差為 5 的 6 個數字的列表
(normal 10 5 6)
;-> (6.5234375 14.91210938 6.748046875 3.540039062 4.94140625 7.1484375)
隨機數
[edit | edit source]rand 建立一個隨機選擇的小於您提供的數字的整數列表
(rand 7 20)
; 20 numbers between 0 and 6 (inclusive) or 7 (exclusive)
;-> (0 0 2 6 6 6 2 1 1 1 6 2 0 6 0 5 2 4 4 3)
顯然 (rand 1) 生成一個零列表,沒有用。(rand 0) 也沒有用,但它被分配了初始化隨機數生成器的任務。
如果省略第二個數字,它只生成一個範圍內的隨機數。
random 生成一個浮點數列表,這些浮點數乘以一個比例因子,從第一個引數開始
(random 0 2 10)
; 10 numbers starting at 0 and scaled by 2
;-> (1.565273852e-05 0.2630755763 1.511210644 0.9173002638
; 1.065534475 0.4379183727 0.09408923243 1.357729434
; 1.358592812 1.869385792))
隨機性
[edit | edit source]使用 seed 來控制 rand(整數)、random(浮點數)、randomize(隨機列表)和 amb(隨機選擇的列表元素)的隨機性。
如果不用 seed,每次執行時都會出現相同的隨機數集。這為您提供了可預測的隨機性——這對除錯很有用。當您想要模擬現實世界的隨機性時,每次執行指令碼時使用不同的值對隨機數生成器進行播種
不使用 seed
; today
(for (i 10 20)
(print (rand i) { }))
7 1 5 10 6 2 8 0 17 18 0
; tomorrow
(for (i 10 20)
(print (rand i) { }))
7 1 5 10 6 2 8 0 17 18 0 ; same as yesterday
使用 seed
; today
(seed (date-value))
(for (i 10 20)
(print (rand i) { }))
2 10 3 10 1 11 8 13 6 4 0
; tomorrow
(seed (date-value))
(for (i 10 20)
(print (rand i) { }))
0 7 10 5 5 8 10 16 3 1 9
通用數字工具
[edit | edit source]min 和 max 按預期工作,儘管它們始終返回浮點數。與許多算術運算子一樣,您可以提供多個值
(max 1 2 13.2 4 2 1 4 3 2 1 0.2)
;-> 13.2
(min -1 2 17 4 2 1 43 -20 1.1 0.2)
;-> -20
(float? (max 1 2 3))
;-> true
比較函式允許您只提供一個引數。如果將它們與數字一起使用,newLISP 會很有用地假設您正在與 0 進行比較。請記住,您正在使用字尾表示法
(set 'n 3)
(> n)
;-> true, assumes test for greater than 0
(< n)
;-> nil, assumes test for less than 0
(set 'n 0)
(>= n)
;-> true
factor 函式找到整數的因子並將它們返回到列表中。這是測試數字是否為素數的一種有用方法
(factor 5)
;-> (5)
(factor 42)
;-> (2 3 7)
(define (prime? n)
(and
(set 'lst (factor n))
(= (length lst) 1)))
(for (i 0 30)
(if (prime? i) (println i)))
2 3 5 7 11 13 17 19 23 29
或者您可以用它來測試數字是否為偶數
(true? (find 2 (factor n)))
;-> true if n is even
gcd 找到兩個或多個數字的最大公約數
(gcd 8 12 16)
;-> 4
浮點數實用程式
[edit | edit source]如果省略,pow 函式的第二個引數預設為 2。
(pow 2) ; default is squared
;-> 4
(pow 2 2 2 2) ; (((2 squared) squared) squared)
;-> 256
(pow 2 8) ; 2 to the 8
;-> 256
(pow 2 3)
;-> 8
(pow 2 0.5) ; square root
;-> 1.414213562
您也可以使用 sqrt 來求平方根。要查詢立方根和其他根,請使用 pow
(pow 8 (div 1 3)) ; 8 to the 1/3
;-> 2
exp 函式計算 ex,其中 e 是數學常數 2.718281828,x 是引數
(exp 1)
;-> 2.71828128
log 函式有兩種形式。如果省略底數,則使用自然對數
(log 3) ; natural (base e) logarithms
;-> 1.098612289
或者您可以指定另一個底數,例如 2 或 10
(log 3 2)
;-> 1.584962501
(log 3 10) ; logarithm base 10
;-> 0.4771212547
newLISP 中預設可用的其他數學函式是 fft(快速傅立葉變換)和 ifft(逆快速傅立葉變換)。
三角學
[edit | edit source]newLISP 的所有三角函式,sin、cos、tan、asin、acos、atan、atan2 以及雙曲函式 sinh、cosh 和 tanh,都在弧度制下工作。如果您喜歡以度數工作,可以將替代版本定義為函式
(constant 'PI 3.141592653589793)
(define (rad->deg r)
(mul r (div 180 PI)))
(define (deg->rad d)
(mul d (div PI 180)))
(define (sind _e)
(sin (deg->rad (eval _e))))
(define (cosd _e)
(cos (deg->rad (eval _e))))
(define (tand _e)
(tan (deg->rad (eval _e))))
(define (asind _e)
(rad->deg (asin (eval _e))))
(define (atan2d _e _f)
(rad->deg (atan2 (deg->rad (eval _e)) (deg->rad (eval _f)))))
等等。
在編寫方程式時,一種方法是從末尾開始構建它們。例如,要轉換以下方程式
將它分階段構建,如下所示
1 (tand beta)
2 (tand beta) (sind epsilon)
3 (mul (tand beta) (sind epsilon))
4 (sind lamda) (mul (tand beta) (sind epsilon))
5 (sind lamda) (cosd epsilon) (mul (tand beta) (sind epsilon))
6 (sub (mul (sind lamda) (cosd epsilon))
(mul (tand beta) (sind epsilon)))
7 (atan2d (sub (mul (sind lamda) (cosd epsilon)) (mul (tand beta)(sind epsilon)))
(cosd lamda))
8 (set 'alpha
等等...
在文字編輯器中將各種表示式對齊通常很有用
(set 'right-ascension
(atan2d
(sub
(mul
(sind lamda)
(cosd epsilon))
(mul
(tand beta)
(sind epsilon)))
(cosd lamda)))
如果您必須將許多數學表示式從中綴轉換為字尾表示法,您可能需要研究 infix.lsp 模組(可從 newLISP 網站獲得)
(load "/usr/share/newlisp/modules/infix.lsp")
(INFIX:xlate
"(sin(lamda) * cos(epsilon)) - (cos(beta) * sin(epsilon))")
;->
(sub (mul (sin lamda) (cos epsilon)) (mul (tan beta) (sin epsilon)))
陣列
[edit | edit source]newLISP 提供多維陣列。陣列與列表非常相似,您也可以對陣列使用大多數對列表進行操作的函式。
大型陣列可能比大小相似的列表更快。以下程式碼使用 time 函式來比較陣列和列表的工作速度。
(for (size 200 1000)
; create an array
(set 'arry (array size (randomize (sequence 0 size))))
; create a list
(set 'lst (randomize (sequence 0 size)))
(set 'array-time
(time (dotimes (x (/ size 2))
(nth x arry)) 100))
; repeat at least 100 times to get non-zero time!
(set 'list-time
(time (dotimes (x (/ size 2))
(nth x lst)) 50))
(println "with " size " elements: array access: "
array-time
"; list access: "
list-time
" "
(div list-time array-time )))
with 200 elements: array access: 1; list access: 1 1 with 201 elements: array access: 1; list access: 1 1 with 202 elements: array access: 1; list access: 1 1 with 203 elements: array access: 1; list access: 1 1 ... with 997 elements: array access: 7; list access: 16 2.285714286 with 998 elements: array access: 7; list access: 17 2.428571429 with 999 elements: array access: 7; list access: 17 2.428571429 with 1000 elements: array access: 7; list access: 17 2.428571429
確切時間因機器而異,但通常,對於 200 個元素,陣列和列表的速度相當。隨著列表和陣列大小的增加,nth 訪問器函式的執行時間也會增加。當列表和陣列分別包含 1000 個元素時,陣列的訪問速度比列表快 2 到 3 倍。
要建立陣列,請使用 array 函式。您可以建立一個新的空陣列,建立一個新陣列並用預設值填充它,或者建立一個新的陣列,該陣列是現有列表的精確副本。
(set 'table (array 10)) ; new empty array
(set 'lst (randomize (sequence 0 20))) ; new full list
(set 'arry (array (length lst) lst)) ; new array copy of a list
要建立一個作為現有陣列副本的新列表,請使用 array-list 函式
(set 'lst2 (array-list arry)) ; makes new list
要區分列表和陣列,可以使用 list? 和 array? 測試
(array? arry)
;-> true
(list? lst)
;-> true
可用於陣列的函式
[edit | edit source]以下通用函式對陣列和列表同樣有效:first、last、rest、mat、nth、setf、sort、append 和 slice。
還有一些針對陣列和列表的特殊函式,提供矩陣運算:invert、det、multiply、transpose。見 矩陣.
陣列可以是 多維的。例如,要建立一個 2x2 表格,用 0 填充,請使用以下方法
(set 'arry (array 2 2 '(0)))
;-> ((0 0) (0 0))
array 的第三個引數提供了一些初始值,newLISP 將使用這些值來填充陣列。newLISP 儘可能有效地使用該值。因此,例如,您可以提供一個足夠的初始化表示式
(set 'arry (array 2 2 (sequence 0 10)))
arry
;-> ((0 1) (2 3)) ; don't need all of them
或者只提供一些提示
(set 'arry (array 2 2 (list 1 2)))
arry
;-> ((1 2) (1 2))
(set 'arry (array 2 2 '(42)))
arry
;-> ((42 42) (42 42))
這個陣列初始化功能很酷,所以我有時甚至在建立列表時也會使用它
(set 'maze (array-list (array 10 10 (randomize (sequence 0 10)))))
;-> ((9 4 0 2 10 6 7 1 8 5)
(3 9 4 0 2 10 6 7 1 8)
(5 3 9 4 0 2 10 6 7 1)
(8 5 3 9 4 0 2 10 6 7)
(1 8 5 3 9 4 0 2 10 6)
(7 1 8 5 3 9 4 0 2 10)
(6 7 1 8 5 3 9 4 0 2)
(10 6 7 1 8 5 3 9 4 0)
(2 10 6 7 1 8 5 3 9 4)
(0 2 10 6 7 1 8 5 3 9))
獲取和設定值
[edit | edit source]要從陣列中獲取值,請使用 nth 函式,該函式期望一個用於陣列維度的索引列表,後跟陣列的名稱
(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))
(dotimes (row size)
(dotimes (column size)
(print (format {%3d} (nth (list row column) table))))
; end of row
(println))0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
(nth 也適用於列表和字串。)
與列表一樣,您可以使用隱式定址來獲取值
(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))
(table 3)
;-> (30 31 32 33 34 35 36 37 38 39) ; row 3 (0-based!)
(table 3 3) ; row 3 column 3 implicitly
;-> 33要設定值,請使用 setf。以下程式碼將所有非素數替換為 0。
(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))
(dotimes (row size)
(dotimes (column size)
(if (not (= 1 (length (factor (nth (list row column) table)))))
(setf (table row column) 0))))
table
;-> ((0 0 2 3 0 5 0 7 0 0)
(0 11 0 13 0 0 0 17 0 19)
(0 0 0 23 0 0 00 0 29)
(0 31 0 0 0 0 0 37 0 0)
(0 41 0 43 0 0 0 47 0 0)
(0 0 0 53 0 0 0 0 0 59)
(0 61 0 0 0 0 0 67 0 0)
(0 71 0 73 0 0 0 0 0 79)
(0 0 0 83 0 0 0 0 0 89)
(0 0 0 0 0 0 0 97 0 0))除了隱式定址 (table row column) 之外,我還可以寫 (setf (nth (list row column) table) 0)。隱式定址速度稍快,但使用 nth 有時可以使程式碼更易讀。
矩陣
[edit | edit source]有一些函式將陣列或列表(具有正確的結構)視為矩陣。
- invert 返回矩陣的逆矩陣
- det 計算行列式
- multiply 乘以兩個矩陣
- mat 將函式應用於兩個矩陣或矩陣和數字
- transpose 返回矩陣的轉置
transpose 在巢狀列表上使用時也很有用(參見 關聯列表)。
統計、金融和建模函式
[edit | edit source]newLISP 擁有大量用於金融和統計分析以及模擬建模的函式。
給定一個數字列表,stats 函式返回值的數量、平均值、與平均值的平均偏差、標準差(總體估計)、方差(總體估計)、分佈偏度和分佈峰度
(set 'data (sequence 1 10))
;->(1 2 3 4 5 6 7 8 9 10)
(stats data)
(10 5.5 2.5 3.02765035409749 9.16666666666667 0 -1.56163636363636)以下是內建的其他函式列表
- beta 計算貝塔函式
- betai 計算不完全貝塔函式
- binomial 計算二項式函式
- corr 計算皮爾遜積矩相關係數
- crit-chi2 計算給定機率的卡方
- crit-f 計算給定置信機率的最小臨界 F
- crit-t 計算給定置信機率的最小臨界學生 t
- crit-z 計算給定累積機率的臨界正態分佈 Z 值
- erf 計算數字的誤差函式
- gammai 計算不完全伽馬函式
- gammaln 計算對數伽馬函式
- kmeans-query 計算資料向量到質心的歐幾里德距離
- kmeans-train 對矩陣資料執行 K 均值聚類分析
- normal 生成一個正態分佈浮點數列表
- prob-chi2 計算卡方的累積機率
- prob-f 查詢觀察到的統計量的機率
- prob-t 查詢正態分佈值的機率
- prob-z 計算 Z 值的累積機率
- stats 查詢值的中心趨勢和分佈矩的統計值
- t-test 使用學生 t 檢驗比較平均值
貝葉斯分析
[edit | edit source]托馬斯·貝葉斯牧師在 18 世紀初發展起來的統計方法已被證明用途廣泛且流行,並已進入當今的程式語言。在 newLISP 中,兩個函式 bayes-train 和 bayes-query 協同工作,提供了一種簡便的方法來計算資料集的貝葉斯機率。
以下是如何使用這兩個函式來預測一小段文字是由兩位作者中的一位撰寫的可能性。
首先,選擇兩位作者的文字,併為每位作者生成資料集。我選擇了奧斯卡·王爾德和柯南·道爾。
(set 'doyle-data
(parse (lower-case
(read-file "/Users/me/Documents/sign-of-four.txt")) {\W} 0))
(set 'wilde-data
(parse (lower-case
(read-file "/Users/me/Documents/dorian-grey.txt")) {\W} 0))現在,bayes-train 函式可以掃描這兩個資料集並將單詞頻率儲存在一個新的上下文中,我將其稱為 Lexicon
(bayes-train doyle-data wilde-data 'Lexicon)此上下文現在包含出現在列表中的單詞列表以及每個單詞的頻率。例如
Lexicon:_always
;-> (21 110)即單詞 always 在柯南·道爾的文字中出現 21 次,在王爾德的文字中出現 110 次。接下來,可以將 Lexicon 上下文儲存到檔案中
(save "/Users/me/Documents/lex.lsp" 'Lexicon)並在需要時使用以下命令重新載入
(load "/Users/me/Documents/lex.lsp")完成訓練後,可以使用 bayes-query 函式在上下文中查詢單詞列表,並返回兩個數字,即這些單詞屬於第一組或第二組單詞的機率。以下列出三個查詢。請記住,第一組是道爾,第二組是王爾德
(set 'quote1
(bayes-query
(parse (lower-case
"the latest vegetable alkaloid" ) {\W} 0)
'Lexicon))
;-> (0.973352412 0.02664758802)
(set 'quote2
(bayes-query
(parse
(lower-case
"observations of threadbare morality to listen to" ) {\W} 0)
'Lexicon))
;-> (0.5 0.5)
(set 'quote3
(bayes-query
(parse
(lower-case
"after breakfast he flung himself down on a divan
and lit a cigarette" ){\W} 0)
'Lexicon))
;-> (0.01961482169 0.9803851783)這些數字表明 quote1 可能(97% 的確定性)來自柯南·道爾,quote2 不屬於道爾或王爾德,quote3 很可能來自奧斯卡·王爾德。
也許這很幸運,但這是一個不錯的結果。第一段引文來自道爾的 血字的研究,第三段引文來自王爾德的 亞瑟·薩維爾勳爵的罪行,這兩個文字都沒有包含在訓練過程中,但顯然屬於作者的詞彙。第二段引文來自簡·奧斯汀,貝葉斯牧師發展的方法無法將其歸類到兩位作者中的任何一位。
金融函式
[edit | edit source]newLISP 提供以下金融函式
- fv 返回投資的未來值
- irr 返回投資的內部收益率
- nper 返回投資的期限數量
- npv 返回投資的淨現值
- pmt 返回貸款的付款額
- pv 返回投資的現值
邏輯程式設計
[edit | edit source]Prolog 程式語言普及了一種稱為統一的邏輯程式設計型別。newLISP 提供了一個 unify 函式,它可以透過匹配表示式來執行統一。
(unify '(X Y) '((+ 1 2) (- (* 4 5))))((X (+ 1 2)) (Y (- (* 4 5))))使用 unify 時,未繫結的變數以大寫字母開頭,以將其與符號區分開來。
位運算子
[edit | edit source]位運算子將數字視為由 1 和 0 組成。我們將使用一個實用程式函式,該函式使用 bits 函式以二進位制格式列印數字
(define (binary n)
(if (< n 0)
; use string format for negative numbers
(println (format "%6d %064s" n (bits n)))
; else, use decimal format to be able to prefix with zeros
(println (format "%6d %064d" n (int (bits n))))))此函式打印出原始數字及其二進位制表示形式
(binary 6)
;-> 6 0000000000000000000000000000000000000000000000000000000000000110
;-> " 6 0000000000000000000000000000000000000000000000000000000000000110"移位函式(<< 和 >>)將位向左或向右移動
(binary (<< 6)) ; shift left
;-> 12 0000000000000000000000000000000000000000000000000000000000001100
;->" 12 0000000000000000000000000000000000000000000000000000000000001100"(binary (>> 6)) ; shift right
;-> 3 0000000000000000000000000000000000000000000000000000000000000011
;->" 3 0000000000000000000000000000000000000000000000000000000000000011"以下運算子比較兩個或多個數字的位。以 4 和 5 為例
(map binary '(5 4))
;-> 5 0000000000000000000000000000000000000000000000000000000000000101
;-> 4 0000000000000000000000000000000000000000000000000000000000000100
;-> (" 5 0000000000000000000000000000000000000000000000000000000000000101"
;-> " 4 0000000000000000000000000000000000000000000000000000000000000100")(binary (^ 4 5)) ; exclusive or: 1 if only 1 of the two bits is 1
;-> 1 0000000000000000000000000000000000000000000000000000000000000001
;->" 1 0000000000000000000000000000000000000000000000000000000000000001"(binary (| 4 5)) ; or: 1 if either or both bits are 1 ;-> 5 0000000000000000000000000000000000000000000000000000000000000101 ;->" 5 0000000000000000000000000000000000000000000000000000000000000101"
(binary (& 4 5)) ; and: 1 only if both are 1
;-> 4 0000000000000000000000000000000000000000000000000000000000000100
;->" 4 0000000000000000000000000000000000000000000000000000000000000100"取反或非函式(~)反轉數字中的所有位,交換 1 和 0
(binary (~ 5)) ; not: 1 <-> 0
;-> -6 1111111111111111111111111111111111111111111111111111111111111010
;->" -6 1111111111111111111111111111111111111111111111111111111111111010"打印出這些字串的二進位制函式使用 & 函式測試數字的最後一位以檢視它是否是 1,並使用 >> 函式將數字向右移動 1 位,準備進行下一次迭代。
OR 運算子(|)的一種用途是在您希望將正則表示式選項與 regex 函式結合使用時。
crc32 為字串計算 32 位 CRC(迴圈冗餘校驗)。
更大的數字
[edit | edit source]對於大多數應用程式,newLISP 中的整數計算涉及從 9223372036854775807 到 -9223372036854775808 的整數。這些是使用 64 位儲存的最大整數。如果您將 1 加到最大的 64 位整數,您將“迴繞”(或迴圈回到)該範圍的負數端
(set 'large-int 9223372036854775807)
(+ large-int 1)
;-> -9223372036854775808但 newLISP 可以處理比這更大的整數,即所謂的“bignums”或“大整數”。
(set 'number-of-atoms-in-the-universe 100000000000000000000000000000000000000000000000000000000000000000000000000000000)
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000000L
(++ number-of-atoms-in-the-universe)
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000001L
(length number-of-atoms-in-the-universe)
;-> 81
(float number-of-atoms-in-the-universe)
;->1e+80請注意,newLISP 使用尾隨的“L”來指示大整數。通常,您可以在不加思考的情況下對大整數進行計算
(* 100000000000000000000000000000000 100000000000000000000000000000)
;-> 10000000000000000000000000000000000000000000000000000000000000L這裡兩個運算元都是大整數,因此答案也會自動變為大整數。
但是,在計算中將大整數與其他型別的數字結合使用時,您需要更加註意。規則是計算的第一個引數決定是否使用大整數。比較此迴圈
(for (i 1 10) (println (+ 9223372036854775800 i)))9223372036854775801 9223372036854775802 9223372036854775803 9223372036854775804 9223372036854775805 9223372036854775806 9223372036854775807 -9223372036854775808 -9223372036854775807 -9223372036854775806 -9223372036854775806
和這個
(for (i 1 10) (println (+ 9223372036854775800L i))) ; notice the "L"9223372036854775801L 9223372036854775802L 9223372036854775803L 9223372036854775804L 9223372036854775805L 9223372036854775806L 9223372036854775807L 9223372036854775808L 9223372036854775809L 9223372036854775810L ;-> 9223372036854775810L
在第一個示例中,函式的第一個引數是大型(64 位整數)。因此,將 1 加到最大的 64 位整數會導致迴繞 - 計算仍然保留在大整數範圍內。
在第二個示例中,追加到第一個引數的 L 強制 newLISP 切換到大整數運算,即使 兩個運算元都是 64 位整數。第一個引數的大小決定結果的大小。
如果您提供一個字面量大整數,則無需追加“L”,因為很明顯該數字是大整數
(for (i 1 10) (println (+ 92233720368547758123421231455634 i)))92233720368547758123421231455635L 92233720368547758123421231455636L 92233720368547758123421231455637L 92233720368547758123421231455638L 92233720368547758123421231455639L 92233720368547758123421231455640L 92233720368547758123421231455641L 92233720368547758123421231455642L 92233720368547758123421231455643L 92233720368547758123421231455644L 92233720368547758123421231455644L
您可以控制 newLISP 在大型整數和大整數之間轉換的方式還有其他方法。例如,您可以使用 bigint 函式將某物轉換為大整數
(set 'bignum (bigint 9223372036854775807))
(* bignum bignum)
;-> 85070591730234615847396907784232501249L
(set 'atoms (bigint 1E+80))
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000000L
(++ atoms)
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000001L使用日期和時間
[edit | edit source]日期和時間函式
[edit | edit source]要使用日期和時間,請使用以下函式
- date 將秒數轉換為日期/時間,或返回當前的日期/時間
- date-value 返回自 1970 年 1 月 1 日以來的日期和時間的秒數,或返回當前時間的秒數
- now 以列表形式返回當前日期/時間資訊
- time-of-day 返回自今天開始到現在的毫秒數
date-value 和 now 在 UT 中工作,而不是在您的本地時間中。date 可以考慮您的本地時間和 UT 之間的時差。
當前時間和日期
[edit | edit source]所有四個函式都可用於返回有關當前時間的 資訊。date-value 返回 1970 年到當前時間(在 UT 中)之間的秒數
1142798985而 now 返回一個包含有關當前日期和時間(在 UT 中)資訊的整數列表
(now)
;-> (2006 3 19 20 5 2 125475 78 1 0 0)這提供以下資訊
- 年、月、日 (2006, 3, 19)
- 時、分、秒、微秒 (20, 5, 2, 125475)
- 當年的第幾天 (78)
- 當前一週的日期(1)
- 本地時區偏移量(以分鐘計,相對於格林威治標準時間)(0)
- 夏令時標誌(0)
要提取所需資訊,請使用切片或提取元素
(slice (now) 0 3) ; year month day using explicit slice
(0 3 (now)) ; year month day using implicit slice
(select (now) '(0 1 2)) ; year month day using selection
(3 3 (now)) ; hour minute second
(nth 8 (now)) ; day of the week, starting from Sundaydate 單獨使用時會返回本地時區的當前日期和時間(now 和 date-value 返回的都是 UCT/UTC 時間,而不是相對於本地時區的日期和時間)
(date)
;-> "Mon Mar 19 20:05:02 2006"它還可以告訴您自 1970 年(Unix 紀元開始)以來的某個整數秒數的日期,並根據您的本地時區進行調整
(date 0) ; a US newLISP user sees this
;-> "Wed Dec 31 16:00:00 1969"
(date 0) ; a European newLISP user sees this
;-> "Thu Jan 1 01:00:00 1970"date-value 可以計算特定日期或日期/時間(以 UT 計)的秒數
(date-value 2006 5 11) ; just the date
;-> 1147305600
(date-value 2006 5 11 23 59 59) ; date and time (in UT)
;-> 1147391999因為 date-value 可以接受年份、月份、日期、小時、分鐘和秒作為輸入,所以它可以應用於 now 的輸出
(apply date-value (now))
;-> 1164723787透過將不同的時間轉換為這些日期值,可以進行計算。例如,要從 2005 年 1 月 3 日減去 2003 年 11 月 13 日
(- (date-value 2005 1 3) (date-value 2003 11 13))
;-> 36028800
; seconds, which is
(/ 36028800 (* 24 60 60))
;-> 417
; this is a duration in days - don't convert this to a date!您可以透過將 12 天的秒數新增到該日期來找出 2005 年聖誕節之後的第 12 天
(+ (date-value 2005 12 25) (* 12 24 60 60))
; seconds in 12 days
;-> 1136505600
; this is an instant in time, so it can be converted!此秒數值可以透過 date 的較長形式轉換為人類可讀的日期,它接受自 1970 年以來的秒數值,並將其轉換為此基於 UT 值的本地時區表示形式
(date 1136505600)
;-> "Fri Jan 6 00:00:00 2006" ; for this European user...當然,(date (date-value)) 與 (date) 相同,但如果要更改日期格式,則必須使用較長形式。date 接受一個額外的格式字串(以分鐘為單位的時區偏移量開頭)。如果您熟悉 C 語言風格的 strftime 格式,那麼您就知道該怎麼做
(date (date-value) 0 "%Y-%m-%d %H:%M:%S") ; ISO 8601
;-> 2006-06-08 11:55:08
(date 1136505600 0 "%Y-%m-%d %H:%M:%S")
;-> "2006-01-06 00:00:00"
(date (date-value) 0 "%Y%m%d-%H%M%S") ; in London
;-> "20061207-144445"
(date (date-value) (* -8 60) "%Y%m%d-%H%M%S") ; in Los Angeles
;-> "20061207-064445" ; 8 hours offset讀取日期和時間:parse-date
[edit | edit source]parse-date 函式(不幸的是,它在 Windows 上不可用)可以將日期和時間字串轉換為自 1970 年以來的秒數值。您在字串之後提供日期時間格式字串
(parse-date "2006-12-13" "%Y-%m-%d")
;-> 1165968000
(date (parse-date "2007-02-08 20:12" "%Y-%m-%d %H:%M"))
;-> "Thu Feb 8 20:12:00 2007"計時和計時器
[edit | edit source]為了計時,您可以使用以下函式
- time 返回評估表示式的花費時間,以毫秒為單位
- timer 設定計時器,等待特定秒數,然後評估表示式
- sleep 停止工作特定毫秒數
time 用於找出表示式評估所需的時間
(time (read-file "/Users/me/Music/iTunes/iTunes Music Library.xml"))
;-> 27 ; milliseconds您也可以提供重複次數,這可能更準確地反映結果
(time (for (x 1 1000) (factor x)) 100) ; 100 repetitions
;-> 426如果您不能或不想將表示式括起來,可以使用 time-of-day 來進行更簡單的計時
(set 'start-time (time-of-day))
(for (i 1 1000000)
(set 'temp (sqrt i)))
(string {that took } (div (- (time-of-day) start-time) 1000) { seconds})
;-> "that took 0.238 seconds"timer 本質上就是一個鬧鐘。設定它,然後忘記它,直到時間到來。您提供一個指定警報操作的符號,以及要等待的秒數
(define (teas-brewed)
(println (date) " Your tea has brewed, sir!"))
(timer teas-brewed (* 3 60))三分鐘後,您將看到以下內容
Sun Mar 19 23:36:33 2006 Your tea has brewed, sir!如果沒有引數,此函式將返回當前已分配為警報操作的符號的名稱
(timer)
;-> teas-brewed如果您正在等待警報發出,並且您迫不及待地想知道迄今為止已經過去了多少時間,請使用分配的符號名稱作為引數,但不提供秒數值
(timer teas-brewed)
;-> 89.135747
; waited only a minute and a bit so far有關這些函式用法的另一個示例,請參見 簡單倒計時器。
處理檔案
[edit | edit source]處理檔案的函式可以分為兩大類:與作業系統互動以及讀寫檔案資料。
與檔案系統互動
[edit | edit source]newLISP 中維護著當前工作目錄的概念。當您在終端中鍵入 newLISP 啟動 newLISP 時,您的當前工作目錄將成為 newLISP 的當前目錄。
$ pwd /Users/me/projects/programming/lisp $ newlisp newLISP v.9.3 on OSX UTF-8, execute 'newlisp -h' for more info. > (env "PWD") "/Users/me/projects/programming/lisp" > (exit) $ pwd /Users/me/projects/programming/lisp $
您還可以使用不帶引數的 real-path 函式來檢查當前工作目錄
(real-path)
;-> "/Users/me/projects/programming/lisp"但是,當您從其他地方執行 newLISP 指令碼時,例如從文字編輯器內部執行,當前工作目錄和其他設定可能不同。因此,如果不能確定,最好使用 change-dir 來建立當前工作目錄
(change-dir "/Users/me/Documents")
;-> true可以使用 env 訪問其他環境變數
(env "HOME")
;-> "/Users/me"
(env "USER")
;-> "cormullion"同樣,從文字編輯器而不是互動式終端會話執行 newLISP 會影響可用的環境變數及其值。
一旦您擁有了正確的工作目錄,就可以使用 directory 來列出其內容
(directory)
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...等等。請注意,它為您提供的是相對檔名,而不是絕對路徑名。directory 可以列出除當前工作目錄之外的其他目錄的內容,前提是您提供了路徑。在這種擴充套件形式中,您可以使用正則表示式來過濾內容
(directory "./") ; just a pathname
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...
(directory "./" {^[^.]}) ; exclude files starting "."
;-> ("bin" "Desktop" "Desktop Folder" "Documents" "Library" ... )同樣,請注意,結果相對於當前工作目錄。通常,將要列出的目錄的路徑儲存起來很有用,以防以後需要使用它來構建完整路徑名。real-path 返回檔案或目錄的完整路徑名,它位於當前工作目錄中
(real-path ".subversion")
;-> "/Users/me/.subversion"或由另一個相對路徑名指定
(real-path "projects/programming/lisp/lex.lsp")
;-> "/Users/me/projects/programming/lisp/lex.lsp"要查詢磁碟上某個專案的包含目錄,您只需從完整路徑名中刪除檔名即可
(set 'f "lex.lsp")
(replace f (real-path f) "")
;-> "/Users/me/projects/programming/lisp/"順便說一下,如果檔名也作為目錄名出現在路徑的前面,則這並不總是有效。一個簡單的解決方案是使用 $ 選項執行對 f 在路徑名末尾出現的正則表示式搜尋
(replace (string f "\$") (real-path f) "" 0)要遞迴掃描檔案系統的某一部分,請使用一個遞迴呼叫自身的函式。此處,只打印完整路徑名
(define (search-tree dir)
(dolist (item (directory dir {^[^.]}))
(if (directory? (append dir item))
; search the directory
(search-tree (append dir item "/"))
; or process the file
(println (append dir item)))))
(search-tree {/usr/share/newlisp/})/usr/share/newlisp/guiserver/allfonts-demo.lsp /usr/share/newlisp/guiserver/animation-demo.lsp ... /usr/share/newlisp/util/newlisp.vim /usr/share/newlisp/util/syntax.cgi
另請參見 編輯資料夾和層次結構中的文字檔案。
您會發現一些測試函式很有用
- file? 此檔案或目錄是否存在?
- directory? 此路徑名是目錄還是檔案?
請記住相對路徑名和絕對路徑名之間的區別
(file? "System")
;-> nil
(file? "/System")
;-> true檔案資訊
[edit | edit source]您可以使用 file-info 獲取有關檔案的資訊。此函式詢問作業系統有關檔案的資訊,並將資訊以一系列數字形式返回
- 0 是大小
- 1 是模式
- 2 是裝置模式
- 3 是使用者 ID
- 4 是組 ID
- 5 是訪問時間
- 6 是修改時間
- 7 是狀態更改時間
例如,要查詢檔案的大小,請檢視 file-info 返回的第一個數字。以下程式碼列出了目錄中的檔案,幷包含它們的大小。
(set 'dir {/usr/share/newlisp/modules})
(dolist (i (directory dir {^[^.]}))
(set 'item (string dir "/" i))
(if (not (directory? item))
(println (format {%7d %-30s} (nth 0 (file-info item)) item))))35935 /usr/share/newlisp/modules/canvas.lsp 6548 /usr/share/newlisp/modules/cgi.lsp 5460 /usr/share/newlisp/modules/crypto.lsp 4577 /usr/share/newlisp/modules/ftp.lsp 16310 /usr/share/newlisp/modules/gmp.lsp 4273 /usr/share/newlisp/modules/infix.lsp 12973 /usr/share/newlisp/modules/mysql.lsp 16606 /usr/share/newlisp/modules/odbc.lsp 9865 /usr/share/newlisp/modules/pop3.lsp 12835 /usr/share/newlisp/modules/postgres.lsp 31416 /usr/share/newlisp/modules/postscript.lsp 4337 /usr/share/newlisp/modules/smtp.lsp 10433 /usr/share/newlisp/modules/smtpx.lsp 16955 /usr/share/newlisp/modules/sqlite3.lsp 21807 /usr/share/newlisp/modules/stat.lsp 7898 /usr/share/newlisp/modules/unix.lsp 6979 /usr/share/newlisp/modules/xmlrpc-client.lsp 3366 /usr/share/newlisp/modules/zlib.lsp
請注意,我們將目錄的名稱儲存在 dir 中。directory 函式返回相對檔名,但必須將絕對路徑名字串傳遞給 file-info,除非字串引用的是當前工作目錄中的檔案。
您可以使用隱式定址來選擇所需的專案。因此,您可以這樣寫(nth 0 (file-info item)),而不是(file-info item 0).
MacOS X:資源分支
[edit | edit source]如果您在 MacOS X 上嘗試了前面的指令碼,您可能會注意到一些檔案的大小為 0 位元組。這可能表明存在從經典(即舊)Macintosh 時代繼承而來的雙分支系統。使用以下版本訪問檔案的資源分支。越來越罕見的資源分支的一個好搜尋區域是字型資料夾
(set 'dir "/Users/me/Library/Fonts") ; fonts folder
(dolist (i (directory dir "^[^.]"))
(set 'item (string dir "/" i))
(and
(not (directory? item)) ; don't do folders
(println
(format "%9d DF %-30s" (nth 0 (file-info item)) item))
(file? (format "%s/..namedfork/rsrc" item)) ; there's a resource fork too
(println (format "%9d RF"
(first (file-info (format "%s/..namedfork/rsrc" item)))))))...
0 DF /Users/me/Library/Fonts/AvantGarBoo
26917 RF
0 DF /Users/me/Library/Fonts/AvantGarBooObl
34982 RF
0 DF /Users/me/Library/Fonts/AvantGarDem
27735 RF
0 DF /Users/me/Library/Fonts/AvantGarDemObl
35859 RF
0 DF /Users/me/Library/Fonts/ITC Avant Garde Gothic 1
116262 RF
...
檔案管理
[edit | edit source]要管理檔案,您可以使用以下函式
- rename-file 重新命名檔案或目錄
- copy-file 複製檔案
- delete-file 刪除檔案
- make-dir 建立新目錄
- remove-dir 刪除空目錄
例如,要將當前工作目錄中的所有檔案重新編號,以便檔案按修改日期排序,您可以編寫如下程式碼
(set 'dir {/Users/me/temp/})
(dolist (i (directory dir {^[^.]}))
(set 'item (string dir "/" i))
(set 'mod-date (date (file-info item 6) 0 "%Y%m%d-%H%M%S"))
(rename-file item (string dir "/" mod-date i)));-> before image-001.png image-002.png image-003.png image-004.png ;-> after 20061116-120534image-001.png 20061116-155127image-002.png 20061117-210447image-003.png 20061118-143510image-004.png
The(file-info item 6)提取 file-info 返回的結果的修改時間(專案 6)。
在實際使用之前,請務必測試此類指令碼!錯誤的標點符號可能會造成嚴重破壞。
讀取和寫入資料
[edit | edit source]newLISP 擁有豐富的輸入和輸出函式。
將文字寫入檔案的一種簡單方法是使用 append-file,它將字串新增到檔案的末尾。如果檔案不存在,則會建立它。它非常適合建立日誌檔案和定期寫入的檔案
(dotimes (x 10)
(append-file "/Users/me/Desktop/log.log"
(string (date) " logging " x "\n")))現在在我的桌面上有一個檔案,內容如下
Sat Sep 26 09:06:08 2009 logging 0 Sat Sep 26 09:06:08 2009 logging 1 Sat Sep 26 09:06:08 2009 logging 2 Sat Sep 26 09:06:08 2009 logging 3 Sat Sep 26 09:06:08 2009 logging 4 Sat Sep 26 09:06:08 2009 logging 5 Sat Sep 26 09:06:08 2009 logging 6 Sat Sep 26 09:06:08 2009 logging 7 Sat Sep 26 09:06:08 2009 logging 8 Sat Sep 26 09:06:08 2009 logging 9
您無需擔心開啟和關閉檔案。
要將檔案的內容一次性載入到一個符號中,請使用 read-file。
(set 'contents (read-file "/usr/share/newlisp/init.lsp.example"))
;->
";; init.lsp - newLISP initialization file\n;; gets loaded automatically on
; ...
(load (append $HOME \"/.init.lsp\")) 'error))\n\n;;;; end of file ;;;;\n\n\n "符號 contents 將檔案的內容作為單個字串儲存。
open 返回一個值,該值充當對檔案的引用或“控制代碼”。您可能以後會使用該檔案,因此請將引用儲存在一個符號中
(set 'data-file (open "newfile.data" "read")) ; in current directory
; and later
(close data-file)使用 read-line 從檔案控制代碼中逐行讀取檔案。每次使用 read-line 時,下一行都會儲存在一個緩衝區中,您可以使用 current-line 函式訪問該緩衝區。讀取檔案的基本方法如下
(set 'file (open ((main-args) 2) "read")) ; the argument to the script
(while (read-line file)
(println (current-line))) ; just output the lineread-line 會丟棄每行末尾的換行符。println 會在您提供的文字末尾新增一個換行符。有關引數處理和 main-args 的更多資訊,請參見 STDIO。
對於大小中等的檔案,逐行讀取原始檔比將整個檔案一次性載入到記憶體中要慢得多。例如,本書的源文件大約有 6000 行文字,約 350KBytes。使用 read-file 和 parse 處理檔案要快大約 10 倍,方法如下
(set 'source-text (read-file "/Users/me/introduction.txt"))
(dolist (ln (parse source-text "\n" 0))
(process-line ln))而不是使用 read-line,方法如下
(set 'source-file (open "/Users/me/introduction.txt" "read"))
(while (read-line source-file)
(process-line (current-line)))device 函式是一種在控制檯和檔案之間切換輸出的便捷方法
(set 'output-file (open "/tmp/file.txt" "write"))
(println "1: this goes to the console")
(device output-file)
(println "2: this goes to the temp file")
(device 0)
(println "3: this goes to the console")
(close output-file)假設你的指令碼接受單個引數,並且你想將輸出寫入與該引數同名但字尾為 .out 的檔案。試試這個
(device (open (string ((main-args) 2) ".out") "write"))
(set 'file-contents (read-file ((main-args) 2)))現在你可以處理檔案的內容,並使用 println 語句輸出任何資訊。
load 和 save 函式用於從檔案載入 newLISP 原始碼,以及將原始碼儲存到檔案。
read-line 和 write-line 函式可用於讀取和寫入執行緒以及檔案中的行。參見 執行緒的讀寫.
要從 STDIO(標準輸入)讀取並寫入 STDOUT(標準輸出),請使用 read-line 和 println。例如,以下是一個將標準輸入轉換為小寫並將其輸出到標準輸出的簡單過濾器
#!/usr/bin/newlisp
(while (read-line)
(println (lower-case (current-line))))
(exit)它可以在 shell 中像這樣執行
$ ./lower-case.lsp HI hi HI THERE hi there ...
以下簡短指令碼是一個有用的 newLISP 格式化程式,由使用者 Echeam 提交到使用者論壇
#!/usr/bin/env newlisp
(set 'indent " ")
(set 'level 0)
(while (read-line)
(if (< level 0) (println "ERROR! Too many close-parenthesis. " level))
(letn ((ln (trim (current-line))) (fc (first ln)))
(if (!= fc ")") (println (dup indent level) ln)) ; (indent & print
(if (and (!= fc ";") (!= fc "#")) ; don't count if line starts with ; or #
(set 'level
(+ level (apply - (count (explode "()") (explode (current-line)))))))
(if (= fc ")") (println (dup indent level) ln))) ; (dedent if close-parenthesis
)
(if (!= level 0) (println "ERROR! Parenthesis not balanced. " level))
(exit)你可以像這樣從命令列執行它
$ format.lsp < inputfile.lsp
要在命令列中使用 newLISP 程式,可以使用 main-args 函式訪問引數。例如,如果你建立了這個檔案
#!/usr/bin/newlisp
(println (main-args))
(exit)使其可執行,然後在 shell 中執行它,你將看到在執行指令碼時傳遞給指令碼的引數列表
$ test.lsp 1 2 3
("/usr/bin/newlisp" "/Users/me/bin/test.lsp" "1" "2" "3")
$
main-args 返回傳遞給程式的引數列表。前兩個引數(你可能不想處理它們)是 newLISP 程式的路徑和正在執行的指令碼的路徑名
(main-args)
;-> ("/usr/bin/newlisp" "/path/script.lsp" "1" "2" "3")因此你可能想從索引 2 開始處理引數
((main-args) 2)
;-> 1
(main-args 2) ; slightly simpler
;-> 1它以字串形式返回。或者,要從索引 2 開始處理所有引數,請使用切片
(2 (main-args))
;-> ("1" "2" "3")引數以字串列表的形式返回。
通常,你希望在指令碼中處理所有主要的引數:一個方便的短語是
(dolist (a (2 (main-args)))
(println a))一個稍微更易讀的等價物是這個,它處理其餘的引數
(dolist (a (rest (rest (main-args))))
(println a))以下簡短指令碼會從文字檔案中過濾掉不需要的 Unicode 字元,但允許透過少數特殊的字元
(set 'file (open ((main-args) 2) "read")) ; one file
(define (in-range? n low high)
; is n between low and high inclusive?
(and (<= n high) (>= n low)))
(while (read-line file)
(dostring (c (current-line))
(if
(or
(in-range? c 32 127) ; ascii
(in-range? c 9 10) ; tab newline
(in-range? c 12 13) ; \f \r
(= c (int "\0xbb")) ; right double angle
(= c (int "\0x25ca")) ; diamond
(= c (int "\0x2022")) ; bullet
(= c (int "\0x201c")) ; open double quote
(= c (int "\0x201d")) ; close double quote
)
(print (char c)))) ; nothing to do
(println) ; because read-line swallows line endings
)有關引數處理的更多示例,請參見 簡單倒計時器.
以下函式允許你與作業系統互動
- ! 在作業系統中執行命令
- abort 停止所有已生成的程序
- destroy 殺死一個程序
- exec 執行一個程序並從中讀取或寫入
- fork 啟動一個 newLISP 子程序執行緒(Unix)
- pipe 建立一個用於程序間通訊的管道
- process 啟動一個子程序,重新對映標準 I/O 和標準錯誤
- semaphore 建立和控制訊號量
- share 與其他程序和執行緒共享記憶體
- spawn 建立一個新的 newLISP 子程序
- sync 監視和同步已生成的程序
- wait-pid 等待子程序結束
由於這些命令與你的作業系統互動,因此你應參閱平臺特定問題的文件和限制。
! 執行一個系統命令並在控制檯中顯示結果。exec 函式的功能類似,但它會等待作業系統完成,然後將標準輸出作為字串列表返回,每行一個字串
(exec "ls -Rat /usr | grep newlisp")
;->
("newlisp" "newlisp-edit" "newlispdoc" "newlisp" "newlisp.1"
"newlispdoc.1" "/usr/share/newlisp:"
"/usr/share/newlisp/guiserver:"
"/usr/share/newlisp/modules:" "/usr/share/newlisp/util:"
"newlisp.vim" "newlisp" "/usr/share/doc/newlisp:"
"newlisp_index.html" "newlisp_manual.html"
"/usr/share/doc/newlisp/guiserver:")與往常一樣,你需要進行引用和雙重引用才能將命令傳遞給 shell。
使用 exec,你的指令碼將等待命令完成,然後才會繼續執行。
(exec (string "du -s " (env "HOME") "/Desktop"))你只有在命令完成時才會看到結果。
要與另一個與 newLISP 並行執行的程序進行互動,而不是等待程序完成,請使用 process。參見 程序.
到目前為止,我們評估的所有 newLISP 表示式都是序列執行的,一個接一個,因此一個表示式必須完成評估才能讓 newLISP 開始評估下一個表示式。這通常是可以的。但有時你希望啟動一個表示式,然後繼續執行另一個表示式,而第一個表示式仍在評估中。或者,你可能希望將一個大型任務劃分為多個較小的任務,也許可以利用計算機的任何額外處理器。newLISP 使用三個函式來執行這種多工處理:spawn 用於建立新的程序以並行執行,sync 用於監視和完成它們,abort 用於在它們完成之前停止它們。
在以下示例中,我將使用這個簡短的“脈衝”函式
(define (pulsar ch interval)
(for (i 1 20)
(print ch)
(sleep interval))
(println "I've finished"))當你正常執行它時,你將看到列印了 20 個字元,每 interval 毫秒列印一個字元。此函式的執行會阻塞所有其他操作,你必須等待所有 20 個字元列印完,或者使用 Control-C 停止執行。
要在當前程序中並行執行此函式,請使用 spawn 函式。提供一個符號來儲存表示式的結果,然後是待評估的表示式
> (spawn 'r1 (pulsar "." 3000)) 2882 > .
函式返回的數字是程序 ID。現在你可以在終端中繼續使用 newLISP,而 pulsar 會繼續中斷你。這非常令人惱火 - 點會不斷出現在你的輸入中!
再啟動幾個
> (spawn 'r2 (pulsar "-" 5000)) 2883 > (spawn 'r3 (pulsar "!" 7000)) 2884 > (spawn 'r4 (pulsar "@" 9000)) 2885
要檢視有多少程序處於活動狀態(尚未完成),請在沒有引數的情況下使用 sync 函式
> (sync) (2885 2884 2883 2882)
如果要停止所有 pulsar 程序,請使用 abort
(abort) true
在 MacOS X 上,嘗試使用駐留語音的更有趣版本
(define (pulsar w interval)
(for (i 1 20)
(! (string " say " w))
(sleep interval))
(println "I've finished"))
(spawn 'r1 (pulsar "spaghetti" 2000))
(spawn 'r2 (pulsar "pizza" 3000))
(spawn 'r3 (pulsar "parmesan" 5000))sync 還可以讓你關注當前執行的程序。提供一個以毫秒為單位的值;newLISP 將等待該時間,然後檢查已生成的程序是否已完成
> (spawn 'r1 (pulsar "." 3000)) 2888 > . > (sync 1000) nil
如果結果為 nil,則程序尚未完成。如果結果為 true,則所有程序均已完成。現在 - 只有在 sync 函式執行並返回 true 後 - 返回符號 r1 的值將設定為程序返回的值。對於 pulsar,這將是字串“I've finished”。
I've finished > r1 nil > (sync 1000) true > r1 "I've finished" >
請注意,程序已完成 - 或者更確切地說,它列印了其結束訊息 - 但直到 sync 函式執行並返回 true 後,符號 r1 才被設定。這是因為 sync 返回 true,並且返回符號具有值,只有當 所有 已生成的程序都已完成時。
如果你想等待所有程序完成,可以執行迴圈
(until (sync 1000))它每秒檢查一次,檢視程序是否已完成。
作為獎勵,許多現代計算機擁有多個處理器,如果每個處理器都能專注於一項任務,你的指令碼可能會執行得更快。newLISP 允許作業系統根據其所處硬體環境來排程任務和處理器。
有一些用於操作程序的較低階函式。這些函式不像上一節中描述的已生成程序技術那樣方便或易於使用,但它們提供了一些額外的功能,你可能會在某一天發現這些功能很有用。
可以使用 fork 在另一個程序中評估表示式。一旦程序啟動,它就不會向父程序返回值,因此你需要考慮如何獲取它的結果。以下是在另一個程序中計算素數並將輸出儲存到檔案的方法
(define (isprime? n)
(if (= 1 (length (factor n)))
true))
(define (find-primes l h)
(for (x l h)
(if (isprime? x)
(push x results -1)))
results)
(fork (append-file "/Users/me/primes.txt"
(string "the result is: " (find-primes 500000 600000))))這裡,由 fork 啟動的新的子程序知道如何查詢素數,但與已生成的程序不同,它無法向其父程序返回資訊以報告它找到了多少個素數。
程序可以共享資訊。share 函式設定一個共同的記憶體區域,就像一個公告板,所有程序都可以讀寫。在後面的章節中有一個簡單的share 示例:請參見一個簡單的 IRC 客戶端)。
為了控制程序如何訪問共享記憶體,newLISP 提供了semaphore 函式。
讀寫執行緒
[edit | edit source]如果希望分叉的執行緒互相通訊,則需要先進行一些管道工作。使用pipe 設定通訊通道,然後安排一個執行緒監聽另一個執行緒。pipe 返回一個包含程序間通訊管道的讀寫控制代碼的列表,然後可以使用這些控制代碼作為read-line 和write-line 函式的通道來讀寫。
(define (isprime? n)
(if (= 1 (length (factor n)))
true))
(define (find-primes l h)
(for (x l h)
(if (isprime? x)
(push x results -1)))
results)
(define (generate-primes channel)
(dolist (x (find-primes 100 300))
(write-line channel (string x)))) ; write a prime
(define (report-results channel)
(do-until (> (int i) 290)
(println (setq i (read-line channel))))) ; get next prime
(define (start)
(map set '(in out) (pipe)) ; do some plumbing
(set 'generator (fork (report-results in)))
(set 'reporter (fork (generate-primes out)))
(println "they've started"))
(start)they've started 101 103 107 109 ...
(wait-pid generator)
(wait-pid reporter)請注意,"they've started" 字串出現在任何素數列印之前,即使該println 表示式出現線上程啟動之後。
wait-pid 函式等待由fork 啟動的執行緒完成——當然,您不必立即執行此操作。
與其他程序通訊
[edit | edit source]要啟動一個與 newLISP 同時執行的新作業系統程序,請使用process。與fork 一樣,首先需要設定一些合適的管道工作,以便 newLISP 可以與程序進行通訊,在下面的示例中,該程序是 Unix 計算器bc。write-buffer 函式寫入myout 管道,該管道由 bc 透過bcin 讀取。bc 的輸出透過bcout 指向,並由 newLISP 使用read-line 讀取。
(map set '(bcin myout) (pipe)) ; set up the plumbing
(map set '(myin bcout) (pipe))
(process "/usr/bin/bc" bcin bcout) ; start the bc process
(set 'sum "123456789012345 * 123456789012345")
(write-buffer myout (string sum "\n"))
(set 'answer (read-line myin))
(println (string sum " = " answer))123456789012345 * 123456789012345 = 15241578753238669120562399025
(write-buffer myout "quit\n") ; don't forget to quit!使用 XML
[edit | edit source]將 XML 轉換為列表
[edit | edit source]如今,XML 檔案被廣泛使用,您可能已經注意到,XML 檔案的高度組織的樹狀結構類似於我們在 newLISP 中遇到的巢狀列表結構。因此,如果您能像處理列表一樣輕鬆地處理 XML 檔案,那不是很好嗎?
您已經瞭解了兩個主要的 XML 處理函式。(請參見ref 和 ref-all)。xml-parse 和xml-type-tags 函式是將 XML 轉換為 newLISP 列表所需的一切。(xml-error 用於診斷錯誤)。xml-type-tags 確定xml-parse 如何處理 XML 標記,xml-parse 執行 XML 檔案的實際處理,將其轉換為列表。
為了說明這些函式的使用,我們將使用 newLISP 論壇的 RSS 新聞提要
(set 'xml (get-url "http://newlispfanclub.alh.net/forum/feed.php"))
並將檢索到的 XML 儲存在一個檔案中,以節省重複訪問伺服器。
(save {/Users/me/Desktop/newlisp.xml} 'xml) ; save symbol in file
(load {/Users/me/Desktop/newlisp.xml}) ; load symbol from file
XML 從以下開始
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-gb">
<link rel="self" type="application/atom+xml" href="http://newlispfanclub.alh.net/forum/feed.php" />
<title>newlispfanclub.alh.net</title>
<subtitle>Friends and Fans of NewLISP</subtitle>
<link href="http://newlispfanclub.alh.net/forum/index.php" />
<updated>2010-01-11T09:51:39+00:00</updated>
<author><name><![CDATA[newlispfanclub.alh.net]]></name></author>
<id>http://newlispfanclub.alh.net/forum/feed.php</id>
<entry>
<author><name><![CDATA[kosh]]></name></author>
<updated>2010-01-10T12:17:53+00:00</updated>
...
如果您使用xml-parse 解析 XML,但沒有先使用xml-type-tags,則輸出如下
(xml-parse xml)
(("ELEMENT" "feed" (("xmlns" "http://www.w3.org/2005/Atom")
("xml:lang" "en-gb"))
(("TEXT" "\n") ("ELEMENT" "link" (("rel" "self")
("type" "application/atom+xml")
("href" "http://newlispfanclub.alh.net/forum/feed.php"))
())
("TEXT" "\n\n")
("ELEMENT" "title" () (("TEXT" "newlispfanclub.alh.net")))
("TEXT" "\n")
("ELEMENT" "subtitle" () (("TEXT" "Friends and Fans of NewLISP")))
("TEXT" "\n")
("ELEMENT" "link" (("href" "http://newlispfanclub.alh.net/forum/index.php")) ())
; ...雖然它已經看起來有點像 LISP,但您可以看到元素已被標記為 "ELEMENT"、"TEXT"。目前,我們可以不用所有這些標籤,這就是xml-type-tags 基本上要做的工作。它允許您確定四種類型的 XML 標記的標籤:TEXT、CDATA、COMMENTS 和 ELEMENTS。我們將使用四個nil 隱藏它們。我們還將使用一些xml-parse 的選項來進一步整理輸出。
(xml-type-tags nil nil nil nil)
(set 'sxml (xml-parse xml 15)) ; options: 15 (see below)
;->
((feed ((xmlns "http://www.w3.org/2005/Atom") (xml:lang "en-gb")) (link ((rel "self")
(type "application/atom+xml")
(href "http://newlispfanclub.alh.net/forum/feed.php")))
(title "newlispfanclub.alh.net")
(subtitle "Friends and Fans of NewLISP")
(link ((href "http://newlispfanclub.alh.net/forum/index.php")))
(updated "2010-01-11T09:51:39+00:00")
(author (name "newlispfanclub.alh.net"))
(id "http://newlispfanclub.alh.net/forum/feed.php")
(entry (author (name "kosh"))
(updated "2010-01-10T12:17:53+00:00")
(id "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471")
(link ((href "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471")))
(title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
(category ((term "newLISP in the real world")
(scheme "http://newlispfanclub.alh.net/forum/viewforum.php?f=16")
(label "newLISP in the real world")))
(content ((type "html")
(xml:base "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471"))
"\nhello kukma.<br />\nI tried to make the newLISP.chm, and this is it.<br />\n
<br />\n<!-- m --><a class=\"postlink\"
; ...現在,這是一個有用的 newLISP 列表,儘管它相當複雜,儲存在一個名為sxml 的符號中。(這種表示 XML 的方式稱為 S-XML)。
如果您想知道 15 在xml-parse 表示式中做了什麼,它只是控制了轉換多少輔助 XML 資訊的一種方法:選項如下
- 1 - 抑制空白文字標籤
- 2 - 抑制空屬性列表
- 4 - 抑制註釋標籤
- 8 - 將字串標籤轉換為符號
- 16 - 新增 SXML(S 表示式 XML)屬性標籤
將它們加起來以獲得選項程式碼編號——因此 15 (+ 1 2 4 8) 使用了前四個選項:抑制不需要的東西,並將字串標籤轉換為符號。因此,新的符號已新增到 newLISP 的符號表中
(author category content entry feed href id label link rel scheme subtitle term
title type updated xml:base xml:lang xmlns)這些對應於 XML 檔案中的字串標籤,它們將幾乎立即派上用場。
現在怎麼辦?
[edit | edit source]到目前為止,故事基本上是這樣的
(set 'xml (get-url "http://newlispfanclub.alh.net/forum/feed.php"))
; we stored this in a temporary file while exploring
(xml-type-tags nil nil nil nil)
(set 'sxml (xml-parse xml 15))它為我們提供了儲存在sxml 符號中的新聞提要的列表版本。
由於此列表具有複雜的巢狀結構,最好使用ref 和ref-all 而不是find 來查詢內容。ref 在列表中找到表示式的第一個出現位置,並返回地址
(ref 'entry sxml)
;-> (0 9 0)這些數字是在列表中item 符號的第一個出現位置的地址:(0 9 0) 表示從整個列表的第 0 項開始,然後轉到該項的第 9 項,然後轉到該項的第 0 項。(當然,基於 0 的索引!)。
要查詢更高級別或封閉項,請使用chop 刪除地址的最後一級
(chop (ref 'entry sxml))
;-> (0 9)現在,它指向包含第一個專案的級別。就像從地址中刪除門牌號一樣,只留下街道名稱。
現在,您可以將此地址與其他接受索引列表的表示式一起使用。最方便和最簡潔的形式可能是隱式地址,它只是源列表的名稱,後面跟著一個索引列表
(sxml (chop (ref 'entry sxml))) ; a (0 9) slice of sxml(entry (author (name "kosh")) (updated "2010-01-10T12:17:53+00:00") (id "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471")
(link ((href "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471")))
(title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
(category ((term "newLISP in the real world") (scheme "http://newlispfanclub.alh.net/forum/viewforum.php?f=16")
(label "newLISP in the real world")))
(content ((type "html") (xml:base "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&p=17471#p17471"))
...它找到了entry 的第一個出現位置,並返回了 SXML 的封閉部分。
您還可以使用另一種技術,將列表的部分內容視為關聯列表
(lookup 'title (sxml (chop (ref 'entry sxml))))
;->
newLISP in the real world • Re: suggesting the newLISP manual as CHM在這裡,我們像之前一樣找到了第一個專案,然後使用lookup 查找了 title 的第一個出現位置。
使用ref-all 在列表中查詢符號的所有出現位置。它返回一個地址列表
(ref-all 'title sxml)
;->
((0 3 0)
(0 9 5 0)
(0 10 5 0)
(0 11 5 0)
(0 12 5 0)
(0 13 5 0)
(0 14 5 0)
(0 15 5 0)
(0 16 5 0)
(0 17 5 0)
(0 18 5 0))透過簡單的列表遍歷,您可以快速顯示檔案中所有標題,無論它們位於哪個級別
(dolist (el (ref-all 'title sxml))
(println (rest (rest (sxml (chop el))))));->
()
("newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
("newLISP newS \226\128\162 Re: newLISP Advocacy")
("newLISP in the real world \226\128\162 Re: newLISP-gs opens only splash picture")
("So, what can you actually DO with newLISP? \226\128\162 Re: Conception of Adaptive Programming Languages")
("Dragonfly \226\128\162 Re: Dragonfly 0.60 Released!")
("So, what can you actually DO with newLISP? \226\128\162 Takuya Mannami, Gauche Newlisp Library")
; ...如果沒有那兩個rest,您會看到這個
(title "newlispfanclub.alh.net")
(title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
(title ((type "html")) "newLISP newS \226\128\162 Re: newLISP Advocacy")
; ...如您所見,有很多不同的方法可以訪問 SXML 資料中的資訊。要生成 XML 檔案中新聞的簡明摘要,一種方法是遍歷所有專案,並提取標題和描述條目。由於描述元素是一堆轉義實體,因此我們也會編寫一個快速且簡單的整理例程
(define (cleanup str)
(let (replacements
'(({&amp;} {&})
({&gt;} {>})
({&lt;} {<})
({&nbsp;} { })
({&apos;} {'})
({&quot;} {"})
({&#40;} {(})
({&#41;} {)})
({&#58;} {:})
("\n" "")))
(and
(!= str "")
(map
(fn (f) (replace (first f) str (last f)))
replacements)
(join (parse str {<.*?>} 4) " "))))
(set 'entries (sxml (chop (chop (ref 'title sxml)))))
(dolist (e (ref-all 'entry entries))
(set 'entry (entries (chop e)))
(set 'author (lookup 'author entry))
(println "Author: " (last author))
(set 'content (lookup 'content entry))
(println "Post: " (0 60 (cleanup content)) {...}))Author: kosh Post: hello kukma. I tried to make the newLISP.chm, and this is it... Author: Lutz Post: ... also, there was a sign-extension error in the newLISP co... Author: kukma Post: Thank you Lutz and welcome home again, the principle has bec... Author: Kazimir Majorinc Post: Apparently, Aparecido Valdemir de Freitas completed his Dr... Author: cormullion Post: Upgrade seemed to go well - I think I found most of the file... Author: Kazimir Majorinc Post: http://github.com/mtakuya/gauche-nl-lib Statistics: Post... Author: itistoday Post: As part of my work on Dragonfly, I've updated newLISP's SMTP... Author: Tim Johnson Post: itistoday wrote: Tim Johnson wrote: Have you done any... ; ...
更改 SXML
[edit | edit source]您可以使用類似的技術來修改 XML 格式的資料。例如,假設您將元素週期表儲存在 XML 檔案中,並且您想更改元素熔點的相關資料,這些資料目前以開氏度為單位儲存,改為攝氏度。XML 資料如下所示
<?xml version="1.0"?>
<PERIODIC_TABLE>
<ATOM>
...
</ATOM>
<ATOM>
<NAME>Mercury</NAME>
<ATOMIC_WEIGHT>200.59</ATOMIC_WEIGHT>
<ATOMIC_NUMBER>80</ATOMIC_NUMBER>
<OXIDATION_STATES>2, 1</OXIDATION_STATES>
<BOILING_POINT UNITS="Kelvin">629.88</BOILING_POINT>
<MELTING_POINT UNITS="Kelvin">234.31</MELTING_POINT>
<SYMBOL>Hg</SYMBOL>
...
當表格已載入到符號sxml 中時,使用(set 'sxml (xml-parse xml 15))(其中 xml 包含 XML 原始碼),我們希望更改每個具有以下形式的子列表
(MELTING_POINT ((UNITS "Kelvin")) "629.88")您可以使用set-ref-all 函式在一個表示式中查詢和替換元素。首先,這裡有一個用於將溫度從開氏度轉換為攝氏度的函式
(define (convert-K-to-C n)
(sub n 273.15))現在,set-ref-all 函式只需呼叫一次即可查詢所有引用並就地修改它們,以便將所有熔點轉換為攝氏度。其形式為
(set-ref-all key list replacement function)其中函式是在給定鍵的情況下查詢列表元素的方法。
(set-ref-all
'(MELTING_POINT ((UNITS "Kelvin")) *)
sxml
(list
(first $0)
'((UNITS "Celsius"))
(string (convert-K-to-C (float (last $0)))))
match)這裡,match 函式使用萬用字元結構搜尋 SXML 列表(MELTING_POINT ( (UNITS "Kelvin") ) *)以查詢每個出現位置。替換表示式使用儲存在 $0 中的匹配表示式構建替換子列表。在評估完此表示式後,SXML 將從以下內容
; ...
(ATOM
(NAME "Mercury")
(ATOMIC_WEIGHT "200.59")
(ATOMIC_NUMBER "80")
(OXIDATION_STATES "2, 1")
(BOILING_POINT
((UNITS "Kelvin")) "629.88")
(MELTING_POINT
((UNITS "Kelvin")) "234.31")
; ...更改為以下內容
; ...
(ATOM
(NAME "Mercury")
(ATOMIC_WEIGHT "200.59")
(ATOMIC_NUMBER "80")
(OXIDATION_STATES "2, 1")
(BOILING_POINT
((UNITS "Kelvin")) "629.88")
(MELTING_POINT
((UNITS "Celsius")) "-38.84")
; ...
XML 並不總是像這樣容易操作——存在屬性、CDATA 部分等等。
將 SXML 輸出到 XML
[edit | edit source]如果您想反過來將 newLISP 列表轉換為 XML,則以下函式提出了一種可能的方法。它遞迴地遍歷列表
(define (expr2xml expr (level 0))
(cond
((or (atom? expr) (quote? expr))
(print (dup " " level))
(println expr))
((list? (first expr))
(expr2xml (first expr) (+ level 1))
(dolist (s (rest expr)) (expr2xml s (+ level 1))))
((symbol? (first expr))
(print (dup " " level))
(println "<" (first expr) ">")
(dolist (s (rest expr)) (expr2xml s (+ level 1)))
(print (dup " " level))
(println "</" (first expr) ">"))
(true
(print (dup " " level))
(println "<error>" (string expr) "<error>"))))
(expr2xml sxml) <rss>
<version>
0.92
</version>
<channel>
<docs>
http://backend.userland.com/rss092
</docs>
<title>
newLISP Fan Club
</title>
<link>
http://www.alh.net/newlisp/phpbb/
</link>
<description>
Friends and Fans of newLISP
</description>
<managingEditor>
newlispfanclub-at-excite.com
</managingEditor>
...
這幾乎就是我們開始的地方!
下面的例子最初是在一家小型企業的運輸部門設定的。我已經將物品更改為水果。XML 資料檔案包含所有銷售商品的條目以及每個商品的費用。我們想生成一個報告,列出每個價格賣出了多少,以及總價值。
以下是 XML 資料的摘錄
<FRUIT>
<NAME>orange</NAME>
<charge>0</charge>
<COLOR>orange</COLOR>
</FRUIT>
<FRUIT>
<NAME>banana</NAME>
<COLOR>yellow</COLOR>
<charge>12.99</charge>
</FRUIT>
<FRUIT>
<NAME>banana</NAME>
<COLOR>yellow</COLOR>
<charge>0</charge>
</FRUIT>
<FRUIT>
<NAME>banana</NAME>
<COLOR>yellow</COLOR>
<charge>No Charge</charge>
</FRUIT>
這是定義和組織任務的主要函式
(define (work-through-files file-list)
(dolist (fl file-list)
(set 'table '())
(scan-file fl)
(write-report fl)))呼叫了兩個函式:scan-file,它掃描 XML 檔案並將所需資訊儲存在一個表中,該表將是某種 newLISP 列表,以及write-report,它掃描該表並輸出一個報告。
scan-file 函式接收一個路徑名,將檔案轉換為 SXML,找到所有charge 專案(使用ref-all),並記錄每個值的計數。我們允許一些免費物品被各種方式標記為 No Charge 或 no charge 或 nocharge
(define (scan-file f)
(xml-type-tags nil nil nil nil)
(set 'sxml (xml-parse (read-file f) 15))
(set 'r-list (ref-all 'charge sxml))
(dolist (r r-list)
(set 'charge-text (last (sxml (chop r))))
(if (= (lower-case (replace " " charge-text "")) "nocharge")
(set 'charge (lower-case charge-text))
(set 'charge (float charge-text 0 10)))
(if (set 'result (lookup charge table 1))
; if this price already exists in table, increment it
(setf (assoc charge table) (list charge (inc result)))
; or create an entry for it
(push (list charge 1) table -1))))write-report 函式對錶進行排序和分析,在執行時保持累積總數
(define (write-report fl)
(set 'total-items 0 'running-total 0 'total-priced-items 0)
(println "sorting")
(sort table (fn (x y) (< (float (x 0)) (float (y 0)))))
(println "sorted ")
(println "File: " fl)
(println " Charge Quantity Subtotal")
(dolist (c table)
(set 'price (float (first c)))
(set 'quantity (int (last c)))
(inc total-items quantity)
(cond
; do the No Charge items:
((= price nil) (println (format " No charge %12d" quantity)))
; do 0.00 items
((= price 0) (println (format " 0.00 %12d" quantity)))
; do priced items:
(true
(begin
(set 'subtotal (mul price quantity))
(inc running-total subtotal)
(if (> price 0) (inc total-priced-items quantity))
(println (format "%8.2f %8d %12.2f" price quantity subtotal))))))
; totals
(println (format "Total charged %8d %12.2f" total-priced-items running-total))
(println (format "Grand Total %8d %12.2f" total-items running-total)))該報告需要比scan-file 函式更多一點的調整,尤其是使用者想要(出於某種原因)將 0 和無費用專案分開。
Charge Quantity Subtotal
No charge 138
0.00 145
0.11 1 0.11
0.29 1 0.29
1.89 72 136.08
1.99 17 33.83
2.99 18 53.82
12.99 55 714.45
17.99 1 17.99
Total charged 165 956.57
Grand Total 448 956.57
本節簡要介紹內建偵錯程式。
關鍵函式是trace。要啟動和停止偵錯程式,請使用 true 或 nil
(trace true) ; start debugging
(trace nil) ; stop debugging不帶引數使用時,如果偵錯程式當前處於活動狀態,它將返回 true。
trace-highlight 命令允許您控制當前正在計算的表示式的顯示,以及一些提示。我正在使用一個相容 VT100 的終端,因此我可以使用奇怪的轉義序列來設定顏色。在 newLISP 提示符下鍵入以下內容
(trace-highlight "\027[0;37m" "\027[0;0m")但是因為我永遠記不住這個,所以它在我的 .init.lsp 檔案中,該檔案在您啟動 newLISP 時載入。如果您不能使用這些序列,則可以使用普通字串代替。
另一個除錯函式是debug。這實際上只是切換跟蹤、在偵錯程式中執行函式,然後再次切換跟蹤的快捷方式。所以,假設我們要執行一個名為 old-file-scanner.lsp 的檔案,其中包含以下程式碼
(define (walk-tree folder)
(dolist (item (directory folder))
(set 'item-name (string folder "/" item))
(if (and (not (starts-with item ".")) (directory? item-name))
; folder
(walk-tree item-name)
; file
(and
(not (starts-with item "."))
(set 'f-name (real-path item-name))
(set 'mtime (file-info f-name 6))
(if
(> (- (date-value) mtime) (* 5 365 24 60 60)) ; non-leap years :)
(push (list mtime item-name) results))))))
(set 'results '())
(walk-tree {/usr/share})
(map (fn (i) (println (date (first i)) { } (last i))) (sort results))這會掃描目錄和子目錄以查詢 5 年或更早修改過的檔案。(注意,該檔案以將在檔案載入時立即計算的表示式結尾。)首先,切換跟蹤
(trace true)然後載入並開始除錯
(load {old-file-scanner.lsp})或者,代替這兩行,鍵入以下內容
(debug (load {old-file-scanner.lsp}))無論哪種方式,您都將看到walk-tree 函式中的第一個表示式突出顯示,等待計算。
現在您可以按 s、n 或 c 鍵(Step、Next 和 Continue)繼續執行您的函式:Step 計算每個表示式,並在呼叫其他函式時進入其他函式;Next 計算所有內容,直到到達同一級別的下一個表示式;Continue 執行而不再次停止。
如果您很聰明,可以在要開始除錯的地方之前放置一個(trace true) 表示式。如果可能,newLISP 將在該表示式之前停止,並向您顯示它即將計算的函式。在這種情況下,您可以使用簡單的 (load...) 表示式開始執行指令碼 - 如果要跳過指令碼的初步部分,請不要使用debug。我認為 newLISP 通常更喜歡在函式或函式呼叫的開頭進入偵錯程式 - 您可能無法中途進入函式。但是您可能能夠組織事物,以便您可以做類似的事情。
以下是如何在迴圈中途進入偵錯程式。這是一小部分程式碼
(set 'i 0)
(define (f1)
(inc i))
(define (f2)
(dotimes (x 100)
(f1)
(if (= i 50) (trace true))))
(f2)
從檔案中載入它
> (load {simpleloop.lsp})
-----
(define (f1)
(inc i))
[-> 5 ] s|tep n|ext c|ont q|uit > i
50
[-> 5 ] s|tep n|ext c|ont q|uit >
注意f1 函式是如何出現的 - 您沒有機會看到f2 中的任何內容。在偵錯程式提示符下,您可以鍵入任何 newLISP 表示式,並計算任何函式。這裡我鍵入了i 來檢視該符號的當前值。newLISP 很樂意讓您更改一些符號的值,例如,您可以更改迴圈變數的值。但不要重新定義任何函式...... 如果您試圖從 newLISP 的腳下拉走地毯,您可能會成功地讓它摔倒!
偵錯程式顯示的原始碼不包含註釋,因此如果您想在檢視程式碼時給自己留下有用的備註或靈感,請使用文字字串而不是註釋
(define (f1)
[text]This will appear in the debugger.[/text]
; But this won't.
(inc i))大多數網路任務都可以使用 newLISP 的網路函式完成
- base64-dec 將字串從 BASE64 格式解碼
- base64-enc 將字串編碼為 BASE64 格式
- delete-url 刪除 URL
- get-url 從網路讀取檔案或頁面
- net-accept 接受新的傳入連線
- net-close 關閉套接字連線
- net-connect 連線到遠端主機
- net-error 返回最後一個錯誤
- net-eval 在多個遠端 newLISP 伺服器上計算表示式
- net-interface 定義預設網路介面
- net-listen 監聽對本地套接字的連線
- net-local 連線的本地 IP 和埠號
- net-lookup IP 號碼的名稱
- net-peek 準備讀取的字元數
- net-peer net-connect 的遠端 IP 和埠
- net-ping 向一個或多個地址傳送 ping 包(ICMP 回顯請求)
- net-receive 在套接字連線上讀取資料
- net-receive-from 在開啟的連線上讀取 UDP 資料報
- net-receive-udp 在開啟的連線上讀取 UDP 資料報並關閉連線
- net-select 檢查套接字或套接字列表的狀態
- net-send 在套接字連線上傳送資料
- net-send-to 在開啟的連線上傳送 UDP 資料報
- net-send-udp 傳送 UDP 資料報並關閉連線
- net-service 將服務名稱轉換為埠號
- net-sessions 返回當前所有開啟連線的列表
- post-url 將資訊釋出到 URL 地址
- put-url 將頁面上傳到 URL 地址。
- xml-error 返回最後一個 XML 解析錯誤
- xml-parse 解析 XML 文件
- xml-type-tags 顯示或修改 XML 型別標籤
使用這些網路函式,您可以構建各種具有網路功能的應用程式。使用net-eval 等函式,您可以將 newLISP 作為守護程式啟動在遠端計算機上,然後在本地計算機上使用它透過網路傳送 newLISP 程式碼進行計算。
以下是一個使用get-url 的非常簡單的例子。給定網頁的 URL,獲取原始碼,然後使用replace 及其列表構建功能生成該頁面上所有 JPEG 影像的列表
(set 'the-source (get-url "http://www.apple.com"))
(replace {src="(http\S*?jpg)"} the-source (push $1 images-list -1) 0)
(println images-list)("http://images.apple.com/home/2006/images/ipodhifititle20060228.jpg"
"http://images.apple.com/home/2006/images/ipodhifitag20060228.jpg"
"http://images.apple.com/home/2006/images/macminiwings20060228.jpg"
"http://images.apple.com/home/2006/images/macminicallouts20060228.jpg"
"http://images.apple.com/home/2006/images/ipodhifititle20060228.jpg"
"http://images.apple.com/home/2006/images/ipodhifitag20060228.jpg")
最簡單的搜尋表單可能類似於以下內容。
(load "cgi.lsp")
(println (string "Content-type: text/html\r\n\r\n"
[text]<!doctype html>
<html>
<head>
<title>Title</title>
</head>
<body>
[/text]))
(set 'search-string (CGI:get "userinput"))
(println (format [text]
<form name="form" class="dialog" method="GET">
<fieldset>
<input type="text" value="search" name="userinput" >
<input type="submit" style="display:none"/>
</fieldset>
</form>[/text]))
(unless (nil? search-string)
(println " I couldn't be bothered to search for \"" search-string "\""))
(println [text]
</body>
</html>
[/text])以下程式碼實現了一個簡單的 IRC(網際網路中繼聊天)客戶端,它展示瞭如何使用基本網路函式。該指令碼使用給定的使用者名稱登入到伺服器,並加入 #newlisp 頻道。然後指令碼分為兩個執行緒:第一個執行緒在迴圈中持續顯示任何頻道活動,而第二個執行緒等待控制檯的輸入。兩個執行緒之間唯一的通訊是透過共享的connected 標誌。
(set 'server (net-connect "irc.freenode.net" 6667))
(net-send server "USER newlispnewb 0 * :XXXXXXX\r\n")
(net-send server "NICK newlispnewb \r\n")
(net-send server "JOIN #newlisp\r\n")
(until (find "366" buffer)
(net-receive server buffer 8192 "\n")
(print buffer))
(set 'connected (share))
(share connected true)
(fork
(while (share connected)
(cond
((net-select server "read" 1000) ; read the latest
(net-receive server buffer 8192 "\n")
; ANSI colouring: output in yellow then switch back
(print "\n\027[0;33m" buffer "\027[0;0m"))
((regex {^PING :(.*)\r\n} buffer) ; play ping-pong
(net-send server (append "PONG :" (string $1 ) "\r\n"))
(sleep 5000))
((net-error) ; error
(println "\n\027[0;34m" "UH-OH: " (net-error) "\027[0;0m")
(share connected nil)))
(sleep 1000)))
(while (share connected)
(sleep 1000)
(set 'message (read-line))
(cond
((starts-with message "/") ; a command?
(net-send server (append (rest message) "\r\n"))
(if
(net-select server "read" 1000)
(begin
(net-receive server buffer 8192 "\n")
(print "\n\027[0;35m" buffer "\027[0;0m"))))
((starts-with message "quit") ; quit
(share connected nil))
(true ; send input as message
(net-send server (append "PRIVMSG #newlisp :" message "\r\n")))))
(println "finished; closing server")
(close server)
(exit)本節包含一些 newLISP 執行的簡單示例。您可以在網路上以及標準 newLISP 發行版中找到大量優秀的 newLISP 程式碼。
您可能會發現您不喜歡某些 newLISP 函式的名稱。您可以使用constant 和global 將另一個符號分配給該函式
(constant (global 'set!) setf)您現在可以使用set! 代替setf。這樣做不會造成速度損失。
還可以定義您自己對內建函式的替代方案。例如,前面我們定義了一個上下文和一個預設函式,它們與println 執行相同的工作,但記錄了輸出字元的數量。要計算此程式碼而不是內建程式碼,請執行以下操作。
首先,定義函式
(define (Output:Output)
(if Output:counter
(inc Output:counter (length (string (args))))
(set 'Output:counter 0))
(map print (args))
(print "\n"))透過為其定義別名,使 newLISP 的原始版本println 可用
(constant (global 'newLISP-println) println)將println 符號分配給您的 Output 函式
(constant (global 'println) Output)現在您可以像往常一樣使用println
(for (i 1 10)
(println (inc i)))2 3 4 5 6 7 8 9 10 11
(map println '(1 2 3 4 5))1 2 3 4 5
它似乎與原始函式執行相同的工作。但現在您還可以利用您定義的替代println 的額外功能
Output:counter
;-> 36
; or
println:counter
;-> 36如果您仔細計算 - 計數器一直在計算提供給 Output 函式的引數的長度。當然,這些包括括號......
有時,使用現有軟體比自己編寫所有例程更容易,儘管從頭開始設計可能很有趣。例如,您可以使用現有的資料庫引擎(如 SQLite)來節省大量時間和精力,而不是構建自定義資料結構和資料庫訪問函式。以下是如何在 newLISP 中使用 SQLite 資料庫引擎。
假設您有一組要分析的資料。例如,我找到了一個儲存為簡單空格分隔表的元素週期表資訊的列表
(set 'elements
[text]1 1.0079 Hydrogen H -259 -253 0.09 0.14 1776 1 13.5984
2 4.0026 Helium He -272 -269 0 0 1895 18 24.5874
3 6.941 Lithium Li 180 1347 0.53 0 1817 1 5.3917
...
108 277 Hassium Hs 0 0 0 0 1984 8 0
109 268 Meitnerium Mt 0 0 0 0 1982 9 0[/text])(您可以在 GitHub 上的此檔案中找到該列表。)
此處的列為原子量、熔點、沸點、密度、地殼中的百分比、發現年份、族和電離能。(我用 0 表示不適用,事實證明這不是一個很好的選擇)。
要載入 newLISP 的 SQLite 模組,請使用以下程式碼行
(load "/usr/share/newlisp/modules/sqlite3.lsp")這將載入包含 SQLite 介面的 newLISP 原始檔。它還會建立一個名為 sql3 的新上下文,其中包含用於處理 SQLite 資料庫的函式和符號。
接下來,我們要建立一個新的資料庫或開啟一個現有的資料庫
(if (sql3:open "periodic_table")
(println "database opened/created")
(println "problem: " (sql3:error)))這將建立一個名為 periodic_table 的新 SQLite 資料庫檔案並開啟它。如果檔案已存在,它將被開啟並準備使用。您無需再次引用此資料庫,因為 newLISP 的 SQLite 庫例程在 sql3 上下文中維護一個 當前資料庫。如果 open 函式失敗,則將列印儲存在 sql3:error 中的最新錯誤。
我剛剛建立了這個資料庫,所以下一步是建立一個表。首先,我將定義一個包含列名稱字串和每個列應使用的 SQLite 資料型別的符號。我不必這樣做,但它可能必須記錄在某個地方,所以,與其寫在紙上,不如用一個 newLISP 符號記錄下來
(set 'column-def "number INTEGER, atomic_weight FLOAT,
element TEXT, symbol TEXT, mp FLOAT, bp FLOAT, density
FLOAT, earth_crust FLOAT, discovered INTEGER, egroup
INTEGER, ionization FLOAT")現在我可以建立一個建立表的函式
(define (create-table)
(if (sql3:sql (string "create table t1 (" column-def ")"))
(println "created table ... OK")
(println "problem " (sql3:error))))這很容易,因為我剛剛建立了 column-def 符號,其格式完全正確!此函式使用 sql3:sql 函式建立一個名為 t1 的表。
我想要一個額外的函式:一個用列表元素中儲存的資料填充 SQLite 表的函式。它不是一個漂亮的函式,但它可以完成任務,並且只需要呼叫一次。
(define (init-table)
(dolist (e (parse elements "\n" 0))
(set 'line (parse e))
(if (sql3:sql
(format "insert into t1 values (%d,%f,'%s','%s',%f,%f,%f,%f,%d,%d,%f);"
(int (line 0))
(float (line 1))
(line 2)
(line 3)
(float (line 4))
(float (line 5))
(float (line 6))
(float (line 7))
(int (line 8))
(int (line 9))
(float (line 10))))
; success
(println "inserted element " e)
; failure
(println (sql3:error) ":" "problem inserting " e))))此函式呼叫 parse 兩次。第一個 parse 將資料分解為行。第二個 parse 將每行分解成一個欄位列表。然後我可以使用 format 將每個欄位的值用單引號括起來,記住根據列定義將字串更改為整數或浮點數(使用 int 和 float)。
現在是構建資料庫的時候了
(if (not (find "t1" (sql3:tables)))
(and
(create-table)
(init-table)))- 如果 t1 表不存在於表列表中,則會呼叫建立和填充它的函式。
查詢資料
[edit | edit source]資料庫現在可以使用了。但首先,我將編寫一個簡單的實用程式函式來簡化查詢
(define (query sql-text)
(set 'sqlarray (sql3:sql sql-text)) ; results of query
(if sqlarray
(map println sqlarray)
(println (sql3:error) " query problem ")))此函式提交提供的文字,並透過對結果列表對映 println 來列印結果,或顯示錯誤訊息。
以下是一些示例查詢。
查詢所有在 1900 年之前發現且構成地球地殼 2% 以上的元素,並按其發現日期對結果進行排序
(query
"select element,earth_crust,discovered
from t1
where discovered < 1900 and earth_crust > 2
order by discovered")("Iron" 5.05 0)
("Magnesium" 2.08 1755)
("Oxygen" 46.71 1774)
("Potassium" 2.58 1807)
("Sodium" 2.75 1807)
("Calcium" 3.65 1808)
("Silicon" 27.69 1824)
("Aluminium" 8.07 1825)
惰性氣體(位於第 18 族)是什麼時候被發現的?
(query
"select symbol, element, discovered
from t1
where egroup = 18")("He" "Helium" 1895)
("Ne" "Neon" 1898)
("Ar" "Argon" 1894)
("Kr" "Krypton" 1898)
("Xe" "Xenon" 1898)
("Rn" "Radon" 1900)
所有符號以 A 開頭的元素的原子量是多少?
(query
"select element,symbol,atomic_weight
from t1
where symbol like 'A%'
order by element")("Actinium" "Ac" 227)
("Aluminium" "Al" 26.9815)
("Americium" "Am" 243)
("Argon" "Ar" 39.948)
("Arsenic" "As" 74.9216)
("Astatine" "At" 210)
("Gold" "Au" 196.9665)
("Silver" "Ag" 107.8682)
這很簡單,親愛的華生!也許外面的科學家可以提供一些更具科學意義的查詢示例?
您也可以在網上找到用於 newLISP 的 MySQL 和 Postgres 模組。
簡單的倒計時器
[edit | edit source]接下來是一個簡單的倒計時器,它作為命令列實用程式執行。此示例展示了一些訪問指令碼中命令列引數的技術。
要開始倒計時,請輸入命令(newLISP 指令碼的名稱),後跟持續時間。持續時間可以是秒;分鐘和秒;小時、分鐘和秒;甚至可以是天、小時、分鐘和秒,用冒號隔開。它也可以是任何 newLISP 表示式。
> countdown 30 Started countdown of 00d 00h 00m 30s at 2006-09-05 15:44:17 Finish time: 2006-09-05 15:44:47 Elapsed: 00d 00h 00m 11s Remaining: 00d 00h 00m 19s
或
> countdown 1:30 Started countdown of 00d 00h 01m 30s at 2006-09-05 15:44:47 Finish time: 2006-09-05 15:46:17 Elapsed: 00d 00h 00m 02s Remaining: 00d 00h 01m 28s
或
> countdown 1:00:00 Started countdown of 00d 01h 00m 00s at 2006-09-05 15:45:15 Finish time: 2006-09-05 16:45:15 Elapsed: 00d 00h 00m 02s Remaining: 00d 00h 59m 58s
或
> countdown 5:04:00:00 Started countdown of 05d 04h 00m 00s at 2006-09-05 15:45:47 Finish time: 2006-09-10 19:45:47 Elapsed: 00d 00h 00m 05s Remaining: 05d 03h 59m 55s
或者,您可以提供一個 newLISP 表示式,而不是數值持續時間。這可能是一個簡單的計算,例如 π 分鐘中的秒數
> countdown "(mul 60 (mul 2 (acos 0)))" Started countdown of 00d 00h 03m 08s at 2006-09-05 15:52:49 Finish time: 2006-09-05 15:55:57 Elapsed: 00d 00h 00m 08s Remaining: 00d 00h 03m 00s
或者,更實用的是,一個倒計時到特定時間點的計時器,您可以透過從目標時間減去現在的時間來提供這個時間點
> countdown "(- (date-value 2006 12 25) (date-value))" Started countdown of 110d 08h 50m 50s at 2006-09-05 16:09:10 Finish time: 2006-12-25 00:00:00 Elapsed: 00d 00h 00m 07s Remaining: 110d 08h 50m 43s
- 在此示例中,我們使用 date-value 指定了聖誕節,它返回自 1970 年以來指定日期和時間的秒數。
表示式的計算由 eval-string 完成,在這裡,如果輸入文字以 "(" 開頭,則將其應用於輸入文字 - 通常表明周圍存在一個 newLISP 表示式!否則,假定輸入是冒號分隔的,並由 parse 分割並轉換為秒。
資訊從命令列提供的引數中獲取,並使用 main-args 提取,main-args 是程式執行時使用的引數列表
(main-args 2)這將獲取引數 2;引數 0 是 newLISP 程式的名稱,引數 1 是指令碼的名稱,所以引數 2 是 countdown 命令後的第一個字串。
將此檔案另存為 countdown,並使其可執行。
#!/usr/bin/newlisp
(if (not (main-args 2))
(begin
(println "usage: countdown duration [message]\n
specify duration in seconds or d:h:m:s")
(exit)))
(define (set-duration)
; convert input to seconds
(if (starts-with duration-input "(")
(set 'duration-input (string (eval-string duration-input))))
(set 'duration
(dolist (e (reverse (parse duration-input ":")))
(if (!= e)
(inc duration (mul (int e) ('(1 60 3600 86400) $idx)))))))
(define (seconds->dhms s)
; convert seconds to day hour min sec display
(letn
((secs (mod s 60))
(mins (mod (div s 60) 60))
(hours (mod (div s 3600) 24))
(days (mod (div s 86400) 86400)))
(format "%02dd %02dh %02dm %02ds" days hours mins secs)))
(define (clear-screen-normans-way)
; clear screen using codes - thanks to norman on newlisp forum :-)
(println "\027[H\027[2J"))
(define (notify announcement)
; MacOS X-only code. Change for other platforms.
(and
(= ostype "OSX")
; beep thrice
(exec (string {osascript -e 'tell application "Finder" to beep 3'}))
; speak announcment:
(if (!= announcement nil)
(exec (string {osascript -e 'say "} announcement {"'})))
; notify using Growl:
(exec (format
"/usr/local/bin/growlnotify %s -m \"Finished count down \""
(date (date-value) 0 "%Y-%m-%d %H:%M:%S")))))
(set 'duration-input (main-args 2) 'duration 0)
(set-duration)
(set 'start-time (date-value))
(set 'target-time (add (date-value) duration))
(set 'banner
(string "Started countdown of "
(seconds->dhms duration)
" at "
(date start-time 0 "%Y-%m-%d %H:%M:%S")
"\nFinish time: "
(date target-time 0 "%Y-%m-%d %H:%M:%S")))
(while (<= (date-value) target-time)
(clear-screen-normans-way)
(println
banner
"\n\n"
"Elapsed: "
(seconds->dhms (- (date-value) start-time ))
" Remaining: "
(seconds->dhms (abs (- (date-value) target-time))))
(sleep 1000))
(println
"Countdown completed at "
(date (date-value) 0
"%Y-%m-%d %H:%M:%S") "\n")
; do any notifications here
(notify (main-args 3))
(exit)在資料夾和層次結構中編輯文字檔案
[edit | edit source]以下是一個簡單的函式,它透過查詢封閉標籤並更改它們之間的文字,更新資料夾中每個檔案中的某些文字日期戳。例如,您可能有一對標籤,用於儲存上次編輯檔案的日期,例如 <last-edited> 和 </last-edited>。
(define (replace-string-in-files start-str end-str repl-str folder)
(set 'path (real-path folder))
(set 'file-list (directory folder {^[^.]}))
(dolist (f file-list)
(println "processing file " f)
(set 'the-file (string path "/" f))
(set 'page (read-file the-file))
(replace
(append start-str "(.*?)" end-str) ; pattern
page ; text
(append start-str repl-str end-str) ; replacement
0) ; regex option number
(write-file the-file page)
))可以這樣呼叫它
(replace-string-in-files
{<last-edited>} {</last-edited>}
(date (date-value) 0 "%Y-%m-%d %H:%M:%S")
"/Users/me/Desktop/temp/")replace-string-in-files 函式接受一個資料夾名稱。第一個任務是提取一個合適的檔案列表 - 我們使用 directory 和正則表示式{^[^.]}來排除所有以點開頭的檔案。然後,對於每個檔案,內容都被載入到一個符號中,replace 函式替換用指定字串括起來的文字,最後修改後的文字被儲存回磁碟。要呼叫該函式,請指定起始標籤和結束標籤,後跟文字和資料夾名稱。在此示例中,我們只使用由 date 和 date-value 提供的簡單 ISO 日期戳。
遞迴版本
[edit | edit source]假設我們現在想要讓它適用於資料夾中的資料夾中的資料夾,即遍歷檔案的層次結構,更改沿途的每個檔案。為此,重新構建 replace-string 函式,使其對傳遞的路徑名起作用。然後編寫一個遞迴函式來查詢資料夾中的資料夾,並生成所有必需的路徑名,將每個路徑名傳遞給 replace-string 函式。這種重新構建本身可能是一件好事:一方面,它使第一個函式更簡單。
(define (replace-string-in-file start-str end-str repl-str pn)
(println "processing file " pn)
(set 'page (read-file pn))
(replace
(append start-str "(.*?)" end-str) ; pattern
page ; text
(append start-str repl-str end-str) ; replacement
0) ; regex option number
(write-file pn page))接下來是該遞迴樹遍歷函式。它檢視資料夾/目錄中的每個正常條目,並測試它是否為目錄(使用 directory?)。如果是,replace-in-tree 函式會呼叫自身並從新位置開始。如果不是,則將檔案的路徑名傳遞給 replace-string-in-file 函式。
(define (replace-in-tree dir s e r)
(dolist (nde (directory dir {^[^.]}))
(if (directory? (append dir nde))
(replace-in-tree (append dir nde "/") s e r)
(replace-string-in-file (append dir nde) s e r))))要更改一棵樹中的所有檔案,請按如下方式呼叫該函式
(replace-in-tree
{/Users/me/Desktop/temp/}
{<last-edited>}
{</last-edited>}
(date (date-value) 0 "%Y-%m-%d %H:%M:%S"))在測試區域中首先測試這些內容非常重要;程式碼中一個小錯誤可能會對您的資料產生重大影響。注意 newLISPer!
與其他應用程式通訊(MacOS X 示例)
[edit | edit source]newLISP 為將應用程式程式中具有自身指令碼語言的功能粘合在一起提供了良好的環境。它速度快,體積小,不會妨礙指令碼解決方案的其他元件,而且非常適合在資訊透過工作流程時處理資訊。
以下是如何使用 newLISP 指令碼將非 newLISP 指令碼命令傳送到應用程式的示例。任務是在 Adobe Illustrator 中構建一個圓形,給定圓周上的三個點。
解決方案分為三個部分。首先,我們從應用程式獲取所選內容的座標。接下來,我們計算透過這些點的圓的半徑和中心點。最後,我們可以繪製圓形。第一部分和最後部分使用 AppleScript,它使用 osascript 命令執行,因為 Adobe Illustrator 不理解任何其他指令碼語言(在 Windows 上,您使用的是 Visual Basic,而不是 AppleScript)。
計算和一般介面使用 newLISP 完成。這通常比使用原生 AppleScript 更好,因為 newLISP 提供了許多在預設 AppleScript 系統中找不到的強大的字串和數學函式。例如,如果我想使用三角函式,我將不得不找到並安裝一個額外的元件 - AppleScript 根本不提供任何三角函式。
newLISP 指令碼可以放在選單欄上的指令碼選單中;將其放入庫 > 指令碼 > 應用程式 > Adobe Illustrator 資料夾,該資料夾接受文字檔案和 AppleScript)。然後,您可以在 Illustrator 中工作時選擇它。要使用它,只需選擇一個至少有三個點的路徑,然後執行該指令碼。前三個點定義新圓的位置。
#!/usr/bin/newlisp
; geometry routines from
; http://cgafaq.info/wiki/Circle_Through_Three_Points
; given three points, draw a circle through them
(set 'pointslist
(exec
(format [text]osascript -e 'tell application "Adobe Illustrator 10"
tell front document
set s to selection
repeat with p in s
set firstItem to p
set pathinfo to entire path of firstItem
set pointslist to ""
repeat with p1 in pathinfo
set a to anchor of p1
set pointslist to pointslist & " " & item 1 of a
set pointslist to pointslist & " " & item 2 of a
end repeat
end repeat
end tell
end tell
pointslist'
[/text])))
; cleanup
(set 'points
(filter float?
(map float (parse (first pointslist) { } 0))))
(set 'ax (points 0)
'ay (points 1)
'bx (points 2)
'by (points 3)
'cx (points 4)
'cy (points 5))
(set 'A (sub bx ax)
'B (sub by ay)
'C (sub cx ax)
'D (sub cy ay)
'E (add
(mul A (add ax bx))
(mul B (add ay by)))
'F (add
(mul C (add ax cx))
(mul D (add ay cy)))
'G (mul 2
(sub
(mul A (sub cy by))
(mul B (sub cx bx)))))
(if (= G 0) ; collinear, forget it
(exit))
(set 'centre-x (div (sub (mul D E) (mul B F)) G)
'centre-y (div (sub (mul A F) (mul C E)) G)
'r
(sqrt
(add
(pow (sub ax centre-x))
(pow (sub ay centre-y)))))
; we have coords of centre and the radius
; in centre-x, centre-y, and r
; Illustrator bounds are left-x, top-y, right-x, bottom-y
; ie centre-x - r, centre-y + r, centre-x + r, centre-y -r
(set 'bounds-string
(string "{" (sub centre-x r) ", "
(add centre-y r) ", "
(add centre-x r) ", "
(sub centre-y r) "}"))
(set 'draw-circle
(exec (format [text]osascript -e 'tell application "Adobe Illustrator 10"
tell front document
set e to make new ellipse at beginning with properties {bounds:%s}
end tell
end tell
'
[/text] bounds-string)))
(exit)此指令碼幾乎沒有錯誤處理!在第一個階段中應該新增更多錯誤處理(因為所選內容可能不適合後續處理)。
使用 newLISP,您可以輕鬆地為您的應用程式構建圖形介面。這份入門文件已經足夠長了,所以我不會詳細描述 newLISP-GS 的功能集。但這裡有一個簡短的示例,讓您體驗它的工作原理。
newLISP-GS 的基本元件是 **容器**、**小部件**、**事件** 和 **標籤**。您的應用程式由容器組成,容器包含小部件和其他容器。透過為所有內容賦予一個標籤(一個符號),您可以輕鬆地控制它們。當應用程式的使用者點選、按下和滑動東西時,事件會發送回 newLISP-GS,您可以編寫程式碼來處理每個事件。
為了介紹基本概念,本章將展示如何輕鬆構建一個簡單的應用程式,一個顏色混合器。
您可以移動滑塊來改變視窗中央區域的顏色。顏色元件(紅色、綠色和藍色的數字介於 0 和 1 之間)在視窗底部的文字字串中顯示。
在 newLISP-GS 中,容器的內容根據您選擇的佈局管理器型別進行排列 - 目前您可以使用 **流動**、**網格** 或 **邊框** 佈局。
下圖顯示了應用程式介面的結構。主容器(在本例中是一個名為 'Mixer' 的框架)包含其他容器和小部件。視窗的頂部區域(包含滑塊)由一個名為 'SliderPanel' 的面板組成,該面板又包含三個面板,每個面板對應一個滑塊,分別稱為 'RedPanel'、'GreenPanel' 和 'BluePanel'。在下方,中間區域包含一個名為 'Swatch' 的畫布,用來顯示顏色,在底部區域有一個名為 'Value' 的文字標籤,以文字形式顯示 RGB 值。每個區域都使用不同的佈局管理器進行佈局。
只需要一個處理程式。它分配給滑塊,並在每次移動滑塊時觸發。
第一步是載入 newLISP-GS 模組。
#!/usr/bin/env newlisp
(load (append (env "NEWLISPDIR") "/guiserver.lsp"))它提供了所有必需的物件和函式,在一個名為 gs 的上下文中。
圖形系統使用單個函式初始化。
(gs:init)可以逐個新增介面的各個部分。首先,我定義主視窗,並選擇邊框佈局。邊框佈局允許您將每個元件放置到五個區域中的一個,分別標記為 "north"、"west"、"center"、"east" 和 "south"。
(gs:frame 'Mixer 200 200 400 300 "Mixer")
(gs:set-resizable 'Mixer nil)
(gs:set-border-layout 'Mixer)現在可以新增用於容納滑塊的頂部面板。我希望滑塊垂直堆疊,因此我將使用 3 行 1 列的網格佈局。
(gs:panel 'SliderPanel)
(gs:set-grid-layout 'SliderPanel 3 1)定義三個顏色面板中的每一個,以及它們相應的標籤和滑塊。滑塊被分配了 slider-handler 函式。我可以在完成介面定義後編寫它。
(gs:panel 'RedPanel)
(gs:panel 'GreenPanel)
(gs:panel 'BluePanel)
(gs:label 'Red "Red" "left" 50 10 )
(gs:label 'Green "Green" "left" 50 10 )
(gs:label 'Blue "Blue" "left" 50 10 )
(gs:slider 'RedSlider 'slider-handler "horizontal" 0 100 0)
(gs:slider 'GreenSlider 'slider-handler "horizontal" 0 100 0)
(gs:slider 'BlueSlider 'slider-handler "horizontal" 0 100 0)
(gs:label 'RedSliderStatus "0" "right" 50 10)
(gs:label 'GreenSliderStatus "0" "right" 50 10)
(gs:label 'BlueSliderStatus "0" "right" 50 10)gs:add-to 函式使用已分配給它的佈局將元件新增到容器中。如果沒有分配佈局,則使用流動佈局(一種簡單的順序佈局)。首先指定目標容器,然後給出要新增的元件。因此,標記為 'Red'、'RedSlider' 和 'RedSliderStatus' 的物件將逐個新增到 'RedPanel' 容器中。完成三個面板後,可以將它們新增到 SliderPanel 中。
(gs:add-to 'RedPanel 'Red 'RedSlider 'RedSliderStatus)
(gs:add-to 'GreenPanel 'Green 'GreenSlider 'GreenSliderStatus)
(gs:add-to 'BluePanel 'Blue 'BlueSlider 'BlueSliderStatus)
(gs:add-to 'SliderPanel 'RedPanel 'GreenPanel 'BluePanel)您可以在畫布上繪製各種圖形,儘管在這個應用程式中,我只打算使用畫布作為色板,一個單一顏色的區域。
(gs:canvas 'Swatch)
(gs:label 'Value "")
(gs:set-font 'Value "Sans Serif" 16)現在,三個主要元件 - 滑塊面板、顏色色板和值的標籤 - 可以新增到主框架中。因為我將邊框佈局分配給了框架,所以可以使用方向來放置每個元件。
(gs:add-to 'Mixer 'SliderPanel "north" 'Swatch "center" 'Value "south")我們沒有使用 east 和 west 區域。
預設情況下,框架和視窗在建立時是不可見的,所以現在是讓我們的主框架可見的好時機。
(gs:set-visible 'Mixer true)這完成了應用程式的結構。現在需要進行一些初始化,以便在應用程式啟動時顯示一些有意義的內容。
(set 'red 0 'green 0 'blue 0)
(gs:set-color 'Swatch (list red green blue))
(gs:set-text 'Value (string (list red green blue)))最後,我不能忘記為滑塊編寫處理程式程式碼。處理程式傳遞了生成事件的物件的 ID 以及滑塊的值。程式碼將值(小於 100 的整數)轉換為 0 到 1 之間的數字。然後可以使用 **set-color** 函式將畫布的顏色設定為顯示新的混合顏色。
(define (slider-handler id value)
(cond
((= id "MAIN:RedSlider")
(set 'red (div value 100))
(gs:set-text 'RedSliderStatus (string red)))
((= id "MAIN:GreenSlider")
(set 'green (div value 100))
(gs:set-text 'GreenSliderStatus (string green)))
((= id "MAIN:BlueSlider")
(set 'blue (div value 100))
(gs:set-text 'BlueSliderStatus (string blue)))
)
(gs:set-color 'Swatch (list red green blue))
(gs:set-text 'Value (string (list red green blue))))只有一行程式碼是必要的,我們就完成了。gs:listen 函式監聽事件並將它們分派給處理程式。它會持續執行,因此您不需要執行任何其他操作。
(gs:listen)這個小小的應用程式只觸及了 newLISP-GS 的皮毛,所以請檢視文件並嘗試一下吧!







