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.
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.
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.
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.