前言中,我说要写一个文式编程工具。它的名字叫 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 语言标准以外的东西。