所謂數據類型,是數據集合以及定義在這個數據集合上的一組運算。從大部分計算機的 CPU 的角度來看,存儲器中的數據,只是以字節爲單位的二值信號,而且 CPU 擁有一組能夠操控它們的指令。所以,字節是一種數據類型,並且對於大部分計算機而言是最爲基本的數據類型。用匯編語言寫程序,就是以字節類型爲基礎,構造更爲複雜的數據類型,而後基於它們用程序模擬真實世界裏的一些不太複雜的現象——沒人願意用匯編語言編寫大規模而且侷限於特定機器的程序。不妨將 Guile 解釋器視爲 CPU,而 Guile 語言內建的數據類型——原子與序對是對存儲器字節類型的抽象。在原子與序對的基礎上,能夠構造一些基本可是經常使用的複合數據類型,譬如列表、關聯列表、樹以及 Hash 表等。以這些數據類型爲原料,即可以對現實世界的一些複雜的現象進行程序模擬了。node
人要用計算機存儲器中的二值信號對真實世界的事物或信息進行模擬,必須對二值信號賦予約定的意義。Guile 認爲,布爾值(真與假)、數字、字符、字符串、數組以及符號等是最爲基本的信息,須要將它們的意義賦予二值信號,並創建一組可以處理這些信息的運算,這樣便創建了一組基本的數據類型,它們被統稱爲原子。git
這些基本的數據類型,一個一個的講出來,實際上是很無趣的。已經有人很耐心的將這些無趣的東西都寫了出來。若是想具體的瞭解一下它們,可閱讀《Teach Yourself Scheme in Fixnum Days》第 2 章。若是英文很差,或不想花太多時間去看英文,能夠閱讀這本書的中文譯本。github
要想真正弄清楚原子是什麼,只需看懂下面這個謂詞的定義:編程
(define (atom? x) (and (not (pair? x)) (not (null? x))))
也就是說,只要 x
不是序對,也不爲 '()
,那麼 x
就是原子類型。Guile 爲 '()
定義了一個等價的值 #nil
,可是從本章開始,我再也不使用 #nil
。由於 Scheme 標準中沒有 #nil
,只有 '()
。segmentfault
Guile 對原子類型的變量或值進行求值的過程,至關於直接從存儲器中取出數據,而後按照人賦予這些數據的具體意義來解讀它們。Guile 解釋器可以根據值的形式推斷值的類型,進而能推斷出綁定到這些值的變量的類型。這樣,在使用 Guile 語言編寫程序時,不須要像 C 語言那樣在代碼中顯式的聲明變量的類型。數組
更有趣的是,像函數的參數也不須要指定類型,Guile 會認爲參數的類型是未知的。例如 atom?
的參數 x
的類型就是未知的。只有在使用函數時,向它傳遞了具體的參數值,例如:數據結構
(atom? #t)
這時 Guile 能夠推斷出所傳入的參數值 #t
的類型是布爾類型,這樣即可以肯定參數 x
的類型爲布爾類型。因爲這個過程發生於程序的運行時,所以像 Guile 這樣的語言,例如 Python, Lua, Ruby, JavaScript 等,它們被稱爲動態類型語言。與 C++ 或 Java 的泛型相比,動態類型語言天生就是泛型的。編程語言
事實上,Guile 中並未定義 atom?
這個函數,這意味着怎麼理解原子,這取決於我的偏好。思考一下,函數應用的表達式是原子麼?函數
(atom? (atom? '()))
序對,就是包含兩個元素的數據類型。cons
運算符能夠將兩個元素粘連爲序對,例如:
> (define foo (cons 1 2)) > (car foo) 1 > (cdr foo) 2 > (pair? foo) #t
car
與 cdr
操做符分別用於取出序對的第一個元素與第二個元素。pair?
是判斷一個表達式是否爲序對的謂詞。
若是用 C 語言來表示這種序對,數據結構只能設計爲:
typedef struct { void *head; void *tail; } Pair;
固然,這個 Pair
類型用起來不像 Guile 的序對那樣隨意。要實現上述 Guile 的效果,還須要定義 cons
,car
以及 cdr
等運算符:
Pair * cons(void *x, void *y) { Pair *pair = malloc(sizeof(Pair)); pair->head = x; pair->tail = y; return pair; } #define car(pair, type) ((type *)((pair)->head)) #define cdr(pair, type) ((type *)((pair)->tail))
爲了向序對中存入整型數據,須要專門定義一個生成整型數據對象的運算符:
int * mk_int_obj(int data) { int *obj = malloc(sizeof(int)); *obj = data; return obj; }
通過上述精心的準備,終於能夠寫出與 Guile 序對等效的 C 代碼了,以下:
Pair *foo = cons(mk_int_obj(1), mk_int_obj(2)); printf("%d\n", *car(foo, int)); printf("%d\n", *cdr(foo, int));
正如上述 C 代碼所模擬的序對那樣,Guile 的序對所包含的兩個元素本質上是兩個指針,它們能夠指向任意類型的數據對象。不過,咱們並不關心序對所包含的元素是否是指針,只關心 car
與 cdr
從序對中所取出的東西,並認爲 car
與 cdr
所取出的東西就是序對所包含的東西。
Guile 序對可以存儲任意類型的數據,包括序對類型的數據:
> (define bar (cons 1 (cons 2 3))) > (car bar) 1 > (car (cdr bar)) 2 > (cdr (cdr bar)) 3
要寫出與上述 Guile 代碼等效的 C 代碼也不難:
Pair *bar = cons(mk_int_obj(1), cons(mk_int_obj(2), mk_int_obj(3))); printf("%d\n", *car(bar, int)); printf("%d\n", *car(cdr(bar, Pair), int)); printf("%d\n", *cdr(cdr(bar, Pair), int));
雖然 Guile 序對所包含的元素是指針類型,可是 car
與 cdr
取出來的東西是指針指向的值。等效的 C 代碼作不到這一點,因此在上述代碼中,須要使用 *
對序對中的指針進行解引用來獲取指針所指的數據。
基於序對能夠構造出列表這種結構:
> (define foo (cons 1 (cons 2 (cons 3 (cons 4 (cons 5 '())))))) > foo (1 2 3 4 5) > (pair? foo) #t > (list? foo) #t
列表是一組級聯的序對,而且最後一個序對的第 2 個元素是 '()
。
'()
表示一個空的列表:
> (list? '()) #t > (pair? '()) #f
由於用一連串的 cons
來構造列表太麻煩了,因此 Guile 提供了一個 list
運算符來簡化列表的構造過程,其用法以下:
> (define foo (list 1 2 3 4 5)) > foo (1 2 3 4 5)
也能夠直接以值的形式構造列表:
> (define foo '(1 2 3 4 5)) > foo (1 2 3 4 5)
'(1 2 3 4 5)
是 (quote 1 2 3 4 5)
的『簡寫』,表示引用一個已經存在的列表。雖然在上面的示例中,列表的引用形式與 list
運算符獲得的結果相同,可是兩者有着本質的區別。以引用的形式構造一個列表,前提是列表的全部元素是已知的,即全部的元素不須要再求值。若是被引用的列表中含有待求值對象,那麼這個對象會被視爲符號。看下面的例子:
> (define x 5) > (define foo '(1 2 3 4 x)) > foo (1 2 3 4 x)
list
運算符本質上是一個函數,它會迫使 Guile 對它的參數進行求值。例如:
> (define x 5) > (define foo (list 1 2 3 4 x)) > foo (1 2 3 4 5)
不過,在列表的引用形式中,有迫使 Guile 對待求值對象進行求值的辦法,請參考本書第二章『輸入/輸出』中的『反引號與逗號』一節。
若是一個列表 x 中某些元素自己也是列表,那麼 x 是樹。除此之外,就沒什麼好說的了。
若是列表 x 中的每一個元素都是一個序對,那麼 x 即是關聯列表。除此之外,就沒什麼好說的了。
Scheme 標準中沒有 Hash 表類型,可是 Guile 內建了這種類型,同時,Guile 也提供了複合Sheme 的準標準SRFI 第 69 條 的 Hash 表類型的實現,還提供了 R6RS 標準中的 Hash 表,三者的用法相差無幾。因爲使用 SRFI 或 R6RS 的 Hash 表,還須要額外導入相應的模塊,因此我選擇使用 Guile 的內建版本。
定義一個 Hash 表:
> (define foo (make-hash-table)) > foo #<hash-table 0/31>
make-hash-table
默認會分配 31 個 Hash 表單元。若是知道要向 Hash 表中存儲的元素的個數,能夠在調用 make-hash-table
函數時設定 Hash 表最少要分配相應數量的單元。例如:
> (define foo (make-hash-table 32)) > foo #<hash-table 0/61>
make-hash-table
分配的單元個數可能不會等於指定的數量,可是它能保證不會比指定的數量少。
hash-set!
能夠向 Hash 表中寫入鍵-值對:
> (hash-set! foo 'name "garfileo") "garfileo"
使用 hash-ref
從 Hash 表中以鍵取值:
> (hash-ref foo 'name) "garfileo" > (hash-ref foo 'not-there) #f
使用 hash-count
函數能夠獲取 Hash 表中知足指定謂詞(檢索條件)的鍵-值對的個數。若是隻想查詢 Hash 表中存入了多少個鍵-值對,可將謂詞設爲 (const #t)
。例如:
> (hash-count (const #t) foo) 1
const
函數是一個二階函數。也就是說,const
返回一個函數,這個函數能夠接受無數個參數,可是它的返回值是 const
所接受的的參數值。
hash-remove!
函數可從 Hash 表中移除一個鍵值對。被移除的鍵值是這個函數的返回值。例如:
> (hash-remove! foo 'name) (name . "garfileo") > (hash-ref foo 'name) #f
Guile 的 Hash 表還有許多細節知識,詳見《The Guile Reference Manual》的『6.7.14 Hash Tables』節。不過,對於本書的主題——編寫一個文式編程工具而言,上述這幾個函數就足夠用了。
上面所提到的數據類型要麼是 Guile 內建的基本類型與序對,要麼是基於序對的各類衍生類型。雖然它們已經足以用於模擬真實世界中的各類現象了,可是並不直觀。例如,能夠用列表能夠模擬人這種事物,只須要將人的屬性抽象爲可表示爲 Guile 原子的信息,而後將這些信息存入表中便可:
(define (創造一個角色 年齡 性別 民族 籍貫) (list 姓名 年齡 性別 民族 籍貫))
下面來模擬一個具體的角色:
> (define 黃蓉 (建立一個角色 16 "女" "漢" "桃花島")) > 黃蓉 (16 "女" "漢" "桃花島")
雖然咱們自覺得是的認爲已經模擬出來黃蓉這個角色了,但實際上這個黃蓉只不過是一個列表而已:
> (list? 黃蓉) #t
既然要模擬,那就應該模擬的更真實一些,像下面這樣:
> (角色? 黃蓉) #t
除此以外,還應當可以根據屬性的名稱來獲取列表中的信息,而不是基於 car
與 cdr
的一堆機械操做。例如:
> (角色的年齡 黃蓉) 16 > (角色的性別 黃蓉) 女 > (角色的民族 黃蓉) 漢 > (角色的籍貫 黃蓉) "桃花島"
也就是說,咱們但願能自行定義一種本身喜歡的數據類型,這種數據類型的每一個元素是一個訪問函數。Scheme 的 SRFI-9 提案爲咱們提供的記錄(Record)類型可幫助咱們達到這一目的:
> (use-modules (srfi srfi-9)) ;;; 加載 srfi-9 模塊 > (define-record-type <角色> (建立一個角色 年齡 性別 民族 籍貫) 角色? (年齡 角色的年齡) (性別 角色的性別) (民族 角色的民族) (籍貫 角色的籍貫)) > (define 黃蓉 (建立一個角色 16 '女 '漢 "桃花島")) > 黃蓉 #<<角色> 年齡: 16 性別: 女 民族: 漢 籍貫: "桃花島"> > (record? 黃蓉) #t > (角色? 黃蓉) #t > (角色的年齡 黃蓉) 16 > (角色的性別 黃蓉) 女 > (角色的民族 黃蓉) 漢 > (角色的籍貫 黃蓉) "桃花島"
<角色>
即是使用 define-record-type
定義的一種記錄類型,它包含年齡
,性別
,民族
以及籍貫
等屬性或域。define-record-type
不是函數,而是一個宏。我打算在下一章講 Guile 的宏。
記錄類型能存儲任意類型的數據,包括函數。例如:
> (define-record-type <角色> (建立一個角色 年齡 性別 民族 籍貫 武功) 角色? (年齡 角色的年齡) (性別 角色的性別) (民族 角色的民族) (籍貫 角色的籍貫) (武功 角色所習的武功)) > (define 黃蓉 (建立一個角色 16 '女 '漢 "桃花島" (lambda () (display "落英神劍掌") (newline)))) > (define 黃蓉施展武功 (角色所習的武功 黃蓉)) > (黃蓉施展武功) 落英神劍掌
就此止步。再往下走,可能就會跌入面向對象編程的坑。由於所謂的面向對象,講究的是封裝、繼承與多態。記錄顯然是支持用戶自定義的一種數據封裝形式。若是想讓 Guile 面向對象,能夠考慮使用 Guile 的 Goops 擴展。
充滿層層括號的 Guile 代碼像是『指令流』,Guile 解釋器可以根據括號所表達的層次關係有序的解釋這些指令。從這個角度來看,Guile 語言像是人類概念上的計算機中的彙編語言。想一想看,前綴表達式不正是彙編語言的一大特色麼?將一個變量綁定到一個值或表達式上,這個變量像不像寄存器?從這一點來看,Guile 的數據類型本質上是人類概念上的計算機存儲空間及基本操做指令集。
也許不會再有比 Scheme 更高層次的編程語言了。雖然人類的大腦依然在源源不斷的構造着抽象之抽象的概念,可是 Scheme 自身能夠隨之進化——經過宏來定義新的語法。會存在與它同一級別可是不須要那麼多括號的編程語言,並且它們已經存在了。可是,要消除將 Scheme 語言中的括號,須要在編寫解釋器方面付出很大的代價,結果會致使解釋器的內部機制也變得很是複雜。複雜的解釋器爲了可以正確的運行,必然要對變量或函數的命名以及代碼的編排格式提出很是嚴格的要求。