函数式编程

我们现在休息片刻来讲讲一种编程风格,你也许听说过,那就是:函数式编程

如果你用过 Python、Ruby、Javascript、特别是 Lisp、Scheme、Clojure 或 Haskell, 那很可能已经熟悉把函数作为变量和使用不可变的数据结构。 如果从来没使用过,你可以放心的跳过这个章节,但是我建议你无论如何都试试,这可以开阔你的视野!

Vimscript 拥有所有函数式编程所必需的元素,但是有点臃肿。 但我们可以创建一些辅助函数来减少痛苦。

先去创建一个 functional.vim 文件,这样你就不需要反复键入所有的内容。这个文件将用于本章。

不可变数据结构

遗憾的是 Vim 并没有不可变类型,类似 Clojure 内置的向量和映射,但是通过创建一些辅助函数, 我们可以一定程度的进行伪造。

把以下函数添加到文件中:

function! Sorted(l)
    let new_list = deepcopy(a:l)
    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:l)。 Vim 的 sort() 会把列表排序并替换,所以我们先创建一份列表的完全拷贝,然后对进行排序,这样初始的列表就不会被更改。

这防止出现副作用,并帮助我们写代码时更轻松的推导和测试。

让我们在多添加一些类似的辅助函数:

function! Reversed(l)
    let new_list = deepcopy(a:l)
    call reverse(new_list)
    return new_list
endfunction

function! Append(l, val)
    let new_list = deepcopy(a:l)
    call add(new_list, a:val)
    return new_list
endfunction

function! Assoc(l, i, val)
    let new_list = deepcopy(a:l)
    let new_list[a:i] = a:val
    return new_list
endfunction

function! Pop(l, i)
    let new_list = deepcopy(a:l)
    call remove(new_list, a:i)
    return new_list
endfunction

这些函数基本都一样,除了中间一行和接收的参数。保存并载入执行文件,并用一些列表来试试看。

Reversed() 接收一个列表,并返回一个元素倒序的新列表。

Append() 返回一个在尾部添加了指定元素的新列表。

Assoc() (“associate”的缩写)返回了一个把指定索引处替换为新元素的新列表。

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, l)
    let new_list = deepcopy(a:l)
    call map(new_list, string(a:fn) . '(v:val)')
    return new_list
endfunction

保存并载入执行文件,试试运行以下命令:

:let mylist = [[1, 2], [3, 4]]
:echo Mapped(function("Reversed"), mylist)

Vim 会显示 [[2, 1], [4, 3]],这就是列表中的元素都运用了 Reversed() 的结果。

Mapped() 是如何起效的?我们再次使用 deepcopy() 创建了一个新列表,然后做了一些事, 并返回这个被修改的列表 —— 这里并没有什么新内容。而最棘手的就是中间这一部分。

Mapped() 接收两个参数:一个函数引用(Vim 中的术语,表示“存储了一个函数的变量”)和一个列表。 我们使用内置的 map 函数来完成实际的工作。阅读 :help map() 看看它是如何工作的。

现在我们会创建另一些通用的高阶函数。把以下代码添加到文件中:

function! Filtered(fn, l)
    let new_list = deepcopy(a:l)
    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, l)
    let new_list = deepcopy(a:l)
    call filter(new_list, '!' . string(a:fn) . '(v:val)')
    return new_list
endfunction

试试和 Filtered() 一样来调用它:

:let mylist = [[1, 2], [], ['foo'], []]
:echo Removed(function('len'), mylist)

Vim 显示 [[], []]Removed()Filtered() 很像,除了它只会保留不会被断言函数判定为真的元素。

代码中唯一的不同就只有调用命令中的 '!' .,它会取反断言函数返回的结果。

性能

你也许会觉得到处都复制一份列表会很浪费,因为 Vim 会不断的创建新副本,并回收旧的。

如果这么说的话:你是对的!Vim 的列表并不像 Clojure 中的向量一样,支持结构共享,所以这些复制操作的开销会很大。

有时候这会产生一些影响。如果你正在操作非常多的列表,事情会变的很慢。 不过在实际应用中,你也许会惊奇地发现你基本不会注意到这几乎没有的差别。

设想一下:在写这个章节的时候,我的 Vim 程序使用了大约80Mb的内存(我安装了很多插件)。 我的笔记本电脑有8Gb的内存。一些列表的副本的总开销真的会让我们感觉到明显的差异么? 当然这取决于列表的大小,但是在大多数情况下,答案是“否”。

作为对比,我的 Firefox 进程,打开了5个标签页,已经使用了1.22Gb的内存。

你需要自行判断这种风格的代码何时会引起无法接受的性能问题。

练习

阅读 :help sort()

阅读 :help reverse()

阅读 :help copy()

阅读 :help deepcopy()

阅读 :help map(),如果刚刚没读过。

阅读 :help function()

修改 Assoc()Pop()Mapped()Filtered() 以及 Removed() 使得能支持字典。你可能需要阅读 :help type()

实现 Reduced()

奖励自己一杯最喜欢的饮料。这一章节确实很费精力!

原文地址:http://learnvimscriptthehardway.stevelosh.com/chapters/39.html