Learn-Vim/ch27_vimscript_functions.md
2021-01-14 17:20:42 -06:00

16 KiB

Ch27. Vimscript Functions

Functions are the pinnacles of programming. Can you imagine a programming language without functions? It is the ultimate means of abstraction. In this chapter, you will learn how to create your own Vimscript functions.

You have seen Vimscript functions in action. len(), filter(), map(), etc. You have also created basic custom Vimscript functions. Let's go deeper.

Function Syntax Rules

At the core, a Vimscript function has the following syntax:

function FunctionName()
  do-something()
endfunction

A function definition must start with an uppercase letter. It starts with the function keyword and ends with endfunction. Below is a valid function:

function! Tasty()
  echo "Tasty"
endfunction

But the following is not a valid function:

function tasty()
  echo "Tasty"
endfunction

If you prepend a function with the script variable (s:), you can use it with a lower case. function s:tasty() is a valid name. The reason why Vim requires you to use an uppercase name is to prevent confusion with Vim's built-in functions (they are all lowercased).

A function name cannot start with a number. 1Tasty() is not a valid function name, but Tasty1() is.

A function also cannot contain non-alphanumeric characters besides _. Tasty-food(), Tasty&food(), and Tasty.food() are not valid function names. Tasty_food() is.

If you define two functions with the same name, Vim will throw an error complaining that the function Tasty already exists. To overwrite the previous function with the same name, add a ! after the function keyword.

function! Tasty()
  echo "Tasty"
endfunction

Listing Functions

To see all the built-in and custom functions in Vim, you can run :function command. To look at the content of the Tasty function, you can run :function Tasty.

You can also search for functions with pattern with :function /pattern, similar to Vim's search navigation (/pattern). To search for all function containing the phrase "map", run :function/map. If you use external plugins, Vim will display the functions defined in those plugins.

If you want to look at where a function originates, you can use the :verbose command with the :function command. To look at where all the functions containing teh word "map" are originated, run:

:verbose function /map

When I ran it, I got a number of results. This one tells me that the function fzf#vim#maps autoload function (to recap, refer to Ch. 23) is written inside ~/.vim/plugged/fzf.vim/autoload/fzf/vim.vim file, on line 1263. This is useful for debugging.

function fzf#vim#maps(mode, ...)
        Last set from ~/.vim/plugged/fzf.vim/autoload/fzf/vim.vim line 1263

Removing A Function

To remove an existing function, use :delfunction {function-name}. To delete Tasty, run :delfunction Tasty.

Function Return Value

For a function to return a value, you need to pass it a return.

If you don't pass it, like the function Tasty, Vim automatically returns an implicit value of 0.

function! Tasty()
  echo "Tasty"
endfunction

Defining an empty return is also equivalent as returning a 0 value.

function! Tasty()
  echo "Tasty"
  return
endfunction

If you run :echo Tasty(), note that after Vim displays "Tasty!", it returns 0, the implicit return value. To make Tasty() to return "Tasty" value, you can do this:

function! Tasty()
  return "Tasty"
endfunction

You can use a function inside an expression. Vim will use the return value of that function. The expression :echo Tasty() . " Food!" evaluates the Tasty function and displays "Tasty Food!"

Formal Arguments

To pass a formal argument food to your Tasty function, you can do this:

function! Tasty(food)
  return "Tasty " . a:food
endfunction

echo Tasty("pastry")
" returns "Tasty pastry"

a: is one of the variable scopes mentioned in the previous chapter. It is the formal parameter variable. It is Vim's way to get a formal parameter value in a function. Without it, Vim will throw an error:

function! Tasty(food)
  return "Tasty " . food
endfunction

echo Tasty("pasta")
" returns "undefined variable name" error

Function Local Variable

Let's address the other variable you didn't learn on the previous chapter: the function local variable (l:).

When writing a function, you can define a variable inside:

function! Yummy()
  let location = "tummy"
  return "Yummy in my " . location
endfunction

echo Yummy()
" returns "Yummy in my tummy"

The variable location is the same as l:location. When you define a variable in a function, that variable is local to that function. I prefer to be more verbose than not, so I prefer to put l: to indicate that this is a function variable.

Vim has special variables with aliases that look like regular variables. v:count for example, has an alias of count. Calling count is the same as calling v:count. It is easy to accidentally use it.

function! Calories()
  let count = "count"
  return "I do not " . count . " my calories"
endfunction

echo Calories()
" throws an error

The execution above throws an error, because let count = "Count" implicitly attempts to redefine Vim's special variable v:count. Recall that special variables (v:) are read-only. You cannot mutate it. To fix it, use l:count:

function! Calories()
  let l:count = "count"
  return "I do not " . l:count . " my calories"
endfunction

echo Calories()
" returns "I do not count my calories"

It works now.

Calling A Function

Vim has a :call command to call a function.

function! Tasty(food)
  return "Tasty " . a:food
endfunction

call Tasty("gravy")

The call command does not output the return value. Let's call it with echo.

echo call Tasty("gravy")

Woops, you get an error. The call command above is a command-line command (:call). The echo command above is also a command-line command (:echo). You cannot call a command-line command with another command-line command. Let's try a different flavor of the call command:

echo call("Tasty", ["gravy"])
" returns "Tasty gravy"

To clear any confusion, you have just used two different call commands: :call command-line command and call() function. The call() function accepts as its first argument the function name (in string) and its second argument the formal parameters (in list).

Default Argument

You can provide a function parameter with a default value with =.

function! Breakfast(meal, beverage = "Milk")
  return "I had " . a:meal . " and " . a:beverage . " for breakfast"
endfunction

echo Breakfast("Hash Browns")
" returns hash browns and milk

echo Breakfast("Cereal", "Orange Juice")
" returns Cereal and Orange Juice

If you call Breakfast with only one argument, the beverage argument will use the "milk" default value.

Variable Arguments

You can pass a variable argument, use .... Variable argument is useful when you don't know how many variables a user will give.

Suppose you are create an all-you-can-eat buffet (because you'll never know how much food your customer will eat):

function! Buffet(...)
  return a:1
endfunction

If you run echo Buffet("Noodles"), it will echo "Noodles". Vim uses a:1 to print the first argument passed to ..., up to 20 (a:1 is the first argument, a:2 is the second argument, etc). If you run echo Buffet("Noodles", "Sushi"), it will still display just "Noodles", let's update it:

function! Buffet(...)
  return a:1 . " " . a:2
endfunction

echo Buffet("Noodles", "Sushi")
" Returns "Noodles Sushi"

The problem with this approach is if you now run echo Buffet("Noodles") (with only one variable), Vim complains that it has an undefined variable a:2. How can you make it flexible enough to display exactly what the user gives?

Luckily, Vim has a special variable a:0 to display the length of the argument passed into ....

function! Buffet(...)
  return a:0
endfunction

echo Buffet("Noodles")
" returns 1

echo Buffet("Noodles", "Sushi")
" returns 2

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns 5

With this, you can iterate using the length of the argument.

function! Buffet(...)
  let l:food_counter = 1
  let l:foods = ""
  while l:food_counter <= a:0
    let l:foods .= a:{l:food_counter} . " " 
    let l:food_counter += 1
  endwhile
  return l:foods
endfunction

The curly braces a:{l:food_counter} is Vim's string interpolation, it uses the value of food_counter counter to call the formal parameter argument a:1, a:2, a:3, etc.

echo Buffet("Noodles")
" returns "Noodles"

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns everything you passed
" returns Noodles Sushi Ice cream Tofu Mochi

The variable argument has one more special variable: a:000. It has the value of all variable arguments in a list format.

function! Buffet(...)
  return a:000
endfunction

echo Buffet("Noodles")
" returns ["Noodles"]

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns ["Noodles", "Sushi", "Ice cream", "Tofu", "Mochi"]

Let's refactor the function to use a for loop:

function! Buffet(...)
  let l:foods = ""
  for food_item in a:000
    let l:foods .= food_item . " "
  endfor
  return l:foods
endfunction

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns Noodles Sushi Ice cream Tofu Mochi

Range

You can define a ranged Vimscript function by adding a range keyword at the end of the function definition. A ranged function has two special variables available: a:firstline and a:lastline.

function! Breakfast() range
  echo a:firstline
  echo a:lastline
endfunction

If you are on line 100 and you run call Breakfast(), it will display 100 for both firstline and lastline. If you visually highlight (with v, V, or Ctrl-V) lines 101 to 105 and run call Breakfast(), firstline displays 101 and lastline displays 105.

The :call command can accepts range argument. If you run :11,20call Breakfast(), it will display 11 for firstline and 20 for lastline.

You might ask, "That's nice that Vimscript function accepts range, but can't I get the line number with line(".")? Won't it do the same thing?"

Good question. If this is what you meant:

function! Breakfast()
  echo line(".")  
endfunction

Calling 11,20call Breakfast() executes the Breakfast function 10 times (one for each line in the range). Compare that if you had passed the range argument:

function! Breakfast() range
  echo line(".")
endfunction

Calling 11,20call Breakfast() executes the Breakfast function once.

If you pass a range keyword and you pass a numerical range (like 11,20) on call, Vim only executes that function once. If you don't pass a range keyword and you pass a numerical range (like 11,20) on call, Vim executes that function N times depending on the range.

Dictionary

You can add a function as a dictionary item by adding a dict keyword when defining a function.

Suppose you have a function SecondBreakfast where you eat the same thing as the first breakfast.

function! SecondBreakfast() dict
  return self.breakfast
endfunction

Let's add this function to the meals dictionary:

let meals = {"breakfast": "pancakes", "second_breakfast": function("SecondBreakfast"), "lunch": "pasta"} 

echo meals.second_breakfast()
" returns "pancakes"

With dict keyword, the key variable self refers to the dictionary where the function is stored (in this case, the meals dictionary). The expression self.breakfast is equal to meals.breakfast.

An alternative way to add a function into a dictionary object is using a namespace.

function! meals.second_lunch()
  return self.lunch
endfunction

echo meals.second_lunch()
" returns "pasta"

Note that with namespace, you do not have to use the dict keyword.

Funcref

A funcref is a reference to a function. It is one of Vimscript's basic data types mentioned in Ch. 24.

The expression function("SecondBreakfast") is an example of funcref. Vim has a built-in function function() that returns a funcref variable when you pass it a function name (in string).

function! Breakfast(item)
  return "I am having " . a:item . " for breakfast"
endfunction

let Breakfastify = Breakfast
" returns error

let Breakfastify = function("Breakfast")

echo Breakfastify("oatmeal")
" returns "I am having oatmeal for breakfast"

echo Breakfastify("pancakes")
" returns "I am having pancakes for breakfast"

In Vim, if you want to assign a function to a variable, you can't just run assign it directly like let MyVar = MyFunc. You need to use the function() function, like let MyFar = function("MyFunc").

You can use funcref with maps and filters. Note that maps and filters will pass an index as the first argument and the iterated value as the second argument.

function! Breakfast(index, item)
  return "I am having " . a:item . " for breakfast"
endfunction

let breakfast_items = ["pancakes", "hash browns", "waffles"]
let first_meals = map(breakfast_items, function("Breakfast"))

for meal in first_meals
  echo meal
endfor

Lambda

A better way to use functions in maps and filters is to use lambda expression (sometimes known as unnamed function). For example:

let Plus = {x,y -> x + y}
echo Plus(1,2)
" returns 3

let Tasty = { -> 'tasty'}
echo Tasty()
" returns "tasty"

You can call a function from insisde a lambda expression:

function! Lunch(item)
  return "I am having " . a:item . " for lunch"
endfunction

let lunch_items = ["sushi", "ramen", "sashimi"]

let day_meals = map(lunch_items, {index, item -> Lunch(item)})

for meal in day_meals
  echo meal
endfor

If you don't want to call the function from inside lambda, you can refactor the map above into the following:

let day_meals = map(lunch_items, {index, item -> "I am having " . item . " for lunch"})

Method Chaining

You can chain several Vimscript functions and lambda expressions sequentially with ->. The syntax is:

Source->Method1()->Method2()->...->MethodN()

Syntactically, -> must be followed by a method name without space.

To convert a float to a number using method chaining:

echo 3.14->float2nr()
" returns 3

Let's do a more complicated example. Suppose that you need to capitalize the first letter of each item on a list, then sort the list, then join the list to form a string.

function! Capitalizer(word)
  return substitute(a:word, "\^\.", "\\u&", "g")
endfunction

function! CapitalizeList(word_list)
  return map(a:word_list, {index, word -> Capitalizer(word)})
endfunction

let dinner_items = ["bruschetta", "antipasto", "calzone"]

echo dinner_items->CapitalizeList()->sort()->join(", ")
" returns "Antipasto, Bruschetta, Calzone"

With method chaining, the sequence is more easily read and understood.

Closure

When you define a variable inside a function, that variable exists within that function boundaries. This is called a lexical scope.

function! Lunch()
  let appetizer = "shrimp"

  function! SecondLunch()
    return appetizer
  endfunction

  return funcref("SecondLunch")
endfunction

appetizer is defined inside the Lunch function, which returns SecondLunch funcref (Lunch is a function that returns a function). Notice that SecondLunch uses the appetizer, but in Vimscript, it doesn't have access to that variable. If you run echo Lunch()(), Vim will throw an undefined variable error.

To fix this issue, use the closure keyword. Let's refactor:

function! Lunch()
  let appetizer = "shrimp"

  function! SecondLunch() closure
    return appetizer
  endfunction

  return funcref("SecondLunch")
endfunction

Now if you run echo Lunch()(), Vim will return "shrimp".

Learn Vimscript Functions The Smart Way

In this chapter, you learned the anatomy of Vim function. You learned how to use different special keywords range, dict, and closure to modify a function's behavior. You also learned how to use lambda and how to chain functions together. Vim functions are important tools to create complex abstractions. Now you should have sufficient knowledge to start writing your own plugins!