我们现在来花一点时间讨论一种你或许听过的编程方式:函数式编程。
如果你曾经使用过Python,Ruby或者Javascript,或者特别是Lisp,Schema,Clojure或者Haskell,那么你就会对对把函数当作变量使用以及使用不可变的变量比较熟悉了。如果你没有的话,你也可以安全地跳过这个章节,但是我还是推荐你学习本章来拓宽你的视野。
Vimscript里包含了函数式编程所需要的所有功能,不过它有点笨拙。我们首先来创建一些帮助函数,来使得函数式编程不那么困难。
创建一个functionnal.vim文件,这样你就没有必要来回地敲所有代码了。这个文件是我们本章的便签条。
不幸的是,在vim里,没有任何内置的不可变数据结构类似Clojure的vector和map,不过我们可以通过创建一些帮助函数来在某种程度上来模拟不可变数据结构。
把下面的代码添加到你的文件里:
function! Sorted(1)
let new_list = deepcopy(a:1)
call sort(new_list)
return new_list
endfunction
写入,并且加载这个文件,然后通过:echo Sorted([3,2,4,1])来测试它。Vim输出“[1,2,3,4]”。
那么这个包装函数和直接调用内置的sort()函数有什么区别呢?关键在于第一行代码:let new_list = deepcopy(a:1)。Vim的sort()函数对原来的list进行原地排序,所以我们要创建这个列表的一个拷贝,然后对这个拷贝进行排序,这样的话原始的列表就不会被改变了。
这样就可以避免任何副作用了,这样就可以帮助我们写出更容易维护和测试的代码了。下面我们添加一些类似的帮助函数:
function! Reversed(1)
let new_list = deepcopy(a:1)
call reverse(new_list)
return new_list
endfunction
function! Append(1,val)
let new_list = deepcopy(a:1)
call add(new_list,a:val)
return new_list
endfunction
function! Assoc(1,i,val)
let new_list = deepcopy(a:1)
let new_list[a:i] = a:val
return new_list
endfunction
function! Pop(1,i)
let new_list = deepcopy(a:1)
call remove(new_list,a:i)
return new_list
endfunction
上面的函数基本上都是相同的,除了中间几行不同以及传入的参数不同。写入和加载上面的文件,然后通过几个列表来测试它们。
Reversed() 返回一个列表的反序
Append() 返回一个新的列表,它是在旧的列表末尾加上给定的元素
Assoc()(associta的简写)返回一个新的列表,它是把旧列表在指定位置上的值替换成给定的值
Pop() 返回一个新的列表,它是就列表删除了末尾的元素
Vimscript支持用变量来引用函数,不过语法上有些怪异。运行下面的命令:
:let Myfunc = function("Append")
:echo Myfunc([1,2],3)
Vim会输出[1,2,3]。要注意的是我们使用的变量的名称是大写字母开头的。如果一个vimscript变量是引用一个函数的,那么它就必须以大写字母开头。
函数也可以像其他变量一样,可以保存在列表里。运行下面的命令:
:let funcs =[function("Append"),function("Pop")]
:echo funcs[1](['a','b','c'],1)
Vim会输出['a','c']。funcs这个变量没必要用大写字母开头,因为它保存的是一个列表,而不是函数,列表是什么内容对它没影响。
我们来创建一些常用的高阶函数。如果你对它们不熟悉的话,简单来说,高阶函数就是用其他函数做为参数,然后用它们来做一些操作的函数。
我们先从map函数开始。把下面的代码添加到你的文件里:
function! Mapped(fn,1)
let new_list = deepcopy(a:1)
call map(new_list,String(a:fn) . '(v:val)')
return new_list
endfunction
写入并且加载上面的文件,然后用下面的命令来试试上面的函数:
:let mylist = [[1,2],[3,4]]
:echo Mapped(function("Resversed",mylist)
Vim会输出[[2,1],[4,3]],也即是对给定的列表里的每一个元素应用了Reversed函数。
那么Mapped()函数究竟是怎么生效的呢?这次我们也是用deepcopy来复制一个新的列表,然后对这个列表进行一些操作,然后返回这个修改后的列表——没有什么新的东西。但是真正的玄机就在中间一部分代码里。
Mapped()接受两个参数:一个函数的引用(也就是vim里指向一个函数的变量)以及一个列表。我们用内置的map函数来进行实际的操作。现在阅读一下:help map()来看看它到底是怎么工作的。
现在,我们再来创建一些其他常用的高阶函数。把下面的代码添加到你的文件里:
function! Filtered(fn,1)
let new_list = deepcopy(a:1)
call filter(new_list,String(a:fn) . '(v:val)')
return new_list
endfunction
用下面的命令来试试Filtered函数的功能:
:let mylist = [[1,2],[],['foo'],[]]
:echo Filtered(function('len'),mylist)
Vim会输出[[1,2],['foo']]
Filtered接受一个描述性的函数和一个列表。它依次作用列表的元素,以它们为参数调用给定的函数,最后返回一个包含所有调用该函数的返回值为真值的元素的列表。在这里,我们用了内置函数len,所以会过滤掉所有长度为0的元素。
最后我们创建一个和Filtered功能相反的函数:
function! Removed(fn,1)
let new_list = deepcopy(a:1)
call filter(new_list,'!' . string(a:fn) . '(v:val)'
return new_list
endfunction
使用上面的例子再试试这个函数:
:let mylist = [[1,2],[],['foo'],[]]
:echo Removed(function('len'),mylist)
Vim会输出[[],[]]。Removed和Filtered很相似,只不过它保留了描述函数返回为假的元素。
上面两段代码唯一的区别就在于下面的代码多了一个“!”。我们把它加在call命令里,用来对描叙函数的返回值进行取反。
或许你会认为在上面的函数里,都进行了数组的拷贝,这样是很浪费的,这样会导致vim需要不断地创建新的数组,并且回收就的数组。
如果这样来考虑的话,你的想法是对的!Vim并不支持类似于Clojure向量的结构共享的功能,所以所有的拷贝操作都是要很大的开销的。
不过只是有些时候这个问题才会出现。如果你要操作相当多的数组,那么速度就会慢下来。不过,在实际生活中,你会为你基本上很少注意到这样的差别而感到惊讶。
考虑这样一个场景:当我在写这个章节的内容的时候,我的Vim值占用了大学80M的内存(同时我安装了很多的插件)。我的笔记本有8G的内存。那么几个列表的拷贝对我有实质性的影响么?当然,这个问题取决于列表的答谢,但是在绝大多数的场景下,是没有影响的!
相比而言,我的Firefox浏览器打开5个标签页就使用了1.22G的内存。
你需要自己来判断这种方式的代码是否会带来不可接受的性能问题。我极力赞同你进行一个反例的常识。使用不可变的数据结构以及高阶函数可以简化你的代码,同时也会减少一大堆和状态相关的bug。