用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计

南楼画角 提交于 2020-04-12 16:52:14

目录

前文列表

用 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

下面我们来实现这个自定义函数的设计。

函数的存储

为了像存储变量那般存储一个函数,我们需要考虑它是由什么组成的:

  1. 形参
  2. Q-Expression
  3. 实参

我们可以使用将内建函数和用户定义的函数都使用 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);
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!