NUS CS1101S:SICP JavaScript 描述:四、元语言抽象(1)

简介: NUS CS1101S:SICP JavaScript 描述:四、元语言抽象

在我们对程序设计的研究中,我们已经看到,专业程序员使用与所有复杂系统设计者使用的相同的一般技术来控制设计的复杂性。他们将原始元素组合成复合对象,将复合对象抽象成更高级的构建块,并通过采用适当的系统结构的大规模视图来保持模块化。在说明这些技术时,我们使用 JavaScript 作为描述过程和构建计算数据对象和过程的语言,以模拟现实世界中复杂现象。然而,随着我们面对越来越复杂的问题,我们会发现 JavaScript,或者任何固定的编程语言,都无法满足我们的需求。为了更有效地表达我们的想法,我们必须不断转向新的语言。建立新语言是控制工程设计复杂性的强大策略;通过采用新语言,我们经常可以增强处理复杂问题的能力,使我们能够以不同的方式描述(因此思考)问题,使用特别适合手头问题的原语、组合手段和抽象手段。¹

编程赋予了多种语言。有物理语言,比如特定计算机的机器语言。这些语言涉及数据和控制的表示,以存储的个别位和原始机器指令。机器语言程序员关心如何利用给定的硬件来建立系统和实用程序,以有效地实现资源有限的计算。高级语言建立在机器语言基础上,隐藏了关于数据表示和程序表示的担忧,这些语言具有组合和抽象的手段,比如函数声明,适用于系统的大规模组织。

元语言抽象——建立新语言——在所有工程设计领域都起着重要作用。对于计算机编程来说尤为重要,因为在编程中,我们不仅可以制定新语言,还可以通过构建求值器来实现这些语言。编程语言的求值器(或解释器)是一个函数,当应用于语言的语句或表达式时,执行求值该语句或表达式所需的操作。把这看作编程中最基本的想法绝非夸大:

确定编程语言中语句和表达式的含义的求值器只是另一个程序。

要理解这一点就是改变我们作为程序员的形象。我们开始把自己看作语言的设计者,而不仅仅是他人设计的语言的使用者。

事实上,我们几乎可以将任何程序视为某种语言的求值器。例如,第 2.5.3 节的多项式处理系统体现了多项式算术规则,并将其实现为对列表结构数据的操作。如果我们增加这个系统的函数来读取和打印多项式表达式,我们就有了一个处理符号数学问题的特定目的语言的核心。第 3.3.4 节的数字逻辑模拟器和第 3.3.5 节的约束传播器本身就是合法的语言,每种语言都有自己的原语、组合手段和抽象手段。从这个角度来看,应对大规模计算机系统的技术与构建新计算机语言的技术融为一体,计算机科学本身不再(也不会更少)只是构建适当描述性语言的学科。

我们现在开始了解语言建立在其他语言基础上的技术之旅。在本章中,我们将以 JavaScript 为基础,将求值器实现为 JavaScript 函数。我们将通过为 JavaScript 本身构建一个求值器来迈出理解语言如何实现的第一步。我们的求值器实现的语言将是 JavaScript 的一个子集。尽管本章描述的求值器是针对 JavaScript 的特定子集编写的,但它包含了为顺序机器编写程序设计语言的求值器的基本结构。(事实上,大多数语言处理器深藏其中一个小型求值器。)为了说明和讨论的目的,求值器已经简化,并且一些重要的特性被省略了,这些特性对于生产质量的 JavaScript 系统来说是重要的。然而,这个简单的求值器足以执行本书中大部分的程序。

将求值器作为 JavaScript 程序可访问的一个重要优势是,我们可以通过将其描述为对求值器程序的修改来实现替代求值规则。我们可以利用这种能力的一个地方是,以更好的效果获得对计算模型体现时间概念的额外控制,这在第 3 章的讨论中是如此核心。在那里,我们通过使用流来解耦世界中的时间表示与计算机中的时间,来减轻一些状态和赋值的复杂性。然而,我们的流程序有时会很笨重,因为它们受到 JavaScript 的应用顺序求值的限制。在 4.2 节中,我们将改变基础语言,以提供更优雅的方法,通过修改求值器来提供正则顺序求值

第 4.3 节实现了一个更有雄心的语言变化,即语句和表达式具有多个值,而不仅仅是一个单一值。在这种非确定性计算语言中,自然地表达生成所有可能值的过程,然后搜索满足某些约束的值。就计算模型和时间而言,这就像时间分支成一组“可能的未来”,然后搜索适当的时间线。通过我们的非确定性求值器,跟踪多个值和执行搜索都由语言的基础机制自动处理。

在第 4.4 节中,我们实现了一种逻辑编程语言,其中知识是以关系的形式表达,而不是以输入和输出的计算形式。尽管这使得语言与 JavaScript 或任何传统语言都大不相同,但我们将看到逻辑编程求值器与 JavaScript 求值器共享基本结构。

4.1 元循环求值器

我们的 JavaScript 求值器将作为 JavaScript 程序实现。用 JavaScript 实现 JavaScript 程序的求值似乎是循环的。然而,求值是一个过程,因此用 JavaScript 描述求值过程是合适的,毕竟,这是我们描述过程的工具。³ 用相同语言编写的求值器被称为元循环求值器。

元循环求值器本质上是对 3.2 节中描述的求值环境模型的 JavaScript 表述。回想一下,该模型指定了函数应用的求值有两个基本步骤:

  1. 求值函数应用时,首先求值子表达式,然后将函数子表达式的值应用于参数子表达式的值。
  1. 2. 将复合函数应用于一组参数时,求值函数体在新环境中。为构造此环境,通过将函数对象的环境部分扩展为函数的参数绑定到应用函数的参数的帧。

这两条规则描述了求值过程的本质,即在环境中要求值的语句和表达式被简化为要应用于参数的函数,然后再简化为在新环境中要求值的新语句和表达式,依此类推,直到我们到达名称,其值在环境中查找,以及运算符和原始函数,这些直接应用(见图 4.1)。⁴ 这种求值循环将由求值器中两个关键函数evaluateapply之间的相互作用体现出来,这些函数在 4.1.1 节中描述(见图 4.1)。


图 4.1 evaluate-apply循环揭示了计算机语言的本质。

求值器的实现将依赖于定义要求值的语句和表达式的语法的函数。我们将使用数据抽象使求值器独立于语言的表示。例如,我们不会选择将赋值表示为以名称开头后跟=的字符串,而是使用抽象谓词is_assignment来测试赋值,并使用抽象选择器assignment_symbolassignment_value_expression来访问赋值的部分。4.1.2 节中提出的数据抽象层将使求值器保持独立于具体的语法问题,例如解释语言的关键字,以及表示程序组件的数据结构的选择。还有在 4.1.3 节中描述的操作,用于指定函数和环境的表示。例如,make_function构造复合函数,lookup_symbol_value访问名称的值,apply_primitive_function将原始函数应用于给定的参数列表。

4.1.1 求值器的核心

求值过程可以描述为evaluateapply两个函数之间的相互作用。

函数evaluate

函数evaluate以程序组件(语句或表达式)和环境作为参数。它对组件进行分类并指导其求值。函数evaluate被构造为对要求值的组件的句法类型进行案例分析。为了保持函数的一般性,我们抽象地表达了组件类型的确定,不对各种组件类型的具体表示做出承诺。每种组件类型都有一个语法谓词来测试它,并选择其部分的抽象手段。这种抽象语法使我们可以通过使用相同的求值器,但使用不同的语法函数集合来轻松地看到如何改变语言的语法。

原始表达式
  • 对于文字表达式,比如数字,evaluate返回它们的值。
  • 函数evaluate必须在环境中查找名称以找到它们的值。
组合
  • 对于函数应用,evaluate必须递归求值应用的函数表达式和参数表达式。得到的函数和参数被传递给apply,后者处理实际的函数应用。
  • 操作符组合被转换为函数应用,然后进行求值。
句法形式
  • 条件表达式或语句需要对其部分进行特殊处理,以便在谓词为真时求值结果,否则求值替代方案。
  • λ表达式必须通过将λ表达式指定的参数和主体与求值的环境一起打包,转换为可应用的函数。
  • 一系列语句需要按照它们出现的顺序求值其组件。
  • 一个块需要在反映块内声明的所有名称的新环境中求值其主体。
  • 返回语句必须产生一个值,该值成为导致返回语句求值的函数调用的结果。
  • 函数声明被转换为常量声明,然后进行求值。
  • 常量或变量声明或赋值必须调用evaluate进行递归计算,以计算与正在声明或分配的名称关联的新值。必须修改环境以反映名称的新值。

这是evaluate的声明:

function evaluate(component, env) {
    return is_literal(component)
           ? literal_value(component)
           : is_name(component)
           ? lookup_symbol_value(symbol_of_name(component), env)
           : is_application(component)
           ? apply(evaluate(function_expression(component), env),
                   list_of_values(arg_expressions(component), env))
           : is_operator_combination(component)
           ? evaluate(operator_combination_to_application(component), env)
           : is_conditional(component)
           ? eval_conditional(component, env)
           : is_lambda_expression(component)
           ? make_function(lambda_parameter_symbols(component),
                           lambda_body(component), env)
           : is_sequence(component)
           ? eval_sequence(sequence_statements(component), env)
           : is_block(component)
           ? eval_block(component, env)
           : is_return_statement(component)
           ? eval_return_statement(component, env)
           : is_function_declaration(component)
           ? evaluate(function_decl_to_constant_decl(component), env)
           : is_declaration(component)
           ? eval_declaration(component, env)
           : is_assignment(component)
           ? eval_assignment(component, env)
           : error(component, "unknown syntax – evaluate");
}

为了清晰起见,evaluate已经被实现为使用条件表达式的案例分析。这样做的缺点是,我们的函数只处理了一些可区分的语句和表达式类型,而且没有新的类型可以在不编辑evaluate的声明的情况下定义。在大多数解释器实现中,根据组件的类型进行分派是以数据导向的方式进行的。这允许用户添加evaluate可以区分的新类型的组件,而无需修改evaluate本身的声明。(见练习 4.3。)

名称的表示由语法抽象处理。在内部,求值器使用字符串来表示名称,我们将这样的字符串称为符号。函数evaluate中使用的symbol_of_name从名称中提取其表示的符号。

应用

函数apply接受两个参数,一个函数和一个应用该函数的参数列表。函数apply将函数分类为两种:它调用apply_primitive_function来应用原始函数;它通过求值组成函数主体的块来应用复合函数。复合函数的主体的求值环境是通过扩展函数携带的基本环境来构建的,以包括将函数的参数绑定到要应用函数的参数的帧。这是apply的声明:

function apply(fun, args) {
    if (is_primitive_function(fun)) {
        return apply_primitive_function(fun, args);
    } else if (is_compound_function(fun)) {
        const result = evaluate(function_body(fun),
                               extend_environment(
                                   function_parameters(fun),
                                   args,
                                   function_environment(fun)));
        return is_return_value(result)
               ? return_value_content(result)
               : undefined;
    } else {
        error(fun, "unknown function type – apply");
    }
}

为了返回一个值,JavaScript 函数需要求值一个返回语句。如果一个函数在不求值返回语句的情况下终止,将返回值undefined。为了区分这两种情况,返回语句的求值将返回表达式的结果包装成一个返回值。如果函数体的求值产生了这样一个返回值,就会检索返回值的内容;否则将返回值undefined。⁶

函数参数

evaluate处理函数应用时,它使用list_of_values来生成要应用函数的参数列表。函数list_of_values以应用的参数表达式作为参数。它求值每个参数表达式并返回相应值的列表:⁷

function list_of_values(exps, env) {
    return map(arg => evaluate(arg, env), exps);
}
条件语句

函数eval_conditional求值给定环境中条件组件的谓词部分。如果结果为真,则求值结果,否则求值替代结果:

function eval_conditional(component, env) {
    return is_truthy(evaluate(conditional_predicate(component), env))
           ? evaluate(conditional_consequent(component), env)
           : evaluate(conditional_alternative(component), env);
}

请注意,求值器不需要区分条件表达式和条件语句。

eval_conditional中使用is_truthy突出了实现语言和实现语言之间的连接问题。conditional_predicate在正在实现的语言中进行求值,因此产生该语言中的一个值。解释器谓词is_truthy将该值转换为可以由实现语言中的条件表达式测试的值:真实的元循环表示可能与底层 JavaScript 的表示不同。⁸

序列

函数eval_sequenceevaluate用于求值顶层或块中的语句序列。它以语句序列和环境作为参数,并按照它们出现的顺序求值这些语句。返回的值是最终语句的值,但如果序列中任何语句的求值产生了返回值,那么将返回该值,并且忽略后续的语句。⁹

function eval_sequence(stmts, env) {
    if (is_empty_sequence(stmts)) {
        return undefined;
    } else if (is_last_statement(stmts)) {
        return evaluate(first_statement(stmts), env);
    } else {
        const first_stmt_value =
            evaluate(first_statement(stmts), env);
        if (is_return_value(first_stmt_value)) {
            return first_stmt_value;
        } else {
            return eval_sequence(rest_statements(stmts), env);
        }
    }
}

函数eval_block处理块。在块中声明的变量和常量(包括函数)具有整个块作为它们的作用域,因此在求值块的主体之前会“扫描出”它们。块的主体是根据通过将每个本地名称绑定到特殊值"unassigned"来扩展当前环境的环境进行求值。这个字符串作为一个占位符,在声明求值之前,访问名称的值会导致运行时错误(见第 1 章脚注 56 中的练习 4.12)。

function eval_block(component, env) {
    const body = block_body(component);
    const locals = scan_out_declarations(body);
    const unassigneds = list_of_unassigned(locals);
    return evaluate(body, extend_environment(locals,
                                             unassigneds,
                                             env));
}
function list_of_unassigned(symbols) {
    return map(symbol => "unassigned", symbols);
}

函数scan_out_declarations收集在函数体中声明的所有符号名称的列表。它使用declaration_symbol从找到的声明语句中检索表示名称的符号。

function scan_out_declarations(component) {
    return is_sequence(component)
           ? accumulate(append,
                        null,
                        map(scan_out_declarations,
                            sequence_statements(component)))
           : is_declaration(component)
           ? list(declaration_symbol(component))
           : null;
}

我们忽略嵌套在另一个块中的声明,因为该块的求值会处理它们。函数scan_out_declarations只在序列中查找声明,因为条件语句、函数声明和 lambda 表达式中的声明总是在嵌套块中。

返回语句

函数eval_return_statement用于求值返回语句。正如在apply和序列的求值中所看到的,返回语句的求值结果需要是可识别的,以便函数体的求值可以立即返回,即使在返回语句之后还有语句。为此,返回语句的求值将返回表达式的求值结果包装在一个返回值对象中。¹⁰

function eval_return_statement(component, env) {
    return make_return_value(evaluate(return_expression(component),
                                      env));
}
赋值和声明

函数eval_assignment处理对名称的赋值。(为了简化我们的求值器的表示,我们不仅允许对变量进行赋值,还允许对常量进行错误的赋值。练习 4.11 解释了我们如何区分常量和变量,并防止对常量进行赋值。)函数eval_assignment调用值表达式上的evaluate来找到要赋值的值,并调用assignment_symbol来检索表示名称的符号。函数eval_assignment将符号和值传递给assign_symbol_value,以安装在指定环境中。赋值的求值返回被赋的值。

function eval_assignment(component, env) {
    const value = evaluate(assignment_value_expression(component),
                           env);
    assign_symbol_value(assignment_symbol(component), value, env);
    return value;
}

常量和变量声明都由is_declaration语法谓词识别。它们的处理方式类似于赋值,因为eval_block已经将它们的符号绑定到当前环境中的"unassigned"。它们的求值将"unassigned"替换为值表达式的求值结果。

function eval_declaration(component, env) {
    assign_symbol_value(
        declaration_symbol(component),
        evaluate(declaration_value_expression(component), env), env);
    return undefined;
}

函数的返回值由return语句确定,因此eval_declaration中的返回值undefined只在声明发生在顶层,即在任何函数体之外时才重要。在这里,我们使用返回值undefined来简化表示;练习 4.8 描述了在 JavaScript 中求值顶层组件的真实结果。

练习 4.1

请注意,我们无法确定元循环求值器是从左到右还是从右到左求值参数表达式。它的求值顺序是从底层 JavaScript 继承的:如果mappair的参数是从左到右求值的,那么list_of_values将从左到右求值参数表达式;如果pair的参数是从右到左求值的,那么list_of_values将从右到左求值参数表达式。

编写一个list_of_values的版本,无论底层 JavaScript 的求值顺序如何,都从左到右求值参数表达式。还要编写一个list_of_values的版本,从右到左求值参数表达式。

4.1.2 表示组件

程序员将程序编写为文本,即一系列以编程环境或文本编辑器输入的字符。要运行我们的求值器,我们需要从 JavaScript 值开始表示这个程序文本。在 2.3.1 节中,我们介绍了字符串来表示文本。我们希望求值诸如 1.1.2 节中的"const size = 2; 5 * size;"之类的程序。不幸的是,这样的程序文本并不能为求值器提供足够的结构。在这个例子中,程序部分"size = 2""5 * size"看起来相似,但含义完全不同。通过检查程序文本来实现抽象语法函数,如declaration_value_expression,将会很困难且容易出错。因此,在本节中,我们引入了一个名为parse的函数,将程序文本转换为标记列表表示,类似于 2.4.2 节的标记数据。例如,对上面的程序字符串应用parse会产生一个反映程序结构的数据结构:一个序列,其中包含一个将名称size与值 2 关联起来的常量声明和一个乘法。

parse("const size = 2; 5 * size;");
list("sequence",
     list(list("constant_declaration",
               list("name", "size"), list("literal", 2)),
          list("binary_operator_combination", "*",
               list("literal", 5), list("name", "size"))))

求值器使用的语法函数访问parse产生的标记列表表示。

求值器类似于第 2.3.2 节讨论的符号微分程序。这两个程序都操作符号数据。在这两个程序中,对对象进行操作的结果是通过递归地对对象的部分进行操作,并以一种取决于对象类型的方式将结果组合起来。在这两个程序中,我们使用数据抽象来将操作的一般规则与对象的表示方式分离开来。在微分程序中,这意味着相同的微分函数可以处理前缀形式的代数表达式,中缀形式的代数表达式,或者其他形式的代数表达式。对于求值器,这意味着被求值的语言的语法完全由parse和分类和提取parse产生的标记列表的部分的函数确定。

图 4.2 描述了由语法谓词和选择器形成的抽象屏障,这些语法谓词和选择器将求值器与程序的标记列表表示接口,这又与字符串表示由parse分隔开。下面我们描述程序组件的解析,并列出相应的语法谓词和选择器,以及如果需要的话的构造函数。


图 4.2 求值器中的语法抽象。

文字表达式

文字表达式被解析为带有标签"literal"和实际值的标记列表。

《literal-expression 》 = list("literal", value)

其中value是由literal-expression字符串表示的 JavaScript 值。这里《literal-expression》表示解析字符串literal-expression的结果。

parse("1;");
list("literal", 1)
parse("'hello world';");
list("literal", "hello world")
parse("null;");
list("literal", null)

文字表达式的语法谓词是is_literal

function is_literal(component) {
   return is_tagged_list(component, "literal");
}

它是根据函数is_tagged_list定义的,该函数标识以指定字符串开头的列表:

function is_tagged_list(component, the_tag) {
   return is_pair(component) && head(component) === the_tag;
}

解析文字表达式产生的列表的第二个元素是其实际的 JavaScript 值。用于检索值的选择器是literal_value

function literal_value(component) {
   return head(tail(component));
}
literal_value(parse("null;"));
null

在本节的其余部分,我们只列出语法谓词和选择器,并省略它们的声明,如果它们只是访问明显的列表元素。

我们为文字提供了一个构造函数,这将很方便:

function make_literal(value) {
   return list("literal", value);
}
名称

名称的标记列表表示包括标签"name"作为第一个元素和表示名称的字符串作为第二个元素。

《 name 》 = list("name", symbol)

其中symbol是一个包含构成程序中name的字符的字符串。名称的语法谓词是is_name。可以使用选择器symbol_of_name访问符号。我们为名称提供了一个构造函数,供operator_combination_to_application使用:

function make_name(symbol) {
   return list("name", symbol);
}
表达式语句

我们不需要区分表达式和表达式语句。因此,parse可以忽略这两种组件之间的区别:

《 expression; 》 = 《 expression 》
函数应用

函数应用的解析如下:

《 fun-expr(arg-expr[1], ..., arg-expr[n]) 》=
     list("application",
          《 fun-expr 》,
          list(《 arg-expr[1] 》, ..., 《 arg-expr[n] 》))

我们将is_application声明为语法谓词,function_expressionarg_expressions作为选择器。我们添加了一个函数应用的构造函数,供operator_combination_to_application使用:

function make_application(function_expression, argument_expressions) {
    return list("application",
                function_expression, argument_expressions);
}
条件

条件表达式的解析如下:

《 predicate ? consequent-expression : alternative-expression 》=
         list("conditional_expression",
              《 predicate 》,
              《 consequent-expression 》,
              《 alternative-expression 》)

类似地,条件语句的解析如下:

《 if (predicate) consequent-block else alternative-block 》=
         list("conditional_statement",
              《 predicate 》,
              《 consequent-block 》,
              《 alternative-block 》)

语法谓词is_conditional对两种条件都返回true,选择器conditional_predicateconditional_consequentconditional_alternative可以应用于两种条件。

Lambda 表达式

解析主体为表达式的 lambda 表达式,就好像主体由包含单个返回语句的块解析,返回表达式是 lambda 表达式的主体。

《 (name[1], ..., name[n]) => expression 》 =
    《 (name[1], ..., name[n]) => { return expression ; } 》

解析主体为块的 lambda 表达式如下:

《 (name[1], ..., name[n]) => block 》=
    list("lambda_expression",
     list(《 name[1] 》, ..., 《 name[n] 》),
     《 block 》)

语法谓词是is_lambda_expression,lambda 表达式的主体选择器是lambda_body。称为lambda_parameter_symbols的参数选择器还从名称中提取符号。

function lambda_parameter_symbols(component) {
    return map(symbol_of_name, head(tail(component)));
}

函数function_decl_to_constant_decl需要一个 lambda 表达式的构造函数:

function make_lambda_expression(parameters, body) {
    return list("lambda_expression", parameters, body);
}
序列

序列语句将一系列语句打包成一个单独的语句。语句序列的解析如下:

《 statement[1] ... statement[n] 》 =
      list("sequence", list(《 statement[1] 》, ..., 《 statement[n] 》))

语法谓词是is_sequence,选择器是sequence_statements。我们使用first_statement检索语句列表的第一个语句,使用rest_statements检索剩余的语句。我们使用谓词is_empty_sequence测试列表是否为空,并使用谓词is_last_statement测试列表是否只包含一个元素。¹¹

function first_statement(stmts) { return head(stmts); }
function rest_statements(stmts) { return tail(stmts); }
function is_empty_sequence(stmts) { return is_null(stmts); }
function is_last_statement(stmts) { return is_null(tail(stmts)); }

块的解析如下:¹²

《 { statements } 》 = list("block", 《 statements 》 )

这里statements指的是一系列语句,如上所示。语法谓词是is_block,选择器是block_body

返回语句

返回语句的解析如下:

《 return expression; 》 = list("return_statement", 《 expression 》 )

语法谓词和选择器分别是is_return_statementreturn_expression

赋值

赋值的解析如下:

《 name = expression 》 = list("assignment", 《 name 》 , 《 expression 》 )

语法谓词是is_assignment,选择器是assignment_symbolassignment_value_expression。符号包装在表示名称的标记列表中,因此assignment_symbol需要将其解包。

function assignment_symbol(component) {
    return symbol_of_name(head(tail(component))));
}
常量、变量和函数声明

常量和变量声明的解析如下:

《 const name = expression; 》 =
    list("constant_declaration", 《 name 》, 《 expression 》)
《 let name = expression; 》 =
    list("variable_declaration", 《 name 》, 《 expression 》)

选择器declaration_symboldeclaration_value_expression适用于两种情况。

function declaration_symbol(component) {
    return symbol_of_name(head(tail(component)));
}
function declaration_value_expression(component) {
    return head(tail(tail(component)));
}

函数function_decl_to_constant_decl需要一个常量声明的构造函数:

function make_constant_declaration(name, value_expression) {
    return list("constant_declaration", name, value_expression);
}

函数声明的解析如下:

function name(name[1], ... name[n]) block 》=
    list("function_declaration",
     《 name 》,
     list(《 name[1] 》, ..., 《 name[n] 》),
     《 block 》)

语法谓词is_function_declaration识别这些。选择器是function_declaration_namefunction_declaration_parametersfunction_declaration_body

语法谓词is_declaration对所有三种声明返回true

function is_declaration(component) {
    return is_tagged_list(component, "constant_declaration") ||
           is_tagged_list(component, "variable_declaration") ||
           is_tagged_list(component, "function_declaration");
}
派生组件

我们语言中的一些语法形式可以根据涉及其他语法形式的组件来定义,而不是直接实现。一个例子是函数声明,evaluate将其转换为值表达式为 lambda 表达式的常量声明。¹³

function function_decl_to_constant_decl(component) {
    return make_constant_declaration(
               function_declaration_name(component),
               make_lambda_expression(
                   function_declaration_parameters(component),
                   function_declaration_body(component)));
}

以这种方式实现函数声明的求值简化了求值器,因为它减少了必须明确指定求值过程的语法形式的数量。

同样,我们定义操作符组合以函数应用的形式。操作符组合是一元或二元的,并且在标记列表表示中携带其操作符符号作为第二个元素:

《 unary-operator expression 》=
    list("unary_operator_combination",
         "unary-operator",
         list(《 expression 》))

其中*!(逻辑否定)或-unary(数值否定),并且

《 expression[1] binary-operator expression[2] 》=
    list("binary_operator_combination",
         "binary-operator",
         list(《 expression[1] 》, 《 expression[2] 》))

其中binary-operator+-*/%===!==><>=<=。语法谓词是is_operator_combinationis_unary_operator_combinationis_binary_operator_combination,选择器是operator_symbolfirst_operandsecond_operand

求值器使用operator_combination_to_application将操作符组合转换为一个函数应用,其函数表达式是操作符的名称:

function operator_combination_to_application(component) {
    const operator = operator_symbol(component);
    return is_unary_operator_combination(component)
           ? make_application(make_name(operator),
                              list(first_operand(component)))
           : make_application(make_name(operator),
                              list(first_operand(component),
                                   second_operand(component)));
}

我们选择将组件(如函数声明和操作符组合)实现为语法转换的派生组件。逻辑组合操作也是派生组件(参见练习 4.4)。

练习 4.2

parse的逆操作称为unparse。它以parse生成的标记列表作为参数,并返回一个符合 JavaScript 表示的字符串。

  1. a. 按照evaluate的结构(不包括环境参数),编写一个名为unparse的函数,但是产生一个表示给定组件的字符串,而不是对其进行求值。回想一下,从第 3.3.4 节中得知,操作符+可以应用于两个字符串以将它们连接起来,原始函数stringify将值(如 1.5、truenullundefined)转换为字符串。请注意,通过使用括号(总是或在必要时)括起来,以保持操作符的优先级。
  2. b. 在解决本节中的后续练习时,您的unparse函数会派上用场。通过向结果字符串添加" "(空格)和"\n"(换行)字符,以遵循本书中 JavaScript 程序中使用的缩进样式,改进unparse。为了使文本更易于阅读,向程序文本中添加(或删除)此类空白字符称为美化打印
练习 4.3

重写evaluate,以便以数据导向的方式进行分派。将此与练习 2.73 中的数据导向微分函数进行比较。 (您可以使用标记列表表示的标记作为组件类型。)

NUS CS1101S:SICP JavaScript 描述:四、元语言抽象(2)https://developer.aliyun.com/article/1427734

相关文章
|
3天前
|
JavaScript 前端开发 安全
抽象语法树(AST):理解JavaScript代码的抽象语法树
抽象语法树(AST):理解JavaScript代码的抽象语法树
|
5天前
|
存储 缓存 JavaScript
请描述一种JavaScript内存泄漏的情况,并说明如何避免这种情况的发生。
JavaScript内存泄漏常由闭包引起,导致无用对象滞留内存,影响性能。例如,当一个函数返回访问大型对象的闭包,即使函数执行完,对象仍被闭包引用,无法被垃圾回收。防止泄漏需及时解除引用,注意事件监听器清理,使用WeakMap或WeakSet,定期清理缓存,以及利用性能分析工具检测。
15 2
|
5天前
|
JavaScript 前端开发 算法
描述 JavaScript 中的垃圾回收机制。
描述 JavaScript 中的垃圾回收机制。
23 1
|
5天前
|
移动开发 JavaScript 前端开发
游戏框架 - 描述Phaser、Three.js等JavaScript游戏框架的核心功能和使用场景。
Phaser是开源2D游戏引擎,适合HTML5游戏,内置物理引擎和强大的图形渲染功能,适用于2D游戏,如消消乐。Three.js是基于WebGL的3D库,用于创建和显示3D图形,支持交互和多种3D效果,广泛应用在游戏、可视化等多个领域。两者各有侧重,选择取决于项目需求和图形交互要求。
53 3
|
5天前
|
开发框架 JavaScript 前端开发
描述JavaScript事件循环机制,并举例说明在游戏循环更新中的应用。
JavaScript的事件循环机制是单线程处理异步操作的关键,由调用栈、事件队列和Web APIs构成。调用栈执行函数,遇到异步操作时交给Web APIs,完成后回调函数进入事件队列。当调用栈空时,事件循环取队列中的任务执行。在游戏开发中,事件循环驱动游戏循环更新,包括输入处理、逻辑更新和渲染。示例代码展示了如何模拟游戏循环,实际开发中常用框架提供更高级别的抽象。
15 1
|
5天前
|
前端开发 JavaScript UED
描述 JavaScript 中的事件循环机制。
描述 JavaScript 中的事件循环机制。
14 1
|
2天前
|
缓存 JavaScript 前端开发
JavaScript:get和post的区别,2024年最新3-6岁儿童学习与发展指南心得体会
JavaScript:get和post的区别,2024年最新3-6岁儿童学习与发展指南心得体会
|
3天前
|
设计模式 存储 前端开发
JS的几种设计模式,Web前端基础三剑客学习知识分享,前端零基础开发
JS的几种设计模式,Web前端基础三剑客学习知识分享,前端零基础开发
|
5天前
|
XML Web App开发 前端开发
字节FE:JavaScript学习路线图
字节FE:JavaScript学习路线图
10 0
|
5天前
|
存储 移动开发 JavaScript
学习javascript,前端知识精讲,助力你轻松掌握
学习javascript,前端知识精讲,助力你轻松掌握