2.4 指称语义
到目前为止,我们已经从操作性方面观察了程序设计语言的含义,它通过展示程序执行之后发生的事情解释了程序的含义。而指称语义(denotational semantic)转而关心从程序本来的语言到其他表示的转换。
这种类型的语义没有直接处理程序的执行,而是关注如何借助另一种语言的已有含义——一种低级的、更形式化的或者至少比正在描述的语言更好理解的语言——解释一个新的语言。
指称语义确实是一种比操作语义更抽象的方法,因为它只是用一种语言替换另一种语言,而不是把一种语言转换成真实的行为。例如,如果我们需要向一个人解释英语动词“walk”的含义,但和他没有共同的口头语言,可以通过来回走的动作来沟通。另一方面,如果我们需要向一个说法语的人解释“walk”,可以跟他讲“marcher”——不可否认这是一种更高层次的沟通方式,不需要麻烦地运动了。
指称语义通常用来把程序转成数学化的对象,所以不出意料,可以用数学工具研究和控制它们,但是我们可以看看如何用另一种方式表示 Simple 程序,借此大致了解指称语义。
把 Simple 转成 Ruby 从而得到 Simple 语言的指称语义,16 事实上,这意味着把一个抽象语法树转成一个 Ruby 代码的字符串。不管怎样,我们得到了那种语法本来的含义。
16这意味着我们将用 Ruby 代码生成 Ruby 代码,但是选择用同样的指称语言和实现元语言只是为了让事情简单。例如我们很容易用 Ruby 写出能生成包含 JavaScript 字符串的代码来。
但“本来的含义”是什么呢?我们表达式和语句的 Ruby 指称(denotation)是什么样的呢?从操作上我们已经看到一个表达式使用一个环境(environment)然后把它转成一个值;在 Ruby 中表达这个过程的一种方式是用一些参数表示环境参数,然后返回一些表示值的 Ruby 对象。对于像 «5» 和 «false» 这样简单的常量表达式,我们根本无需使用环境,而只需要关心它们最终的结果如何能表示成一个 Ruby 对象。幸运的是,Ruby 已经设计了专门的对象表示这些值:我们可以使用 Ruby 值 5 作为 Simple 表达式«5» 的结果,同样地,把 Ruby 的值 false 作为 «false» 的结果。
2.4.1 表达式
我们可以用这个思想为 Number 类和 Boolean 类写一个 #to_ruby 的实现:
class Number def to_ruby "-> e { #{value.inspect} }" end end class Boolean def to_ruby "-> e { #{value.inspect} }" end end
下面在控制台运行它们:
>> Number.new(5).to_ruby => "-> e { 5 }" >> Boolean.new(false).to_ruby => "-> e { false }"
这些方法每个都产生一个刚好包含 Ruby 代码的字符串,并且因为 Ruby 是一种我们已经理解其含义的语言,所以可以看到这些字符串都是构造 proc 的程序。每一个 proc 都带有一个叫 e 的环境参数,它们完全忽略这个参数而直接返回一个 Ruby 值。
因为这些符号都是 Ruby 代码组成的字符串,所以可以使用 Kernel#eval 转换成可调用的 Proc 对象实际执行,然后在 IRB 中检查它们的行为 17:
17只有 Ruby 既做实现语言又作为指称语言的时候我们才能这么做。如果指称是 JavaScript 源代码,我们就得到 JavaScript 的控制台去实验它们了。
>> proc = eval(Number.new(5).to_ruby) => #<Proc (lambda)> >> proc.call({}) => 5 >> proc = eval(Boolean.new(false).to_ruby) => #<Proc (lambda)> >> proc.call({}) => false
现阶段,完全避免 proc,而使用更简单的 #to_ruby 实现是很诱人的,这只需要把 Number.new(5) 转换成字符串'5' 而不是'-> e {5}' 等,但是从源语言结构中获得其本质语义是指称语义这一方法的一部分,那么我们需要知道,即便某些特定的表达式不会用到环境,通常的表达式也还是需要一个环境的。
为了表示确实使用环境的表达式,我们需要决定如何用 Ruby 表示环境(environment)。在研究操作语义时我们已经了解了环境,那么既然它们已经用 Ruby 实现了,现在可以重用早期的思想——把一个环境表示成一个散列表。不过细节需要做一些改动,因此要注意其中微妙的差别:在我们的操作语义中,环境是生存在虚拟机中的,并且把变量名与 Number.new(5)这样的 Simple 抽象语法树联系起来;但在我们的指称语义中,环境存在于我们要把程序转换得到的语言中,因此要在那个世界而不是在一个虚拟机的“外部世界”起作用。
注意,这意味着指称环境(denotational environment)应该把变量名与 5 这样的原生 Ruby值,而不是与表示 Simple 语法的对象关联起来。我们把 { x: Number.new(5) } 这样的操作环境(operational environment)看成在要转换成的语言中拥有指称 '{ x: 5 }',并且因为实现的元语言和指称语言正好都是 Ruby,所以不必有什么顾忌。
既然知道环境将是一个散列,那么就可以实现 Variable#to_ruby了:
class Variable def to_ruby "-> e { e[#{name.inspect}] }" end end
这段代码,把一个变量表达式转换成一个在环境散列中查找合适值的 Ruby proc:
>> expression = Variable.new(:x) => «x» >> expression.to_ruby => "-> e { e[:x] }" >> proc = eval(expression.to_ruby) => #<Proc (lambda)> >> proc.call({ x: 7 }) => 7
关于指称语义重要的一点是它是组合式的:一个程序的指称由组成它的各部分的指示构成。在开始指称(denotating)Add、Multiply 和 LessThan 这样的更大表达式时,我们就能理解这种合成性了:
class Add def to_ruby "-> e { (#{left.to_ruby}).call(e) + (#{right.to_ruby}).call(e) }" end end class Multiply def to_ruby "-> e { (#{left.to_ruby}).call(e) * (#{right.to_ruby}).call(e) }" end end class LessThan def to_ruby "-> e { (#{left.to_ruby}).call(e) < (#{right.to_ruby}).call(e) }" end end
这里使用字符串串联操作把子表达式的指称组成一个大表达式的指称。我们知道每一个子表达式都将在 Ruby 源码中用一个 proc 表示,因此可以将它们作为更大段 Ruby 代码的一部分,那些更大段的代码使用提供的环境调用这些 proc,并使用它们返回的值进行一些计算。下面是得到结果:
>> Add.new(Variable.new(:x), Number.new(1)).to_ruby => "-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }" >> LessThan.new(Add.new(Variable.new(:x), Number.new(1)), Number.new(3)).to_ruby => "-> e { (-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }).call(e) < (-> e { 3 }).call(e) }"
这些指称已经够复杂的了,很难了解它们做的事情是否正确。让我们运行它们确认一下:
>> environment = { x: 3 } => {:x=>3} >> proc = eval(Add.new(Variable.new(:x), Number.new(1)).to_ruby) => #<Proc (lambda)> >> proc.call(environment) => 4 >> proc = eval( LessThan.new(Add.new(Variable.new(:x), Number.new(1)), Number.new(3)).to_ruby ) => #<Proc (lambda)> >> proc.call(environment) => false
2.4.2 语句
我们可以用类似的方式定义语句的指称语义,但是要记住操作语义中提到的:对一个语句求值产生的是一个新的环境而不是一个值。这意味着 Assign#to_ruby 需要为 proc 构造一些代码,以使结果是一个更新了的环境散列:
class Assign def to_ruby "-> e { e.merge({ #{name.inspect} => (#{expression.to_ruby}).call(e) }) }" end end
还是可以在控制台对其进行检查:
>> statement = Assign.new(:y, Add.new(Variable.new(:x), Number.new(1))) => «y = x + 1» >> statement.to_ruby => "-> e { e.merge({ :y => (-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }) .call(e) }) }" >> proc = eval(statement.to_ruby) => #<Proc (lambda)> >> proc.call({ x: 3 }) => {:x=>3, :y=>4}
和之前一样,DoNothing 的语义非常简单:
class DoNothing def to_ruby '-> e { e }' end end
对于条件语句,我们可以把 Simple 的 «if (...) { ... } else { ... }»转换成一个 Ruby的 if ... then ... else ... end,确保环境传到了需要它的地方:
class If def to_ruby "-> e { if (#{condition.to_ruby}).call(e)" + " then (#{consequence.to_ruby}).call(e)" + " else (#{alternative.to_ruby}).call(e)" + " end }" end end
就像在大步操作语义中一样,我们需要小心地定义序列语句:对第一个语句求值的结果作为对第二个语句求值时的环境。
class Sequence def to_ruby "-> e { (#{second.to_ruby}).call((#{first.to_ruby}).call(e)) }" end end
最后,就像处理条件语句那样,我们可以把«while» 语句转成 proc,在返回最终环境之前,它使用 Ruby 的while重复执行语句主体:
class While def to_ruby "-> e {" + " while (#{condition.to_ruby}).call(e); e = (#{body.to_ruby}).call(e); end;" + " e" + " }" end end
哪怕是一个简单的 «while» 都具有一个冗长的表示,所以有必要用 Ruby 解释器检查一下它的含义正确与否:
>> statement = While.new( LessThan.new(Variable.new(:x), Number.new(5)), Assign.new(:x, Multiply.new(Variable.new(:x), Number.new(3))) ) => «while (x < 5) { x = x * 3 }» >> statement.to_ruby => "-> e { while (-> e { (-> e { e[:x] }).call(e) < (-> e { 5 }).call(e) }).call(e); e = (-> e { e.merge({ :x => (-> e { (-> e { e[:x] }).call(e) * (-> e { 3 }).call(e) }).call(e) }) }).call(e); end; e }" >> proc = eval(statement.to_ruby) => #<Proc (lambda)> >> proc.call({ x: 1 }) => {:x=>9}
语义类型比较 «while» 是一个区分小步语义、大步语义和指称语义的好例子。
«while» 的小步操作语义是以一台抽象机器的归约规则形式写成的。整个循环并不是规约行为的一部分——规约只是把一个 «while» 语句转成一个«if» 语句——但是它会作为将来由机器执行的规约序列的一部分。为了理解 «while»做了什么,我们需要考虑所有的小步规则,并弄懂随着一个 Simple 程序的执行它们之间是如何互相作用的。
«while» 的大步操作语义是以一个求值规则的形式写成的,这个规则说明如何把最终的环境直接计算出来。这个规则包含了对其本身的递归调用,因此明显表明 «while» 在求值过程中会引发一个循环,但不是 Simple 程序员熟悉的那种循环。大步的规则是递归的形式,描述了如何根据对其他语法结构的求值对一个表达式或者语句完整地求值,因此这个规则告诉我们,对一个 «while» 语句求值的结果可能会依赖于一个不同环境下同样语句的求值结果,但把这种思想与 «while» 应该展现的迭代方式联系起来需要跳跃性思维。幸运的是这种跳跃并不太大:一点点的数学推理可以表明两种类型的循环在本质上是等价的,并且在元语言支持尾调用优化的时候,它们事实上也是等价的。
«while» 的指称语义展示了如何用 Ruby 对其重写,也就是如何通过 Ruby 的 while 关键字对其重写。这是一个简单直接得多的转换:Ruby 提供对迭代循环的原生支持,而指称规则也表明 «while» 能用 Ruby 的这个特性实现。要理解这两种类型的循环没有什么困难,所以如果我们理解了 Ruby 中 while 循环的工作方式,也能理解 Simple 的«while» 循环。当然,这意味着我们已经把理解 Simple 的问题转换成了理解指称语言的问题,而如果指称语言像 Ruby 一样庞大而且定义不良,这就是一个严重的缺点;但在有一个能用来写指称的小型数学语言时,这就成了一个优点。
2.4.3 应用
做完所有这些工作之后,指称语义完成了什么目标呢?它的主要目的是展示如何把 Simple翻译成 Ruby,它将后者作为工具来解释不同的语言结构是什么意思。这恰巧给了我们执行 Simple 程序的一种途径——因为已经用可执行的 Ruby 写下了指称语义的规则,而且这些规则的输出本身就是可执行的 Ruby——但这只是偶然事件,因为我们之前有可能用普通的英语写规则并用一些数学语言写下指称。真正重要的是我们自己随意设计了一种语 言,并把它转换成一种其他人或者其他东西能理解的语言。
为了赋予这种转换一些解释能力,把一部分语言含义放到表面而不再只是隐含在背后会非常有帮助。例如,这种语义把环境表示成具体的 Ruby 对象——在 proc 中传入和返回的散列,而不是把 Simple 中的变量表示成真正的 Ruby 变量,然后依赖 Ruby 自己微妙的变量作用域规则去定义 Simple 的变量访问机制;这样表示环境更为明确直接。在这方面这种语义除了把解释性的工作交给 Ruby,还多做了一些事情;它把 Ruby 作为一个简单的基础,但是在表面做了一些额外的工作,从而准确地展示了不同程序结构是如何使用和改变环境的。
这之前我们看到过,操作语义通过为一种语言设计一个解释器来解释这种语言的含义。与此对比,语言到语言的指称语义更像是一个编译器:在这种情况下,我们的#to_ruby实现高效地把 Simple 编译成 Ruby。这些类型的语义虽然都对如何为一种语言高效地实现一个解释器或者编译器只字不提,但确实提供了一个基础标准可以检验任何生效了的实现。
这些指称的定义还在一些语言的原始状态中出现过。早期版本的 Scheme 标准使用指称语 义(http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-10.html#%25_sec_7.2)定义核心语言,而不像现在的标准使用小步操作语义来定义,并且 XSLT 文本转 换语言的开发是由 Philip Wadler 对 XSLT 模式(http://homepages.inf/ed.ac.uk/wadler/topics/xml.html#xsl-semantics) 和 XPath 表 达 式(http://homepages.inf.ed.ac.uk/wadler/topics/xml.html#xpath-semantics)的指称定义来引导的。
3.3.2 节有一个实际使用指称语义定义正则表达式的例子。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论