4
\$\begingroup\$

I have a Lua project that uses some syntax sugar described in DecoratorsAndDocstrings, near the bottom of the page. It looks like this:

random =
  docstring[[Compute random number.]] ..
  function(n)
    return math.random(n)
  end

Notice how the function definition is concatenated to the result of the function previous function call. I also have similar sugar like this:

something = foo.bar "Blah" { a = 1, b = 2 }

Basically, the idea is that instead of foo.bar taking two arguments (a string and a table), it only takes the first argument, and returns a function that finishes the operation (like a continuation). In the first example, instead of returning a function, we'd return a table with a __concat meta method.

I ended up with a handful of functions in my project that use syntax like this. The problem is:

  • The code is not straightforward.
  • It's difficult to document properly.
  • Adding support for non-sugared calls compounds the previous two problems.

So, I had the idea to just write them as normal functions, and create a "sweeten" function that takes two arguments: a function to "sweeten," and the number of args it wants. It returns a wrapper for the function. If you call the wrapper with at least the required number of arguments, it will simply tail-call the wrapped function. If you call it with less arguments, it will instead return a sort of "continuation table" with both __call and __concat metamethods set up as wrappers to the wrapper -- calling them will invoke the wrapper again with the previous args and the new args.

local function sweeten(fn, argc)
  local wrapper

  argc = tonumber(argc) or 2

  local function unpack_plus(t, i, ...)
    i = i or 1
    if i <= #t then
      return t[i], unpack_plus(t, i + 1, ...)
    else
      return ...
    end
  end

  local function resume(t, ...)
    return wrapper(unpack_plus(t, 1, ...))
  end

  local meta = { __call = resume, __concat = resume }

  wrapper = function(...)
    if select('#', ...) >= argc then
      return fn(...)
    else
      return setmetatable({...}, meta)
    end
  end

  return wrapper
end

To test if out, you can do something like:

function foo(a, b, c) print(a, b, c) end
function sweet_foo = sweeten(foo, 3)
sweet_foo "qwe" "asd" "zxc" -- prints stuff

I feel like I should be able to simplify sweeten and still get the same result. I'd especially like to get rid of unpack_plus, but I can't see a way to do it. Any thoughts?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.