Lua SPE 論文

轉載自 軟體:實務與經驗 26 #6 (1996) 635–652。版權所有 © 1996 John Wiley & Sons, Ltd. [ps · doi]

Lua – 一種可擴充的擴充語言

作者:Roberto Ierusalimschy、Luiz Henrique de Figueiredo、Waldemar Celes Filho

摘要。

本文描述 Lua,一種用於擴充應用程式的語言。Lua 結合了程序化功能與強大的資料描述功能,透過使用簡單但強大的表格機制。此機制實作了記錄、陣列和遞迴資料類型 (指標) 的概念,並加入了一些物件導向功能,例如具有動態調度的函式。Lua 提出了一種後備機制,允許程式設計師以一些非傳統方式擴充語言的語意。值得注意的是,後備允許使用者將不同類型的繼承新增到語言中。目前,Lua 已廣泛用於生產中,執行多項任務,包括使用者設定、一般用途資料輸入、使用者介面描述、結構化圖形元檔案儲存,以及有限元素網格的通用屬性設定。

引言

對可自訂應用程式的需求與日俱增。隨著應用程式變得越來越複雜,使用簡單參數進行自訂變得不可能:使用者現在希望在執行時做出設定決策;使用者也希望撰寫巨集和腳本以提高生產力 [1,2,3,4]. 為了回應這些需求,現今有一個重要的趨勢是將複雜系統分成兩部分:核心設定。核心實作系統的基本類別和物件,通常使用編譯式、靜態型別語言撰寫,例如 C 或 Modula-2。設定部分通常使用直譯式、彈性語言撰寫,將這些類別和物件連接起來,賦予應用程式最終的樣貌 [5].

組態語言有許多種,從用於選擇偏好的簡單語言,通常實作為命令列中的參數清單或從組態檔中讀取的變數值對(例如,MS-Windows 的 .ini 檔、X11 資源檔),到內嵌語言,用於根據應用程式提供的原語,以使用者定義的函式來擴充應用程式。內嵌語言可能非常強大,有時是主流程式語言(例如 Lisp 和 C)的簡化變體。這種組態語言也稱為擴充語言,因為它們允許使用新的使用者定義功能來擴充基本核心語意。

擴充語言與獨立語言不同之處在於,它們只能內嵌在稱為主機程式的主機用戶端中。此外,主機程式通常可以提供特定於網域的擴充,以自訂內嵌語言以符合自己的目的,通常是透過提供較高層次的抽象。因此,內嵌語言同時具有用於其自己程式的語法,以及用於與主機通訊的應用程式介面 (API)。與用於提供參數值和動作序列給主機的較簡單組態語言不同,內嵌語言和主機程式之間存在雙向通訊。

重要的是要注意,擴充語言的要求與一般用途程式語言的要求不同。擴充語言的主要要求為

本文說明 Lua,一種可擴充的程序語言,具有強大的資料描述功能,設計用作通用擴充語言。Lua 是兩種描述性語言融合的產物,設計用於設定兩個特定應用程式:一個用於科學資料輸入 [6 ],另一個用於視覺化從地質探測器取得的岩性剖面。當使用者開始要求這些語言提供更多功能時,顯然需要真正的程式設計功能。解決方案並非平行升級和維護兩種不同的語言,而是設計一種單一語言,不僅可用於這兩個應用程式,還能用於任何其他應用程式。因此,Lua 結合了大多數程序設計語言常見的功能(控制結構(whileif 等)、賦值、子程式和中綴運算子),但抽象出特定於任何特定領域的功能。如此一來,Lua 不僅可用作一種完整的語言,還能用作一種語言架構

Lua 非常符合上述需求。它的語法和控制結構非常簡單,類似 Pascal。Lua 很小;整個函式庫約有六千行 ANSI C,其中將近兩千行是由 yacc 產生的。最後,Lua 是可擴充的。在設計中,新增許多不同功能已改為建立一些元機制,讓程式設計人員能自行實作這些功能。這些元機制包括:動態關聯陣列反射功能後備

動態關聯陣列直接實作許多資料類型,例如一般陣列、記錄、集合和多重集合。它們也透過建構函式提升語言的資料描述能力。

反射功能允許建立高度多型的部分。持續性和多個名稱空間是 Lua 中沒有直接提供的功能,但可以使用反射功能在 Lua 中輕鬆實作。

最後,儘管 Lua 有固定的語法,後備可以擴充許多語法結構的意義。例如,後備可用於實作不同類型的繼承,這項功能在 Lua 中並不存在。

Lua 概觀

本節包含 Lua 主要概念的簡要說明。包含一些實際程式碼範例,以提供此語言的概況。語言的完整定義可以在其參考手冊 [7] 中找到。

Lua 是一種通用嵌入式程式語言,旨在支援具備資料描述功能的程序設計。作為一種嵌入式語言,Lua 沒有「主程式」的概念;它只能嵌入在主機用戶端中。Lua 以 C 函式庫的形式提供,以連結到主機應用程式。主機可以呼叫函式庫中的函式來執行 Lua 中的程式碼片段、寫入和讀取 Lua 變數,以及註冊 C 函式以供 Lua 程式碼呼叫。此外,可以指定後備,只要 Lua 不知道如何繼續進行時,就會呼叫後備。透過這種方式,可以擴充 Lua 以應對不同的網域,從而建立共用單一語法架構的客製化程式語言 [8]。正因如此,Lua 才會被稱為語言架構。另一方面,撰寫一個互動式的獨立 Lua 解譯器非常容易(圖 1)。

      #include <stdio.h>
      #include "lua.h"              /* lua header file */
      #include "lualib.h"           /* extra libraries (optional) */

      int main (int argc, char *argv[])
      {
       char line[BUFSIZ];
       iolib_open();               /* opens I/O library (optional) */
       strlib_open();              /* opens string lib (optional) */
       mathlib_open();             /* opens math lib (optional) */
       while (gets(line) != 0)
         lua_dostring(line);
      }

圖 1:Lua 的互動式解譯器。

Lua 中的所有陳述式都在一個保留所有全域變數和函式的全域環境中執行。此環境在主機程式開始時初始化,並持續到程式結束。

Lua 的執行單位稱為區塊。區塊可能包含陳述式和函式定義。執行區塊時,會先編譯所有函式和陳述式,並將函式新增到全域環境中;然後依序執行陳述式。

圖 2 顯示一個範例,說明如何將 Lua 用作非常簡單的組態語言。此程式碼定義三個全域變數並為它們指定值。Lua 是一種動態型別語言:變數沒有型別;只有值才有。所有值都攜帶自己的型別。因此,Lua 中沒有型別定義。

      width = 420
      height = width*3/2     -- ensures 3/2 aspect ratio
      color = "blue"

圖 2:非常簡單的組態檔。

可以使用流程控制和函式定義撰寫更強大的組態。Lua 使用傳統的類似 Pascal 的語法,包含保留字和明確終止的區塊;分號是選用的。此類語法既熟悉又強大,而且容易剖析。圖 3 中提供一個小範例。請注意,函式可以傳回多個值,而且可以使用多重指定來收集這些值。因此,可以從語言中捨棄參數傳遞參照(這總是會造成一些語意上的小困難)。

      function Bound (w, h)
        if w < 20 then w = 20
        elseif w > 500 then w = 500
        end
        local minH = w*3/2             -- local variable
        if h < minH then h = minH end
        return w, h
      end

      width, height = Bound(420, 500)
      if monochrome then color = "black" else color = "blue" end

圖 3:使用函式的組態檔。

Lua 中的函式是一等值。函式定義會建立一個型別為function的值,並將此值指定給全域變數(圖 3 中的Bound)。函式值就像任何其他值一樣,可以儲存在變數中、傳遞為其他函式的引數,以及傳回為結果。此功能大幅簡化了物件導向功能的實作,如本節稍後所述。

除了基本類型數字(浮點數)和字串,以及類型函式,Lua 提供三種其他資料類型:nil使用者資料表格。每當需要明確類型檢查時,可以使用原始函式類型;它會傳回一個描述其引數類型的字串。

類型nil有一個單一值,也稱為nil,其主要屬性與任何其他值不同。在第一次指定之前,變數的值為nil。因此,未初始化的變數,是程式設計錯誤的主要來源,在 Lua 中不存在。在需要實際值的情況下使用nil(例如在算術表達式中)會導致執行錯誤,提醒程式設計師變數未正確初始化。

類型使用者資料用於允許將任意主機資料(表示為void* C 指標)儲存在 Lua 變數中。此類型值上唯一有效的運算為指定和相等性測試。

最後,類型表格實作關聯陣列,也就是說,不僅可以用整數索引,還可以字串、實數、表格和函式值索引的陣列。

關聯陣列

關聯陣列是一種強大的語言建構;許多演算法簡化到微不足道的程度,因為搜尋它們所需的資料結構和演算法是由語言隱式提供的 [9]。大多數典型的資料容器,例如一般陣列、集合、袋子和符號表,都可以直接透過表格實作。表格也可以透過簡單地使用欄位名稱作為索引,來模擬記錄。Lua 透過提供a.name作為a["name"]的語法糖,來支援此表示法。

與實作關聯陣列的其他語言(例如 AWK [10], Tcl [11] 和 Perl [12])不同,Lua 中的表格不受變數名稱約束;相反地,它們是動態建立的物件,可以像傳統語言中的指標一樣進行操作。這種選擇的缺點是必須在使用前明確建立表格。優點是表格可以自由地參照其他表格,因此具有表達能力,可以建模遞迴資料類型,並建立通用的圖形結構,可能帶有迴圈。舉例來說,圖 4 顯示如何在 Lua 中建立循環連結清單。

      list = {}                    -- creates an empty table
      current = list
      i = 0
      while i < 10 do
        current.value = i
        current.next = {}
        current = current.next
        i = i+1
      end
      current.value = i
      current.next = list

圖 4:Lua 中的循環連結清單。

Lua 提供許多有趣的方式來建立表格。最簡單的形式是表達式{},它會傳回一個新的空表格。下面顯示一種更具描述性的方式,它會建立一個表格並初始化一些欄位;語法在某種程度上受到 BibTeX [13] 資料庫格式的啟發

      window1 = {x = 200, y = 300, foreground = "blue"}

這個命令會建立一個表格,初始化其欄位xy前景,並將其指定給變數window1。請注意,表格不必是同質的;它們可以同時儲存所有類型的值。

類似的語法可用於建立清單

      colors = {"blue", "yellow", "red", "green", "black"}

此陳述等同於

      colors = {}
      colors[1] = "blue";  colors[2] = "yellow"; colors[3] = "red"
      colors[4] = "green"; colors[5] = "black"

有時,需要更強大的建構設施。Lua 沒有嘗試提供所有功能,而是提供了一個簡單的建構函式機制。建構函式寫成 name{...},這只是 name({...}) 的語法糖。因此,使用建構函式時,會建立一個表格,初始化它,並將它作為參數傳遞給函式。這個函式可以執行任何需要的初始化,例如(動態)類型檢查、初始化不存在的欄位,以及更新輔助資料結構,甚至在主程式中也是如此。通常,建構函式會在 C 或 Lua 中預先定義,而且組態使用者通常不知道建構函式是一個函式;他們只是寫一些像

      window1 = Window{ x = 200, y = 300, foreground = "blue" }

這樣的東西,並思考「視窗」和其他高階抽象概念。因此,儘管 Lua 是動態類型的,但它提供了使用者控制的類型建構函式

由於建構函式是表達式,因此可以巢狀它們,以宣告式樣式描述更複雜的結構,如下面的程式碼所示

      d = dialog{
                 hbox{
                      button{ label = "ok" },
                      button{ label = "cancel" }
                 }
          }

自省設施

Lua 的另一個強大機制是它使用內建函式 next 來遍歷表格的能力。這個函式有兩個引數:要遍歷的表格和這個表格的索引。當索引為 nil 時,函式會傳回給定表格的第一個索引和與此索引關聯的值;當索引不為 nil 時,函式會傳回下一個索引和它的值。索引會以任意順序擷取,並傳回 nil 索引來表示遍歷結束。作為使用 Lua 遍歷設施的一個範例,圖 5 顯示了一個用於複製物件的常式。區域變數 i 會遍歷物件 o 的索引,而 v 會接收它們的值。這些值會與它們對應的索引關聯,並儲存在區域表格 new_o 中。

   function clone (o)
     local new_o = {}           -- creates a new object
     local i, v = next(o,nil)   -- get first index of "o" and its value
     while i do
       new_o[i] = v             -- store them in new table
       i, v = next(o,i)         -- get next index and its value
     end
     return new_o
   end

圖 5:複製一般物件的函式。

next 遍歷表格的方式與相關函式 nextvar 遍歷 Lua 的全域變數的方式相同。圖 6 顯示了一個函式,它會將 Lua 的全域環境儲存在一個表格中。與 clone 函式一樣,區域變數 n 會遍歷所有全域變數的名稱,而 v 會接收它們的值,這些值會儲存在區域表格 env 中。在離開時,函式 save 會傳回這個表格,稍後可以將它傳給函式 restore 來還原環境(圖 7)。這個函式有兩個階段。首先,會清除整個目前的環境,包括預先定義的函式。然後,區域變數 nv 會遍歷給定表格的索引和值,將這些值儲存在對應的全域變數中。一個棘手的點是 restore 所呼叫的函式必須保留在區域變數中,因為所有全域名稱都會被清除。

   function save ()
     local env = {}             -- create a new table
     local n, v = nextvar(nil)  -- get first global var and its value
     while n do
       env[n] = v               -- store global variable in table
       n, v = nextvar(n)        -- get next global var and its value
     end
     return env
   end

圖 6:儲存 Lua 環境的函式。

   function restore (env)
     -- save some built-in functions before erasing global environment
     local nextvar, next, setglobal = nextvar, next, setglobal
     -- erase all global variables
     local n, v = nextvar(nil)
     while n do
       setglobal(n, nil)
       n, v = nextvar(n)
     end
     -- restore old values
     n, v = next(env, nil)      -- get first index; v = env[n]
     while n do
      setglobal(n, v)           -- set global variable with name n
      n, v = next(env, n)
     end
   end

圖 7:還原 Lua 環境的函式。

儘管這是一個有趣的範例,但幾乎不需要在 Lua 中操作全域環境,因為用作物件的表格提供了維護多個環境的更好方法。

支援物件導向程式設計

由於函式是一等值,因此表格欄位可以參照函式。此特性允許實作一些有趣的物件導向設施,而定義和呼叫方法的語法糖讓這些設施更容易使用。

首先,方法定義可以寫成

      function object:method (params)
        ...
      end

這等於

      function dummy_name (self, params)
        ...
      end
      object.method = dummy_name

也就是說,會建立一個匿名函式並儲存在表格欄位中;此外,此函式有一個稱為 self 的隱藏參數。

其次,方法呼叫可以寫成

      receiver:method(params)

這會轉譯成

      receiver.method(receiver,params)

換句話說,方法的接收者會傳遞為其第一個引數,賦予參數 self 預期的意義。

值得注意上述結構的一些特性。首先,它不提供資訊隱藏。因此,純粹主義者可能會(正確地)宣稱物件導向的重要部分遺失了。其次,它不提供類別;每個物件都承載其運算。不過,此結構極為輕量(只有語法糖),而類別可以使用繼承來模擬,這在其他基於原型語言中很常見,例如 Self [14]。不過,在討論繼承之前,有必要討論遞迴。

遞迴

Lua 是一種非類型語言,其語意具有許多執行時期異常狀況。範例包括套用在非數值運算元上的算術運算、嘗試索引非表格值,或嘗試呼叫非函式值。由於在這些情況下中斷並不適合嵌入式語言,因此 Lua 允許程式設計師設定自己的函式來處理錯誤狀況;這些函式稱為遞迴函式。遞迴也用於提供掛鉤來處理其他不完全是錯誤狀況的情況,例如存取表格中不存在的欄位和發出垃圾回收訊號。

若要設定遞迴函式,程式設計師會呼叫函式 setfallback,並提供兩個引數:識別遞迴的字串,以及在發生對應狀況時要呼叫的新函式。函式 setfallback 會傳回舊的遞迴函式,因此程式可以串連不同種類物件的遞迴。

Lua 支援下列由指定字串識別的遞迴

「arith」、「order」、「concat」
當運算套用至無效運算元時,會呼叫這些後備函式。它們會收到三個參數:兩個運算元和描述有問題運算子(「add」、「sub」...)的字串。它們的傳回值是運算的最終結果。這些後備函式的預設函式會傳回錯誤。
「index」
當 Lua 嘗試擷取不在資料表中的索引值時,會呼叫此後備函式。它會收到資料表和索引作為參數。它的傳回值是索引運算的最終結果。預設函式會傳回 nil。
「gettable」、「settable」
當 Lua 嘗試讀取或寫入非資料表值中的索引值時,會呼叫此後備函式。預設函式會傳回錯誤。
「function」
當 Lua 嘗試呼叫非函式值時,會呼叫此後備函式。它會收到非函式值和原始呼叫中提供的參數作為參數。它的傳回值是呼叫運算的最終結果。預設函式會傳回錯誤。
「gc」
在垃圾回收期間呼叫。它會收到作為參數的正在回收的資料表,以及 nil 來表示垃圾回收結束。預設函式不會執行任何動作。

在繼續之前,請務必注意,後備函式通常不會由一般的 Lua 程式設計師設定。後備函式主要由專家級程式設計師在將 Lua 繫結至特定應用程式時使用。之後,此功能會作為語言的整合部分使用。一個典型的範例是,大多數實際應用程式會使用後備函式來實作繼承,如下所述,但大多數 Lua 程式設計師會在不知道(或不在乎)如何實作的情況下使用繼承。

使用後備函式

圖 8 顯示一個使用後備函式來允許更面向物件的二元運算子解譯風格的範例。當設定此後備函式時,像 a+b 的運算式,其中 a 是資料表,會執行為 a:add(b)。請注意使用全域變數 oldFallback 來串連後備函式。

      function dispatch (receiver, parameter, operator)
        if type(receiver) == "table" then
          return receiver[operator](receiver, parameter)
        else
          return oldFallback(receiver, parameter, operator)
        end
      end

      oldFallback = setfallback("arith", dispatch)

圖 8:後備函式的範例。

回退提供的另一項不尋常的功能是重複使用 Lua 的剖析器。許多應用程式會受益於算術表達式剖析器,但並未包含一個,因為並非每個人都具備必要的專業知識或意願從頭開始撰寫剖析器,或使用 yacc 等剖析器產生器。圖 9 顯示使用回退的表達式剖析器的完整實作。此程式會讀取變數 a、...、z 的算術表達式,並輸出評估表達式所需的原始運算系列,使用變數 t1t2、... 作為暫時變數。例如,為表達式產生之程式碼

      (a*a+b*b)*(a*a-b*b)/(a*a+b*b+c)+(a*(b*b)*c)

      t1=mul(a,a)      t2=mul(b,b)      t3=add(t1,t2)
      t4=sub(t1,t2)    t5=mul(t3,t4)    t6=add(t3,c)
      t7=div(t5,t6)    t8=mul(a,t2)     t9=mul(t8,c)
      t10=add(t7,t9)

此程式的主要部分為函式 arithfb,它被設定為算術運算的回退。函式 create 用於初始化變數 a、...、z 與表格,每個表格都有一個包含變數名稱的欄位 name。初始化後,一個迴圈會讀取包含算術表達式的行,建立對變數 E 的指派,並將其傳遞給 Lua 詮釋器,呼叫 dostring。每當詮釋器嘗試執行像 a*a 的程式碼時,它會呼叫 "arith" 回退,因為 a 的值是一個表格,而不是一個數字。回退會建立一個暫時變數來儲存每個原始算術運算結果的符號表示。

儘管很小,但此程式碼實際上會執行全域共用子表達式識別並產生最佳化程式碼。請注意在上述範例中,a*a+b*ba*a-b*b 如何都根據 a*ab*b 的單一評估進行評估。另請注意,a*a+b*b 僅評估一次。程式碼最佳化僅透過將先前計算的數量快取在表格 T 中來完成,其索引為原始運算的文字表示,其值是包含結果的暫時變數。例如,T["mul(a,a)"] 的值為 t1

圖 9 中的程式碼可以輕鬆修改,以處理加法和乘法的交換律,以及減法和除法的反交換律。將其變更為輸出後置表示或其他格式也很容易。

在實際應用中,變數 a、...、z 會表示應用程式物件,例如複數、矩陣,甚至影像,而 "arith" 回退會呼叫應用程式函式對這些物件執行實際運算。因此,Lua 剖析器的主要用途是允許程式設計師使用熟悉的算術表達式來表示應用程式物件上的複雜運算。

      n=0                            -- counter of temporary variables
      T={}                           -- table of temporary variables

      function arithfb(a,b,op)
       local i=op .. "(" .. a.name .. "," .. b.name .. ")"
       if T[i]==nil then             -- expression not seen yet
         n=n+1
         T[i]=create("t"..n)         -- save result in cache
         print(T[i].name ..'='..i)
       end
       return T[i]
      end

      setfallback("arith",arithfb)   -- set arithmetic fallback

      function create(v)             -- create symbolic variable
       local t={name=v}
       setglobal(v,t)
       return t
      end

      create("a") create("b") create("c") ... create("z")

      while 1 do                     -- read expressions
       local s=read()
       if (s==nil) then exit() end
       dostring("E="..s)             -- execute fake assignment
       print(s.."="..E.name.."\n")
      end

圖 9:Lua 中最佳化的算術表達式編譯器。

透過回退繼承

當然,回退最有趣的用途之一是在 Lua 中實作繼承。簡單繼承允許物件在另一個稱為其父項的物件中尋找不存在欄位的值;特別是,此欄位可以是方法。此機制是一種物件繼承,與 Smalltalk 和 C++ 中採用的更傳統的類別繼承不同。在 Lua 中實作簡單繼承的一種方法是將父項物件儲存在一個稱為 parent 的特殊欄位中,並設定一個索引回退函式,如圖 10 所示。此程式碼定義了一個函式 Inherit,並將其設定為 "index" 回退。每當 Lua 嘗試存取物件中不存在的欄位時,回退機制就會呼叫函式 Inherit。此函式首先檢查物件是否有一個包含表格值的欄位 parent。如果是,它會嘗試存取此父項物件中的所需欄位。如果父項中不存在此欄位,則會自動再次呼叫回退;此程序會重複「向上」執行,直到找到欄位的值或父項鏈結束為止。

      function Inherit (object, field)
        if field == "parent" then     -- avoid loops
          return nil
        end
        local p = object.parent       -- access parent object
        if type(p) == "table" then    -- check if parent is a table
          return p[field]             -- (this may call Inherit again)
        else
          return nil
        end
      end

      setfallback("index", Inherit)

圖 10:在 Lua 中實作簡單繼承。

上述方案允許無窮的變化。例如,只能繼承方法,或只能繼承以底線開頭的欄位。許多形式的多重繼承也可以實作。其中,一種常用的形式是雙重繼承。在此模型中,每當在父層級中找不到欄位時,搜尋會透過另一種父層級繼續進行,通常稱為「教父」。在多數情況下,一個額外的父層級就足夠了。此外,雙重繼承可以建模一般多重繼承。例如,在以下程式碼中,a 繼承自 a1、a2 和 a3,順序為此

      a = {parent = a1, godparent = {parent = a2, godparent = a3}}

Lua 在實際應用中的使用

TeCGraf 是里約熱內盧教皇天主教大學 (PUC-Rio) 的一個研究和開發實驗室,擁有許多產業合作夥伴。在過去兩年中,TeCGraf 的約四十位程式設計師已使用 Lua 開發了數個實質產品。本節說明其中一些用途。

岩性剖面可組態報告產生器

如引言中所述,Lua 最初是為了支援兩個不同的應用程式而產生的,這些應用程式有自己的,但有限的延伸語言。其中一個應用程式是一個工具,用於視覺化從地質探測器取得的岩性剖面。其主要特點是允許使用者組態剖面配置,結合物件實例並指定要顯示的資料。該程式支援數種物件,例如連續曲線、直方圖、岩性表示、比例尺等。

若要建立配置,使用者可以撰寫描述這些物件的 Lua 程式碼(圖 11)。應用程式本身也有 Lua 程式碼,允許透過圖形使用者介面建立此類描述。此功能建立在 EDG 架構上,如下所述。

      Grid{
        name = "log",
        log = TRUE,
        h_step = 25,
        v_step = 25,
        v_tick = 5,
        step_line = Line {color = RED, width = SIMPLE},
        tick_line = Line {color = CORAL}
      }

圖 11:在 Lua 中描述岩性剖面物件。

儲存結構化圖形圖元檔案

Lua 的另一個重要用途是儲存結構化圖形圖元檔案。由 TeCGraf 開發的通用繪圖編輯器 TeCDraw 儲存圖元檔案,其中包含以 Lua 撰寫的繪圖中圖形物件的高階描述。圖 12 說明了這些描述。

      line{
         x = { 0.0, 1.0 },
         y = { 5.0, 8.0 },
         color = RED
      }
      text{
         x = 0.8,
         y = 0.5,
         text = 'an example of text',
         color = BLUE
      }
      circle{
         x = 1.0,
         y = 1.0,
         r = 5.0
      }

圖 12:結構化圖形圖元檔案的摘錄。

此類通用結構化圖元檔案為開發帶來許多好處

高階、通用的圖形資料輸入

Lua 功能也在 EDG 的實作中被大量使用,EDG 是支援開發資料輸入程式的高抽象層級系統。此系統提供介面物件(例如按鈕、選單、清單)和圖形物件(例如線條、圓形和基本圖形群組)的操作。因此,程式設計師可以在高抽象程式設計層級中建立複雜的介面對話框。程式設計師也可以將回呼動作關聯到圖形物件,從而建立主動物件,以程序方式對使用者輸入做出反應。

EDG 系統使用 Lua 後備功能來實作雙重繼承,如上所述。因此,可以建立新的介面和圖形物件,繼承原始物件行為。EDG 中繼承的另一個有趣用法是「跨語言繼承」。EDG 建立在可攜式使用者介面工具組 IUP [15] 之上。為了避免在 Lua 中重複儲存在主機中的 IUP 資料,EDG 使用後備來取得「可取得」和「可設定」欄位,以便直接從 Lua 存取工具組中的欄位。因此,可以使用直觀的記錄語法直接存取主機資料,而無需為主機中的每個匯出資料項目建立存取函數。

EDG 系統已用於開發多個資料輸入程式。在許多工程系統中,完整的分析分為三個步驟:資料輸入(稱為前處理);分析本身(稱為處理或模擬);以及結果報告和驗證(稱為後處理)。透過繪製必須指定為分析輸入的資料圖形表示,可以簡化資料輸入任務。對於此類應用程式,EDG 系統非常有幫助,並提供自訂資料輸入的快速開發工具。這些圖形資料輸入工具為批次模擬程式的舊有程式碼賦予新生命。

有限元素網格的通用屬性設定

Lua 被使用的另一個工程領域是有限元素網格的產生。有限元素網格由節點和元素組成,分解分析領域。為完成模型,必須將物理屬性(屬性)與節點和元素關聯起來,例如材料類型、支撐條件和載重情況。必須指定的屬性集會根據要執行的分析而有很大差異。因此,為實作多功能有限元素網格產生器,建議屬性保持使用者可設定,而不是在程式中硬編碼。

ESAM [16] 是一個通用系統,使用 Lua 提供屬性設定支援。與 EDG 一樣,ESAM 採用物件導向方法:使用者建立從預先定義的核心類別衍生的特定屬性。圖 13 顯示如何建立一種稱為「等向性」的新材料的範例。

      ISO_MAT = ctrclass{ parent = MATERIAL,
                           name = "Isotropic",
                           vars = {"e", "nu"}
                }

      function ISO_MAT:CrtDlg ()
        ...  -- creates a dialog to specify this material
      end

圖 13:在 ESAM 中建立新材料。

相關工作

本節討論其他一些擴充語言,並將它們與 Lua 進行比較。無意求全,而是選擇了擴充語言當前趨勢的一些代表:Scheme、Tcl 和 Python。網際網路上提供了嵌入式語言的完整清單 [17]。本節還將後備機制與其他一些語言機制進行比較。

Lisp 方言,特別是 Scheme,一直是擴充語言的熱門選擇,因為它們的語法簡單、易於解析且具有內建的可擴充性 [8,18,19]。例如,文字編輯器 Emacs 的主要部分實際上是用其自己的 Lisp 變體編寫的;其他幾個文字編輯器也遵循相同的路徑。目前有許多 Scheme 實作是以函式庫的形式,特別設計為嵌入式語言使用(例如,libscheme [18]、OScheme [20] 和 Elk [3])。然而,在自訂方面,Lisp 不能稱為使用者友善。對於非程式設計師來說,它的語法相當粗糙。此外,很少有 Lisp 或 Scheme 實作具有真正的可移植性。

另一種現今非常流行的擴充語言是 Tcl [11]。無庸置疑,其成功的原因之一是 Tk 的存在,Tk 是一個用於建立圖形使用者介面的強大 Tcl 工具包。Tcl 有一個非常原始的語法,這極大地簡化了其直譯器,但也讓撰寫稍微複雜一點的結構變得複雜。例如,將變數 A 的值加倍的 Tcl 程式碼是 set A [expr $A*2]。Tcl 支援單一原始類型,也就是 *字串*。這個事實,加上沒有預編譯,讓 Tcl 即使對於擴充語言來說都相當沒有效率。修正這些問題可以將 Tcl 的效率提升 5 到 10 倍,如同 TC [21] 所示。Lua 擁有更充足的資料類型和預編譯,執行速度比 Tcl 快 10 到 20 倍。一個簡單的測試顯示,在 Sparcstation 1 中執行的 Tcl 7.3 中,一個沒有參數的程序呼叫大約需要 44 �s,而一個全域變數的遞增則需要 76 �s。在 Lua v. 2.1 中,相同的運算分別需要 6 �s 和 4 �s。另一方面,Lua 比 C 慢大約 20 倍。這似乎是直譯語言的典型值 [22]。

Tcl 沒有內建的控制結構,例如 *while* 和 *if*。取而代之的是,控制結構是透過延遲評估來編程的,就像 Smalltalk 一樣。儘管強大且優雅,可編程控制結構可能會導致非常難懂的程式,而且在實務上很少使用。此外,它們通常會帶來高效能損失。

Python [23] 是一個有趣的語言,也被提議作為擴充語言。然而,根據其作者的說法,仍然需要「改善在其他應用程式中嵌入 Python 的支援,例如,透過將大多數全域符號重新命名為有 `Py` 前綴」[24]。Python 不是一個微小的語言,而且有許多擴充語言中不需要的功能,例如模組和例外處理。這些功能會為使用該語言的應用程式增加額外的成本。

Lua 被設計為結合現有語言的優點,以實現其作為可擴充擴充語言的目標。與 Tcl 一樣,Lua 是一個小型函式庫,具有與 C 的簡單介面;這個介面是一個包含 100 行的單一標頭檔。然而,與 Tcl 不同的是,Lua 會預編譯為標準的位元組碼中間形式。與 Python 一樣,Lua 有一個乾淨但熟悉的語法,以及內建的物件概念。與 Lisp 一樣,Lua 有單一的資料結構機制(表格),功能強大到足以 *有效率地* 實作大多數資料結構。表格是使用雜湊實作的。衝突是由線性探查處理的,當表格已超過 70% 滿時會自動重新配置和重新雜湊。雜湊值會快取以改善存取效能。

Lua 中提供的後備機制可視為一種具有恢復功能的例外處理機制 [25]。然而,Lua 的動態特性允許在許多情況下使用它,而靜態類型語言會在編譯時發出錯誤;上面提供的兩個範例就是這種情況。三個特定的後備 "arith""order""concat" 主要用於實作重載。特別是,圖 9 中的範例可以輕易轉換為其他支援重載的語言,例如 Ada 或 C++。然而,由於其動態特性,後備比例外處理或重載機制更靈活。另一方面,一些作者 [26] 認為使用這些機制的程式往往難以驗證、理解和除錯;使用後備時這些困難會惡化。後備應謹慎適度地編寫,而且只能由專家程式設計師編寫。

結論

對組態應用程式的需求日益增加,正在改變程式的結構。現今,許多程式使用兩種不同的語言撰寫:一種用於撰寫強大的「虛擬機器」,另一種用於為此機器撰寫單一程式。Lua 是一種專門為後者任務而設計的語言。它很小:如前所述,整個函式庫大約有六千行 ANSI C。它是可移植的:Lua 已用於從 PC-DOS 到 CRAY 的各種平台。它有簡單的語法和簡單的語意。而且它很靈活。

這種靈活性是透過一些不尋常的機制實現的,這些機制使語言高度可擴充。在這些機制中,我們強調以下幾點

關聯陣列是一種強大的統一資料建構函式。此外,它允許比其他統一建構函式(例如字串或清單)更有效率的演算法。與實作關聯陣列的其他語言不同 [10,11,12],Lua 中的表格是具有身分的動態建立物件。這大大簡化了將表格用作物件以及加入物件導向功能。

後備允許程式設計師擴充大多數內建運算的意義。特別是,透過索引運算的後備,可以將不同類型的繼承新增到語言中,而 "arith" 和其他運算子的後備可以實作動態重載。

資料結構遍歷的自省功能有助於產生高度多態的程式碼。許多運算必須在其他系統中提供為基本函式,或為每個新類型個別編碼,可以在 Lua 中以單一通用形式編寫程式。範例包括複製物件和操作全域環境。

除了在多項工業應用中使用 Lua 之外,我們目前正在多項研究計畫中實驗使用 Lua,範圍從使用分散式物件進行運算,這些物件會彼此傳送包含 Lua 程式碼的訊息 [27](這個想法先前已在 Tcl 中提出 [4]),到使用客戶端 Lua 程式碼透明地擴充 WWW 瀏覽器。由於所有讓 Lua 與作業系統介接的函式都提供在外部函式庫中,因此很容易限制詮釋器的功能,以提供足夠的安全性。

我們也計畫改善 Lua 的除錯功能;目前,僅提供簡單的堆疊追蹤。遵循提供強大元機制的理念,讓程式設計人員可以建置自己的擴充功能,我們計畫新增簡單的掛鉤到執行時間系統,讓使用者程式可以在發生重要事件時收到通知,例如進入或離開函式、執行使用者程式碼的一行等等。可以在這些基本掛鉤之上建置不同的除錯介面。此外,掛鉤對於建置其他工具也很有用,例如用於效能分析的剖析器。

本文中所述的 Lua 實作可在網際網路上取得,網址為

      https://lua.dev.org.tw/ftp/lua-2.1.tar.gz

致謝

我們要感謝 ICAD 和 TeCGraf 的員工使用和測試 Lua,以及 John Roll,感謝他透過電子郵件提供有關 Lua 先前版本中備援的寶貴建議。本文中提到的工業應用是由 PETROBRAS(巴西石油公司)和 ELETROBRAS(巴西電力公司)的研究中心合作開發的。作者部分獲得巴西政府(CNPq 和 CAPES)的研究和開發補助。Lua 在葡萄牙語中表示「月亮」。

參考文獻

[1] B. Ryan,〈Scripts unbounded〉,Byte15(8),235–240(1990)。

[2] N. Franks,〈Adding an extension language to your software〉,Dr. Dobb's Journal16(9),34–43(1991)。

[3] O. Laumann 和 C. Bormann。Elk:擴充語言套件。 ftp://ftp.cs.indiana.edu:/pub/scheme-repository/imp/elk-2.2.tar.gz,德國柏林工業大學。

[4] J. Ousterhout,〈Tcl:an embeddable command language〉,1990 年冬季 USENIX 會議論文集。USENIX 協會,1990 年。

[5] D. Cowan、R. Ierusalimschy 和 T. Stepien,「終端使用者程式環境」,第 12 屆世界電腦大會。國際資訊處理聯合會,1992 年 9 月,第 A-14 卷,第 54–60 頁。

[6] L. H. Figueiredo、C. S. Souza、M. Gattass 和 L. C. Coelho,「繪圖資料擷取介面產生」,第 5 屆巴西電腦圖學研討會,1992 年,第 169–175 頁。

[7] R. Ierusalimschy、L. H. Figueiredo 和 W. Celes,「Lua 程式語言第 2.1 版參考手冊」,電腦科學專題論文 08/95,巴西里約熱內盧教宗天主教大學,巴西里約熱內盧,1995 年。(可透過 ftp 於 ftp.inf.puc-rio.br/pub/docs/techreports 取得)

[8] B. Beckman,「互動式圖形中小型語言的架構」,軟體、實務與經驗21,187–207(1991)。

[9] J. Bentley,更多程式設計寶典,艾迪生-威斯理,1988 年。

[10] A. V. Aho、B. W. Kerninghan 和 P. J. Weinberger,AWK 程式語言,艾迪生-威斯理,1988 年。

[11] J. K. Ousterhout,Tcl 和 Tk 工具包,艾迪生-威斯理,1994 年。

[12] L. Wall 和 R. L. Schwartz,Perl 程式設計,歐萊禮媒體,1991 年。

[13] L. Lamport,LaTeX:文件編製系統,艾迪生-威斯理,1986 年。

[14] D. Ungar 等人,「Self:簡潔的力量」,Sigplan 公告22(12),227–242(1987)(OOPSLA'87)。

[15] C. H. Levy、L. H. de Figueiredo、C. J. Lucena 和 D. D. Cowan。「IUP/LED:可攜式使用者介面開發工具」,軟體:實務與經驗 26 #7(1996)737–762。

[16] M. T. de Carvalho 和 L. F. Martha,「幾何建模器組態架構:應用於計算力學」,PANEL95 - 第 21 屆拉丁美洲資訊學會議,1995 年,第 123–134 頁。

[17] C. Nahaboo。嵌入式語言目錄。 ftp://koala.inria.fr:/pub/EmbeddedInterpretersCatalog.txt

[18] B. W. Benson Jr.,「libscheme:Scheme 作為 C 函式庫」,1994 年 USENIX 極高階語言研討會論文集。USENIX,1994 年 10 月,第 7–19 頁。

[19] A. Sah 和 J. Blow,「腳本語言實作的新架構」,USENIX 極高階語言研討會論文集,1994 年。

[20] A. Baird-Smith. 「OScheme 手冊」。http://www.inria.fr/koala/abaird/oscheme/manual.html,1995 年。

[21] A. Sah,「TC:Tcl 語言的高效率實作」,碩士論文,加州大學柏克萊分校,電腦科學系,柏克萊,加州,1994 年。

[22] Sun Microsystems,Java,這門語言,1995 年。http://java.sun.com/people/avh/talk.ps

[23] G. van Rossum,「Python for UNIX/C 程式設計師簡介」,UUG najaarsconferentie 會議錄。荷蘭 UNIX 使用者群組,1993 年。(ftp://ftp.cwi.nl/pub/python/nluug-paper.ps)。

[24] G. van Rossum。Python 常見問答,版本 1.20++。ftp://ftp.cwi.nl/pub/python/python-FAQ,1995 年 3 月。

[25] S. Yemini 和 D. Berry,「一個模組化可驗證例外處理機制」,程式語言與系統 ACM 交易7(2)(1985 年)。

[26] A. Black,「例外處理:反對的理由」,博士論文,牛津大學,1982 年。

[27] R. Cerqueira、N. Rodriguez 和 R. Ierusalimschy,「一個事件導向的分布式程式設計經驗」,PANEL95 - 第 21 屆拉丁美洲資訊學會議,1995 年,第 225–236 頁。