Sunday, December 16, 2012

call() for a Good Time

Simple functions in Vim are declared like this:

function! A(a, b, c)
  echo a:a a:b a:c
endfunction

call A(1, 2, 3)

There’s probably nothing surprising there except for the a:a syntax, which is how Vim insists on accessing the function’s arguments (mnemonic: a: for argument).
Just as simple is calling function A() from another function, B(), passing its arguments directly along to A():

function! B(a, b, c)
  return A(a:a, a:b, a:c)
endfunction

call B(1, 2, 3)

Nothing surprising there at all. But we’ve just laid the groundwork for the main attraction tonight. In VimL, you can call a function using the library function call(func, arglist) where arglist is a list. If you’re calling a function that takes multiple arguments, collect them in an actual list like this:

function! C(a, b, c)
  return call("A", [a:a, a:b, a:c])
endfunction

call C(1, 2, 3)

If you already have the elements in a list, no need to wrap it in an explicit list:

function! D(a)
  return call("A", a:a)
endfunction

call D([1, 2, 3])

Let’s step it up a notch. What if you want to be able to accept the args as either separate arguments or as a list? Vim has your back with variadic functions cloaked in a syntax similar to C’s:

Variadics in the key of V:
  • a:0 is a count of the variadic arguments
  • a:000 is all of the variadic arguments in a single list
  • a:1 to a:20 are positional accessors to the variadic arguments


So now it doesn’t matter how we receive the arguments — standalone or in a list — we can keep Vim happy and call A() appropriately.

function! E(...)
  if a:0 == 1
    return call("A", a:1)
  else
    return call("A", a:000)
  endif
endfunction

call E(1, 2, 3)
call E([1, 2, 3])

Ok. That’s not too bad; it’s perhaps a little awkward. We’re calling A() directly here, but it shouldn’t be a surprise to see that we can call C() in the same way too:

function! F(...)
  if a:0 == 1
    return call("C", a:1)
  else
    return call("C", a:000)
  endif
endfunction

call F(1, 2, 3)
call F([1, 2, 3])

Pretty straightforward. What about calling D() instead which expects a single list argument? Hmm… if Vim wants a list, give him a list:

function! G(...)
  if a:0 == 1
    return call("D", [a:1])
  else
    return call("D", [a:000])
  endif
endfunction

call G(1, 2, 3)
call G([1, 2, 3])

It’s worth stopping briefly here to consider what call() is doing to that arglist: It’s splatting it (extracting the arguments and passing them as separate members to the called function). Nice. Wouldn’t it be nice if we could splat lists ourselves? Well, be envious of Ruby coders no more because we can splat lists in VimL!

Splat!
To splat a list into separate variables (a, b and c here):
let [a, b, c] = somelist
Read :help :let-unpack for the juicy extras.

I like the splatting approach because it gives us variable names to play with inside our function:

function! H(...)
  if a:0 == 1
    let [a, b, c] = a:1
  else
    let [a, b, c] = a:000
  endif
  return D([a, b, c])
endfunction

call H(1, 2, 3)
call H([1, 2, 3])

Of course, it works just as well for calling functions with explicit multiple arguments, like C():

function! I(...)
  if a:0 == 1
    let [a, b, c] = a:1
  else
    let [a, b, c] = a:000
  endif
  return C(a, b, c)
endfunction

call I(1, 2, 3)
call I([1, 2, 3])

You’ll notice that the splat semantics are identical between H() and I() and only the call of D() and C() change, respectively. This is very neat, I think.
So far we’ve been calling through to functions that call A() directly. Happily, we can call through to one of these dynamic functions (like E(), but any would work as well) and have it Just Work too:

function! J(...)
  if a:0 == 1
    let [a, b, c] = a:1
  else
    let [a, b, c] = a:000
  endif
  return E(a, b, c)
endfunction

call J(1, 2, 3)
call J([1, 2, 3])

So, that’s it. Vim has variadic functions and splats. And splats are my recommended pattern for handling deep call chains between variadic functions.
There’s one last, cute, little thing about splats: you can collect a certain number of explicit arguments as you require, and then have any remaining arguments dumped into a list for you. The rest variable here will be a list containing [4, 5, 6] from the subsequent calls:

function! K(...)
  if a:0 == 1
    let [a, b, c; rest] = a:1
  else
    let [a, b, c; rest] = a:000
  endif
  echo "rest: " . string(rest)
  return E(a, b, c)
endfunction

call K([1, 2, 3, 4, 5, 6])
call K(1, 2, 3, 4, 5, 6)

And I thought this was going to be a short post when I started. I almost didn’t bother posting it because of that reason.