About Erlang: Records
1. tuple and record
现在有一些键值对,它们和给定 record 的项一一对应,如何转换成 record?
-record(test, {
a::binary(),
b::binary()
}).
KeyValuePairs = [{a, <<"a">>},{b, <<"b">>}].
很基础的问题,我们这样做:
Result = #test{
a = get_value(a, KeyValuePairs),
b = get_value(a, KeyValuePairs)
}.
如果 record 有一百个项呢?
重复的写 a = get_value(a, KeyValuePairs)
这样的代码一百次大概会让人怀疑「猿」生吧 :(
虽然 Erlang 并没有提供动态生成 record 的方法,但是我们知道
- Erlang 的 record 实际上是用 tuple 来表示的,即
#test{a = <<"a">>, b = <<"b">>}
实际上是{test, <<"a">>, <<"b">>}
- 所有在运行时对 record 的操作实际上都是对 tuple 的操作
Result#test.a
实际上是 tuple 的 index- 可以使用
record_info(fields, record)
获取 Record 的 fields 信息
所以我们可以这样
Result = list_to_tuple([test |
[get_value(X, KeyValuePairs) || X <- record_info(fields, test)]]).
Result#test.a.
%% <<"a">>
2. record_info
很容易想到,我们要是把处理函数抽象出来,不是多了一个 kvpairs_to_record
的接口了么?
-spec kvpairs_to_record(kvpairs(), atom()) -> rec().
kvpairs_to_record(KeyValuePairs, Record) ->
list_to_tuple([Record |
[get_value(X, KeyValuePairs) || X <- record_info(fields, Record)]]).
很遗憾,行不通。编译的时候会报出 illegal record info
错误。
WTF?
record_info
并不是一个通常意义上的 BIF,它不能接受变量- 其原因在于 record structure 只存在于编译期,在运行时是不可见的,编译完成后,record 就已经被表示成为 tuple 了,自然没有办法在运行时再获取 record info 了
所以你只能这么「曲线救国」了
kvpairs_to_record(KeyValuePairs, Record, RecordInfo) ->
list_to_tuple([Record | [get_value(X, KeyValuePairs) || X <- RecordInfo]]).
Result = kvpairs_to_record(KeyValuePairs, test, record_info(fields, test)).
3. macro and record
所以就这样放弃了?
当然不,为了「代码洁癖」我们可以「不择手段」。
考虑到 define macro 也是编译期的,我们可以这样 trick
-module(test).
-export([do/0]).
-define(fuxk(Record, Val),
fun() ->
list_to_tuple([Record | [get(X, Val)
|| X <- record_info(fields, Record)]])
end
).
-record(test, {
a :: binary(),
b :: binary()
}).
do() ->
KV = [{a, <<"a">>},{b, <<"b">>}],
Result = ?fuxk(test, KV)(),
Result#test.a.
% <<"a">>
get(Key, KeyValuePairs) ->
proplists:get_value(Key, KeyValuePairs, undefined).
Goodness gracious - it works!
4. beam and record
当然像上面那样写实际上也没有好多少,依然还是不完美。
如果一定需要在运行时得到 record info 呢?
比如我们热升级代码,需要更新 record 定义怎么办?
幸好,record structure 会被写入到 beam 中,我们只需要 load beam 然后解析它,还是可以达到运行时获取 record info 的效果的。
具体实现可以参考 https://github.com/esl/parse_trans
当然,除非知道自己在做什么,否则不推荐这么做。
5. 最后
实际上我很好奇为什么 Erlang 不提供运行时访问 record structure 呢?信息已经存在于 beam 中了,实现一下不难吧。
最后,如果不需要考虑兼容,推荐使用 map
来替代 record
,map
在运行时数据结构可见并且可以增删成员。