LV02-vimscript-06-函数.md

本文主要是vimscript一些基础知识和操作的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
点击查看相关文件下载
--- ---

一、函数

接下来,就该了解一下函数了。官方文档关于函数说明在这里:VIM: eval - user-functions

在此之前需要先了解函数名称的规范:函数名须以大写字母开始,以免和内建函数引起混淆。要避免在不同脚本使用相同的名字,避免显见的或者过短的名字。一个好习惯是使用脚本名字作为函数名字的开头,比如HTMLcolor()

局部于脚本的函数必须以 s: 开始。局部于脚本的函数只能在同一脚本和脚本中定义的 函数、用户命令和自动命令里调用。也可以在脚本定义的映射里调用该函数,但必须使用<SID> 而不是s:,如果映射会在脚本之外被扩展的话。 只有局部于脚本的函数,没有局部于缓冲区或局部于窗口的函数

上边是Vim中文文档中关于用户函数的一些说明,没有作用域限制的Vimscript函数必须以一个大写字母开头!而且大多数的开发者都是这么做的,这样可以为自己避免一些不必要的麻烦。

二、基本语法

1. 一些命令

1
2
3
:fu[nction]        " 列出所有函数和它们的参数。
:fu[nction] {name} " 列出 {name} 命名的函数。
:fu[nction] /{pattern} " 列出名字匹配 {pattern} 的函数。

2. 定义新函数

1
2
3
4
fu[nction][!] {name}([arguments]) [range] [abort] [dict] [closure]
command
retu[rn] [expr] " 这个是可选的,就是返回值
endf[unction]

定义 {name} 命名的新函数。函数体在之后的行给出,直到匹配的endfunction 为止。

点击查看格式说明

详情可查看VIM: eval - function

name 名字必须由字母数字和 '_' 字符组成,而且必须以大写字母或者 "s:" 开头.
! 如果同名的函数已经存在而且没有使用 [!],Vim会给出错误信息。如果给出 [!],已有的函数被悄然替代。如果该函数正在执行期间除外。此时,这是一个错误。
arguments 参数的定义只要给出它的名字。在函数里,可以使用 "a:name" 来访问 ("a:" 代表参数(argument))。
range 如果给出 [range] 参数,则该函数自己能理解并处理行范围。该范围通过 "a:firstline" 和 "a:lastline" 定义。如果没有 [range],":{range}call" 会在该范围的每一行分别执行该函数,每次光标都定位在处理行的行首。
abort 如果给出 [abort] 参数,该函数在遇到错误时立即中止。
dict 如果给出 [dict] 参数,该函数必须通过 Dictionary 的项目才能调用。局部变量 "self" 这时设为该字典。
closure 加入 [closure] 参数时,函数可以访问外部作用域的变量和参数,通常这被称为闭包。
retu[rn] [expr] 从函数返回。如果给出 "[expr]",计算该表达式的结果成为函数的返回值。如果没有给出 "[expr]",返回 0。

3. 删除函数

Vim中,提供了用户函数删除命令

1
:delf[unction][!] {name}

name为要删除的函数的名称。

4. 函数调用

4.1 调用格式

1
2
3
4
" 方法一: call命令调用
call Function_name()
" 方法二: 表达式中直接调用,以 echo 为例
echo Function_name()

4.2 实例1:没有返回值

text.vim文件中添加以下内容:

1
2
3
4
5
6
function Test()
echo "Hello, World!"
endfunction
call Test()
echo "\n"
echo Test()

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
3
Hello, World!

Hello, World!

4.3 实例2:有返回值

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
function Test()
echo "Hello, World!"
return "return Hello, World!"
endfunction
call Test()
echo "\n"
echo Test()

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
3
4
Hello, World!

Hello, World!
return Hello, World!

第一个call test()输出Hello, World!,然而第二个却输出的是Hello, World! return Hello, World!。我们使用call时,返回值会被丢弃, 所以这种方法仅在函数具有副作用时才有用。

4.4 实例3:隐式返回

text.vim文件中添加以下内容:

1
2
3
4
function Test()
echo "Hello, World!"
endfunction
echo Test()

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
Hello, World!
0

这将会显示两行:Hello, World!0。第一个字符串显然来自于Test()内部的echo。第二个则告诉我们, 如果一个Vimscript函数不返回一个值,它将隐式返回0。那么这有什么用呢?在text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
" 检查文本宽度是否过宽函数,超过80返回1,否则隐式返回0
function TextwidthIsTooWide()
if &l:textwidth ># 80
return 1
endif
endfunction

set textwidth=20
if TextwidthIsTooWide()
echom "WARNING: Wide text!"
else
echom "ok!"
endif

set textwidth=90
if TextwidthIsTooWide()
echom "WARNING: Wide text!"
else
echom "ok!"
endif

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
ok!
WARNING: Wide text!

从测试中看出,这样可以很方便的用于返回真或者假用于判断。

5. 函数参数

很多时候,函数是需要传入参数的,接下来就来看一看函数的传参。

5.1 普通参数

text.vim文件中添加以下内容:

1
2
3
4
5
6
function DisplayName(name)
echo "Hello! My name is:"
echo a:name
endfunction

call DisplayName("fanhua")

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
Hello!  My name is:
fanhua

看到这里,我可能会问,a:name是啥个意思呢?往前看,在变量与表达式一文中有说明,a:var_name表示函数参数(只限在函数内部使用)但是放我们去掉这个a:的时候,会发现,重新执行时会产生错误:

1
E121: 未定义的变量: name

所以,我们在写需要参数的Vimscript函数的时候,总是需要在参数加上前缀a:,来告诉Vim去参数作用域查找

5.2 可变参数

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
function Varg(...)
echo "a:0="a:0
echo "a:1="a:1
echo "a:2="a:2
echo "a:3="a:3
echo "a:000="a:000
endfunction

call Varg("a", "b", 1)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
3
4
5
a:0= 3
a:1= a
a:2= b
a:3= 1
a:000= ['a', 'b', 1]

【注意】

(1)函数使用可变参数时,不限制输入参数的个数。

(2)函数内部不可以引用未传入的参数,例如,函数内用a:4来引用第四个参数,这个时候若是调用函数的时候未传入参数小于四个时,就会报错:

1
E121: 未定义的变量: a:4
点击查看输出说明
... 说明这个函数可以接受任意数目的参数。
a:0 当在Vim中定义了一个接受可变参数的函数时,a:0将被设置为我们额外给的参数数量。
a:1 可以使用a:1, a:2, a:3等等来引用函数接受的每一个额外参数。
a:000 该值将被设置为一个包括所有传递过来的额外参数的列表(list)。

5.3 同时使用

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
function Varg(name, ...)
echo "a:name="a:name
echo "a:0="a:0
echo "a:1="a:1
echo "a:2="a:2
echo "a:3="a:3
echo "a:4="a:4
echo "a:000="a:000
endfunction

call Varg("fanhua", "a", "b", 1, 100)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
3
4
5
6
7
a:name= fanhua
a:0= 4
a:1= a
a:2= b
a:3= 1
a:4= 100
a:000= ['a', 'b', 1, 100]

三、函数式编程

函数式编程?这是啥?看到这个词的第一眼,这就是我的反应,以前学的CShell里边没得啊。其实我也不知道啦,不过网上一搜有一大堆的解释,但是似乎都不明让我理解,但是可以参考这篇文档,解释的相对很明白了:函数式编程入门教程 - 阮一峰的网络日志

1. 不可变的数据结构

什么是不可变的数据结构?意思就是,以列表为例,在text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
function! Sorted(mylist)
let new_list = deepcopy(a:mylist)
call sort(new_list)
return new_list
endfunction

let mylist = [2, 1, 4, 3]
echo "Before sort: mylist="mylist
let mylist_s = sort(mylist)
echo "After sort: mylist="mylist
echo "\n"
echo Sorted(mylist)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
3
4
Before sort: mylist= [2, 1, 4, 3]
After sort: mylist= [1, 2, 3, 4]

[1, 2, 3, 4]

我们会发现,我们使用了排序函数对列表进行排序,即便是用另一个变量接收结果,我们原始的列表也还是发生了变化。

但是这并不是我们的本意,原来的列表我们依然需要保留,那么这个时候,刚才定义的函数就排上了用场,Vimsort()就原列表进行了重排列表,所以我们先创建一个列表的副本,并对副本进行排序, 这样原本的列表不会被改变。这样就避免了副作用,并帮助我们写出更容易推断和测试的代码。

  • 更多辅助函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
" 接受一个列表并返回一个新的倒置了元素的列表。
function! Reversed(mylist)
let new_list = deepcopy(a:mylist)
call reverse(new_list)
return new_list
endfunction

" 返回一个在原列表的基础上增加了给定值的新列表。
function! Append(mylist, val)
let new_list = deepcopy(a:mylist)
call add(new_list, a:val)
return new_list
endfunction

" (associate的缩写)返回一个给定索引上的元素被替换成新值的新列表。
function! Assoc(mylist, i, val)
let new_list = deepcopy(a:mylist)
let new_list[a:i] = a:val
return new_list
endfunction

" 返回一个给定索引上的元素被移除的新列表。
function! Pop(mylist, i)
let new_list = deepcopy(a:mylist)
call remove(new_list, a:i)
return new_list
endfunction

2. 作为变量的函数

Vimscript支持使用变量储存函数,这似乎就跟C中的指针函数一样。

2.1 变量存储函数

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
" 返回一个在原列表的基础上增加了给定值的新列表。
function! Append(mylist, val)
let new_list = deepcopy(a:mylist)
call add(new_list, a:val)
return new_list
endfunction
let Myfunc = function("Append")
echo Myfunc([1, 2], 3)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
[1, 2, 3]

无可厚非,最后显示了一个添加了元素的列表。

【注意】如果一个Vimscript变量要引用一个函数,它就要以大写字母开头。

2.2 列表存储函数

我们不仅可以使用变量存储和调用函数,也可以通过列表。在text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
" 返回一个在原列表的基础上增加了给定值的新列表。
function! Append(mylist, val)
let new_list = deepcopy(a:mylist)
call add(new_list, a:val)
return new_list
endfunction
" 返回一个给定索引上的元素被移除的新列表。
function! Pop(mylist, i)
let new_list = deepcopy(a:mylist)
call remove(new_list, a:i)
return new_list
endfunction
let funcs = [function("Append"), function("Pop")]
echo funcs[1](['a', 'b', 'c'], 1)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
['a', 'c']

【注意】当变量中存储的是列表的时候,首字母不必大写,列表的内容即使是函数,但是合在一起的时候终归还是一个列表,然后就可以通过列表索引来引用相应的函数。

3. 高阶函数

高阶函数就是接受别的函数并使用它们的函数。

3.1 map()

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
" 接受一个列表并返回一个新的倒置了元素的列表。
function! Reversed(mylist)
let new_list = deepcopy(a:mylist)
call reverse(new_list)
return new_list
endfunction

function! Mapped(fn, mylist)
let new_list = deepcopy(a:mylist)
call map(new_list, string(a:fn) . '(v:val)')
return new_list
endfunction

let mylist = [[1, 2], [3, 4], [5, 7, 9, 8, 6]]
echo Mapped(function("Reversed"), mylist)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
[[2, 1], [4, 3], [6, 8, 9, 7, 5]]

上边的函数什么意思?接下来我们来分析一下。

点击查看map()函数说明

可以使用:help map()来查看帮助文档。VIM: eval - map()

1
map({expr1}, {expr2}) " {expr1} 里的每个项目被 {expr2} 的计算结果替代。

【说明】

(1){expr1} 必须是 List]Dictionary{expr2} 必须是stringFuncref

(2)关于{expr2}

如果 {expr2}string{expr2} 内的 v:val包含当前项目的值。 Dictionaryv:key 包含当前项目的键。Listv:key包含当前项目的索引(其中,v:val就是List Dictionary当前项目的值;v:key就是Dictionary当前项目的值)。例如:在text.vim文件中添加以下内容:

1
2
3
let mylist = [1, 2, 3]
echo map(mylist, '"> " . v:val . " <"')
" mylist 里的每个项目之前放上 > ,而之后放上 < 。

在命令模式中执行:source test.vim可以看到输出结果如下:

1
['> 1 <', '> 2 <', '> 3 <']

如果 {expr2}Funcref,它必须接受两个参数:当前项目的键或索引和当前项目的值。

点击查看string()函数说明

可以使用:help string()来查看帮助文档。

1
string({expr})  " 返回 {expr} 转换后的字符串。

【说明】如果 {expr} 为数值、浮点数、字符串、blob 或它们的复合形式,那么用 eval() 可以把结果转回去。

Mapped()接受两个参数:一个funcref(储存一个函数的变量)和一个列表。 我们用deepcopy()创建新的列表,然后调用map()函数,对传入列表的数据进行倒置,最后返回新倒置后的列表。

3.2 filter()

text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
function! Filtered(fn, mylist)
let new_list = deepcopy(a:mylist)
call filter(new_list, string(a:fn) . '(v:val)')
return new_list
endfunction

let mylist = [[1, 2], [], ['fanhua'], []]
echo Filtered(function('len'), mylist)

在命令模式中执行:source test.vim可以看到输出结果如下:

1
[[1, 2], ['fanhua']]
点击查看filter()函数说明

可以使用:help filter()来查看帮助文档。VIM: eval - filter()

1
2
3
filter({expr1}, {expr2}) 
" 对 {expr1} 里的每个项目计算 {expr2},如果结果为零,
" 就从该 List 或 Dictionary 里删除该项目。

【说明】

(1){expr1} 必须是List或者Dictionary{expr2} 必须是stringFuncref

(2)关于{expr2}

如果 {expr2}string{expr2} 内的 v:val包含当前项目的值。 Dictionaryv:key 包含当前项目的键。Listv:key包含当前项目的索引(其中,v:val就是List Dictionary当前项目的值;v:key就是Dictionary当前项目的值)。例如:在text.vim文件中添加以下内容:

1
2
3
4
5
6
7
let mylist = [1, 2, 3, 'OLD', 'OLDh']
" 删除所有出现 OLD 的项目
echo filter(mylist, 'v:val !~ "OLD"')

let mydict = {'1':1, '9':9, '10':5 }
" 删除所有键小于 8 的值。
echo filter(mydict, 'v:key >= 8')

在命令模式中执行:source test.vim可以看到输出结果如下:

1
2
[1, 2, 3]
{'10': 5, '9': 9}

如果 {expr2}Funcref,它必须接受两个参数:当前项目的键或索引和当前项目的值。

点击查看len()函数说明

可以使用:help len()来查看帮助文档。VIM: eval -len()

1
len({expr})     " 返回数值,参数的长度。
expr 返回值
字符串 返回它使用的字节数
数值 返回它使用的字节数
list 返回 List 的项目数量
Blob 返回字节数
Dictionary 返回 Dictionary 的项目数量。

Filtered()接受一个谓词函数和一个列表,它返回一个列表的副本, 而这个列表只包括将自身作为谓词函数的输入参数并返回真值的元素。 这里使用了内置的len(),让它来过滤掉所有长度为0的元素。

那如果我想要获取列表中长度为0的元素呢?在text.vim文件中添加以下内容:

1
2
3
4
5
6
7
8
function! Removed(fn, mylist)
let new_list = deepcopy(a:mylist)
call filter(new_list, '!' . string(a:fn) . '(v:val)')
return new_list
endfunction

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

在命令模式中执行:source test.vim可以看到输出结果如下:

1
[[], []]

Vim显示[[], []]Removed()就像Filtered(),不过它只保留谓词函数返回非真值的元素。代码中的唯一不同在于调用命令前面的'!' .,它把谓词函数的结果取反。

3.3 效率

考虑到Vim不得不持续地创建新的副本并垃圾回收旧的对象,可能会认为不停地制造副本是种浪费。是的,就是这样的!Vim的列表不像Clojurevector那样支持结构共享(structural sharing), 所以这里所有的复制操作是昂贵的。当需要使用庞大的列表,程序就会因此变慢。 在现实世界,我们可能会吃惊地发现自己几乎不会注意到其中的差别。