模块化和事物的命名
模块和局部变量 | 上下文和程序包 |
局部常量 | Wolfram 语言程序包 |
模块工作方式 | 建立 Wolfram 语言程序包 |
纯函数和规则中的变量 | 程序包中的文件 |
数学中的哑元 | 包的自动调入 |
块和局部值 | 通过名称操作符号和内容 |
块与模块的比较 | 拦截新符号的产生 |
上下文 |
Module[{x,y,…},body] | 具有局部变量 x,y, … 的模块 |
Length 得到变量的个数:
Module[{x=x0,y=y0,…},body] | 局部变量有初始值的模块 |
lhs:=Module[vars,rhs/;cond] | 在 rhs 和 cond 中共用局部变量 |
With[{x=x0,y=y0,…},body] | 定义局部常量 x, y, … |
用 With 而不用 Module 的主要原因之一是使 Wolfram 语言程序容易理解. 在模块中的某一处遇到局部变量 x 时很可能需要跟踪整个模块的代码来得到 x 在该处的值. 而在 With 结构中,只需要观察初始值列表就能得到局部常量的值.
在嵌套 With 结构中,总是最里面的一个起作用:
除了 x 和 body 在何时计算外,With[{x=x0,…},body] 和 body/.x->x0 的做法基本类似. 然而,当表达式 body 内部都包含 With 或 Module 结构时, With 显示特殊的状态. 主要的问题是防止不同 With 结构中的局部常量相互冲突,或与全局目标冲突. 有关细节将在 "模块工作方式" 节讨论.
Wolfram 语言中模块的基本工作方式非常简单. 任何模块每一次使用时,就产生一个新符号去代表每一个局部变量. 新符号的名字被唯一地给定,不能跟任何其其名字冲突. 命名的方法是在给定的局部变量后加 $,并给出唯一的序号.
Module 中产生形如 x$nnn 的符号去代表每个局部变量. |
在绝大部分情况下,不需要直接涉及模块内产生的实际符号. 但在一个模块的执行过程中打开对话时就会看到这些符号. 同样,用函数如 Trace 等也能观察模块的计算.
用 Trace 观察模块内部产生的符号:
每调用一次 Unique 就得到一个序列号更大的符号:
对一个集合调用 Unique 时,得到每个符号有相同的序列号:
模块 Module 所产生的符号与计算中的符号性能相同. 但这些符号具有 Temporary 属性,当不再使用时会被系统删除,所以在模块内产生的符号当模块执行完时就被删除. 那些明确返回的符号才能继续存在.
这意味着将含有所产生符号的表达式存在一个文件中,然后在另一个进程中打开该文件时无法保证不冲突. 在实践中,这是不太可能发生的,因为生成的符号通常是临时的. 处理冲突的一种可能方法是,在读入带有临时符号的文件会话开始时,手动将 $ModuleNumber 设置为一个较大的值,例如 2^($SystemWordLength-8).
在产生了决定局部变量的符号后,Module[vars,body] 就用这些符号计算 body. 首先取模块中出现的 body 的实际表达式,用 With 将每个局部变量的名称用所产生的符号代替,然后模块 Module 来计算结果中表达式的值.
大部分情况下,通过 Module[vars,body] 的直接输入来在 Wolfram 语言中建立模块. 由于 Module 函数具有属性HoldAll,所以在模块执行之前 body 将维持不计算的状态.
Wolfram 语言在对纯函数中的形式参数重命名时比较自由. 原则上,函数中的形式参数与代换到纯函数中表达式的项不冲突时可以不用重新命名. 但为了一致起见,在这种情况下,Wolfram 语言还是对形式参数重新命名.
正如 "纯函数" 所述,Wolfram 语言中纯函数类似于形式逻辑中的 表达式. 对形式参数重新命名使 Wolfram 语言纯函数再次产生标准 表达式的所有语法.
Function[{x,…},body] | 局部参数 |
lhs->rhs and lhs:>rhs | 局部模式名 |
lhs=rhs and lhs:=rhs | 局部模式名 |
With[{x=x0,…},body] | 局部常数 |
Module[{x,…},body] | 局部变量 |
With 内的局部参数被重新命名以避免冲突:
当使用规则如 f[x_]->rhs 或定义如 f[x_]:=rhs 时,Wolfram 语言必须间接地替换出现在表达式 rhs 中的 x,用 /. 运算有效地完成此项工作. 于是,这些替换不遵循定界结构. 然而,当定界结构的内部由替换修改后时,在定界结构中的其他变量被重新命名.
另一个需要目标具有唯一性的地方是 “积分常数” 的表示. 积分时是解一个微分方程. 一般地,这有无限个解,每个仅差一个 “积分常数”. Wolfram 系统的标准函数 Integrate 总是得到一个没有积分常数的解. 但是,要引入积分常数时,必须用模块保证积分常数总是唯一的.
Wolfram 语言中的模块使变量名具有局部性,但有时需要名是全局的,值是局部的,这可以用 Wolfram 语言中的 Block 来实现.
如前一节 "模块和局部变量" 所述,在模块如 Module[{x},body] 中的变量 x 总是有一个唯一符号,模块每次调用这符号不同,且与全局符号 x 也有区别. Block[{x},body] 中的 x 是一个全局的符号 x. 这个块的作用是让 x 有局部值. 进入这个块时,x 的值在退出该块时恢复. 而在块执行时,x 可以取任意值.
在 Wolfram 语言中定义函数时,用不直接给出变量,但能影响函数的全局变量是方便的. 例如,Wolfram 语言有一个全局变量 $RecursionLimit,其影响所有函数的计算但又不直接是函数的变量.
在 C 和 Java 等编译语言中,其变量在使用之前就要声明类型,因此在编译前就已经确定了变量的类型;“代码” 和 “执行历史” 之间的区分非常明显. 而 Wolfram 语言属于动态类型语言,其符号特性使这个区别不明显,其原因是代码在程序的执行过程中可以动态地生成.
Module[vars,body] 的作用是在模块作为 Wolfram 语言的代码被执行时处理表达式 body 的形式,并检查表达式,当任何 vars 明显地出现在代码中时,就被当作局部变量. 然后继续正常计算.
Block[vars,body] 不注意表达式 body 的形式. 而是,记录 vars 的当前值. 在 body 的全局计算过程中使用 vars 的局部值. 并且当计算 body 完成时,存储他们的初始值.
在 Wolfram 语言中,可以用 “上下文” 来组织符合名. 在引入与其他符号不冲突的变量名的 Wolfram 语言程序包中上下文特别有用. 在编写或者调用 Wolfram 语言程序包时,就需要了解上下文.
其基本的思想是任何符号的全名为两部分:上下文和短名. 全名被写为 context`short,其中 ` 是倒引号或重音符字符( ASCII 二进制代码 96),在 Wolfram 语言中称为 “上下文标记”.
典型的情况是让与一个特殊的主题相关的符号有相同的上下文. 例如,表示物理单位的符号具有上下文PhysicalUnits`. 这类符号的全名可能是 PhysicalUnits`Joule 或者 PhysicalUnits`Mole.
在 Wolfram 语言进程中的任何点,总有一个当前上下文 $Context. 可以用短名简单地指代这个上下文中的符号,除非该符号被 $ContextPath 中具有相同短名的符号屏蔽. 如果具有给定短名的符号在上下文路径中存在,其将被使用,而不是当前上下文中的符号被使用.
上下文在 Wolfram 语言中的作用在某种程度上类似于许多操作系统的文件目录,可以通过路径和全名指定一个文件. 但在任何点,总有一个当前目录,这类似于 Wolfram 语言的当前上下文. 在当前目录下的文件就可以仅用其短名指定.
context `name or c1`c2 … `name | 在明确指定的上下文中的符号 |
`name | 当前上下文中的符号 |
`context`name or `c1`c2` … `name | 与当前上下文相关的在一个指定上下文中的符号 |
name | 在当前上下文或者在上下文搜索路径中的符号 |
为了方便地处理 Global` 和 System` 中的符号,Wolfram 语言支持上下文搜索路径. 在 Wolfram 语言进程中的任一点,有当前上下文 $Context 和当前上下文搜索路径 $ContextPath. 搜索路径的意思是在输入一个符号的短名后,Wolfram 语言在一系列上下文中搜索去找到有这个短名的符号.
Context[s] | 一个符号的上下文 |
$Context | 在 Wolfram 语言进程中的当前上下文 |
$ContextPath | 当前上下文搜索路径 |
$ContextAliases | 上下文的别名列表 |
Contexts[] | 所有上下文组成的集合 |
在 Wolfram 语言中使用上下文时,在两个不同的上下文中两个符号可以有相同的短名. 例如,在上下文 PhysicalUnits` 和 BiologicalOrganisms` 中都可以使用短名 Mole 的符号.
一般地,当输入一个符号的短名后,Wolfram 语言认为用户需要在上下文搜索路径中最早出现的上下文中的符号. 结果,上下文出现晚的符号或者在当前上下文中具有相同短名的符号将被屏蔽,为了调用这些符号必须使用全名.
如果引入的符号屏蔽了已经存在的符号,则需要重新安排 $ContextPath,或者直接删去这个符号. 用户应该意识到,仅清除这个符号的值是不够的,必须从 Wolfram 语言中全部删除这个符号. 这可以用函数 Remove[s] 来实现.
当输入了一个短名,其既不在当前上下文内,也不在上下文的搜索路径内,Wolfram 语言就产生一个具有该短名的新符号,且总是把新符号放在由 $Context 指定的当前上下文中.
约定在一个程序包中引入的新符号放在名与该程序包的相关的上下文内. 在这个包中阅读时,这个上下文将加在上下文搜索路径 $ContextPath 的开头.
这个包将上下文添加在 $ContextPath 之前:
在一个程序包中定义的变量的全名往往很长,大部分情况下,只需要用其短名即可,原因是在读入这个程序包时,其上下文被加在 $ContextPath 之中,当敲入短名后就会自动搜索这个上下文.
冲突不仅在不同程序包的符号间发生,而且也在程序包的符号与 Wolfram 语言的进程中引入的符号间发生. 在当前上下文中定义了符号后,这个符号将被从程序包中读入的短名相同的符号所屏蔽,其原因是 Wolfram 语言总是先在上下文搜索路径中寻找符号,然后才在当前上下文中寻找符号.
使用程序包中的 ScalarQ:
当不需要的符号屏蔽了所需的符号时,最好的途径是用 Remove[s] 去删除这个不需要的符号. 有时另一个合适的选择是去重新排列 $ContextPath 中的元素,重新设置 $Context 的值,以便包含所需符号的上下文先出现.
$Packages | 一个对应于 Wolfram 语言进程中加载的所有程序包的上下文集合 |
在这种情况下,用户或许能在 Wolfram 语言程序包中找到所需的函数. Wolfram 语言程序包是用 Wolfram 语言写成的文件. 这些文件是由 Wolfram 语言定义集合组成,使 Wolfram 语言能进行特殊应用领域的工作.
如果要使用某个程序包中的函数,必须首先把程序包读入 Wolfram 语言中. 具体做法将在 "外部程序" 节讨论. 使用软件包时有许多规定.
该程序包中定义了 ProvablePrimeQ 函数:
在不同程序包的函数名之间有许多矛盾. 这些将在 "上下文和程序包" 节讨论. 需要注意一点,在读入某个程序包之前不要使用名称与该程序包中的函数名相同的函数. 如果你错误地这样做了,Wolfram 语言将会发出一条警告信息并且使用最后定义的名称. 这意味着你定义的函数版本将不会被使用;而其将从程序包中得到. 必须执行命令Remove["name"] 来取消程序包函数.
Remove["name"] | 取消用户自定义的函数 |
为了进一步淡化 Wolfram 语言的边界, "包的自动调入" 节介绍了如何使 Wolfram 语言自动装入特定包. 如果用户从未使用过该包中某个函数,那么该函数是不在 Wolfram 语言中的,但是一旦用户要使用,就被从程序包中读入Wolfram 语言中.
当然,能够设置 Wolfram 语言,使得特定的程序包被预先装入或当用户需要的时候自动装入. 如果这样设置了,那么将有许多函数作为标准函数出现在所使用的 Wolfram 语言版本中,不过没有在 Wolfram 语言参考文档中出现.
有一点要提到的是程序包和笔记本之间的关系. 二者都是作为文件储存在计算机系统中,都能被读入 Wolfram 语言中. 但是,笔记本是要被显示的,而程序包只是作为 Wolfram 语言的输入. 事实上,许多笔记本包含了被认为是程序包的段落,其中包含着打算作为 Wolfram 语言输入的定义序列. 另外,还可以建立程序包来自动维护笔记本.
Package` | 用于输出的符号 |
Package`Private` | 仅在内部使用的符号 |
System` | Wolfram 语言中的内部符号 |
Needed1`
,
Needed2`
,
…
| 在程序包中需要的其他上下文 |
BeginPackage["Package`"] | 将 Package` 作为当前上下文,仅将 System` 放在上下文搜索路径中 |
f::usage="text"
,
…
| 引入作为输出的目标 |
Begin["`Private`"] | 设置当前上下文为 Package`Private` |
f[args]=value
,
…
| 给出在程序包中定义的主体 |
End[] | 转换到前一个上下文(这里为 Package`) |
EndPackage[] | 上下文结束程序包,将 Package` 放到上下文搜索路径前面 |
BeginPackage["Collatz`"]
Collatz::usage =
"Collatz[n] gives a list of the iterates in the 3n+1 problem,
starting from n. The conjecture is that this sequence always
terminates."
Begin["`Private`"]
Collatz[1] := {1}
Collatz[n_Integer] := Prepend[Collatz[3 n + 1], n] /; OddQ[n] && n > 0
Collatz[n_Integer] := Prepend[Collatz[n/2], n] /; EvenQ[n] && n > 0
End[ ]
EndPackage[ ]
定义在一个程序包开头使用 usage 信息的约定是在合适的上下文中产生用作输出的符号的一个基本的技巧. 在定义这些信息时,所涉及的符号都是用作输出的符号,这些符号都在作为当前上下文的 Package` 中产生.
在这个包中的 EndPackage 指令将与该包对应的上下文放入上下文搜索路径中:
可以明确告诉 Wolfram 语言在任一处读入程序包,使用的指令为 <<context`. ("程序包中的文件" 讨论了从与系统不相关的上下文名称到与系统相关的文件名之间的转换). 然后,设置为需要时就读入某一程序包. 指令 Needs["context`"] 告诉 Wolfram 语言当与程序包相关的上下文不在 $Packages 列表中时就读入这个程序包.
Get["context`"] or <<context` | 读入特定上下文对应的程序包 |
Needs["context`"] | 当指定的上下文不在 $Packages 中时读入程序包 |
BeginPackage["Package`",{"Needed1`", … }] | |
开始一个包,指定除 System` 外所需要的上下文 |
当使用一个具有一个变量的 BeginPackage["Package`"] 时,Wolfram 语言仅将 Package` 上下文和 Wolfram 语言内部符号上下文放在上下文的搜索路径之中. 在自定义包中涉及到其他包中的函数时,一定要确认这些包含在上下文的搜索路径之中,这可以在 BeginPackage 的第二个变量中给出另外的上下文列表即可. BeginPackage 就自动调用 Needs,必要时读入与这些上下文对应的程序包,确定这些上下文在上下文的搜索路径之中.
Begin 等上下文操作函数的执行改变了 Wolfram 语言翻译所输入名字的方式. 但要意识到这种改变仅对输入的下一个表达式有效,其原因是 Wolfram 语言总是先读入一个完整的表达式,在执行表达式的任何项之前翻译其中的名字. 于是,当一个特定的表达式中执行 Begin 时,表达式中的名称已被翻译,使 Begin 无法再产生效果.
例如,在执行一个包中定义的函数时用 TraceDialog 进行一个对话,函数中的参数和临时变量一般是在与这个有关的上下文中. 由于这个上下文不在上下文的搜索路径之中,Wolfram 语言将显示这些符号的全名,并且需要用户输入全名去调用这些变量. 但还可以用 Begin["Package`Private`"] 使包专用上下文成为当前上下文. 这就使 Wolfram 语言显示符号的短名,也就可以用短名调用这些符号.
其基本思想是每一个计算机系统有一个约定,决定怎样根据 Wolfram 语言的上下文去命名文件. 当用一个上下文去引用一个文件时,所用的 Wolfram 语言版本就将上下文名转换为适应于所用计算机系统的文件名.
name.mx | DumpSave 格式文件 |
name.mx/$SystemID/name.mx | 所用计算机系统的 DumpSave 格式文件 |
name.m | Wolfram 语言资源格式文件 |
name/init.m | 一个目录的初始化文件 |
dir/… | 在 $Path 指定的其他目录中的文件 |
Wolfram 语言的设置使 <<name` 自动装载一个文件的适当版本. 其先试装载对所用计算机系统优化过的 name.mx 文件,然后如果找不到这样的文件,则试装载与常用系统无关的 Wolfram 语言输入的 name.m 文件.
当 name 是一个目录时,Wolfram 语言就试装载该目录中的初始化文件 init.m. init.m 文件的目的是为设置含有许多文件的 Wolfram 语言程序包提供一个方便的途径. 其思想是先给出一个指令 <<name`,然后装载 init.m 将整个程序包初始化,需要时读入其他相关文件.
可以用 DeclarePackage 去给出在一个包中定义的符号名,然后当某一符号被调用时,Wolfram 语言就自动调入包含这个符号的包.
DeclarePackage["context`",{"name1","name2",…}] | |
指明当名为 namei 的符号使用时自动调入一个包 |
第一次使用 VariationalD 时,Wolfram 语言自动调入定义的包:
当建立大量的 Wolfram 语言程序包时,最好产生一系列 DeclarePackage 命令的名称文件,以便在某一名称被使用时调入一个程序包. 在一个 Wolfram 系统进程中,仅需要调入名称文件,随后其他的程序包在需要时就被自动调入.
DeclarePackage 对指定的名称立即产生符号,但给每个称号一个特定的属性 Stub. 当 Wolfram 语言发现一个符号具有 Stub 属性时就自动调入与该符号对应的内容以得到这个符号的定义.
Symbol["name"] | 建立一个具有给定名称的符号 |
SymbolName[symb] | 找出一个符号的名称 |
NameQ["form"] | 测试是否有与 form 匹配的已命令名符号 |
Names["form"] | 给出与 form 匹配的符号列表 |
Contexts["form`"] | 给出与 form 匹配的所有上下文名称的列表 |
Clear["form"] | 删除名与 form 匹配的所有符号的值 |
Clear["context`*"] | 删除在指定上下文中所有符号的值 |
Remove["form"] | 完全删除名称与 form 匹配的符号 |
Remove["context`*"] | 在指定的上下文中删除所有符号 |
Remove["Global`*"] | 完全删除在 Global` 的值 context 中的符号 |
当没有建立其他上下文时,所有在 Wolfram 语言进程中引入的符号都放在 Global` context 中. 可以用Remove["Global`*"] 完全删除这些符号. 而 Wolfram 语言的内部对象都在 System` context 中,所以他们不受影响.
在 Wolfram 语言产生新符号时显示信息是发现输入错误的一个好方法. Wolfram 语言自身不能区分要产生的新名称或者一个已有的拼写错误,但通过提示新产生符号的信息,Wolfram 语言可以使我们看到是否有错.
$NewSymbol | 作用于新产生符号的名称和内容上的函数 |
当 Wolfram 语言产生一个新符号时,有可能不需要显示信息,需要做一些其他的事情. 所指定的全局变量 $NewSymbol 函数将自动作用到给出 Wolfram 语言所产生的每一个新符号名称的字符串上.