目录
前文列表
《用 C 语言开发一门编程语言 — 交互式解析器》
《用 C 语言开发一门编程语言 — 跨平台的可移植性》
《用 C 语言开发一门编程语言 — 语法解析器》
《用 C 语言开发一门编程语言 — 抽象语法树》
《用 C 语言开发一门编程语言 — 异常处理》
《用 C 语言开发一门编程语言 — S-表达式》
《用 C 语言开发一门编程语言 — Q-表达式》
《用 C 语言开发一门编程语言 — 变量元素设计》
函数
函数是所有程序设计的关键,其本质源自于一个数学概念,有了函数之后,程序员就可以只考虑它的意义,而不用考虑它的内部结构。在计算机科学的早期,程序员会将复杂的任务分解成一个个小的函数。那时就有人提出了一个设想:只要有足够的时间,程序员们就可以建立一个完整的函数库,以此满足所有计算的要求。当然,现今为止这个设想仍未预见有实现的苗头,主要是因为随着科技的发展计算问题也越发复杂。但很显然的,现在所有受到欢迎的编程语言都有这个趋向,提供更多的库,更好的代码重用率,更好的抽象,让我们的工作更简单。Python 就是一个非常好的例子。
Lambda 表达式
Lambda 表达式(Lambda Expression)是一种简单而强大的定义函数的方法,虽然语法有点笨拙,有很多括号和符号。Lambda 表达式的命名来自数学中的 λ 运算,对应了其中的 Lambda 抽象 (Lambda Abstraction)。
Lambda 表达式让程序员在一个列表中提供函数的名称和形式参数,它将第一个参数的作为函数名,其余的是形式参数,将它们分离出来之后,并在函数定义中使用它们。
通过 Lambda 表达式,我们可以尝试使用一些更简单的语法编写一个定义函数本身的函数。
函数设计
在以往的文章中,我们实现了 S-Expression、Q-Expression 以及变量结构,有了这些条件,我们就可以继续实现函数的定义机制了。
我们不妨首先设计一个函数定义的语法规则,函数定义的语法使用 /
进行标识,这是为了向 Lambda 表达式致敬:
\ {x y} {+ x y}
将个函数定义放入 S-Expression 中,以接受参数并进行运算:
(\ {x y} {+ x y}) 10 20
为了更友好的阅读体验,程序员还可以通过以往我们内建的 def 函数来进行创建 “别名”,就像其他的输入一样,这个 “别名” 和自定义函数的内容都会被保存在变量环境中:
def {add-together} (\ {x y} {+ x y})
最终,程序员可以如此的调用它:
add-together 10 20
下面我们来实现这个自定义函数的设计。
函数的存储
为了像存储变量那般存储一个函数,我们需要考虑它是由什么组成的:
- 形参
- Q-Expression
- 实参
我们可以使用将内建函数和用户定义的函数都使用 LAVL_FUN 类型进行识别,并通过 lbuiltin 函数指针是否为 NULL 来进行区别:
struct lval {
int type;
/* Basic */
long num;
char* err;
char* sym;
/* Function */
lbuiltin builtin;
lenv* env;
lval* formals;
lval* body;
/* Expression */
int count;
lval** cell;
};
我们可以重新命名 lbuiltin,将 func 改为 builtin,以提高代码的利用率。我们在变量环境中添加了 lval_lambda 函数,并分配了两个参数 formals 和 body:
lval* lval_lambda(lval* formals, lval* body) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_FUN;
/* Set Builtin to Null */
v->builtin = NULL;
/* Build new environment */
v->env = lenv_new();
/* Set Formals and Body */
v->formals = formals;
v->body = body;
return v;
}
为此我们修改了 lval 结构体,所以在其他的关联函数中也需要添加相应的代码:
// 删除的部分
case LVAL_FUN:
if (!v->builtin) {
lenv_del(v->env);
lval_del(v->formals);
lval_del(v->body);
}
break;
// 复制的部分
case LVAL_FUN:
if (v->builtin) {
x->builtin = v->builtin;
} else {
x->builtin = NULL;
x->env = lenv_copy(v->env);
x->formals = lval_copy(v->formals);
x->body = lval_copy(v->body);
}
break;
// 打印的部分
case LVAL_FUN:
if (v->builtin) {
printf("<builtin>");
} else {
printf("(\\ "); lval_print(v->formals);
putchar(' '); lval_print(v->body); putchar(')');
}
break;
Lambda 函数
现在可以编写 Lambda 函数,类似 def,需要检查类型是否正确,接着做其他的操作:
lval* builtin_lambda(lenv* e, lval* a) {
/* Check Two arguments, each of which are Q-Expressions */
LASSERT_NUM("\\", a, 2);
LASSERT_TYPE("\\", a, 0, LVAL_QEXPR);
LASSERT_TYPE("\\", a, 1, LVAL_QEXPR);
/* Check first Q-Expression contains only Symbols */
for (int i = 0; i < a->cell[0]->count; i++) {
LASSERT(a, (a->cell[0]->cell[i]->type == LVAL_SYM),
"Cannot define non-symbol. Got %s, Expected %s.",
ltype_name(a->cell[0]->cell[i]->type),ltype_name(LVAL_SYM));
}
/* Pop first two arguments and pass them to lval_lambda */
lval* formals = lval_pop(a, 0);
lval* body = lval_pop(a, 0);
lval_del(a);
return lval_lambda(formals, body);
}
父类环境
我们可以给予函数相关的环境,在这些环境里,代入形参,计算相关的值。但是这是理想状态,实际情况下我们需要全局环境,就像我们的内建函数一样。
为了解决这个问题,我们可以修改环境的的定义,可以引用一些父类环境。通过父类环境我们可以设置全局环境,从而达到我们的目的:
struct lenv {
lenv* par;
int count;
char** syms;
lval** vals;
};
lenv* lenv_new(void) {
lenv* e = malloc(sizeof(lenv));
e->par = NULL;
e->count = 0;
e->syms = NULL;
e->vals = NULL;
return e;
}
为了在环境中找到我们需要的变量,如果在自己环境中没有找到,可以遍历父类环境:
lval* lenv_get(lenv* e, lval* k) {
for (int i = 0; i < e->count; i++) {
if (strcmp(e->syms[i], k->sym) == 0) {
return lval_copy(e->vals[i]);
}
}
/* If no symbol check in parent otherwise error */
if (e->par) {
return lenv_get(e->par, k);
} else {
return lval_err("Unbound Symbol '%s'", k->sym);
}
}
我们需要一个新的函数来复制环境当我们使用 lval 结构体时:
lenv* lenv_copy(lenv* e) {
lenv* n = malloc(sizeof(lenv));
n->par = e->par;
n->count = e->count;
n->syms = malloc(sizeof(char*) * n->count);
n->vals = malloc(sizeof(lval*) * n->count);
for (int i = 0; i < e->count; i++) {
n->syms[i] = malloc(strlen(e->syms[i]) + 1);
strcpy(n->syms[i], e->syms[i]);
n->vals[i] = lval_copy(e->vals[i]);
}
return n;
}
拥有父环境也改变了我们定义变量的概念。有两种方法可以定义一个变量:
- 我们可以在本地,最内层环境中定义它,
- 或者我们可以在全局最外层环境中定义它。
我们将添加函数来做这两个。我们将 lenv_put 方法保持不变。 它可以用于在本地环境中定义。 但是我们将在全局环境中添加一个新的函数 lenv_def 用于定义:
void lenv_def(lenv* e, lval* k, lval* v) {
/* Iterate till e has no parent */
while (e->par) { e = e->par; }
/* Put value in e */
lenv_put(e, k, v);
}
目前这种区分似乎没有用处,但稍后我们将使用它将部分计算结果写入函数内的局部变量。 我们应该为本地赋值添加另一个内置函数。 我们将这个 put 放在 C 中,但在 Lisp 中给它赋予 = 符号。 我们可以调整我们的 builtin_def 函数并重用代码,就像我们的数学运算符一样。
我们需要注册这些函数作为内置函数:
lenv_add_builtin(e, "def", builtin_def);
lenv_add_builtin(e, "=", builtin_put);
lval* builtin_def(lenv* e, lval* a) {
return builtin_var(e, a, "def");
}
lval* builtin_put(lenv* e, lval* a) {
return builtin_var(e, a, "=");
}
lval* builtin_var(lenv* e, lval* a, char* func) {
LASSERT_TYPE(func, a, 0, LVAL_QEXPR);
lval* syms = a->cell[0];
for (int i = 0; i < syms->count; i++) {
LASSERT(a, (syms->cell[i]->type == LVAL_SYM),
"Function '%s' cannot define non-symbol. "
"Got %s, Expected %s.", func,
ltype_name(syms->cell[i]->type),
ltype_name(LVAL_SYM));
}
LASSERT(a, (syms->count == a->count-1),
"Function '%s' passed too many arguments for symbols. "
"Got %i, Expected %i.", func, syms->count, a->count-1);
for (int i = 0; i < syms->count; i++) {
/* If 'def' define in globally. If 'put' define in locally */
if (strcmp(func, "def") == 0) {
lenv_def(e, syms->cell[i], a->cell[i+1]);
}
if (strcmp(func, "=") == 0) {
lenv_put(e, syms->cell[i], a->cell[i+1]);
}
}
lval_del(a);
return lval_sexpr();
}
可变的函数参数
我们定义了一些内建函数,以便他们可以接受可变数量的参数。像 + 和 join 这样的函数可以取任意数量的参数,并在逻辑上对它们进行操作。我们应该找到一种方法让用户定义的函数也可以在多个参数上工作。
不幸的是,没有一个好的方式让我们做这个。因此,我们将使用特殊符号 & 硬编码转换为我们的语言。我们将让用户定义看起来像 {x&xs} 的形式参数,这意味着一个函数将接受一个参数 x ,后跟零个或多个其他参数,连接在一起成为一个名为 xs 的列表。这有点像我们在 C 中声明可变参数的省略号。
当分配我们的形式参数时,我们将寻找一个 & 符号,如果它存在,采用下一个形参,并为它分配剩余的参数。重要的是我们将这个参数列表转换为 Q-Expression 。我们还需要记住检查 & 后跟一个真正的符号,如果不是,我们应该抛出一个错误。
在第一个符号从 lval_call 的 while 循环中的 formals 中弹出后,在 lval_call 我们可以添加这个特殊情况。
/* Special Case to deal with '&' */
if (strcmp(sym->sym, "&") == 0) {
/* Ensure '&' is followed by another symbol */
if (f->formals->count != 1) {
lval_del(a);
return lval_err("Function format invalid. "
"Symbol '&' not followed by single symbol.");
}
/* Next formal should be bound to remaining arguments */
lval* nsym = lval_pop(f->formals, 0);
lenv_put(f->env, nsym, builtin_list(e, a));
lval_del(sym); lval_del(nsym);
break;
}
假设调用函数时,用户不提供任何变量参数,而只提供第一个命名的参数。在这种情况下,我们需要在空列表后面设置符号。 在删除参数列表之后,检查所有的 formal 求值之前,把这个特例添加进去。
/* If '&' remains in formal list bind to empty list */
if (f->formals->count > 0 &&
strcmp(f->formals->cell[0]->sym, "&") == 0) {
/* Check to ensure that & is not passed invalidly. */
if (f->formals->count != 2) {
return lval_err("Function format invalid. "
"Symbol '&' not followed by single symbol.");
}
/* Pop and delete '&' symbol */
lval_del(lval_pop(f->formals, 0));
/* Pop next symbol and create empty list */
lval* sym = lval_pop(f->formals, 0);
lval* val = lval_qexpr();
/* Bind to environment and delete */
lenv_put(f->env, sym, val);
lval_del(sym); lval_del(val);
}
来源:oschina
链接:https://my.oschina.net/u/4404772/blog/3230983