現在讓我們小憩一下,聊一聊一種你可能聽過的編程風格:[函數式編程](https://secure.wikimedia.org/wikipedia/en/wiki/Functional_programming)。
如果你用過Python,Ruby或Javascript,_甚或_Lisp,Scheme,Clojure或Haskell, 你應該會覺得把函數作為變量類型,用不可變的狀態作為數據結構是平常的事。 如果你沒用過,你可以放心地跳過這一章了,但我還是鼓勵你找機會去試試并拓寬自己的視野。
Vimscript具有使用函數式風格進行編程的潛力,不過會有點吃力。 我們可以創建一些輔助函數來讓這個過程少些痛苦。
繼續前進并創建`functional.vim`文件,這樣你就不用反復地重新擊打每一行代碼。 這個文件將會成為這一章的草稿本。
## 不可變的數據結構
不幸的是,Vim沒有類似于Clojure內置的vector和map那樣的不可變集合, 不過通過一些輔助函數,我們可以在一定程度上模擬出來。
在你的文件加上下面的函數:
~~~
function! Sorted(l)
let new_list = deepcopy(a:l)
call sort(new_list)
return new_list
endfunction
~~~
保存并source文件,然后執行`: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
~~~
除了中間的一行和它們接受的參數,每一個函數都是一樣的。保存并source文件,在一些列表上試試它們。
`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
~~~
保存并source文件,執行下面命令試試看:
~~~
:let mylist = [[1, 2], [3, 4]]
:echo Mapped(function("Reversed"), mylist)
~~~
Vim顯示`[[2, 1], [4, 3]]`,正好是對列表中的每一個元素應用了`Reversed()`的結果。
`Mapped()`是如何起作用的?我們又一次用`deepcopy()`創建新的列表,修修改改,返回修改后的副本 —— 沒什么是新的。有門道的是中間的部分。
`Mapped()`接受兩個參數:一個funcref("儲存一個函數的變量"在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()`的好基友(counterpart):
~~~
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的vector那樣支持結構共享(structural sharing), 所以這里所有的復制操作是昂貴的。
有時這的確是個問題。如果你需要使用龐大的列表,程序就會因此變慢。 在現實世界,你可能會吃驚地發現你幾乎不會注意到其中的差別。
想想看吧:當我正寫下本章時,Vim占用了80M內存(而且我可是裝了_一堆_插件)。 我的筆記本總共有_8G_內存。有一些列表的副本被創建出來,這會造成可被察覺的不同嗎? 當然這取決于列表的大小,但在大多數情況下答案將會是"No"。
作為比較,我的Firefox打開了五個tab,現在正饕餮著_1.22G_內存。
你將需要自己判斷,什么時候這種編程風格會導致不可接受的低效率。
## 練習
閱讀`:help sort()`。
閱讀`:help reverse()`。
閱讀`:help copy()`。
閱讀`:help deepcopy()`。
閱讀`:help map()`,如果你未曾讀過。
閱讀`:help function()`。
修改`Assoc()`,?`Pop()`,?`Mapped()`,?`Filtered()`和`Removed()`來支持字典類型。 你可能需要閱讀`:help type()`來幫助自己。
實現`Reduced()`。
倒給自己一杯最喜歡的飲料。這一章真激烈(intense)!
- 前言
- 鳴謝
- 預備知識
- 打印信息
- 設置選項
- 基本映射
- 模式映射
- 精確映射
- Leaders
- 編輯你的Vimrc文件
- Abbreviations
- 更多的Mappings
- 鍛煉你的手指
- 本地緩沖區的選項設置和映射
- 自動命令
- 本地緩沖區縮寫
- 自動命令組
- Operator-Pending映射
- 更多Operator-Pending映射
- 狀態條
- 負責任的編碼
- 變量
- 變量作用域
- 條件語句
- 比較
- 函數
- 函數參數
- 數字
- 字符串
- 字符串函數
- Execute命令
- Normal命令
- 執行normal!
- 基本的正則表達式
- 實例研究:Grep 運算符(Operator),第一部分
- 實例研究:Grep運算符(Operator),第二部分
- 實例研究:Grep運算符(Operator),第三部分
- 列表
- 循環
- 字典
- 切換
- 函數式編程
- 路徑
- 創建一個完整的插件
- 舊社會下的插件配置方式
- 新希望:用Pathogen配置插件
- 檢測文件類型
- 基本語法高亮
- 高級語法高亮
- 更高級的語法高亮
- 基本折疊
- 高級折疊
- 段移動原理
- Potion段移動
- 外部命令
- 自動加載
- 文檔
- 發布
- 還剩下什么?