Wolfram语言中的作用域结构主要有如下5种:
Module
Block
With
DynamicModule
- 命名空间
Module
按照文档中的说法,Module
提供变量的词法(lexical)定界。它使范围内的变量具有类似C语言中局部变量的行为。
然而,和C语言等不同,Wolfram语言没有对变量的名称和对象的区分,同样的名称总是用于指代同一变量。因此,Module
在创建局部变量时,实际是创建新的符号并进行替换,比如运行下面的代码:
1 | Module[{x}, x] |
从输出结果中可以看到形如x$nnn
的局部变量。
正如前面提到的,Module
提供的是变量的词法定界。因此,只有显式出现在作用域中的变量会被局部化。偶尔,这可能带来一些意想不到的结果。典型地,下面的代码
1 | m = i^2; |
会输出a+i2
。
为了有效地实现定界,Module
创建的变量都有Temporary
属性来控制变量的生存期。它采用类似引用计数的方式来考察一个变量是否还在使用,并决定是否要将变量移除。因此,我们基本不需要考虑可能的变量冲突。
1 | Module[{x}, Print[x]; Attributes[x]] |
多数时候,我们可能喜欢使用Module
,因为它确实会如我们期望地那样创建新的变量。但它毕竟不是万能的,只有始终明确它的含义和适用的场合,才能保证正确地使用它。
思考题1:
下面的程序会输出什么?
1 | x = 1; |
Block
与Module
不同,Block
提供变量的动态定界。它不产生新的变量,只是在作用域内临时地修改变量的值。因此,前面的例子如果用Block
改写
1 | m = i^2; |
则会输出a+a2
。
基于这种特性,Block
经常被用在将副作用局域化的场合。
比如,局部地进行深度递归
1 | cl[1] = 1; |
又比如,局部地清除值
1 | x = 0; |
值得注意的是,上面使用了Print
直接输出来避免从Block
返回的值再被计算。如果普通地从Block
返回值,则其中的表达式会由于x=0
而重新计算,从而输出1
。利用Trace
我们可以看到这其中的过程
1 | x = 0; |
1 | Block[{x},Expand[(1+x)^3]] |
Block
虽然很便利,但“成也动态定界败也动态定界”,它所带来副作用往往不是显然的,更容易引入一些潜在的错误。另一点值得注意的是,像Table
、Plot
之类的函数在运算时使用与Block相同的方式局部化变量的值。这意味着使用这些函数时必须同使用Block
一样小心
1 | f[x_] := i*x |
1 | {1, 4, 9, 16, 25} |
另一方面,这个特性在使用EvaluationMonitor
之类的监视器时会带来很大的方便。比如下面的代码可以直接追踪出求根过程中的步骤
1 | FindRoot[x^2 - 2, {x, 1}, EvaluationMonitor :> Print["x=", x, " Bias:", Abs[x^2 - 2]]] |
思考题2:
下面的程序会输出什么?与Module
时的情况进行比较。
1 | x = 1; |
With
With
的目的是实现局部常量,但它实质上不过是一个替换罢了。在大多数情况下
1 | With[{x = x0, y = y0}, expr] |
等价于
1 | Unevaluated[expr] /. {HoldPattern[x] -> x0, HoldPattern[y] -> y0} |
可以看到,With
并不会创建变量,相反,在替换的过程中往往还会减少变量。这个性质往往非常有用。比如,它可以把值插入到保持(held)表达式中:
1 | Table[With[{i = i}, Hold[i]], {i, 5}] |
输出为
1 | {Hold[1], Hold[2], Hold[3], Hold[4], Hold[5]} |
但如果不借助With
传递一下的话
1 | Table[Hold[i], {i, 5}] |
输出结果则会是
1 | {Hold[i], Hold[i], Hold[i], Hold[i], Hold[i]} |
在下面的延伸阅读中的“How To”主题里,可以看到有关这种性质更实际的用法。
思考题3:
下面的程序会输出什么?
1 | With[{x = y}, |
思考题4:
前面提到With
可以将值插入保持表达式,考虑如果将With
替换为Module
或者Block
是否能实现同样的效果?为什么?
思考题5:
前面提到,大多数情况下,With
可以等价于一个替换。那么,在什么情况下不能简单地进行替换?比较下面两段程序,思考造成差异的原因,
并考虑With
的适用范围。
1 | With[{y = x + a}, Function[{x}, x + y]] |
1 | Unevaluated[Function[{x}, x + y]] /. {HoldPattern[y] -> x + a} |
延伸阅读:
局部常量、How To | 在 Dynamic 或 Manipulate 内部计算表达式、纯函数和规则中的变量
DynamicModule
与Module
类似,DynamicModule
也建立变量的词法作用域,但两者又有不同:如果说Module
将变量局域在一个时间段的话,那么DynamicModule
将变量局域在其输出的一个空间区域上。而为了实现这一点,Module
对变量的局域化发生在内核中,而DynamicModule
对变量的局域化发生在前端。这也可以通过运行下面两段代码验证
1 | Module[{x}, Slider[Dynamic[x]]] // FullForm |
1 | DynamicModule[{x}, Slider[Dynamic[x]]] // FullForm |
从输出结果中可以看到DynamicModule在内核中是以不计算的方式保持着原本的形式,实际上DynamicModule
在前端产生一个DynamicModuleBox
的框符结构,它虽然不会像ButtonBox
或者RowBox
之类的显示成一个控件或者布局,但前端会根据它来对内部的变量局域化。
1 | DynamicModule[{x}, Slider[Dynamic[x]]] |
复制上面代码的输出,在后面加上 //ToBoxes
并计算,得到其框符表示。滑动滑动条,再次计算,可以看到滑动条的值其实是由DynamicModuleBox
结构所记录的。这也正是DynamicModule
内的状态能保存在文件中,并在跨越不同的内核会话时保持一致的原因。
相对地,Module
就没有这个能力,比如下面这段代码
1 | Module[{x}, Slider[Dynamic[x]]] |
由其得到的滑动条,随意滑动一下,如果关闭文件并退出内核的话,再次打开文件它会回到初始的位置上;而如果不关闭文件直接退出内核的话,甚至会出现拖动滑动条却无法将其移动的情况。对后一种情况,个人猜测是由于前端和内核重新连接后,前端原有控件没能和内核中的变量关联上的缘故。
思考题6:
分别将由DynamicModule
和Module
得到的滑动条复制到其它地方,拖动滑动条,观察其现象,思考造成这种现象的原因。
命名空间
命名空间也被称作上下文。顾名思义,它可以看作一段程序执行的语境,它影响符号的含义。
附带的笔记本中,在不同单元中多次出现符号x
,但它们之间没有任何关联,也不会互相干扰。这是由于该笔记本默认在每个单元编组都使用独立的上下文。通过计算$Context
获取当前上下文,可以得到一个形如Cell$$nnnn`
的上下文名称。
事实上,Wolfram语言中任何符号的全名都包括两个部分:上下文和短名。全名的典型形式是context`short
。其中,符号`
在Wolfram语言中被称为上下文标记,它是符号全名的一部分,在使用时又有些类似文件系统中的路径分隔符/
或\
。
正如我们在命令行环境下键入程序不需要完整的路径,系统会自动在PATH
环境变量指定的路径中搜索,在Wolfram语言中的如果只键入符号短名的话,系统首先会在$ContextPath
指定的上下文中搜索,如果在既有上下文中找不到该符号,才会在当前上下文中创建一个以此为短名的新符号。
上下文的应用通常和程序包联系在一起,以减少不同程序包间可能的符号冲突。因而在Wolfram语言中有两组典型的方式来开启一个上下文环境:
一组是Begin["context`"]
和End[]
;另一组是BeginPackage["context`"]
和EndPackage[]
。下面的代码简单演示了两者对上下文环境的作用
1 | Print["0:", $Context, "|", $ContextPath] |
1 | 0:Cell$$nnnn`|{Cell$$nnnn`,System`} |
可以看到,Begin
-End
所产生的作用比较纯粹,就是在其作用的范围内改变当前上下文$Context
,而对$ContextPath
毫无影响。相对地,BeginPackage
-EndPackage
则有几项副作用,它除了在作用范围内改变$Context
和$ContextPath
外,在使用EndPackage[]
离开其作用范围时不仅将$Context
和$ContextPath
复原,而且会将还原前的上下文添加到$ContextPath
中,从而方便我们直接使用导入包中的符号。
在实际的程序包开发中,这两种结构一般都会用到。比如在Mathematica自带示例程序包ExampleData/Collatz.m
中有如下代码
1 | BeginPackage["Collatz`"] |
这个例子中我们可以看到一个程序包典型的上下文结构安排。BeginPackage
-EndPackage
主要用于引入接口性质的符号,而具体实现部分则往往置于Begin
-End
结构中以尽可能避免符号污染。
思考题7:
x
和 `x
之间有什么区别?运行下面两段代码,观察结果。思考并理解`
的含义。
1 | x = 1; |
1 | `x = 1; |
延伸阅读:
上下文、上下文和程序包、建立 Wolfram 语言程序包、对不同的笔记本自动使用独立的上下文环境、处理符号名称遮盖的问题
相关代码交互见 笔记本。