Vector Glue

lua-users home
wiki

Here is a simple wrapper around Lua tables that provides the restricted semantics of Vectors. This is not necessarily the most inspired code, nor the most bug-free, but it might prove useful to someone. Aside from the Vector stuff, it shows a few examples of using functionals. This came up from a mailing list discussion on getn and the use of the key n to hold the length of the list (well, I would say Vector).

[!] VersionNotice: The below code pertains to an older Lua version, Lua 4. Certain features used like tag methods (settagmethod) are no longer present in Lua 5 but have been replaced with metamethods.

Some sample output appears at the end. Reading the last function (tmap) might show the value of the redefined / and ^ operators.

Code

do
    local TAG = newtag()

    local bless = function(t)
                      t.n = getn(t)
                      return settag(t, %TAG)
                  end

    function vector_seti(t, i, v)
        if type(i) == "number" and i > 0 and i == floor(i)
            then rawset(t, i, v)
                if t.n < i then rawset(t, "n", i) end
        elseif i == "n" and type(v) == "number"
               and v >= 0 and v == floor(v)
            then rawset(t, "n", v)
        else error("Invalid index to vector")
        end
    end

    settagmethod(TAG, "settable", vector_seti)

    function vector(...)
        return settag(arg, %TAG)
    end

    function vector_concat(t1, t2)
        if tag(t1) ~= %TAG or tag(t2) ~= %TAG
            then error("Can't concat a vector with a non-vector")
        end
        local t3 = {}
        for i = 1, t1.n do t3[i] = t1[i] end
        for i = 1, t2.n do t3[t1.n + i] = t2[i] end
        return %bless(t3)
    end

    settagmethod(TAG, "concat", vector_concat)

-- this should be safe from optimizations since 1*t is defined
-- to be t. Consequently, it doesn't guarantee to create a
-- new vector

    function vector_repeat(t, n)
        if tag(t) ~= %TAG then n, t = t, n end
        if not tonumber(n)
            then error("Repeat needs a vector and a number")
        end
        local tt = {}
        if n == 1 then
            return t
        elseif n > 0 then
            for j = 1, t.n do
                local v = t[j]
                for i = 1, n do tt[(i-1)*t.n + j] = v end
            end
        elseif n < 0 then
            for j = 0, t.n - 1 do
                local v = t[j + 1]
                for i = 1, -n do tt[i*t.n - j] = v end
            end
        end
        return %bless(tt)
    end

    settagmethod(TAG, "mul", vector_repeat)
    settagmethod(TAG, "unm",
        function(t) return vector_repeat(t, -1) end)

    function vector_reduce(t, fn)
        if tag(t) ~= %TAG then fn, t = t, fn end
        if type(fn) ~= "function" then
            error ("Reduce needs a function of two arguments")
        end
        if t.n == 0 then return nil end
        local val = t[1]
        for i = 2, t.n do
            val = fn(val, t[i])
        end
        return val
    end

    settagmethod(TAG, "div", vector_reduce)

    function tjoin(t, str)
        str = str or " "
        return t / function(a, b) return a .. %str .. b end
    end

    function vector_map(t, fn)
        if tag(t) ~= %TAG then fn, t = t, fn end
        if type(fn) ~= "function" then
            error ("Map needs a function of one argument")
        end
        local tt = {}
        for i = 1, t.n do
            tt[i] = fn(t[i])
        end
        return %bless(tt)
    end

    settagmethod(TAG, "pow", vector_map)

    function andf(a, b) return a and b end

    function vectorp(a) return tag(a) == %TAG end

    function tmap(fn, ...)
        local targ = %bless(arg)
        if type(fn) ~= "function" then
            error("tmap requires a function") end
        if 0 == andf/ vectorp^targ then
            error("map only works on vectors") end
        local tt = {}
        for i = 1, min/ getn^targ do
            local sel = function(t) return t[%i] end
            tt[i] = call(fn, sel^targ)
        end
        return %bless(tt)
    end
end

Sample Usage

Here's some sample output. The function p just makes it a little easier to see the results.

> function p(t) print(tjoin(t)) end
>
Create a vector

> a = vector("this", "is", "a", "vector")
> p(a)
this is a vector
repeat, concatenate:
> p(2 * a)
this is a vector this is a vector
> p(a .. a)
this is a vector this is a vector
works backwards, too, so we define unary minus as well
> p(-3 * a)
vector a is this vector a is this vector a is this
> p(-a)
vector a is this

getn is possible, but it's just as easy to use the n key.

> print(a.n)
4

We can use tinsert and tremove normally, and index the vector by an integer.

> tinsert(a, ".")
> print(a.n)
5
> p(a)
this is a vector .
> tremove(a, 3)
> p(a)
this is vector .
> a[4] = "a"
> p(a)
this is vector a
> a[3], a[4] = a[4], a[3]
> p(a)
this is a vector

But the vector doesn't like be used as a table

> a.note = 27
error: Invalid index to vector
stack traceback:
   1:  function `error' [C]
   2:  function `vector_seti' at line 16 [file `table.lua']
   3:  main of string "a.note = 27" at line 1

We can also set n to "truncate" the vector, and then change it back to restore the truncated values

> a.n = 2
> p(a)
this is
> a.n = 3
> p(a)
this is a
> a.n = 4
> p(a)
this is a test
However, setting n beyond the end doesn't create any values, so we can get ourselves into trouble:
> a.n = 5
> p(a)
error: attempt to concat local `b' (a nil value)
stack traceback:
   1:  function `fn' at line 82 [file `table.lua']
   2:  function `vector_reduce' at line 73 [file `table.lua']
   3:  function `tjoin' at line 82 [file `table.lua']
   4:  function `p' at line 116 [file `table.lua']
   5:  main of string "p(a)" at line 1
As you might see from the code, the reduce (/) and map (^) operators can be extremely expressive...

> p(strupper^ a)
THIS IS A TEST
> b = vector(4, 8, -3.4, 7)
> print (max/ b)
8

Unfortunately, we have to define primitives ourselves

> function sum(a, b) return a + b end
> print (sum/ b)
15.6

So it's quite handy to have some functional manipulators around

> function bind2(fn, b) return function(a) return %fn(a, %b) end end
> add1 = bind2(sum, 1)
> p(add1^ b)
5 9 -2.4 8
> function average(a) return (sum/ a) / a.n end
> print(average(b))
3.9

tmap is defined to allow mapping over more than one argument

> function ratio(a, b) return a / b end
> p(tmap(ratio, b, add1^ b))
0.8 0.8888888888888888 1.416666666666667 0.875

The definition of tjoin shows another use of reduce. (Note that the reduce operator is commutative, if not symmetrical)

> p(a)
this is a test
> print(tjoin(a, ", "))
this, is, a, test
> print(tjoin(a, "\n"))
this
is
a
test

But this little functional might be useful, too...

> function joiner(s) return function(a, b) return a .. %s .. b end end
> print(joiner("\n")/ tmap(joiner("\t"), a, b))
this    4
is      8
a       -3.4
test    7

And following up on that theme, I leave you with the following possibly revealing bit of code:

> function tab2vec(t) \
      local v = vector() \
      for key, val in t do tinsert(v, vector(key, val)) end \
      return v \
end
> family = {juan = 23, marie = 16, alfonso = 45, silvana = 42}
> -- sort by age
> sort(ft, function(a, b) return a[2] < b[2] end)
> print(joiner("\n")/ bind2(tjoin, "\t")^ ft)
marie   16
juan    23
silvana 42
alfonso 45
> -- sort by name
> sort(ft, function(a, b) return a[1] < b[1] end)
> print(joiner("\n")/ bind2(tjoin, "\t")^ ft)
alfonso 45
juan    23
marie   16
silvana 42


RecentChanges · preferences
edit · history
Last edited January 6, 2007 6:43 pm GMT (diff)