cat /dev/brain |

Lua — Wat?!‽

published on Thursday, February 2, 2017

As simple as it gets

Take a minute to contemplate this fine piece of lua, making use of the ... syntax for variadic functions and the builtin unpack() function:

local function bind(f, ...)
    local x = {...}
    return function()
        return f(unpack(x))
    end
end

Now what do we have here?

Right, it takes a function and some arguments and returns a callable that, when invoked, will call the function with said arguments. It's a basic version of what is called in python a partial.

Looks simple enough, but does it work?

Sure it does, lets see:

> bind(print, 1, 2)()
1 2

> bind(print, 1, 2, 3)()
1 2 3

Great!

But what happens if we pass in a nil?

> bind(print, 1, 2, 3, nil)()
1 2 3

Okay, it gets swallowed. So, a nil terminates the argument list. Got it!

> bind(print, 1, 2, 3, nil, 5)()
1   2   3   nil 5

Oh, nevermind. The truth is: only a trailing nil will not be forwarded as an argument.

> bind(print, 1, 2, 3, nil, 5, nil)()
1   2   3

I meant, a trailing nil terminates the argument list at the first occurence of a nil.

> bind(print, 1, 2, 3, nil, 5, nil, 7)()
1   2   3   nil 5   nil 7

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil)()
1   2   3

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9)()
1   2   3   nil 5   nil 7   nil 9

What I thought, it's all consistent!

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil)()
1   2   3   nil 5   nil 7

Okay, lua is much smarter than I thought. I guess, the actual rule of thumb is: a trailing nil terminates the argument list at first nil, unless its the fourth nil, then it terminates at the third. Makes sense to me!

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil)()
1   2   3   nil 5

Oh, this will be easy to integrate in the mental ruleset.

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil)()
1   2   3

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil)()
1   2   3

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil, nil)()
1   2   3   nil 5   nil 7

> bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil, nil, nil)()
1   2   3   nil 5   nil 7   nil 9

This is even easier to predict than ever anticipated. :)

Note, this feature works on lua 5.1-5.3.

The complete code

Again, the complete code-example looks like this:

local function bind(f, ...)
    local x = {...}
    return function()
        f(unpack(x))
    end
end

bind(print, 1, 2)()
bind(print, 1, 2, 3)()
bind(print, 1, 2, 3, nil)()
bind(print, 1, 2, 3, nil, 5)()
bind(print, 1, 2, 3, nil, 5, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil, nil)()
bind(print, 1, 2, 3, nil, 5, nil, 7, nil, 9, nil, nil, nil, nil, nil, nil)()

And the corresponding output:

1   2
1   2   3
1   2   3
1   2   3   nil 5
1   2   3
1   2   3   nil 5   nil 7
1   2   3
1   2   3   nil 5   nil 7   nil 9
1   2   3   nil 5   nil 7
1   2   3   nil 5
1   2   3
1   2   3
1   2   3   nil 5   nil 7
1   2   3   nil 5   nil 7   nil 9

Do not use this

For the love of all that is good and descent, if you have any sanity left, please don't use this bugged variant of bind:

-- pack function arguments. Use unpack2() for unpacking! This differs
-- from the builtin method `x = {...}; unpack(x)` in that it unpacks the
-- correct number of arguments, even in the presence of nil values.
function pack2(...)
    return {n = select('#', ...), ...}
end

-- unpack function arguments that were packed by pack2()
function unpack2(t, start)
    return unpack(t, start, t.n)
end

-- concat two parameter packs that were packed by pack2. This is
-- necessary to prevent multiple nils being joined at the end of the first
-- pack.
function pack_concat(a, b)
    local ret = {n = a.n+b.n, unpack2(a)}
    for i = 1, b.n do
        ret[a.n+i] = b[i]
    end
    return ret
end

-- bind initial arguments to a function (partial)
-- bind(f, x)(y) = f(x, y)
function bind(func, ...)
    local head = pack2(...)
    return function(...)
        local tail = pack2(...)
        local args = pack_concat(head, tail)
        return func(unpack2(args))
    end
end

It delivers completely unpredictable output such as this:

1   2
1   2   3
1   2   3   nil
1   2   3   nil 5
1   2   3   nil 5   nil
1   2   3   nil 5   nil 7
1   2   3   nil 5   nil 7   nil
1   2   3   nil 5   nil 7   nil 9
1   2   3   nil 5   nil 7   nil 9   nil
1   2   3   nil 5   nil 7   nil 9   nil nil
1   2   3   nil 5   nil 7   nil 9   nil nil nil
1   2   3   nil 5   nil 7   nil 9   nil nil nil nil
1   2   3   nil 5   nil 7   nil 9   nil nil nil nil nil
1   2   3   nil 5   nil 7   nil 9   nil nil nil nil nil nil

EDIT: or this

UPDATE: Note my follow-up post.

This entry was tagged fun, lua, quirks and wat