函数

发布: (2026年1月5日 GMT+8 15:04)
9 min read
原文: Dev.to

Source: Dev.to

截图

我构建的

提交 3da669d、930014e、5adb148 和 9d534f3

我理解的内容

1️⃣ 更新语法

  • 优先级 – 函数调用现在拥有最高的优先级,位于一元运算规则之上。
  • 可调用对象 – 引入了 Callable 类(实际为 LoxCallable),用于判断一个实体是否可以被调用。
    • 示例:字符串不可调用,而类和函数(用户自定义或原生)是可调用的。
    • 如果 被调用者 没有实现 LoxCallable,解释器会抛出运行时错误。
  • 柯里化 / 链式调用 – 解析器现在允许类似 (( "(" arguments? ")" )* 的结构,从而支持 function(arg)(arg)(arg) 这样的调用。

Grammar diagram

2️⃣ 参数

  • 参数个数检查 – 解释器会验证提供的实参数量是否与函数定义的形参数量相匹配。

Arity check screenshot

3️⃣ 原生函数

  • 在 Java 中实现了 clock() 原生函数。
  • 它是实现了 LoxCallable 的匿名类,并绑定到解释器全局环境中的标识符 clock
  • 返回当前系统时间(秒),可用于基准测试。

4️⃣ 函数声明

  • 遇到 fun 时会创建一个可调用对象(LoxCallable),并将其绑定到函数名所在的新环境中。
  • 函数的形参在该局部环境中绑定。
  • 每次函数调用也会创建一个全新的环境,从而支持递归。
  • 当函数体执行完毕后,局部环境被销毁,执行返回到调用者的环境。

5️⃣ 返回

  • 解释器会查找 return 语句,后面可以跟一个表达式。
  • 如果没有提供表达式,则隐式返回 nil
  • 当执行 return 时,解释器必须立即展开当前调用栈。
  • 这通过一个简单的异常类 Return 来实现,该异常携带返回值。
  • 调用点用 try‑catch 包裹函数执行,捕获该异常;如果未抛出异常,函数正常结束并返回 nil

6️⃣ 作用域

  • 之前 Lox 使用动态作用域:被调用函数的作用域始终是全局环境,而不是函数定义时所在的环境。
  • 这导致函数在定义环境已经结束后,无法访问在定义时捕获的变量。
  • 解决办法是捕获词法环境(函数创建时存在的环境),而不是在调用点再进行名称解析。

示例说明问题(以及修复)

fun getName(name) {
    var person = name;
    fun greet() {
        print "Hello, " + person + "!";
    }
    return greet;
}

var momo    = getName("Momo");
var chutney = getName("Chutney");

momo();    // 应该打印 "Hello, Momo!"
chutney(); // 应该打印 "Hello, Chutney!"
  • 修复前 由于 person 在全局作用域中查找,会抛出错误。
  • 实现词法作用域后 每个返回的 greet 闭包都会正确捕获各自的 person 变量,产生预期的输出。

Source:

闭包、作用域与解析器

1. 闭包示例

function getName(name) {
  var person = "Hello, " + name + "!";
  return function () {
    console.log(person);
  };
}

var momo = getName("Momo");   // Returns a function that closes over `person`
momo();                       // → “Hello, Momo!”

function chutney() {
  console.log("Hello, Chutney!");
}
chutney();                    // → “Hello, Chutney!”

2. 为什么第一个示例会失败

  • getName("Momo") 执行完毕时,变量 person 会因为函数作用域结束而 被销毁
  • 当随后调用 momo() 时,它会尝试在其父作用域中寻找 person,此时父作用域已经是 全局作用域(其中并不存在 person)。→ 错误!

3. 词法(静态)作用域

  • 我们通过 词法作用域 来解决这个问题。
  • 当函数 声明 时,它会 闭合(捕获)其周围的环境。
  • 内部函数持有对外部环境的 活跃引用,即使外部函数已经返回,垃圾回收器也不会销毁该环境。
  • 当内部函数随后 调用 时,捕获的环境成为它的 父作用域,而不是全局作用域。

4. 解析器

通过词法作用域实现闭包会引入另一个问题:

  • 闭包持有对表示当前作用域的 可变 映射的活跃引用。
  • 该作用域的捕获视图可能会因同一块中后续代码的执行而改变,而这 不应该发生

因为 Lox 采用 词法/静态作用域,变量的使用必须在整个程序执行期间始终解析到 相同的声明,即使该变量随后被重新声明或重新定义。

示例

var a = "one";

{
  fun showA() {
    print a;
  }

  showA();          // → "one"

  var a = "two";

  showA();          // Should still print "one", but would print "two" without a resolver
}
  • a 属于 全局作用域,值为 "one"
  • showA 创建了一个闭包,捕获了它声明所在的 块/环境
  • 第一次调用 showA() 时,块中找不到 a,于是查找继续到全局作用域 → 打印 "one"
  • 在同一块中执行 var a = "two" 重新声明 a 后,可变的作用域导致第二次调用 showA() 打印 "two"——这是一种错误行为。

解析器如何修复

解析器(在解析器之后、解释器之前加入)使用 持久化数据结构

  • 原始数据结构不能被直接篡改。
  • 任何修改都会创建一个 新结构,该结构包含原始信息以及此次修改。

这类似于 区块链:每个新块引用前一个块的哈希,同时包含新的变更。

当解析器处理一个块并看到变量被 使用(在其声明之后),它会进行一次 静态遍历

Scope 0 (function's scope): a is not found
Scope 1 (block scope):    a is not found (var a = "two" hasn't been defined yet)
Scope 2 (global scope):    a is found
  • 因为变量在离最内层作用域 2 步之外被找到,解析器会在该使用位置标注数字 2
  • 在运行时,解释器利用这个跳数直接跳到正确的声明处,避免了可变作用域带来的意外。

5. 接下来会做什么?

我们将继续保持 Classy! 💅
(接下来是类与继承。)

6. 随想

过去几个月里,我有幸目睹了一些最奇妙、最令人困惑的事物。感觉自己仿佛经历了人生的全部光谱,正如莎士比亚的 七个阶段。在某种程度上,这让我变得非常谦卑。

很长一段时间,我害怕放手——放手我的信念、兴趣,甚至一些梦想——因为我担心会失去自我。现在我明白,就像函数一样,拥有一套不可协商的模板是可以的,在此基础上我们仍然可以…

生活不断添加它自己的“implementations”。(我真的必须使用那个类比——请见谅。)

变化和不可预测性不再显得如此令人生畏,也许(大约 0.05 %)它们也有好处。我想 c’est la vie

Back to Blog

相关文章

阅读更多 »