类Python的List
 
 

   lua的优美之处在于把数组和关联数组都用table实现了(Python中叫list和dict,C++中叫vector和map)。 
 
 
 

   一般我们把数字索引的table叫做list。penlight里的List模仿了Python,看起来从Python借鉴是个好主意。 
 
 
 

   下面是一个List的例子,List实现了__tostirng,因此可以输出自己。 
 
 
 
> List  
    = require  
    'pl.List'  
    -- 
    > automatic with require  
    'pl'  
    < 
    -- 
    - 
    
> l  
    = List() 
    
> l 
    :append( 
    
10 
    
) 
    
> l 
    :append( 
    
20 
    
) 
    
>  
    = l 
    
{ 
    
10, 
    20 
    
} 
    
> l 
    :extend { 
    
30, 
    40 
    
} 
    
{ 
    
10, 
    20, 
    30, 
    40 
    
} 
    
> l 
    :insert( 
    
1, 
    5 
    
) 
    
{ 
    
5, 
    10, 
    20, 
    30, 
    40 
    
} 
    
>  
    = l 
    :pop() 
    
  
    
40 
    
>  
    = l 
    
{ 
    
5, 
    10, 
    20, 
    30 
    
} 
    
>  
    = l 
    :index( 
    
30 
    
) 
    
  
    
4  
    >  
    = l 
    :contains( 
    30 
    
) 
    
  
    
true  
    >  
    = l 
    :reverse()   
    -- 
    - 
    > note 
    : doesn 
    't make a copy! {30,20,10,5} 
  

    尽管一些方法,如 sort和reverse,会就地操作改变list,但会返回原始的list。这样做可以链式调用,如 
  
 
  
 
   ls = ls:append(10):append(20):reverse():append(1)。但是没有额外的复制开销,因此ls没有改变 
  
 
  
 
   等式。不像string,List是可变的,如果想复制一个list,List(ls)就行。如果传入的参数是qit表,List 
  
 
  
 
   会设置这个表的元表为List,而不是复制。 
  
 
  
 
   Python list的一个特性是slice,如同string.sub。Penlight的List也支持注意索引从1 开始。 
  
 
  
> l  
     = List { 
     10, 
     20, 
     30, 
     40 
     
} 
     
>  
     = l 
     :slice( 
     
1, 
     1)   
     -- 
     - 
     > note 
     : creates a  
     new list 
     ! { 
     10 
     
} 
     
>  
     = l 
     :slice( 
     
2, 
     2 
     
) 
     
{ 
     
20 
     
} 
     
>  
     = l 
     :slice( 
     
2, 
     3 
     
) 
     
{ 
     
20, 
     30 
     
} 
     
>  
     = l 
     :slice( 
     
2, 
     - 
     2 
     
) 
     
{ 
     
20, 
     30 
     
} 
     
>  
     = l 
     :slice_assign( 
     
2, 
     2,{ 
     21, 
     22, 
     23 
     
}) 
     
{ 
     
10, 
     21, 
     22, 
     23, 
     30, 
     40 
     
} 
     
>  
     = l 
     :chop( 
     
1, 
     1 
     
) 
     
{ 
     
21, 
     22, 
     23, 
     30, 
     40} 
     
   
 
    slice_assign和chop会修改list,前者如同Python的  l[i1:i2]=seq,后者如同 del l[i1:i2]。 
   
 
   
 
    List对象如果lua 表,而且增加了等于和连接操作符,两个list相等,仅当list里的所有对象相等。 
   
 
   
 
    > l1 = List {1,2,3
}
> l2 = List {
1,2,3
}
> = l1 == l2
 
true
> = l1..l2
{
1,2,3,1,2,3} 
   
 
   
 
      
   
 
   
 
    List的构造函数可以用函数做参数,此时它假设此函数是个迭代器,并可以重复调用产生序列。 
   
 
   
 
    比如io.lines. 
   
 
   
 
    -- linecount
require 'pl' 
ls = List(io.lines())
print(#ls)
  
   
 
   List iterate可以捕获其认为是序列的内容,如 
 
> 
  for 
   ch  
  in 
   List.iterate  
  'help' 
    
  do 
    
  io 
  .write(ch, 
  ' ' 
  )  
  end
 
 
>h e l p
 
 

   List内部构造函数调用了 
  此函数,所以string可以轻松转为list。 
 
 
 
 
    
 
 
 
 
  还有些函数超过了Python原有List。如, 
  partition分割list为子list。 
 
 
 
> ls  
    = List{ 
    1, 
    2, 
    3, 
    4} 
    
> ops  
    =require  
    'pl.operator' 
   
> ls 
    :partition( 
    function(x)  
    return x  
    >  
    2 end) 
    
{false 
    ={ 
    1, 
    2},true 
    ={ 
    3, 
    4}} 
    
> ls  
    = List{ 
    'one',math.sin,List{ 
    1}, 
    10, 
    20,List{ 
    1, 
    2}} 
    
> ls 
    :partition(type) 
    
{ 
    function 
    ={ 
    function 
    :  
    00369110},string 
    ={one},number 
    ={ 
    10, 
    20},table 
    ={{ 
    1},{ 
    1, 
    2}}} 
    
  
 
   这个函数返回了table而不是List。注意List里的函数可以使用普通的table,但仅对table的数组部分有些。 
     
  
 
  

      
  
 
  

    栈在计算机中经常使用。List支持类栈的操作,如pop(移除并返回最后一个元素),push(在末尾添加一个元素),push是append的别名,size即#操作符。队列也被实现了,使用pop出对,put入队。 
  
 
  

      
  
 
  

    你可能会继承List,由于list的返回类型会相应变化,如slice会返回子类型,而不是List。例子,如下: 
  
 
  
 
   tests/test-list.lua
n1 = NA{10,20,30}
n2 = NA{1,2,3}
ns = n1 +2*n2
asserteq(ns,{12,24,36})
min,max = ns:slice(1,2):minmax()
asserteq(T(min,max),T(12,24))
asserteq(n1:normalize():sum(),1,1e-8) 
  
 
  
 
    
   
 
    Map和Set 
   
 
   

       
   
 
     
  

    set和get方法,当Map被继承是很有用,还有updat。 
  
 
  
 
   > Map = require 'pl.Map'
> m = Map{one=1,two=2}
> m:update {three=3,four=4,two=20}
> = m == M{one=1,two=20,three=3,four=4}
true 
  
 
  

      
  
 
  

    方法values返回value序列,keys返回key序列,都是无序的。getvalues, 
  
 
  

    返回与key关联的value。 
  
 
  
 
   > m = Map{one=1,two=2,three=3}
> = m:getvalues {'one','three'}
{1,3}
> = m:getvalues(m:keys()) == m:values()
true 
  
 
  

    当查询Map的值时,最后用get方法: 
  
 
  

      > print(m:get 'one', m:get 'two') 
  
 
  

     1     2 
  
 
  

      
  
 
  

    m[key]是模棱两可的,例如一个索引是get,那么m["get"],总会成功,原因是如果 
  
 
  

    找不到get,它会在Map的元表里找,而元表里有get方法。还没有解决这个恼人问题 
  
 
  

    的简单法子。 
  
 
  

    有一些有用的继承自Map的类,如有序的OrderedMap,当用其迭代器遍历时,会按插入 
  
 
  

    顺序返回。 
  
 
  

    Set集合,特殊类型的Map,所有的value都是true,提供了并集(+),交集(*)操作。 
  
 
  
 
   > Set = require 'pl.Set'
> = Set{'one','two'} == Set{'two','one'}
true
> fruit = Set{'apple','banana','orange'}
> = fruit['banana']
true
> = fruit['hazelnut']
nil
> = fruit:values()
{apple,orange,banana}
> colours = Set{'red','orange','green','blue'}
> = fruit,colours
 [apple,orange,banana]   [blue,green,orange,red]
> = fruit+colours
[blue,green,apple,red,orange,banana]
> = fruit*colours
[orange] 
  
 
  

      
  
 
  

    还有Set.difference(差集)和Set.symmetric_difference(先求并集再除去交集)。 
  
 
  

    > = fruit - colours 
  
 
  

    [apple,banana] 
  
 
  

    > = fruit ^ colours 
  
 
  

    [blue,green,apple,red,banana] 
  
 
  

    添加元素只需fruit[‘peach’] = true,删除只需fruit[‘apple’] = nil,为了更简单, 
  
 
  

    Set类没有方法,你可以使用[]操作符,也可以使用Set.intersect,从而避免了Map的问题。 
  
 
  

      
  
 
  
 
   Table的一些有用操作 
  
 
  

    Lua的table有数组和关联数组两部分,一些操作只对数组有效,另一些则只对关联数组有效。 
  
 
  

    (实际都是关联数组,只是对数组的实现非常高效)。 
  
 
  

    List提供了一些高级的更有效的操作,如复制表: 
  
 
  

      local res = {} 
   
 
   

     for k,v in pairs(T) do 
   
 
   

          res[k] = v 
   
 
   

     end 
   
 
   

     return res 
   
 
  

    tablex模块提供了copy,浅复制;还有deepcopy深复制,可以复制元表和嵌套表。还提供了 
  
 
  

    icopy可以对类list的表有选择的部分复制,同时删除左边的部分。 
  
 
  

      asserteq(icopy({1,2,3,4,5,6},{20,30}),{20,30})   -- start at 1 
   
 
   

     asserteq(icopy({1,2,3,4,5,6},{20,30},2),{1,20,30}) -- start at 2 
   
 
   

     asserteq(icopy({1,2,3,4,5,6},{20,30},2,2),{1,30}) -- start at 2, copy from 2 
   
 
  

    move会覆盖,而不出是删除。 
  
 
  

     asserteq(move({1,2,3,4,5,6},{20,30}),{20,30,3,4,5,6}) 
   
 
   

     asserteq(move({1,2,3,4,5,6},{20,30},2),{1,20,30,4,5,6}) 
   
 
   

     asserteq(move({1,2,3,4,5,6},{20,30},2,2),{1,30,3,4,5,6}) 
   
 
  

    (类似C里的strcpy和memmove) 
  
 
  

    总结一下,用copy和deepcopy复制一个表。复制到类map的表用updae;复制到类list的用icopy, 
  
 
  

    如果是更新目的表用move。 
  
 
  

    insertvalues和table.insert类似,它可以插入table里值,removevalues移除某些值。 
  
 
  

     asserteq(insertvalues({1,2,3,4},2,{20,30}),{1,20,30,2,3,4}) 
   
 
   

     asserteq(insertvalues({1,2},{3,4}),{1,2,3,4}) 
   
 
  

    和 
  
 
  

      > T = require 'pl.tablex' 
   
 
   

     > t = {10,20,30,40} 
   
 
   

     > = T.removevalues(t,2,3) 
   
 
   

     {10,40} 
   
 
   

     > = T.insertvalues(t,2,{20,30}) 
   
 
   

     {10,20,30,40} 
   
 
  

    和deepcopy类似,deepcompare会深入比较两个表,如果它们有相同的值和结够才返回true。 
  
 
  

      > t1 = {1,{2,3},4} 
   
 
   

     > t2 = deepcopy(t1) 
   
 
   

     > = t1 == t2 
   
 
   

     false 
   
 
   

     > = deepcompare(t1,t2) 
   
 
   

     true 
   
 
  

    find可以查找类list表的值,并返回索引。和string.find类似,你可以指定起点,还有可选的终点参数。 
  
 
  

    这样可以定义rfind,从末尾查找。 
  
 
  

     function rfind(t,val,istart) 
   
 
   

          return tablex.find(t,val,istart,true) 
   
 
   

     end 
   
 
  

    find是线性搜索,可能会减慢代码的运行。对很大的表可以用index_map,它会返回一个表,其索引 
  
 
  

    是原来表的值。 
  
 
  

      > t = {'one','two','three'} 
   
 
   

     > = tablex.find(t,'two') 
   
 
   

     2 
   
 
   

     > = tablex.find(t,'four') 
   
 
   

     nil 
   
 
   

     > il = tablex.index_map(t) 
   
 
   

     > = il['two'] 
   
 
   

     2 
   
 
   

     > = il.two 
   
 
   

     2 
   
 
  

    另一个版本的index_map是makeset,其值都是true。它很有用,尤其是用deepcompar比较两个set时。 
  
 
  

      > = deepcompare(makeset {1,2,3},makeset {2,1,3}) 
   
 
   

     true 
   
 
  

      
  
 
  

    让我们思考一个关于职工的问题。假设我们有两份职工的文件名: 
  
 
  

     (last-month.txt) 
   
 
   

     smith,john 
   
 
   

     brady,maureen 
   
 
   

     mongale,thabo 
   
 
   

       
   
 
   

     (this-month.txt) 
   
 
   

     smith,john 
   
 
   

     smit,johan 
   
 
   

     brady,maureen 
   
 
   

     mogale,thabo 
   
 
   

     van der Merwe,Piet 
   
 
  

    想找到不同,只需把职工list转为set,如下: 
  
 
  

      require 'pl' 
   
 
   

       
   
 
   

     function read_employees(file) 
   
 
   

        local ls = List(io.lines(file)) -- a list of employees 
   
 
   

        return tablex.makeset(ls) 
   
 
   

     end 
   
 
   

       
   
 
   

     last = read_employees 'last-month.txt' 
   
 
   

     this = read_employees 'this-month.txt' 
   
 
   

       
   
 
   

     -- who is in this but not in last? 
   
 
   

     diff = tablex.difference(this,last) 
   
 
   

       
   
 
   

     -- in a set, the keys are the values... 
   
 
   

     for e in pairs(diff) do print(e) end 
   
 
   

       
   
 
   

      --  *output* 
   
 
   

     -- van der Merwe,Piet 
   
 
   

     -- smit,johan 
   
 
  

    difference操作符的实现并不难; 
  
 
  

     for e in pairs(this) do 
   
 
   

        if not last[e] then 
   
 
   

          print(e) 
   
 
   

        end 
   
 
   

     end 
   
 
  

    用difference并不是代码技巧,而是让读你代码的人更易懂(对六个月后的你,也是如此)。 
  
 
  

    find_if使用函数查找表,可选的第三个参数,将会作为函数的第二个参数传入。pl.operator为 
  
 
  

    lua提供了操作符的函数包装,这样可以实现一些基本的比较操作。 
  
 
  

      > ops = require 'pl.operator' 
   
 
   

     > = tablex.find_if({10,20,30,40},ops.gt,20) 
   
 
   

      3       true 
   
 
  

    注意find_if会返回传入函数返回的值。 
  
 
  

      
  
 
  

    deepcompare会递归比较,但是默认的compare允许你使用指定的函数比较类list的表。 
  
 
  

    当类list表包含相同元素时,compare_no_order返回true,不需要特定的比较函数。如 
  
 
  

    比较set: 
  
 
  

     > = compare_no_order({1,2,3},{2,1,3}) 
   
 
   

     true 
   
 
   

     > = compare_no_order({1,2,3},{2,1,3},'==') 
   
 
   

     true 
   
 
  

    (注意,上面的“==”字符串,不必用ops.gt,ops.eq,直接用'>'或'==") 
  
 
  

      
  
 
  

    sort和sortv返回待排序表里的迭代器。sort以key的顺序遍历,sortv以value的顺序遍历。 
  
 
  

    如下面的姓名年龄表: 
  
 
  

     > t = {john=27,jane=31,mary=24} 
   
 
   

     > for name,age in tablex.sort(t) do print(name,age) end 
   
 
   

      jane    31 
   
 
   

      john    27 
   
 
   

      mary    24 
   
 
   

     > for name,age in tablex.sortv(t) do print(name,age) end 
   
 
   

      mary    24 
   
 
   

      john    27 
   
 
   

      jane    31 
   
 
  

    在PL里有多种合并表的方法。如果是类list的可以用pl.List里的的操作,如concatenation; 
  
 
  

    如果是类map的用merge。若merge的第三个参数是假,则只会取两个表里相同key的;为真 
  
 
  

    ,则是两个表里所有的key。 
  
 
  

      > S1 = {john=27,jane=31,mary=24} 
   
 
   

     > S2 = {jane=31,jones=50} 
   
 
   

     > = tablex.merge(S1, S2, false) 
   
 
   

     {jane=31} 
   
 
   

     > = tablex.merge(S1, S2, true) 
   
 
   

     {mary=24,jane=31,john=27,jones=50} 
   
 
  

      
  
 
  

    当用table时,你发现自己掉入了循环里。循环是程序员的第二天性,但不是最优雅和自我描述 
  
 
  

    的方式。看一下map函数,它把原始表的每个元素传入到函数,返回一个新表。 
  
 
  

     > = map(math.sin, {1,2,3,4}) 
   
 
   

      {  0.84,  0.91,  0.14, -0.76} 
   
 
   

     > = map(function(x) return x*x end, {1,2,3,4}) 
   
 
   

     {1,4,9,16} 
   
 
  

      
  
 
  

    map让你少写循环,代码更整洁。 
  
 
  

    pairmap将传入的函数作用到key和value上。 
  
 
  

     > t = {fred=10,bonzo=20,alice=4} 
   
 
   

     > = pairmap(function(k,v) return v end, t) 
   
 
   

     {4,10,20} 
   
 
   

     > = pairmap(function(k,v) return k end, t) 
   
 
   

     {'alice','fred','bonzo'} 
   
 
  

    (通常第一个值被作为value,第二个作为key)如果函数返回两个值,则第二个值被作为新的key。 
  
 
  

      > = pairmap(t,function(k,v) return v+10, k:upper() end) 
   
 
   

     {BONZO=30,FRED=20,ALICE=14} 
   
 
  

      
  
 
  

    map2作用到两个表上: 
  
 
  

     > map2(ops.add,{1,2},{10,20}) 
   
 
   

     {11,22} 
   
 
   

     > map2('*',{1,2},{10,20}) 
   
 
   

     {10,40} 
   
 
  

    以上操作都是产生表,reduce则表里的元素都作用到有两个参数的函数,返回一个标量。 
  
 
  

      > reduce ('+', {1,2,3}) 
   
 
   

     6 
   
 
   

     > reduce ('..', {'one','two','three'}) 
   
 
   

     'onetwothree' 
   
 
  

      
  
 
  

    最后,zip把不同的表里的元素组成新表。 
  
 
  

      > = zip({1,2,3},{10,20,30}) 
   
 
   

     {{1,10},{2,20},{3,30}} 
   
 
  

    查看PL的文档,你会发现tablex和List共享一下函数。如tablex.imap和List.map基本是相同的函数。 
  
 
  

    这也可以表达为 'f(x) for x' (t),可以让操作更明确。List是一个对table的轻包装,List接口并不强调 
  
 
  

    使用Penlight的table。 
  
 
  

      
  
 
  
 
   二维表操作符 
  
 
  
 
   二维表很容易在lua表里表示,如{{1,2},{3,4}},即把行作为子表存储,即A[col][row]。与lua的矩阵库 
  
 
  
 
   LuaMatrix类似。pl.array2d并不提供矩阵操作,这是另外库的工作。 
  
 
  
 
   iter是ipairs生成器(额外参数指定索引). 
  
 
  

      > a = {{1,2},{3,4}} 
   
 
   

     > for i,j,v in array2d.iter(a,true) do print(i,j,v) end 
   
 
   

      1       1       1 
   
 
   

      1       2       2 
   
 
   

      2       1       3 
   
 
   

      2       2       4 
   
 
   

       
   
 
   

     注意你可以用List(tablex.map(List,a))把2d数组转为list中的list。 
   
 
   

     map也可作用到arra2d上: 
   
 
   

       > array2d.map('-',a,1) 
    
 
    

      {{0,1},{2,3}} 
    
 
   

     2d数组按行存储,但是列可以提取。 
   
 
   

      > array2d.column(a,1) 
    
 
    

      {1,3} 
    
 
   

     还有reduce2函数,或者行,或者列。 
   
 
   

      > array2d.reduce_rows('+',a) 
    
 
    

      {3,7} 
    
 
    

      > array2d.reduce_cols('+',a) 
    
 
    

      {4,6} 
    
 
   

     和tablex.reduce('*',array.reduce_rows('+',a))等效。 
   
 
   

       > array2d.reduce2('*','+',a) 
    
 
    

       21    ` 
    
 
   

     与tablex.map2对应,array2d提供了array2d.map2. 
   
 
   

       > b = {{10,20},{30,40}} 
    
 
    

      > a = {{1,2},{3,4}} 
    
 
    

       > = array2d.map2('+',2,2,a,b)  -- two 2D arrays 
    
 
    

      {{11,22},{33,44}} 
    
 
    

       > = array2d.map2('+',1,2,{10,100},a)  -- 1D, 2D 
    
 
   

      {{11,102},{13,104}} 
    
 
    

       > = array2d.map2('*',2,1,a,{1,-1})  -- 2D, 1D 
    
 
    

      {{1,-2},{3,-4}} 
    
 
   

       
   
 
   

     当然你可以简化算术。有一个2d array 字符串,用合适的方式打印出来。第一步, 
   
 
   

     给map string.len函数作用到array上;第二步,使用math.max换算列宽;最后,用 
   
 
   

     stringx.rjust处理这些宽度。 
   
 
   

       maxlens = reduce_cols(math.max,map('#',lines)) 
    
 
    

      lines = map2(stringx.rjust,2,1,lines,maxlens) 
    
 
   

       
   
 
   

     product用来计算1d数组的笛卡尔乘积,返回2d数组。 
   
 
   

      > array2d.product('{}',{1,2},{'a','b'}) 
    
 
    

      {{{1,'b'},{2,'a'}},{{1,'a'},{2,'b'}}} 
    
 
   

       
   
 
   

     PL为2d array提供了一系列操作,如swap_rows、swap_cols,第一个是效率是线性的。 
   
 
   

     remove_row和remove_col是table.remove的特例。还有extract_rows,extract_cols,可以 
   
 
   

     提取指定的索引。 extract_cols(A,{2,4})仅保存第2列和第4列。 
   
 
   

       
   
 
   

     List.slice用在1d数组上。slice与其相同,只需提供起始(row,column)。 
   
 
   

      > A = {{1,2,3},{4,5,6},{7,8,9}} 
    
 
    

      > B = slice(A,1,1,2,2) 
    
 
    

      > write(B) 
    
 
    

        1 2 
    
 
    

        4 5 
    
 
    

      > B = slice(A,2,2) 
    
 
    

      > write(B,nil,'%4.1f') 
    
 
    

        5.0 6.0 
    
 
    

        8.0 9.0 
    
 
   

       
   
 
   

     write用漂亮的来输出数组,第二个参数为空时,使用标准输出(stdout),也可以使用文件对象, 
   
 
   

     第三个可选参数用来格式化(用法同string.format)。 
   
 
   

     parse_range 可以解析电子表格似的范围表示法,如"A1:B2"或“R1C1:R2C2",返回四个number, 
   
 
   

     返回值可以作为slice的参数。slice依据范围返回合适的数组,如果范围代表row或column,返回 
   
 
   

     值是1d,其它则2d。 
   
 
   

     这对iter一样,也可以选择范围。 
   
 
   

       > for i,j,v in iter(A,true,2,2) do print(i,j,v) end 
    
 
    

       2       2       5 
    
 
    

       2       3       6 
    
 
    

       3       2       8 
    
 
    

       3       3       9 
    
 
   

       
   
 
   

     new构造一个新的2d数组,可以提供初始化元素,如果参数可调用,加上L前缀,表示utils.string_lambda。 
   
 
   

     我们可以按照下面的方式,产生一个单位矩阵。 
   
 
   

       asserteq( 
    
 
    

           array.new(3,3,L'|i,j| i==j and 1 or 0'), 
    
 
    

           {{1,0,0},{0,1,0},{0,0,1}} 
    
 
    

      ) 
    
 

 


    注意大多数array2d函数是协变的,即返回类型和传入类型相同。  

   当reshaped或sliced时,  

   任何用data.new或matrix.new将保留数据或矩阵对象。数据对象含有array2d的函数。