前言中,我說要寫一個文式編程工具。它的名字叫 zero,是個命令行程序,運行時須要由使用者提供一些參數與文式編程元文檔路徑。zero 讀取元文檔,而後根據使用者設定的參數對元文檔進行處理,最終給出相應的輸出。本章內容主要講述如何用 Guile 寫一個命令行程序的界面——對於使用者而言,zero 程序可見的部分。編程
C 程序能夠經過 main
函數的參數得到命令行文本的分割結果,即一個字符串數組:segmentfault
/* foo.c */ #include<stdio.h> int main(int argc, char **argv) { for (int i = 0; i < argc; i++) { printf("%s\n", argv[i]); } return 0; }
設編譯所得程序爲 foo,執行它,數組
$ ./foo bar foobar
可得數據結構
./foo bar foobar
用 Guile 語言也能寫出相似的程序:編程語言
;; foo.scm (define (display-args args) (cond ((null? args) #nil) (else (begin (display (car args)) (newline) (display-args (cdr args)))))) (display-args (command-line))
須要用 Guile 解釋器來運行這個程序:編輯器
$ guile foo.scm bar foobar
程序運行結果爲:函數式編程
foo.scm bar foobar
若是在上述 Guile 代碼的首部增長函數
#!/usr/bin/guile -s !#
而後將 foo.scm
文件更名爲 foo
,並使之具有可執行權限:工具
$ chomd +x ./foo
這樣,這個 Guile 腳本程序在行爲上與上述 C 程序徹底同樣。
如今,假設 C 語言未提供 for
與 while
循環(迭代)結構,那麼使用函數對自身的調用來模擬迭代過程,能夠寫出與上述 Guile 代碼類似的形式:
#include<stdio.h> void display_args(char **args, int i, int n) { if (i >= n) { return; } else { printf("%s\n", args[i]); display_args(args, i + 1, n); } } int main(int argc, char **argv) { display_args(argv, 0, argc); return 0; }
若是將 argv
轉換爲一個以 NULL
爲結尾的字符串數組,即可以讓 C 語言版的 display_args
在形式上很像 Guile 版的 display-args
函數:
#include <stdio.h> #include <stdlib.h> #include <string.h> void display_args(char **args) { if (*args == NULL) { return; } else { printf("%s\n", *args); display_args(args + 1); } } int main(int argc, char **argv) { char **new_argv = malloc((argc + 1) * sizeof(char *)); memcpy(new_argv, argv, argc * sizeof(char *)); new_argv[argc] = NULL; display_args(new_argv); free(new_argv); return 0; }
上文中的 Guile 代碼,通過 C 代碼的詮釋,可觀其大略——用函數的遞歸形式模擬了 C 的循環結構。至於代碼中的一些細節,後文逐一給出解釋。
在 C 程序中,命令行文本是保存在 main
函數的 argv
參數中的,這個參數是個字符串數組。在 Guile 腳本中,命令行文本是經過函數 command-line
函數在運行時獲取的,即
(command-line)
該函數的返回結果是一個字符串列表。這行代碼即是 command-line
函數的調用代碼。command-line
函數不須要參數,對它的調用,可用下面這行 C 代碼來詮釋:
command-line(); /* 僞代碼,由於 C 語言不支持這種函數命名方式 */
那麼,command-line
函數的返回結果——字符串列表是怎樣的一種數據結構?答案是,不清楚。咱們只知道,它是列表類型的數據。
在 Guile 中,對於全部的列表類型的數據,使用 car
函數能夠從列表中取出首個元素;使用 cdr
函數能夠從列表中取出除首個元素以外的全部元素,所取出的元素構成一個新的列表,而且這些元素在新列表中的次序與原列表相同。
下面這份 Guile 腳本:
;; demo-01.scm #!/usr/bin/guile -s !# (display (car (command-line))) (newline) (display (cdr (command-line))) (newline) (display (car (cdr (command-line)))) (newline)
執行它:
$ ./demo-01.scm foo bar foobar
獲得的結果依序以下:
./demo-01.scm (foo bar foobar) foo
若是看不懂上述 Guile 代碼,能夠看下面的等效的僞 C 代碼:
printf("%s", car(command-line())); printf("\n"); printf("%s", cdr(command-line())); printf("\n"); printf("%s", car(cdr(command-line()))); printf("\n");
經過這些等效的僞 C 代碼,能夠理解 Guile 函數的調用方式,以及 display
與 newline
函數的效用。
下面這段 Guile 代碼
(cond ((null? args) #nil) (else (begin (display (car args)) (newline) (display-args (cdr args))))))
與之等效的 C 代碼以下:
if (*args == NULL) { return; } else { printf("%s\n", *args); display_args(args + 1); }
cond
是 condition
的縮寫,其用法以下:
(cond (<謂詞 1> <表達式 1>) (<謂詞 2> <表達式 2>) ... ... (<謂詞 n> <表達式 n>) (else <表達式 n + 1>)
等效的 C 條件表達式結構以下:
if (<謂詞 1>) { <表達式 1> } else if (<謂詞 1>) { } else if (...) { ... } else if (<謂詞 n>) { <表達式 n> } else { <表達式 n + 1> }
所謂的謂詞是指能夠返回『真』或『假』的計算過程。(null? args)
即是 Guile 的一個謂詞——若是 args
列表非空,它返回『假』,不然返回『真』。
下面這個條件表達式
(cond ((null? args) #nil) (else (car args)))
它表達的意思是,若是列表 args
爲空,那麼這個條件表達式的計算結果爲 #nil
——空的列表,不然計算結果爲 args
的首元素。
cond
表達式中,對各個條件分支中的謂詞是按順序求值的。在這個過程當中,若是某個謂詞的求知結果爲真,那麼該謂詞以後的表達式的求值結果即是 cond
表達式的求值結果。
有時,咱們須要無條件的依序執行一些計算過程,例如:
(display (car args)) (newline) (display-args (cdr args))
這在 C 語言裏是很平淡無奇的過程,可是 Guile 語言卻不能直接支持,由於它的任何語句都必須是一條完整的表達式,而不能使多個獨立的表達式的陳列。爲了可以依序執行一組表達式,能夠用 begin
語句:
(begin <表達式 1> <表達式 2> ... <表達式 n>)
<表達式 n>
的求值結果是 begin
語句的求值結果。
下面這條 begin
語句:
(begin (display (car args)) (newline) (display-args (cdr args)))
它的含義應該很明顯了。
下面這些代碼,除了 args
以外,其餘元素都是肯定的,這意味着 args
是個未知數或變量。
(cond ((null? args) #nil) (else (begin (display (car args)) (newline) (display-args (cdr args)))))
若是一個未知的事物與一些肯定的事物之間存在着肯定的聯繫,這些聯繫能夠將未知的事物轉換爲另外一個未知的事物,這個過程就是所謂的『映射』或『函數』。在 Guile 中,定義一個函數須要遵照下面這樣的格式:
(define (<函數> <未知的事物>) <未知的事物與一些肯定的事物之間所存在的肯定的聯繫>)
前文中,咱們已經定義了一個函數 display-args
:
(define (display-args args) (cond ((null? args) #nil) (else (begin (display (car args)) (newline) (display-args (cdr args))))))
函數 y = f(x)
,若是咱們已知 x = 2
,那麼根據 f(2)
就能夠獲得相應的 y
值。在 C 語言中,這叫函數調用。在 Guile 中,這叫函數應用。不必在這些文字遊戲上浪費時間,本質上就是將肯定的自變量 x = 2
代入 y = f(x)
這個函數或映射,從而獲得肯定的因變量。在編程中,咱們一般將自變量稱爲參數,將因變量稱爲返回值。這其實都是玩弄文字的把戲……
有些函數是沒有求值結果的,例如 display
函數,它的任務是將用戶傳入的參數顯示於終端(顯示器屏幕或文件)。這相似於,你給朋友一些錢,讓他去書店爲你買本書,這本書是『你朋友從你哪裏接過錢,而後去書店買書』這個過程的『求值結果』,可是你給一個畫家一些錢,讓他在人民公園的牆上爲你塗鴉,結果你獲得了什麼?多是他人的駐足圍觀,也多是公園管理人員給你開罰單……
對於 display-args
函數而言,若是它的參數是列表類型,那麼它老是有求值結果的,即 #nil
,可是它除了能夠獲得這個結果,在其執行過程當中還不斷的在終端中塗鴉……也就是說 display-args
是個有反作用的函數。它的反作用是 display
函數帶來的。
數學家們不喜歡有反作用的函數,由於他們是數學家。他們喜歡的那種編程語言,叫作『純函數式編程語言』。像 C 語言這種處處都充滿着反作用的編程語言,他們是很是很是的拒絕的,他們討厭 x = x - 1
這樣的代碼,由於他們認爲 0 = -1
這樣的推導結果是荒謬的。想必他們對現實世界也很是的不習慣吧,他們從藥瓶裏倒出一粒藥吃下去,而後他們獲得了兩個藥瓶 :D
若是像下面這樣應用 display-args
函數:
(display-args (cons 1 (cons 2 (cons 3 #nil))))
能夠獲得什麼結果?能夠獲得 #nil
,同時終端中會顯示:
1 2 3
(cons 1 (cons 2 (cons 3 #nil)))
是什麼?它是一連串 cons
運算符的應用。若是將 cons
視爲一個函數,那麼等效的 C 代碼以下:
cons(1, cons(2, cons(3, #nil)));
結果是一個列表,其元素依次爲 1, 2, 3。將這個列表傳入 display-args
,便會將其元素逐一顯示於終端。
cons
運算符的第一個參數能夠是任意類型的數據,而它的第二個參數必須是列表類型。它的工做是,將第一個參數所表示的數據添加到第二個參數所表示的列表的首部,而後返回這個新的列表。上文中說過,#nil
表示空的列表。(cons 3, #nil)
可將 3
添加到一個空的列表的首部,返回一個新的列表——只含有元素 3 的列表。以此類推,(cons 2 (cons 3 #nil))
的結果是依序包含 2
與 3
的列表,(cons 1 (cons 2 (cons 3 #nil)))
的結果是依序包含 1
, 2
, 3
的列表。
zero 程序的用法以下:
$ zero [選項] 文件
zero 程序能夠支持如下選項:
-m, --mode=moon 或 sun 指定 zero 的工做模式是 moon 仍是 sun -e, --entrance=代碼塊 將指定的代碼塊設爲代碼的抽取入口 -o, --output=文件 將提取到的代碼輸出至指定的文件 -b, --backtrace 開啓代碼反向定位功能 -p, --prism=棱鏡程序 爲 sun 模式指定一個棱鏡程序
因爲這些選項在形式上大同小異,所以下面僅以 -m
與 --mode
選項爲例,講述如何爲 zero 程序構造一個簡單的命令行界面。-m
選項爲短選項,--mode
爲長選項,它們是同一個選項的兩種表現形式。也就是說,下面這兩行代碼是等價的:
$ zero -m moon foo.zero $ zero --mode=moon foo.zero
要構建的這個命令行界面程序的主要任務是,從命令行文本中獲取 -m
或 --mode
的參數值以及文件名。對於上面示例中的 zero
命令行文本而言,要獲取的是 moon
與 foo.zero
。
(define (get-filename args) (cond ((null? (cdr args)) (car args)) (else (get-filename (cdr args)))))
這個函數的求值結果爲字符串類型,是 zero 程序要讀取的文件的名字(或路徑)。
因爲 -m
或 -mode
選項只有兩個值 moon
與 sun
可選,能夠將它們映射爲整型數:
參數 moon
對應 1;
參數 sun
對應 2;
若通過解析,發現命令行文本中即未出現 -m
也未出現 --mode
,這種狀況對應 0;
若命令行文本中即出現了 -m
或 --mode
,可是參數值既非 moon
,亦非 sun
,這種狀況對應 -1.
根據上述映射,寫出如下 Guile 代碼:
#!/usr/bin/guile -s !# (define (filter-mode-opt args) (cond ((null? args) 0) (else (let ((fst (car args)) (snd (cadr args))) (cond ((string=? fst "-m") (cond ((string=? snd "moon") 1) ((string=? snd "sun") 2) (else -1))) ((string-prefix? "--mode=" fst) (let ((mode (cadr (string-split fst #\=)))) (cond ((string=? mode "moon") 1) ((string=? mode "sun") 2) (else -1)))) (else (filter-mode-opt (cdr args)))))))) (display (filter-mode-opt (command-line))) (newline)
上述代碼中,出現了上文未涉及的一些語法——let
,cadr
,string-prefix?
,string=?
,string-split
。這些語法的含義,暫時不予追究,先來看下面的等效 C 代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> int filter_mode_opt(char **args) { if (*args == NULL) { return 0; } else { if (strcmp(*args, "-m") == 0) { char *next_arg = *(args + 1); if (strcmp(next_arg, "moon") == 0) return 1; else if (strcmp(next_arg, "sun") == 0) return 2; else return -1; } else if (strncmp(*args, "--mode", 6) == 0) { int mode; char *new_arg = malloc((strlen(*args) + 1) * sizeof(char)); strcpy(new_arg, *args); strtok(new_arg, "="); char *mode_text = strtok(NULL, "="); if (strcmp(mode_text, "moon") == 0) mode = 1; else if (strcmp(mode_text, "sun") == 0) mode = 2; else mode = -1; free(new_arg); return mode; } else { filter_mode_opt(args + 1); } } } int main(int argc, char **argv) { char **new_argv = malloc((argc + 1) * sizeof(char *)); memcpy(new_argv, argv, argc * sizeof(char *)); new_argv[argc] = NULL; printf("%d\n", filter_mode_opt(new_argv)); free(new_argv); return 0; }
C 代碼看上去要羅嗦一點,主要是由於 C 語言在字符串處理方面的功能弱一些,不過在邏輯上與上面的 Guile 代碼等價。若是咱們動用 for
循環,C 的代碼反而會更清晰一些:
#include <stdio.h> #include <stdlib.h> #include <string.h> int filter_mode_opt(int argc, char **args) { int mode = 0; for (int i = 0; i < argc; i++) { if (strcmp(args[i], "-m") == 0) { if (strcmp(args[i + 1], "moon") == 0) return 1; else if (strcmp(args[i + 1], "sun") == 0) return 2; else return -1; } else if (strncmp(args[i], "--mode", 6) == 0) { char *new_arg = malloc((strlen(args[i]) + 1) * sizeof(char)); strcpy(new_arg, args[i]); strtok(new_arg, "="); char *mode_text = strtok(NULL, "="); if (strcmp(mode_text, "moon") == 0) mode = 1; else if (strcmp(mode_text, "sun") == 0) mode = 2; else mode = -1; free(new_arg); } } return mode; } int main(int argc, char **argv) { printf("%d\n", filter_mode_opt(argc, argv)); return 0; }
上述的 Guile 程序能夠簡化爲:
#!/usr/bin/guile -s !# (define (which-mode? x) (cond ((string=? x "moon") 1) ((string=? x "sun") 2) (else -1))) (define (filter-mode-opt args) (cond ((null? args) 0) (else (let ((fst (car args)) (snd (cadr args))) (cond ((string=? fst "-m") (which-mode? snd)) ((string-prefix? "--mode=" fst) (which-mode? (cadr (string-split fst #\=)))) (else (filter-mode-opt (cdr args)))))))) (display (filter-mode-opt (command-line))) (newline)
同理,也可將 C 程序簡化爲:
#include <stdio.h> #include <stdlib.h> #include <string.h> int which_mode(char *mode_text) { if (strcmp(mode_text, "moon") == 0) { return 1; } else if (strcmp(mode_text, "sun") == 0) { return 2; } else { return -1; } } int filter_mode_opt(int argc, char **args) { int mode = 0; for (int i = 0; i < argc; i++) { if (strcmp(args[i], "-m") == 0) mode = which_mode(args[i + 1]); else if (strncmp(args[i], "--mode", 6) == 0) { char *new_arg = malloc((strlen(args[i]) + 1) * sizeof(char)); strcpy(new_arg, args[i]); strtok(new_arg, "="); mode = which_mode(strtok(NULL, "=")); free(new_arg); } } return mode; } int main(int argc, char **argv) { printf("%d\n", filter_mode_opt(argc, argv)); return 0; }
如今來看一些以前未遭遇的一些細節。首先看 let
:
(let ((args (cons 1 (cons 2 (cons 3 #nil))))) (let ((fst (car args)) (snd (car (cdr args)))) (begin (display fst) (newline) (display snd) (newline))))
上述這段代碼,經 Guile 解釋器運行後,會輸出如下結果:
1 2
與之大體等效的 C 代碼以下:
#include <stdio.h> int main(void) { /* 局部塊 */ { int args[] = {1, 2, 3, 4}; /* 局部塊 */ { int fst = *args; /* args[0] */ int snd = *(args + 1); /* args[1] */ { printf("%d", fst); printf("\n"); printf("%d", snd); printf("\n"); } } } }
也就是說,let
每次都能構建一個『局部環境』,而後定義一些局部變量以供爲這個局部環境內代碼使用,其語法結構以下:
(let ((<變量 1> <表達式 1>) (<變量 2> <表達式 2>) ... ... ... (<變量 n> <表達式 n>)) <須要使用上述變量的表達式>)
上面的 let
語句示例中,出現了 (car (cdr args))
這樣的表達式,它的含義是取 args
列表的第 2 個元素。Guile 爲這種操做提供了一個簡化運算符 cadr
,用法爲 (cadr args)
。同理,對於 (cdr (cdr args))
這樣的運算,Guile 提供了 cddr
,用法爲 (cddr args)
。
由於在解析命令行文本過程當中,一些字符串運算是不可避免的。Guile 爲字符串運算提供了很豐富的函數。本節中用到了 string-prefix?
,string=?
,string-split
。只需經過下面幾個示例即可瞭解它們的功能及用法。在終端中輸入 guile
命令,進入 Guile 交互解釋器環境,而後執行如下代碼:
> (string-prefix? "--mode" "--mode=sun") $1 = #t > (string-prefix? "--mode" "--node=sun") $2 = #f > (string=? "sun" "sun") $3 = #t > (string=? "sun" "moon") $4 = #f > (string-split "--mode=sun" #\=) $5 = ("--mode" "sun") > (string-split "--mode=sun=cpu" #\=) $6 = ("--mode" "sun" "cpu")
在 Guile 中,#t
與 #f
分別表示布爾真值(True)與假值(False),而 ("--mode" "sun")
與 ("--mode" "sun" "cpu")
這樣結構是列表。
爲每一個命令行選項都像上一節中所作的那樣,寫一個專用的解析函數,這太過於浪費代碼了。考察 filter-mode-opt
過程:
(define (filter-mode-opt args) (cond ((null? args) 0) (else (let ((fst (car args)) (snd (cadr args))) (cond ((string=? fst "-m") (which-mode? snd)) ((string-prefix? "--mode=" fst) (which-mode? (cadr (string-split fst #\=)))) (else (filter-mode-opt (cdr args))))))))
在這個過程當中,只有 -m
, --mode
以及 which-mode?
函數須要特別指定。若是將這些須要特別指定的因素做爲參數傳遞給 filter-mode-opt
這樣的函數,那麼 filter-mode-opt
的通用性便會獲得顯著提高——它不只僅可以處理 zero
的 -m
與 --mode
選項,只要是將選項參數映射爲整數的任務,它都能作。這時,再稱它爲 filter-mode-opt
就不是很合理了,叫它 arg-to-int-parser
吧。
(define (arg-to-int-parser args short-opt long-opt text-to-int) (cond ((null? args) 0) (else (let ((fst (car args)) (snd (cadr args))) (cond ((string=? fst short-opt) (text-to-int snd)) ((string-prefix? long-opt fst) (text-to-int (cadr (string-split fst #\=)))) (else (arg-to-int-parser (cdr args) short-opt long-opt text-to-int)))))))
要用這個函數解析 -m
或 --mode
選項,只需:
(arg-to-int-parser (command-line) "-m" "--mode" which-mode?)
若是將 arg-to-int-parser
函數的最後一個參數 text-to-int
重命名爲 map_text_into_what?
,而後將第一個條件分支
(null? args) 0)
改成
(null? args) (map_text_into_what? "")
而後將 "arg-to-int-parser" 重命名爲 arg-parser
,即可獲得:
(define (arg-parser args short-opt long-opt map-text-into-what?) (cond ((null? args) 0) (else (let ((fst (car args)) (snd (cadr args))) (cond ((string=? fst short-opt) (map-text-into-what? snd)) ((string-prefix? long-opt fst) (map-text-into-what? (cadr (string-split fst #\=)))) (else (arg-parser (cdr args) short-opt long-opt map-text-into-what?)))))))
只要能提供正確的 map-text-into-what?
函數,那麼 arg-parser
函數幾乎可勝任全部的命令行解析工做,其用法示例以下:
(define (which-mode? x) (cond ((string=? x "") 0) ((string=? x "moon") 1) ((string=? x "sun") 2) (else -1))) (display (arg-parser (command-line) "-m" "--mode" which-mode?)) (newline)
如今,等效的 C 代碼已經很難寫出來了,由於 C 語言是靜態(編譯型)語言,它難以實現 arg-parser
這種返回值類型是動態可變的函數。不過,在現實中,arg-parser
的返回值類型並非太多,能夠爲每種類型定義一個 arg-parser
,例如:
int arg_parser_return_int(int argc, char **argv, char *short-opt, char *long-opt, int (*map_text_into_int)(char *)); char * arg_parser_return_str(int argc, char **argv, char *short-opt, char *long-opt, char * (*map_text_into_text)(char *));
若是不畏懼指針與內存慣例,那麼想要一個萬能的 arg_parser
,能夠用 void *
類型:
void * arg_parser(int argc, char **argv, char *short-opt, char *long-opt, void * (*map_text_into_int)(char *));
雖然我不會去實現這些函數,可是對於 void *
版本的 arg_parser
,我能夠給出它的一個用法示例,即用於解析 zero 程序的 -m
或 --mode
選項:
int *mode = arg_parser(argc, argv, "-m", "--mode", map_text_into_int); printf("%d\n", *mode); free(mode);
C 能寫的程序,Guile 也能寫得出來,反之亦然。不要再說 C 能直接操做內存,操做硬件,而 Guile 不能……用 Guile 也能夠模擬出內存和硬件,而後再操做。大體的感受是,用 C 寫程序,會以爲本身在擺弄一臺小馬達,而用 Guile 寫程序,則以爲本身拿了根小數樹枝唆使一隻毛毛蟲。
Guile 語言最顯著的特色有兩個。第一個特色是,列表無處不在,甚至函數的定義、應用也都以列表的形式呈現的。第二個特色是,前綴表達式無處不在,正由於如此,咱們能夠在函數命名時可使用 =
,-
,?
之類的特殊符號。這兩個特色是其餘語言所不具有的,固然它也帶來重重的括號。說到括號,可能像 Guile 這些 Scheme 系的 Lisp 風格的語言,它們的括號嚇退了許多初學者。事實上,只要有個好一些的編輯器——我用的是 Emacs,而後動手寫一些代碼,很快就不怕了,甚至會感受它們很天然。
Guile 語言在語法上未提供循環,初次用遞歸來模擬迭代,會有些不直觀。多寫寫就習慣了。事實上,Guile 以宏的形式提供了功能強大的循環機制,對此之後再做介紹……其實如今我還不會用。在符合 Scheme 語言標準的前提下,Guile 也實現了一些屬於它本身的東西。本文中用到的 #nil
以及一些字符串運算函數,這都是 Scheme 語言標準以外的東西。