zoukankan      html  css  js  c++  java
  • [Erlang 0116] 当我们谈论Erlang Maps时,我们谈论什么 Part 1

     


         Erlang 增加 Maps数据类型并不是很突然,因为这个提议已经进行了2~3年之久,只不过Joe Armstrong老爷子最近一篇文章Big changes to Erlang掀起不小了风浪.这篇文章用了比较夸张的说法:"Records are dead - long live maps !",紧接着在国内国外社区这句话就传遍了.马上就有开发者忧心忡忡的在Stackoverflow上提问:Will Erlang R17 still have records? 
     
       套用一句文艺的话,当我们谈论Maps时,实际上是表达我们对record的不满,这些不满/痛点恰好就是我们寄希望于Maps能够提供给我们的.本文将尽可能的逐一列出这些点,并尝试分析原因,下篇文章将深入分析Maps的一些细节.
     
     

    Record的痛点

     
      使用Record我们遇到哪些痛点呢?这些痛点在Maps出现之后有所改善吗?我们先从细数痛点开始:
     
    1.可以把record的name用作参数吗?

    简单讲就是#RecordName{} 可以吗? 
     
    7> rd(person,{name,id}).
    person
    8> #person{}.
    #person{name = undefined,id = undefined}
    9> P=person.
    person
    10> #P{}.
    * 1: syntax error before: P
    10>
     
    

      

     
     
    2.可以把record的filed作为参数使用吗?
     
    10> N=name.
    name
    11> #person{N="zen"}.
    * 1: field 'N' is not an atom or _ in record person
    12> 
    

       

    Modify a record in Erlang by programmatically specifying the field to modify
     
    解决这个问题可以关注dynarec项目,可以动态生成record字段值的getter和setter访问入口. https://github.com/jcomellas/mlapi/blob/master/src/dynarec.erl
     
    3. a.b.c.d.e.f 能实现吗?
     
    在有些语言中会有Fluent API(或 Fluent Interface)的设计,目的是在语法层面方便完成一系列连续的逻辑.在使用嵌套record的时候,我们特别希望能用a.b.c.d.e.f的方式来简化代码,而实际上是下面这个样子:
     
    Eshell V6.0  (abort with ^G)
    1> rd(foo,{a,b,c}).
    foo
    2>  rd(a,{f,m}).
    a
    3>  rd(f,{id,name}).
    f
    4>  #foo{a=#a{f=#f{id=2002,name="zen"},m=1984},b=1234,c=2465}.
    #foo{a = #a{f = #f{id = 2002,name = "zen"},m = 1984},
         b = 1234,c = 2465}
    5> D=v(4).
    #foo{a = #a{f = #f{id = 2002,name = "zen"},m = 1984},
         b = 1234,c = 2465}
    6> D#foo.a#a.f#f.name.
    "zen"
    

      

    有一个开源项目recbird就可以实现这种效果,解决的路子当然是parse_transform, 需要在代码中添加-compile({parse_transform, recbird}).选项
    recbird的作者是dcaoyuan,这个代码也是作为ErlyBird的一部分host在sourceforge:
     
     
    4.record转proplists proplists转record
     
      为什么要转换properlist?其目的就是方便检索字段值.
     
     
    5.key只能是atom
      的确有人提过这个 
     
    6.record往往要定义在hrl中
     
     

    原因何在?

     
       在record相关的问题中,常常提到的一个词就是"compile-time dependency",即record只存在于编译时,并没有对应实际的数据类型.record本质上是tuple在语法层面的语法糖,而上面record的诸多问题其实就是源于tuple,在著名的exprecs项目,有这样一段描述:
     
    This parse transform can be used to reduce compile-time dependencies in large systems.

    In the old days, before records, Erlang programmers often wrote access functions for tuple data. This was tedious and error-prone. The record syntax made this easier, but since records were implemented fully in the pre-processor, a nasty compile-time dependency was introduced.

    This module automates the generation of access functions for records. While this method cannot fully replace the utility of pattern matching, it does allow a fair bit of functionality on records without the need for compile-time dependencies.
     
     
     
    Record即Tuple
     
      在内部表示没有record只有tuple, 下面是Erlang数据内部表示的介绍,我做了一个长图:
    源文档地址:http://www.erlang-factory.com/upload/presentations/467/Halfword_EUC_2011.pdf  (这个文档在我们的 Erlang Resources 小站多次推荐过)
     
     
     
     
     
     这几张图可以帮助我们建立起来Erlang数据内部表示的思考模型,我们简单梳理一下:
       Beam(Björns/Bogdans Erlang Abstract Machine)虚拟机,包含一个拥有1024个虚拟寄存器的虚拟寄存器机,程序变量可能存储在register或stack;垃圾回收是以进程为单位,逐代进行;Beam包含一个常量池( constant pool)不被GC.大型二进制数据在Heap外,并可被多个进程共享;VM Code中用来表达数据类型使用的概念是Eterm:一个Eterm通常一个字(word)大小( sizeof(void *)),进程的Heap实际上就是Eterm构成的数组,ETS也是以Eterm的形式存储数据.寄存器(register)也是Eterm,VM中的stack也是由Eterm组成;VM需要在进程heap上分配一些Eterm来表示一些复杂的数据结构比如list,tuple;如果变量指向的数据复杂,那么stack/register会包含指向heap的指针,换句话话说,Eterm要支持指针;

     Eterm其实是使用一些二进制数据位来标记当前的数据类型,Erlang使用了一个层次化的标记系统,最基础的是使用最低两位primary tags来标识:
     00 = Continuation pointer (return address on stack) or header word on heap
     01 = Cons cell (list)
     10 = Boxed (tuple, float, bignum, binary, external pid/port, exterrnal/internal ref ...)
     11 =  Immediate (the rest - secondary tag present)
     
    具体到Boxed类型,继续细分:
    – 0000 = Tuple
    – 0001 = Binary match state (internal type)
    – 001x = Bignum (needs more than 28 bits)
    – 0100 = Ref
    – 0101 = Fun
    – 0110 = Float
    – 0111 = Export fun (make_fun/3)
    – 1000 - 1010 = Binaries
    – 1100 - 1110 = External entities (Pids, Ports and Refs)
     
    看到了吧,这里已经没有record的踪影了,只有tuple,而对于Maps,我们已经可以在17.0-rc2/erts/emulator/beam/erl_term.h的代码中找到它的subtag:
     
    #define ARITYVAL_SUBTAG          (0x0 << _TAG_PRIMARY_SIZE) /* TUPLE */
    #define BIN_MATCHSTATE_SUBTAG     (0x1 << _TAG_PRIMARY_SIZE)
    #define POS_BIG_SUBTAG          (0x2 << _TAG_PRIMARY_SIZE) /* BIG: tags 2&3 */
    #define NEG_BIG_SUBTAG          (0x3 << _TAG_PRIMARY_SIZE) /* BIG: tags 2&3 */
    #define _BIG_SIGN_BIT          (0x1 << _TAG_PRIMARY_SIZE)
    #define REF_SUBTAG          (0x4 << _TAG_PRIMARY_SIZE) /* REF */
    #define FUN_SUBTAG          (0x5 << _TAG_PRIMARY_SIZE) /* FUN */
    #define FLOAT_SUBTAG          (0x6 << _TAG_PRIMARY_SIZE) /* FLOAT */
    #define EXPORT_SUBTAG          (0x7 << _TAG_PRIMARY_SIZE) /* FLOAT */
    #define _BINARY_XXX_MASK     (0x3 << _TAG_PRIMARY_SIZE)
    #define REFC_BINARY_SUBTAG     (0x8 << _TAG_PRIMARY_SIZE) /* BINARY */
    #define HEAP_BINARY_SUBTAG     (0x9 << _TAG_PRIMARY_SIZE) /* BINARY */
    #define SUB_BINARY_SUBTAG     (0xA << _TAG_PRIMARY_SIZE) /* BINARY */
    #define MAP_SUBTAG          (0xB << _TAG_PRIMARY_SIZE) /* MAP */
    #define EXTERNAL_PID_SUBTAG     (0xC << _TAG_PRIMARY_SIZE) /* EXTERNAL_PID */
    #define EXTERNAL_PORT_SUBTAG     (0xD << _TAG_PRIMARY_SIZE) /* EXTERNAL_PORT */
    #define EXTERNAL_REF_SUBTAG     (0xE << _TAG_PRIMARY_SIZE) /* EXTERNAL_REF */
     
      感兴趣的话,可以继续在otp_src_17.0-rc2ertsemulatoreamerl_term.h中看到tuple实现相关的代码,搜索/* tuple access methods */代码段.
     
      看到这里,Stackoverflow 有个问题讨论"Does erlang implement record copy-and-modify in any clever way?"
     
      注意里面提到的erts_debug:size/1 和 erts_debug:flat_size/1方法,可以帮助我们查看共享和非共享状态数据占用的字数.所谓的共享和非共享,就是通过复用一些数据块(即指针指向)而不是通过数据拷贝,这样提高效率.在一些万不得已的情况下再触发拷贝,比如数据发往别的节点,存入ETS等等, Erlang Efficiency Guide 很多优化的小技巧都是从这个出发点考虑的.
     
     那去掉primary tag和sub tag之后tuple是一个什么样的数据结构呢?我们可以从两个角度来看,首先是Erlang Interface Reference Manual
    erl_mk_tuple方法明确指示了tuple实际上是一个Eterm的数组:
     
    ETERM *erl_mk_tuple(array, arrsize)
    Types:
    ETERM **array;
    int arrsize;
    Creates an Erlang tuple from an array of Erlang terms.
    array is an array of Erlang terms.
    arrsize is the number of elements in array.
     
      另外一个角度就是在bif.c中,tuple_to_list和list_to_tuple的实现,其实就是数组和链表的互相转换,看代码还可以知道通过make_arityval(len)冗余了数组的长度.对于tuple,获得size和按照索引访问数据都是很快的.这也就是找EEP43中提到过的Record的优势:
    • 快速查询 O(1), 编译期间完成了对key的索引,对于小数据量存取相当快 (~50 values),
    • 没有过多额外的内存消耗,只有Value和name 2+ N个字 (name + size+ N)
    • 函数头完成匹配
    而编译期一过,record提供的语法红利没有了,剩下的也就是快速获得tuple size和按照索引访问数据了.exprecs项目所谓 reduce compile-time dependencies 其实就是在编译阶段把一些语法红利继续保持下去,比如可以按照record name去new一个record,按照字段索引位置访问数据等等.上面提到的record与proplists的转换,实际上是把解决问题的时机从编译期推迟到了运行时.
     
     
     
    说到这里,你可能非常期待了,Erlang R17之后加入的Maps又解决了什么问题?带来了什么惊喜呢?Maps与Record是一场你死我活的PK么?我们明天再说,敬请关注.
     
     
    PS. Joe Armstrong老爷子文章中提到的Names in Funs 之前我们已经讨论过多次了:
     

    [Erlang 0056] 用fun在Erlang Shell中编写尾递归 Ⅱ
    http://www.cnblogs.com/me-sa/archive/2012/04/28/2474892.html

    [Erlang 0063] Joe Armstrong 《A Few Improvements to Erlang》EUC 2012
    http://www.cnblogs.com/me-sa/archive/2012/06/06/2538941.html
     
     
    相关资料:
     
     
  • 相关阅读:
    POJ-1189 钉子和小球(动态规划)
    POJ-1191-棋盘分割(动态规划)
    Java实现 LeetCode 730 统计不同回文子字符串(动态规划)
    Java实现 LeetCode 730 统计不同回文子字符串(动态规划)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 728 自除数(暴力)
    Java实现 LeetCode 728 自除数(暴力)
    Java实现 LeetCode 728 自除数(暴力)
  • 原文地址:https://www.cnblogs.com/me-sa/p/when_we_talk_about_erlang_maps_1.html
Copyright © 2011-2022 走看看