且听疯吟 如此生活三十年
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 来替代 recordmap 在运行时数据结构可见并且可以增删成员。