背景和问题的引入
众所周知,Wolfram语言作为一个极为“高级”的编程语言,并没有提供指针这类较为底层的内存管理手段。而符号本身几乎总是充当了类似引用的作用,比如
1 | set[sym_Symbol]:=sym=1 |
可以看到符号a
的值通过set[b]
赋为了1
。在这里b
形式上充当了类似其它语言中的引用的作用。但这种方式并不通用,只要a
已经具有值了,这种平凡的方式就不能起作用了。有的读者可能会想到:=
或者Hold
封装,不过单靠这些也不能简单地对已具有本值的符号进行修改。因此,本文试图提出一种具有类似其它语言中指针语义的封装,实现对符号的间接操作。
预期的目标
这里想实现一种具有类似C语言用法的指针(引用)封装。当然,由于Mathematica没有直接访问内存的手法,所有引用都是针对符号展开的。具体而言,应该可以实现下面的效果:
解引用:
1 | a=1; |
得到1
;
左值语义:
1 | a=1; |
得到2
;
更复杂的左值:
1 | a={1,2,3}; |
得到{1,4,3}
;
多重引用:
1 | a=1; |
得到2
。
这里只是举几个例子,文章毕竟不是测试集,也就不再赘述。
当然,我们希望能更接近C语言指针的语义。在这里,ref
相当于C语言中的&
单目运算符,而deref
则相当于C语言的*
单目运算符。
思路与实现
考虑我们需要保有符号,才可能对符号进行引用,因此首先ref
必须具有HoldFirst
属性。然后deref
能在计算时解引用,也就是
1 | deref[ref[sym_]] := sym |
然后我们需要重载Set
以获得左值语义。由于deref
可能出现在很深的层次中,单靠TagSetDelayed
不能覆盖重载的各种情况。但需要重载的情况必定含有deref
,因此可以利用条件模式决定重载策略
1 | Unprotect[Set]; |
由于引用在这里总是保有着符号,因此左值语义在这里更类似一个宏展开,将=
左侧的所有直接或隐含的deref[ref[sym]]
结构展开成对应的sym
。为了达到这个目的,首先需要保护=
的左侧不进行计算,这里只需要一个简单的Hold
封装即可。然后上面的所说的“宏展开”细节上应该分两步进行:
一是将“隐含”转换为“显含”:直观上来说,可能大家会写成这样的替换规则:deref[r_]:>deref[Evaluate[r]]
,但这是不对的。由于Hold
封装的存在,需要用到一个名为Trott-Strzebonski的技巧,而且由于前面已经定义了deref[ref[sym_]]:=sym
,因此这里必须借助Block
局部地清除这个定义才行,另一方面,为了处理多重引用,还需要排除deref[deref[xxx]]
这种模式被直接展开。
二是对显式结构进行替换:这部分比较简单,直接使用deref@ref[sym_Symbol]:>sym
即可。同样也是因为Hold
封装的存在,符号sym
即使有本值也不会计算。
最后将展开的结果还原为赋值表达式即可。
综合一下,我们可以得到一个比较完整的代码
1 | SetAttributes[ref, HoldFirst] |
当然,对于一个真正实用的模块而言,还需要一些诸如错误处理之类的细节,但如果只考虑正确使用的话,实现指针语义的核心部分都已经列在这里了。一个相对实用的版本可以在 https://github.com/miRoox/LValueRef 找到。