GeneralUtilities` 是Mathematica从版本10开始新加入的一个上下文。其中提供了大量的实用函数,包括代码生成、调试、静态分析、迭代器对象等各个领域,一定程度上弥补了Mathematica基础设施不足的状况。
这个工具包内包含的函数十分繁杂,在当前版本11.3下,可以通过Names["GeneralUtilities`*"]//Length看到其共包含了514个符号。这篇文章简单介绍与宏有关的部分。
“宏”在编程领域往往作为一种代码生成技术使用,例如在一些编译型语言中,宏展开往往发生在编译或预编译阶段。而在GeneralUtilities` 的语境下,宏默认在定义时自动展开,同样也是一种代码生成技术。
1 | Needs["GeneralUtilities`"] |
下面介绍GeneralUtilities` 中的一些实用宏
Scope/ModuleScope
我们知道,Mathematica中的局部符号往往需要显式地引入,而不像很多语言在函数体内自动带有作用域。这在使用大量局部变量的时候会带来一些麻烦。而Scope给出了一个解决办法,它自动解析Scope[body]内部的赋值语句,提取与之相关联的符号自动局域化。通过?GeneralUtilities`Scope可以看到它的用法如下:
Scope[body]is a macro that expands to aBlockwith automatically populated local variable list.
- Variables are detected syntactically by the presence of
=and:=withinbody.^=and^:=can be used to avoid this localization.{sym1,sym2,...} = rhswill localizesym1,sym2,....sym := rhswill localizesym.- Local functions definitions
head[...] := rhsdo not cause localizalization ofhead.
而且它作为一个宏,使用在定义中,则展开发生在定义阶段,从而可以避免调用时因解析和变换带来的额外开销。例如
1 | f[x_]:=Scope[a=x;b^=a;c:=a++;{d,e}={b,c};a] |
可以看到f的定义中,Scope已经转换成了Block作用域,并自动将需要局域化的符号按Block的规则列出了。
类似的,ModuleScope自动展开成Module作用域结构。
Memoized
Memoized[body]specifies thatbodyshould be evaluted but cached so that subsequent calls with the same value for any bound symbol use the cached value.
Memoized[body,Method->method]can be used, wheremethodis one of{"Association", "Symbol", "Inline", "SystemCache"}, to choose a specific caching method.
即所谓的记忆化手法,可以实现空间换时间的优化目的。例如对于Mathematica中一个比较经典的记忆化案例
1 | fib[0]=0; |
利用Memoized可以等价地写成
1 | fib[0]=0; |
除了"Inline",Memoized还提供了其它几种记忆化的实现手段,包括默认的"SystemCache"、基于关联"Association"和面向符号的"Symbol",这里不多赘述了。不过似乎目前的"Symbol"方法的实现有问题,无法应用于函数定义中。
SetupTeardown
SetupTeardown[setup,body,teardown]evaluatessetup, thenbody, and thenteardown, even if anAbortorThrowoccurs during evaluation.
SetupTeardown是一个确保“初始化-主体-清理”执行顺序的封装,哪怕其中某部分中断或者抛出也会确保其余部分顺序进行。
举一个简单的例子
1 | f[]:=SetupTeardown[Print["setup"],Print["before"];Abort[];Print["after"],Print["teardown"]] |
调用f[],可以看到即使Abort[]中断计算后,依然继续进行了Print["teardown"]的计算。
这个宏可以用于在计算流程中确保资源的获取和释放,不过实际在Mathematica中应用比较少。
Match/StringMatch
Match[value,patt1:>val1,patt2:>val2,…,default]matches the value to thepattiin turn and gives the correspondingvali, or evaluatesdefaultif none matched.
Match[value,patts...,...]panics if none of thepattsmatched.
Match[patts]is the operator form ofMatch.
长得像Rust的match,用法大抵类似Switch,实际只是Replace的一个封装。
StringMatch无非是StringReplace的一个封装。
CatchFailure/CatchFailureAsMessage
CatchFailure[body]is a macro evaluatesbody, but returns aFailure[...]object if aThrowFailure[...]occurred during evaluation. The current function is automatically used as the message head.
CatchFailure[head,body]explicitly usesheadas the message head for the failure message.
CatchFailureAsMessage[body]is a macro evaluatesbody, but issues a message if aThrowFailure[...]occurred during evaluation. The current function is automatically used as the message head.
CatchFailureAsMessage[head,body]explicitly usesheadas the message head for the failure message.
需要配合ThrowFailure一起使用,语义比较清晰的错误捕获。
UnpackAssociation/UnpackOptions
UnpackAssociation[assoc,“Name1”,“Name2”,…]extracts the given keys from the association and sets variables namedNamei.
UnpackAssociation[assoc,symbol1,symbol2,…]uppercases the first letter of the symbol name to get the key.
UnpackAssociation[assoc,symbol1:“Name1”,…]gives an explicit name for each symbol.
UnpackOptions[sym1,sym2,…]extracts options with names“Sym1”, “Sym1”, …and assigns them to thesymi, where the keys are the title cased version of the symbol names.
用法说明已经介绍得比较详细了,下面给一个简单的用例
1 | Options[f] = {"A" -> 1, "Op" -> 2}; |
CollectTo
CollectTo需要配合BagInsert使用,实际是Internal`Bag相关函数的封装,目前来看意义不大,不过这个宏没有用法说明,也可能尚未完善。
根据定义,可以大致推测CollectTo[{x,y,...},body]可以通过在body部分使用BagInsert[x,val]或者BagInsert[x,val,index]的方式高效地将val添加或插入到动态列表x中。
下面给一个简单的用例说明一下用法
1 | f1[n_]:=Scope[CollectTo[{x},Do[BagInsert[x,i],{i,n}]];x] |
我的电脑上给出的结果为:
可以看到Bag的添加效率基本是普通列表的好几倍。
DoWhile
DoWhile[body,test]就像C语言之类的一样,先计算body,再计算test并决定是否循环,
实际上就等价于While[body;test]。
Excise
Excise[args...]evaluates to an empty sequence, effectively removing its arguments without evaluation.
个人认为没什么用,注释可以做到同样的事,甚至用途更广(Excise只能在使用宏的情况下工作)。
UseMacros
UseMacros[body]does nothing more than trigger macro expansion, use it if you want to use macros in a function but don’t needScope.
在不了解GeneralUtilities` 中宏的作用机理的情况下,这个宏的作用可能会令人困惑。事实上,在默认条件下,上述各种宏的自动展开只会发生在赋值等号右边的最外层使用了宏的时候发生。这一事实可以在上述各种的定义中一窥究竟,以Scope为例
1 | Scope /: HoldPattern[s:Set[_, _Scope]] := MacroEvaluate @ s; |
同样,UseMacros位于赋值等号右边的最外层时会触发宏自动展开,从而可以解决在内层使用宏无法展开的问题。
GeneralUtilities` 除了提供上述宏之外,还提供了一些计算和生成宏的辅助工具。
Quoted
Quoted[code]is the inert body ofcodefor the purposes of macro expansion.
一个代码封装,作用基本和HoldComplete一样,不过使用了一个漂亮的方式输出显示代码。
在宏的实现中大量使用。
MacroExpand/MacroExpandList
MacroExpand[expr]evaluates all macros present inexprand returns the result in aQuotedexpression.
Anywhere in a macro,'can be used to injectEchoRawcalls,''to injectEchoHoldcalls, and'''to wrap a function inTap.
MacroExpandList[expr]expands all macros that occur inexpr, returning a list ofQuotedexpressions that give the intermediate results after each expansion step.
MacroExpand展开宏,但是不计算,可以用来预览宏使用的效果。而MacroExpandList列出展开宏的每一步。
MacroEvaluate
MacroEvaluate[expr]evaluates all macros present inexprand then evaluates the result.
展开宏并计算。
可以尝试令$Pre=MacroEvaluate,这样每次计算都会尝试展开宏,从而也就不需要UseMacros来触发展开了。
MacroRules
MacroRules[symbol]gives all the macro application rules associated with the headsymbol.
可以通过PrintDefinitions[MacroRules]hack得到已定义的函数宏和变换规则。
DefineLiteralMacro/DefineMacro/DefineAlias
DefineLiteralMacro[symbol,lhs:=rhs,...]defines a literal macro such that whenlhsit is substituted forrhswithout evaluation.
The same replacement is attached to symbol for use outside a macro context.
DefineAlias[newsymbol,oldsymbol]does what it says on the tin.
这三者都用来实现自定义宏,但使用和效果上略有不同:
DefineLiteralMacro[symbol,lhs:=rhs,...]定义的是一个字面宏,在替换展开的过程中并不会计算rhs。DefineMacro[symbol,lhs:=rhs,...]定义的宏则会在展开时计算rhs。DefineAlias[newsymbol,oldsymbol]定义符号的别名,只对符号有效,而且其展开发生在其它宏展开之前。
尝试下面的例子以便更直观地认识到这三者间的联系和区别:
1 | DefineMacro[fac1, fac1[x_]:=x!]; |