Monday, January 13, 2014

Anonymous Functions in VimL

Admit it, you’ve wanted this for a long time now.

Update: These two functions are now available in VimaholicsAnonymous.

VimL’s list sort() method allows the caller to provide a custom comparison function with which to sort by. Unfortunately, VimL doesn’t support anonymous functions (sometimes called lambdas), so the caller is forced to pre-write their comparator as a full-fledged function and provide its funcref to sort(). Well, that ends today. Now, VimL has anonymous function love! Check it:

This humble little snippet of code lets vimmers declare anonymous functions on the fly:


let fn_idx = 0
function! Fn(fn_form)
  let [fn_args, fn_body] = split(a:fn_form, '\s*=>\s*')
  let g:fn_idx = g:fn_idx + 1
  let fname = 'AnonFn_' . g:fn_idx
  let b_elems = split(fn_body, '|')
  let b_elems[-1] = 'return ' . b_elems[-1]
  exe 'func! ' . fname . fn_args . "\n"
        \. join(b_elems, "\n") . "\n"
        \. 'endfunc'
  return function(fname)
endfunction

Like this:


let x = ["one","two","three","four","five","six","seven","eight","nine","ten"]
echo sort(x, Fn('(a, b) => len(a:a) > len(a:b)'))

The magic is in the Fn() call. It takes a string argument of the form:
(arguments) => function-body statements separated by | (pipe)

The last statement will be implicitly returned by the anonymous function, so no need to explicitly add a return statement.

Unicorns, or what?!

What? You want more. Certainly, sir. Behold:


function! Fx(fn, ...)
  return call(a:fn, a:000)
endfunction

That lets you execute an anonymous function, like:


echo '2 ^ 6 = ' . string(Fx(Fn('(a, b) => pow(a:a, a:b)'), 2, 6))

Any cooler and you’d need your jumper! :-D Oh, but there’s more… I’m just getting warmed up!

Just because I like you, here’s a little extra gift:


let Mul = Fn('(a,b)=>let x = a:a | let y=a:b | x*y')
echo '5 * 6 = ' . Fx(Mul, 5, 6)

That lets you call your funcref'd anonymous function by name. Did you see what I did there? Yes, I cheated God! I named an annonymous function. Cool, eh?

So, apart from the obvious, is this really that amazing? My vote is: YES! I have plans for this technology. Here’s a sneaky hint of where I’m looking to take this:


CompileMacros

Mul = ((a,b) => let x = a:a | let y=a:b | x*y)

echo '5 * 6 = ' . #(Mul, 5, 6)
echo '2 * 6 = ' . #(((a, b) => a:a * a:b), 2, 6)

That’s right. I want hygenic macros in VimL. And that code works in my current experiments. The notation is stolen from… clojure I believe, where #(…) executes a lambda.

The call to CompileMacros will process any macros in the surrounding expressions before sourcing the result. Just for completeness, defining macros currently looks like this:


call Macro('(\+\((.\{-})\s*=>.\{-}\))', "Fn('\\1')")

call Macro('\%(\n\|^\)\@<=\s*\(\w\+\)\s*=\s*\(Fn(.*=>.\{-})\)\s*\n',
      \ "let \\1 = \\2\n" )

call Macro('#(\(.*\))\n', "Fx(\\1)\n")

call Macro('#Fn(\(.\{-}\))\n', "Fx(Fn(\\1)\n")

But that will change in the next version. Using regex to parse is an abomination; I was merely proof-of-concepting here. I’ll use VimPEG instead.

Stay tuned, if bending the spoon is what you’re in to.

No comments:

Post a Comment