title logo

back contents forward

Chapter 5

structured objects

In Basic if we want to define structured objects other than fixed size arrays we must use  DIM  to allocate memory, and use the indirection operators  ! ,  ?  and  $  to access their components. For example, if we want a list of pairs of integers, with the variable  list%  pointing to the head of the list we might initialize it to zero and create a new pair of integers  (x%,y%)  in the list with the procedure defined as follows


    DEF PROCpair(RETURN list%,x%,y%)
    LOCAL adr%
    DIM adr% 11:REM 3 words
    !adr% = list%:adr%!4 = x%:adr%!8 = y%
    list% = adr%
    ENDPROC

In other words, Basic lets us manipulate memory directly. However there are some disadvantages that inevitably accompany this power. It is very easy to write Basic procedures that make the computer crash. If you run a program written by somebody else either you have to inspect the code carefully or you have to trust that there is nothing dangerous in it. Apart from lack of security there is another, more subtle, disadvantage: lack of abstraction. Writing Basic programs means having to waste time with details of memory management, with the details of concrete representation, with the procedures for creating objects, for reading or writing their components, and so on. Ideally you should not have to waste intellectual effort on these details.
tables

You can think of a table as a kind of function. Instead of an argument we talk of a key or an index to a table. If t is a table and x is one of its keys then its value at that key is denoted t[x] - round brackets for functions, square brackets for tables. Whereas functions calculate their values when they are applied, tables already know about all their values.

The table is Lua's basic datatype. All structured objects are constructed from tables. How they are stored in memory is of no concern to the programmer.

Any non-nil value can be a key for a table. If the keys of a table are consecutive integers from 1 upwards to some integer n then we say that the table is a list . Tables are mutable objects, and you should think of a variable whose value is a table as a pointer to where the table is stored. An assignment

          t[k] = v
will either update the value of  t[k]  to v if the key k already exists, or it will create a new key k and assign v to it. All the memory management required for this is taken care of automatically. The expression {} creates an empty table.

There is a well-known way of speeding up certain kinds of recursive calculations called memoization. Every time you calculate a value, store it in a table, and the next time you need it, looking it up in the table will be quicker than recalculating. Of course, you will be using more memory for storing your results, so you are trading space for time.

  memoize = \(f)
            local t = {}
            => \(x)
               local y = t[x]
               if not y then
                 y = f(x) -- calculate new value
                 t[x] = y -- store it
               end -- if
               => y
               end -- function
            end -- function

A very inefficient way of calculating the n-th term of the Fibonacci series is

          fib = \(n)
                if n < 2 then => n end -- if
                => fib(n - 1) + fib(n - 2)
                end -- function
It takes time varying exponentially with n to complete. However, the function  memoize(fib) takes time varying linearly with  n ; a huge speedup, as you may verify for yourself.
table constructors

You do not have to create tables by starting with an empty table. A table constructor is an expression of the form {   key_values } where key_values is a comma- or semicolon-separated list of expressions of the form

        [key] = value
Both key and value  can be expressions, but key  must not evaluate to nil . For lists you can omit the key part and simply write

          vector = { x, y, z }
instead of

vector = { [1] = x, [2] = y, [3] = z }
Any table can be thought of as having a list part and a non-list part, and you can mix both parts in a table constructor.

The operator # applied to a table returns the number of items in its list part. The Lua analogue of the Basic procedure we gave above for adding another pair of quantities to a list is

list[1 + #list] = {x, y}
Note that we have used a table as a way of constructing a pair of things, as well as for constructing a list of things. We have to initialize the variable  list  by setting it to  {} , of course. In the Basic example we specified a pair of integers, because we know that integers require four bytes; that way we know that we have to reserve twelve bytes per item. By contrast, in the Lua case the values  x  and  y  can be of any kind we like. The details of storage management are not the programmer's concern.

When a table constructor is an argument to a function of one variable, the parentheses around it may be omitted, just as for literal strings.

For keys which are strings that satisfy the rules for an identifier, say fred for example, you can simplify

        ["fred"] = v
to

          fred = v
inside a table constructor, and you can write t["fred"]  as t.fred . If this makes a key_value look like an assignment, then consider that the global variables are held as keys in a table called _G   and it should be clear that _G._G  has the value _G . Lua's standard libraries are, of course, simply tables.

A further piece of notation can be used if t.fred  is a function. For then, the expression t:fred(...) is defined to mean t.fred(t,...) . The function  t:fred is called a method of  t. The point is that a table is automatically in scope within the body of one of its methods.

The following notation is particular to RiscLua.

local variables from table keys

Quite often one wants to use local variables that are initialised to values in a table. An expression of the form

    local x,y, ... in t
is equivalent to

    local T = t
    local x,y, ... = T.x,T.y, ...
This form can be particularly useful for speeding up library functions used inside loops.
A classic example of the use of tables is something like this:

     do
      local announce = \(self,mesg)
       local stmnt = "Your account stands at %d."
       print (("Dear %s"):format(self.name))
       print (mesg or "")
       print (stmnt:format(self.balance))
      end
      local withdraw = \(self,amount)
       if amount <= self.balance then
        self.balance = self.balance - amount
        self:announce()
        => amount
       else
        self:announce("Your balance is insufficient.")
        => 0
       end -- if
      end
      local deposit = \(self,amount)
        => self:withdraw(-amount)
      end
      make_account = \(name)
        => {
             name = name,
             balance = 0,
             announce = announce,
             withdraw = withdraw,
             deposit = deposit,
           }
      end -- function
     end -- do

     fred = make_account "Fred"
     fred:deposit(100)
  --> Dear Fred
  -->
  --> Your account stands at 100.

     fred:withdraw(117)
  --> Dear Fred
  --> Your balance is insufficient.
  --> Your account stands at 100.

The methods are given by values local to the chunk so the code for them is not reduplicated in each account that is created. Each account has its own name and balance but the methods are shared.

Here is a short script for removing duplicate lines in a file. It works by setting up a table of boolean values indexed by the lines of the file.

  #! lua
  do
  local used, print = {}, print
  local lines in io
  for line in lines(arg[1]) do
    if not used[line] then
      used[line] = true; print(line)
    end -- if
  end -- for
  end -- do
Note how all the variables used are local, even print .

If you are coming from Basic or other traditional languages, it is important to get out of the habit of thinking that only numbers can be indices to tables.

Just as Lisp uses lists for all its complex data structuring, so Lua uses tables. Because Lisp's lists have to be accessed from their head they are less efficient than tables; it is like the difference for storage media between tape cassettes and floppies - serial or random access. Tables are stored in a garbage collected area of memory called the heap. Once there are no references to an object in the heap, the garbage collector can reuse its memory. Garbage collection takes place continuously in the background, in small steps. There are means to monitor its performance and adjust it. You can find out about these things, and the notion of weak reference in the manual.


back contents forward