简单易懂的程序语言入门小册子(9):环境,引入环境

\newcommand{\mt}[1]{\text{#1}}
\newcommand{\mE}{\mathcal{E}}
\newcommand{\tup}[1]{\left<{#1}\right>}

环境类似于其他语言(C++、JAVA等)的“符号表”。 所谓符号表,是一张将变量名与变量代表的内容联系起来的一张表。
不过这里我们抛弃符号表的观点,单纯地从算法角度上引入环境这一概念。

引入环境

通过修改解释器求值过程的算法,可以很自然的引入环境这个概念。

在前面基于文本替换的解释器里可以看到,所谓的“计算”,其实不过是一堆符号根据某种规则替换来替换去最终得到一个不能再归约的结果的过程。
这个替换过程需要遍历表达式。 比如计算表达式(\lambda X.M \;
V)(λX.MV)

,需要进行替换M[X \leftarrow V]M[X←V]

。 这个替换过程需要遍历表达式MM

。 而求值过程本身也需要遍历表达式MM

。 这样就至少需要遍历两次表达式MM

。 另外,替换过程本身也很麻烦,其中有这么一条规则: \begin{equation*}\begin{array}{lcl}   (\lambda
X_1.M)[X_2 \leftarrow N] &=& (\lambda X_3.M[X_1 \leftarrow X_3][X_2
\leftarrow N]) \\   &&  
\begin{array}{ll}     \text{其中} & X_1 \neq X_2, X_3
\notin FV(N), \\     & X_3 \notin
FV(M)\backslash\{X_1\}   \end{array} \end{array}\end{equation*}

(λX1.M)[X2←N]  =(λX3.M[X1←X3][X2←N])      其中    X1≠X2,X3?FV(N),X3?FV(M)?{X1}  

在这种情况下,一次替换要遍历两次MM

这样一来,在原来的算法里,由于直接使用替换,计算一个表达式需要一遍遍地遍历这个表达式的子表达式们。 这是一个效率很低的方法。
为了避免这些多余的遍历过程,我们可以延后替换过程,等到遍历到需要替换的变量时再对这个变量做替换。 这样只需要一次遍历就够了。
为了延后替换过程,需要一个保存这些“延后的替换”的数据结构。 这个数据结构叫做环境。 我们用字母\mE

表示环境。

下面举个例子来粗略地说明如何用环境来进行计算(粗略的展示,不代表实际的计算步骤): \begin{equation*}\begin{array}{lcl}  
&& (\underline{(\lambda x.\lambda y.(+ \; x \; y) \; 11)} \; 22)
\\   &\rightarrow& \underline{(\tup{\lambda y.(+ \; x \; y),
\tup{(x \leftarrow 11)}} \; 22)} \\   &\rightarrow&
\underline{\tup{(+ \; x \; y), \tup{(y \leftarrow 22), (x \leftarrow 11)}}}
\\   &\rightarrow& \tup{\underline{(+ \; 11 \; 22)}, \tup{(y
\leftarrow 22), (x \leftarrow 11)}} \\   &\rightarrow&
\tup{\underline{33}, \tup{(y \leftarrow 22), (x \leftarrow 11)}} \\  
&\rightarrow& 33 \end{array}\end{equation*}

引入环境后,一个表达式就可能不是一个“完整的表达式”了。 因为这个表达式可能有些自由变量应该被替换,只是替换被延后了,所以它看起来仍保持还没被替换的样子。
所以,一个“完整的表达式”,应该包括表达式和“延后的替换”的信息——也就是环境。
表达式和环境这个二元组叫做闭包,记为\tup{M,
\mE}

我一直故意没有描述环境的具体定义。 有了闭包这个概念后,现在可以说说环境的定义了。
从用途上看,环境可以理解成一个函数,它的输入是一个变量,它的输出是这个变量应该被替换成的东西。 这个“被替换成的东西”是什么东西呢? 一个直接的想法是表达式。
但这是不正确的! 因为一个表达式可能不是一个“完整的表达式”。 所以这个“被替换成的东西”应该是一个闭包。 也就是说,环境是一个将变量映射成闭包的函数:
\mE: X \rightarrow \tup{M, \mE}

为什么定义环境这么麻烦? 因为它是个递归定义。 考虑上闭包的话,环境和闭包就是互递归定义。

构造环境

考虑表达式(\mt{let} \; a \; 1 \; (\mt{let} \; a \; 2
\; a))

。 这个表达式共有两次替换,第一次将变量a

替换成1

,第二次将变量a

替换成2

,而表达式最终的值是2

。 可以看到,当遇到同名变量时,后替换的变量有效。 用于保存替换信息的环境应该具有后进入先找到(栈!)的性质。 环境可以用链表的形式保存。
每次保存替换信息时,将要替换的变量和应该替换成的闭包插入到链表的头部; 而查找是从头部开始查找。

构造环境的方法如下面公式所示: \begin{equation*}\begin{array}{lcl}   \mE
&=& \mt{empty-env} \\       &|&
\tup{\mt{extend-env}, \mE, X, \tup{M, \mE‘}} \end{array}\end{equation*}

\mt{empty-env}

表示空环境。 空环境没有保存任何的“延后的替换”。 由于空环境表示没有替换,所以对空环境输入任何变量X

,它的输出是个包含X

的闭包\tup{X, \mt{empty-env}}

(另一种处理方式是输出错误“未定义/无绑定的变量X

”)。 \mt{extend-env}

这一行扩展了原来的环境,这个构造过程称为将变量X

绑定到闭包\tup{M, \mE‘}

当输入变量X

时,从链表的头部开始搜索,返回第一次找到变量X

时对应的闭包。 用\mE(X)

表示在环境\mE

中查找X

的过程,这个过程如下: \begin{equation*}\begin{array}{lcl}  
\mt{empty-env}(X) &=& \tup{X, \mt{empty-env}} \\  
\tup{\mt{extend-env}, \mE, X, \tup{M, \mE‘}}(X) &=& \tup{M, \mE‘}
\\   \tup{\mt{extend-env}, \mE, X‘, \tup{M, \mE‘}}(X) &=&
\mE(X) \\   && \text{其中} X‘ \neq X
\end{array}\end{equation*}

CEK machine

还记得CK machine吗? CEK machine指的是加入环境后的CK machine。

为了简化问题,暂时先不考虑递归表达式(\mt{fix} \; X_1 \; X_2 \;
M)

。 无fix表达式的Alligator的语法(我真的没在凑字数): \begin{equation*}\begin{array}{lcl}   M, N, L
&=& X \\          
&|& b \\          
&|& \lambda X.M
\\           &|& (+ \;
M \; N) \\          
&|& (- \; M \; N)
\\           &|&
(\mt{iszero} \; M)
\\           &|& (M \;
N) \\ \end{array}\end{equation*}

CEK machine的求值过程从CK machine的求值过程稍微改改得到。 无fix表达式的Alligator的语法中只有函数调用(M \; N)

会用到替换。 现在我们不使用替换,取而代之的是扩展环境。 其他的改动还有:原来的表达式M

改成闭包\tup{M, \mE}

; 而变量X

现在要到环境查找对应的闭包\mE(X)

。 另外,Alligator使用call-by-value的调用顺序,所以要求环境值域的闭包里的表达式是一个值。 也就是说,CEK
machine里用到的环境应该是这样的函数: \mE: X \rightarrow \tup{V,
\mE}

新的求值过程如下(可以对比一下CK machine的求值过程)(公式太多mathjax出问题只好不得不截图……):

递归的处理

fix表达式的求值过程需要特别的处理。 按照上面的技巧修改fix表达式的求值过程,这个求值过程有如下形式:
\tup{\tup{(\mt{fix} \; X_1 \; X_2 \; M), \mE}, \kappa}_v
\rightarrow_v   \tup{\tup{\lambda X_2.M, \mE^{fix}}, \kappa}_c

关键是\mE^{fix}

是什么? 由于\lambda X_2.M

还有自由变量X_1

,所以\mE^{fix}

是在\mE

基础上将变量X_1

绑定到代表这个递归函数的闭包。 这个闭包是\tup{\lambda X_2.M,
\mE^{fix}}

。 所以\mE^{fix}

满足下面这个方程: \mE^{fix} = \tup{\mt{extend-env}, \mE,
X_1, \tup{\lambda X_2.M, \mE^{fix}}}

这个方程是没法解出来咯。 只能换个方式来得到\mE^{fix}

。 直接扩展构造环境的方法: \begin{equation*}\begin{array}{lcl}   \mE
&=& ... \\       &|&
\tup{\mt{extendrec-env}, \mE, X_1, X_2, M} \end{array}\end{equation*}

由\mt{extendrec-env}

方式扩展的环境在查找变量X

时的行为如下: \begin{equation*}\begin{array}{lcl}  
\tup{\mt{extendrec-env}, \mE, X, X_2, M}(X) &=&    
\tup{\lambda X_2.M, \tup{\mt{extendrec-env}, \mE, X, X_2, M}} \\  
\tup{\mt{extendrec-env}, \mE, X_1, X_2, M}(X) &=& \mE(X) \\  
&& \text{其中} X_1 \neq X \end{array}\end{equation*}

增加了构造环境方法后,fix表达式的求值过程如下: \tup{\tup{(\mt{fix}
\; X_1 \; X_2 \; M), \mE}, \kappa}_v \rightarrow_v   \tup{\tup{\lambda
X_2.M, \tup{\mt{extendrec-env}, \mE, X_1, X_2, M}}, \kappa}_c

代码实现

写代码咯,首先,用Racket的结构来表示闭包:

环境的代码,同样用结构来表示环境:

函数\mt{apply-env}

是在环境中查找变量的过程:

最后是求值过程的代码:

简化CEK machine

CEK machine里环境的值域只有三种类型的闭包: 包含变量的闭包\tup{X,
\mt{empty-env}}

、 包含常数的闭包\tup{b, \mE}

以及包含函数的闭包\tup{\lambda X.M, \mE}

。 这三种闭包只有函数的情况需要环境。 而变量和常数的情况下不需要环境。 修改值类型为: \begin{equation*}\begin{array}{lcl}   V
&=& X \\     &|& b
\\     &|& \tup{\lambda X.M, \mE}
\end{array}\end{equation*}

我们一直以来将值当作一种特殊的表达式,也就是说值是表达式的子集。 而修改后的值类型已经不是表达式的子集了。

修改环境的定义,将环境的定义改为变量到值的映射: \mE: X \rightarrow V

构造环境的方法: \begin{equation*}\begin{array}{lcl}   \mE
&=& \mt{empty-env} \\       &|&
\tup{\mt{extend-env}, \mE, X, V} \\      
&|& \tup{\mt{extendrec-env}, \mE, X_1, X_2, M}
\end{array}\end{equation*}

求值过程的简化主要是去掉一些不必要的闭包的构造。 简化后的求值过程如下: \begin{equation*}\begin{array}{lcl}   \tup{X,
\mE, \kappa}_v &\rightarrow_v& \tup{\mE(X), \kappa}_c \\  
\tup{b, \mE, \kappa}_v &\rightarrow_v& \tup{b, \kappa}_c \\  
\tup{\lambda X.M, \mE, \kappa}_v &\rightarrow_v& \tup{\tup{\lambda X.M,
\mE}, \kappa}_c \\   \tup{(\mt{fix} \; X_1 \; X_2 \; M), \mE,
\kappa}_v &\rightarrow_v&     \tup{\tup{\lambda
X_2.M, \tup{\mt{extendrec-env}, \mE, X_1, X_2, M}}, \kappa}_c \\  
\tup{(o^n \; M_1 \; M_2 \; ... \; M_n), \mE, \kappa}_v
&\rightarrow_v&     \tup{M_1, \mE, \tup{\mt{opd},
\kappa, \mE, o^n, (M_2 \; ... \; M_n), ()}}_v \\   \tup{V, \mE‘,
\tup{\mt{opd}, \kappa, \mE, o^n, (M_{i+1} \; ... \; M_n), (V_1 \; ... \;
V_{i-1})}}_c    
&\rightarrow_c&     \tup{M_{i+1}, \mE,
\tup{\mt{opd}, \kappa, \mE, o^n, (... \; M_n), (V_1 \; ... \; V_{i-1} V)}}_v
\\   \tup{V, \tup{\mt{opd}, \kappa, \mE, o^n, (), (V_1 \; ... \;
V_{n-1})}}_c &\rightarrow_c&     \tup{V‘, \kappa}_c
\\   && \text{其中} V‘ = o^n(V_1, ..., V_{n-1}, V)
\\   \tup{(M \; N), \mE, \kappa}_v &\rightarrow_v& \tup{M,
\mE, \tup{\mt{arg}, \kappa, \mE, N}}_v \\   \tup{V, \tup{\mt{arg},
\kappa, \mE, N}}_c &\rightarrow_c&     \tup{N, \mE,
\tup{\mt{fun}, \kappa, V}}_v \\   \tup{V, \tup{\mt{fun}, \kappa,
\tup{\lambda X.L, \mE}}}_c &\rightarrow_c&    
\tup{L, \tup{\mt{extend-env}, \mE, X, V}, \kappa}_v
\end{array}\end{equation*}

代码实现:略。

思考


  1. 用函数表示环境。

  2. 关于递归的处理,\mt{extendrec-env}

    的扩展方式可能看起来有点违和感。
    使用带有“副作用”的方法可以避免这种扩展方式的使用。修改环境为带有存储的环境(方括号表示这个是一个引用,这个引用指向的存储里的值是V

    ): \begin{equation*}\begin{array}{lcl}   \mE
    &=& \mt{empty-env} \\       &|&
    \tup{\mt{extend-env}, \mE, X, [V]} \end{array}\end{equation*}

    另外增加一个修改存储的方法(\mt{set-value-env!} \; \mE \;
    V‘)

    , 其中这里的\mE=\tup{\mt{extend-env}, \mE, X,
    [V]}

    。 这个方法修改环境\mE

    为\tup{\mt{extend-env}, \mE, X, [V‘]}

    。为了计算fix表达式(\mt{fix} \; X_1 \; X_2 \;
    M)

    扩展的环境\mE^{fix}

    , 先将X_1

    绑定到一个未初始化的值: \mE^{fix} = \tup{\mt{extend-env},
    \mE, X_1, [uninitialized]}

    其中uninitialized

    表示未初始化。 然后修改\mE^{fix}

    : (\mt{set-value-env!} \; \mE^{fix} \;
    \tup{\lambda X_2.M, \mE^{fix}})

    这样就能得了\mE^{fix}


  3. 这是一个算法练习。 在一些代码分析过程中,知道代码中所有变量的使用次数是很有用的。 我们希望统计所有变量的使用次数,并标记在变量声明的位置。
    例如输入一段代码:

    ?





    1

    2

    3

    4

    5

    6

    7

    (((lambda
    x

        (lambda
    y

          (+
    ((lambda x x)

              33)

             (+
    y (+ x x)))))

       11)

     22)

    输出:

    ?





    1

    2

    3

    4

    5

    6

    7

    (((lambda
    (x 2)

        (lambda
    (y 1)

          (+
    ((lambda (x 1) x)

              33)

             (+
    y (+ x x)))))

      11)

     22)

    如何实现这个算法?要求只遍历代码一次。

简单易懂的程序语言入门小册子(9):环境,引入环境,布布扣,bubuko.com

时间: 2024-10-16 00:57:33

简单易懂的程序语言入门小册子(9):环境,引入环境的相关文章

简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation

当我写到这里的时候,我自己都吃了一惊. 环境.存储这些比较让人耳熟的还没讲到,continuation先出来了. 维基百科里对continuation的翻译是"延续性". 这翻译看着总有些违和感而且那个条目也令人不忍直视. 总之continuation似乎没有好的中文翻译,仿佛中国的计算机科学里没有continuation这个概念似的. Continuation这个概念相当于过程式语言里的函数调用栈. 它是用于保存"现在没空处理,待会再处理的事"的数据结构. 这样说

简单易懂的程序语言入门小册子(7):基于文本替换的解释器,加入continuation,重构解释器

或许在加入continuation之前要先讲讲费这么大劲做这个有什么意义. 毕竟用不用continuation的计算结果都是一样的. 不过,这是一个兴趣使然的系列,学习这些知识应该完全出于好奇与好玩的想法. 所以我才不会告诉你们通过控制continuation可以实现call-with-current-continuation和异常处理等功能呢. 我先简要描述一下加入continuation后解释器是怎么工作的. 加入continuation后的解释器是以迭代的方式工作的. 迭代的状态量有两个,

《Go语言入门》第一个Go语言Web程序——简单的Web服务器

概述 上一篇讲了 <Go语言入门>第一个Go语言程序--HelloWorld,接下来讲一下Go语言Web开发入门必修课:第一个Go语言Web程序--简单的Web服务器. 与其它Web后端语言不同,Go语言需要自己编写Web服务器. 有关本地环境的搭建与基础学习,请参考: <Go语言入门>如何在Windows下安装Go语言编程环境 Go语言Web应用:IBM的云平台Bluemix使用初体验--创建Go语言 Web 应用程序,添加并使用语言翻译服务 Web服务器代码 Google在ht

《Go语言入门》如何在Windows下安装Go语言编程环境

概述 本文为Go语言学习入门第一篇,<Go语言入门>如何在Windows下安装Go语言编程环境 . 主要讲Go语言编译环境的安装以及基于Notepad++(Go语言插件.语法高亮)的开发环境配置. 下载安装包 安装包下载地址:https://golang.org/dl/ 这里选择下载Windows版本,点击链接打开的页面可能不会开始下载:地址栏里会显示完整的下载地址,如:https://golang.org/doc/install?download=go1.5.1.windows-amd64.

C语言入门(1)——C语言概述

1.程序与编程语言 我们使用计算机离不开程序.程序告诉计算机应该怎样运行.程序(Program)是一个精确说明怎样进行计算的指令序列.这里的计算能够是数学运算,比方通过一些数学公式求解,也能够是符号运算.比方我们使用world编辑一个文档或是通过搜索引擎检索信息. 计算机本质上是由数字电子电路组成的运算机器,全部的运算都仅仅能通过数字来表示和处理.通过将各种数据转换为数字表示,能够使计算机处理各种信息.比方音乐.图片.电影等. 程序由一系列指令组成的,指令就是指挥计算机做某种运算的命令.通常包括

C语言入门guide (仅供参考)

C语言书籍推荐 浙工大图书馆中,计算机的书都集中在三楼TP区. <Head First C>.Head First系列的书质量基本都很高.该书有很多插图,总体上就是用一种轻松愉快的方式给新手讲解令人头疼的C语言.书虽然有些厚但其实并没多多少字,内容上也相当丰富,英文原版的阅读难度大概也就高中阅读理解的难度.图书馆有其中译本<嗨翻C语言>,但我强烈推荐去看其英文影印版<深入浅出C语言>,一来这书的英文版本身难度就不大,二来容易树立阅读英文资料的信息(特别是看到自己看完这么

使用NSIS制作Windows安装程序快速入门

使用NSIS制作Windows安装程序快速入门 这里使用的NSIS版本为3.04,HM NIS Edit版本为2.0.3. 制作安装程序的过程: 确定安装的功能和界面元素 编写 NSIS 脚本 使用 NSIS 提供的 makensis 或者 makensisw 程序,将步骤 2 编写的脚本编译成可执行的安装程序(点击HM NIS Edit编译按钮) 配置使用 NSIS 的环境 安装 NSIS 安装脚本编辑工具 HM NIS Edit 也可以使用VS Code安装NSIS脚本插件 使用HM NIS

【南阳OJ分类之语言入门】80题题目+AC代码汇总

声明: 题目部分皆为南阳OJ题目. 代码部分包含AC代码(可能不止一个)和最优代码,大部分都是本人写的,并且大部分为c代码和少部分c++代码and极少java代码,但基本都是c语言知识点,没有太多差别,可能代码有的写的比较丑,毕竟知识有限. 语言入门部分题基本都较为简单,是学习编程入门的很好练习,也是ACM的第一步,入门的最佳方法,望认真对待. 本文由csdn-jtahstu原创,转载请注明出处,欢迎志同道合的朋友一起交流学习.本人QQ:1373758426和csdn博客地址. now begi

c语言入门经典(第5版)

文章转载:http://mrcaoyc.blog.163.com/blog/static/23939201520159135915734 文件大小:126MB 文件格式:PDF    [点击下载] C语言入门经典(第5版)  内容简介: C语言是每一位程序员都应该掌握的基础语言.C语言是微软.NET编程中使用的C#语言的基础:C语言是iPhone.iPad和其他苹果设备编程中使用的Objective-C语言的基础:C语言是在很多环境中(包括GNU项目)被广泛使用的C++语言的基础.C语言也是Li