问题的引入
ValueQ
是Matheamtica中的一个非常有趣的函数,它判断一个表达式是否会进行求值,而且这种判断有时可以做到无副作用,哪怕求值本身是具有副作用的。比如
1 | x=0; |
会得到{True,0}
的结果。这里的ValueQ[y]
显然并没有真正对y
求值就作出了判断。不过很遗憾的是这其中哪怕只有略微的改动也会使其作用发生变化,例如
1 | x=0; |
则会得到{True,1}
。
具有值集知识的读者可能很容易猜想ValueQ
可能具有类似下面的实现
1 | Attributes[ValueQ] = {HoldAll, Protected, ReadProtected} |
因而具有上面那样的行为也是可以理解的。
不过这种不一致性总归让人觉得别扭,尽管这种不一致性本质上来源于Wolfram语言求值策略的复杂性,尤其是Condition
的灵活性,使得上面这种策略注定无法真正消除副作用。哪怕是现在这种状况,我们依然可以让ValueQ
对符号作用时产生副作用,
例如
1 | x=0; |
也会得到{True,1}
。这里用到的技巧叫Trott-Strzebonski,是一种常用的元编程技巧,不过这里不做赘述,对于理解Condition
的工作方式的读者想必并不复杂。
因此,这里想介绍另一种消去副作用的思路,也就是所谓的“符号沙盒”。
思路与实现
如果说ValueQ
的思路是尽可能避免求值来消去副作用的话,那么接下来的处理方式则是尽可能将副作用局域化,就好像在沙盒环境中一样。而这也正是标题由来。
事实上,Block
动态作用域就具有类似的效果。不过直接使用Block
的话则略显繁琐,我们希望有一个能自动分析需要局域化的符号并且能继承既有定义的动态作用域。幸运的是,Mathematica中既有的函数足以覆盖我们的需求:Internal`EmbeddedSymbols
和Internal`InheritedBlock
。尽管这两者都是没有文档的“内部函数”,不过通过测试和分析可以推测他们的功能:
Internal`EmbeddedSymbols
:可以获取一个表达式所有直接或间接使用的符号Internal`InheritedBlock
:继承符号既有定义的动态作用域
组合这两者不难得到
1 | SetAttributes[{embeddedUserSymbols,symbolSandbox},HoldFirst] |
这里需要注意的是Internal`EmbeddedSymbols
获得的符号不能全部局域化,因此加入了Select
滤过具有Locked
和Protected
属性的符号。
然后我们可以给出一个更“安全”的ValueQ
1 | SetAttributes[valueQ,HoldFirst] |
此时
1 | x=0; |
会给出{True, 0}
的结果,x
因对y
的求值而产生的改变被限制在valueQ
的计算过程中。
不过这种局域化仅限于符号,并不能限制诸如文件IO等其它方式的副作用,因此只能叫“符号沙盒”。
补充:
经过更多测试,发现Internal`EmbeddedSymbols
似乎有不太稳定,有时会导致内核崩溃,而且似乎不能完全获取有关的符号。这部分可能有待改进。