logo Lua

Jiggery pokery

The term prelude usually refers to a file loaded in by a piece of software when it starts up, before its users have a chance of getting to grips with it. In a networked classroom, for example, each student may have their own prelude loaded by the server, so that the classwork can be tailored to the individual. A lesser known facility of RiscLua is that if you define a system variable called LUA_INIT with the value@pathname, where pathname is the pathname of some file containing Lua code, then that code will be run as a prelude before anything anything else. Let us have a look at some things that could be put in such a prelude.

One of my favourites is

        (getmetatable "").__call = string.format

This has the effect of allowing format strings to be used as functions. So for example, instead of writing

         s:format (x,y,...)

we can now just write

          s (x,y,...)

so that the word format can be banished from your program. The number of arguments must equal the number of format specifiers (that start with a %) occurring in the string. This brevity is obtained by assigning a call metamethod to the string library. It comes at the cost of no longer being free to assign other 'call' metamethods to it, of course.

Somewhat similar is

        (getmetatable "").__bor = \ (s, replace)
                        s:gsub ("%$(%a[_%w]*)", replace) end
Then we can let strings contain variables, preceded by a dollar sign, and use a vertical bar ( | ) followed by a table to substitute in the values.
     report = "Sold $customer $quantity $goods."
     invoice = { customer = "Acme Ltd";
                 quantity = "10,000";
                 goods = "barrels of glue"; }
     print (report|invoice) --> Sold Acme Ltd 10,000 barrels of glue.

Another candidate is to brush up Lua's default behaviour with global variables. In Programming in Lua (fourth edition) - the BOOK - chapter 22 (The Environment), Roberto Ierusalimschy points out a problem, and says

Lua solves this conundrum by not having global variables, but
going to great lengths to pretend it has. ... 

Part of this problem is that global variables (sorry, things that look like global variables!) which have not been explicitly assigned a value previously in a program, are by default automatically assigned the value nil when encountered in an expression. For example

   print (x)      --> nil, as x has not been defined

Perhaps we would prefer that Lua caught such undefined expressions as an error as a warning to the programmer? Say

   print (x)      --> error: undefined x ....

unless x had previously been declared, for example by a declaration like

    global (var.x, ... )

We can achieve this by putting the following in our prelude file:

local global = setmetatable ({ }, {
    __call = \ (t, ...)
         for _,x in ipairs {...} do t[x] = true end -- for
        end } )
local var = setmetable ({ }, {
    __index = \ (t, x) => x end
       } )
setmetatable (_G, {
   __index = \ (t, k)
      if global[k] then => rawget (t, k)
      else error ("undefined "..k, 2) end -- if
   end;
   __newindex = \ (t, k, v)
      if global[k] then => rawset (t, k, v)
      else error ("undefined "..k, 2) end -- if
   end;
      })

The table var is what I call a pseudotable. It is empty but has an index metamethod. The expression var.foo just has the value "foo", so var is nothing more than a method of getting rid of quote characters, so it can pretend to be a reserved word. RiscLua's use of !,? and $ to mimic BASIC's, works along the same lines. The global table has two jobs: one as a table to keep track of defined global variables, the other as a function of an indefinite number of arguments, so as to declare them. Remember that ... is a reserved word in Lua, not just a textual abbreviation.

Lua's provision of syntactic sugar approaches hyperglycaemia in its efforts to make life easier for the programmer (and harder for the parser). It offers two notations for comments and three notations for strings. Single arguments to functions need not be circumscribed by parentheses if they are literal strings or table constructors. Numeric table indices can be omitted if they start at 1 and increase consecutively by 1. Table items can be separated either by commas or semicolons. Function definitions can be either assignments or follow the conventions of first order languages (as a sop, maybe, to those who are scared by higher order notions). The dot and colon symbols cater for those who like object-oriented programming. The nil value can often be expressed by omission. A single = sign can be used for multiple assignments in parallel. Textual layout has minimal semantic consequence.

In chapter 16 of the BOOK, the concept of a reader-function is introduced. Such a function, of no arguments, produces either a string or nil when it is called. For example

        io.lines (filename, "*L")

returns the (newline-terminated) lines in turn of the file specified by the first argument, each time it is called, and nil when it has reached the end of the file. It can be used as an iterator in constructions that start

  for line in io.lines (filename, "*L") do ....

The function load takes either a string or a reader-function as argument, and compiles the concatenated result to a function of no arguments, which it returns (or nil if something has gone wrong). The function loadfile is effectively just

               load (io.lines (filename, "*L"))

Exercise 16.2 asks the reader to define a multiload function that can use multiple reader-functions or strings to create code, but without performing any string concatenations. We solve this by defining

          multiload = \ ( ... ) => load (gather { ... }) end

where gather takes as argument a list whose items are strings, reader-functions or similar such lists, recursively.

  gather = \ (t)
   local i = 1
   local x, f
     => \ ( )
     while i <= #t do
      f = t[i]
      if type (f) == "string" and #f > 0 then
         i += 1
         => f
      end -- if
      if type (f) == "table" and #f > 0 then
         i += 1
         => gather (f)
      end -- if
      x = f ( )
      if x then => x end -- if, get next arg if nil
      i += 1
     end -- while
     end -- function
   end -- function

Then exercise 16.1 is answered by taking

   loadwithprefix = \ (s, filename) =>
               load (gather { s, io.lines (filename, "*L")}) end

I recommend the BOOK and its exercises to anybody interested in programming.