newLISP/Contexts 簡介
我們都喜歡將自己的物品整理到不同的區域或隔間。廚師將魚、肉和甜點區域分開,電子工程師將電源供應器與射頻和音訊級分開,newLISP 程式設計師使用上下文來組織他們的程式碼。
newLISP 上下文為符號提供一個命名的容器。不同上下文中的符號可以具有相同的名稱而不會衝突。因此,例如,在一個上下文中,我可以將名為meaning-of-life的符號定義為值為 42,但在另一個上下文中,同名符號的值可以是dna-propagation,在另一個上下文中可以是worship-of-deity。
除非你明確選擇建立和/或切換上下文,否則所有 newLISP 工作都在預設上下文 MAIN 中進行。到目前為止,在本檔案中,當建立新符號時,它們都已新增到 MAIN 上下文中。
上下文非常靈活——你可以根據手頭的任務,將它們用於字典、軟體物件或超級函式。
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
除了使用 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")
上下文可以包含函式和符號。要在除 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 語法呼叫它們。
如果上下文中的符號與上下文名稱相同,則稱為預設函式(儘管實際上它可以是函式,也可以是包含列表或字串的符號)。例如,這裡有一個名為 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 足夠智慧,能夠從你的程式碼中判斷是使用上下文的預設函式還是上下文字身。
當用作符號時,預設函式與它們更普通的同類函式之間存在重要區別。當使用預設函式將資料傳遞給函式時,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)
作為預設函式(作為引用)傳遞的列表被修改,而普通列表引數按預期複製,沒有被修改。
在下面的示例中,我們建立一個名為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 使建立字典變得容易。為了說明,我將求助於偉大的偵探,夏洛克·福爾摩斯。首先,我從古騰堡計劃下載了阿瑟·柯南·道爾的四簽名,然後將其載入為一個詞語列表。
(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 天的修改副本。
換句話說,要更改物件,請使用熟悉的新LISP 方法,使用函式返回的值
(set 'christmas-day (:adjust-days christmas-day 3))
(:show christmas-day)
;-> "Sun Dec 28 00:00:00 2008"
christmas-day 現在包含修改後的日期。
您可以在 newLISP 論壇中搜索以下內容,找到此想法的更完整的闡述timeutilities。另外,請務必閱讀參考手冊中有關 FOOP 的部分,其中有一個關於巢狀物件的很好的例子,即包含其他物件的物體。