We're going to take a short break now to talk about a style of programming you may have heard of: functional programming.
Vimscript has all the pieces necessary to program in a kind-of-functional style, but it's a bit clunky. We can create a few helpers that will make it less painful though.
Go ahead and create a
functional.vim file somewhere so you don't have to keep
typing everything over and over. This file will be our scratchpad for this
Unfortunately Vim doesn't have any immutable collections like Clojure's vectors and maps built-in, but by creating some helper functions we can fake it to some degree.
Add the following function to your file:
function! Sorted(l) let new_list = deepcopy(a:l) call sort(new_list) return new_list endfunction
Source and write the file, then run
:echo Sorted([3, 2, 4, 1]) to try it out.
[1, 2, 3, 4].
How is this different from simply calling the built-in
sort() function? The
key is the first line:
let new_list = deepcopy(a:l). Vim's
sort() sorts the
list in place, so we first create a full copy of the list and sort that so
the original list won't be changed.
This prevents side effects and helps us write code that is easier to reason about and test. Let's add a few more helper functions in this same style:
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
Each of these functions is exactly the same except for the middle line and the arguments they take. Source and write the file and try them out on a few lists.
Reversed() takes a list and returns a new list with the elements reversed.
Append() returns a new list with the given value appended to the end of the
Assoc() (short for "associate") returns a new list, with the element at the
given index replaced by the new value.
Pop() returns a new list with the element at the given index removed.
Vimscript supports using variables to store functions, but the syntax is a bit obtuse. Run the following commands:
:let Myfunc = function("Append") :echo Myfunc([1, 2], 3)
Vim will display
[1, 2, 3] as expected. Notice that the variable we used
started with a capital letter. If a Vimscript variable refers to a function it
must start with a capital letter.
Functions can be stored in lists just like any other kind of variable. Run the following commands:
:let funcs = [function("Append"), function("Pop")] :echo funcs(['a', 'b', 'c'], 1)
['a', 'c']. The
funcs variable does not need to start with
a capital letter because it's storing a list, not a function. The contents of
the list don't matter at all.
Let's create a few of the most commonly-used higher-order functions. If you're not familiar with that term, higher-order functions are functions that take other functions and do something with them.
We'll begin with the trusty
map function. Add this to your file:
function! Mapped(fn, l) let new_list = deepcopy(a:l) call map(new_list, string(a:fn) . '(v:val)') return new_list endfunction
Source and write the file, and try it out by running the following commands:
:let mylist = [[1, 2], [3, 4]] :echo Mapped(function("Reversed"), mylist)
[[2, 1], [4, 3]], which is the result of applying
every element in the list.
Mapped() work? Once again we create a fresh list with
do something to it, and return the modified copy -- nothing new there. The
tricky part is the middle.
Mapped() takes two arguments: a funcref (Vim's term for "variable holding
a function") and a list. We use the built-in
map() function to perform the
actual work. Read
:help map() now to see how it works.
Now we'll create a few other common higher-order functions. Add the following to your file:
function! Filtered(fn, l) let new_list = deepcopy(a:l) call filter(new_list, string(a:fn) . '(v:val)') return new_list endfunction
Filtered() out with the following commands:
:let mylist = [[1, 2], , ['foo'], ] :echo Filtered(function('len'), mylist)
[[1, 2], ['foo']].
Filtered() takes a predicate function and a list. It returns a copy of the
list that contains only the elements of the original where the result of calling
the function on it is "truthy". In this case we use the built-in
function, so it filters out all the elements whose length is zero.
Finally we'll create the counterpart to
function! Removed(fn, l) let new_list = deepcopy(a:l) call filter(new_list, '!' . string(a:fn) . '(v:val)') return new_list endfunction
Try it out just like we did with
:let mylist = [[1, 2], , ['foo'], ] :echo Removed(function('len'), mylist)
Removed() is like
Filtered() except it only keeps
elements where the predicate function does not return something truthy.
The only difference in the code is the single
'!' . we added to the call
command, which negates the result of the predicate.
You might be thinking that copying lists all over the place is wasteful, since Vim has to constantly create new copies and garbage collect old ones.
If so: you're right! Vim's lists don't support the same kind of structural sharing as Clojure's vectors, so all those copy operations can be expensive.
Sometimes this will matter. If you're working with enormous lists, things can slow down. In real life, though, you might be surprised at how little you'll actually notice the difference.
Consider this: as I'm writing this chapter my Vim program is using about 80 megabytes of memory (and I have a lot of plugins installed). My laptop has 8 gigabytes of memory in it. Is the overhead of having a few copies of a list around really going to make a noticeable difference? Of course that depends on the size of the list, but in most cases the answer will be "no".
To contrast, my Firefox instance with five tabs open is currently using 1.22 gigabytes of RAM.
You'll need to use your own judgement about when this style of programming creates unacceptably bad performance.
:help map() if you haven't already.
Removed() to support
dictionaries. You'll probably need
:help type() for this.
Pour yourself a glass of your favorite drink. This chapter was intense!