昨天已經討論過了 Fennel 的核心語法,那些已經可以寫一些純粹的資料轉換了。另一方面,真實世界的軟體開發,有很大一部分都是跟函式庫有關,所以我們接下來就要談論更多的 Lua 。
之前有提到,Lua 則是生產環境中廣泛使用的高階語言裡最簡單的。為什麼呢?這是因為它通常是做為嵌入式使用:在一個主應用程式裡,Lua 通常以函式庫的形式被嵌入。它之所以輕量,是因為它沒有內建的檔案系統、系統呼叫、網路連線等功能。這些底層操作通常會交由主應用程式(通常是 C 或 C++ 語言)來完成。這種設計哲學使得 Lua 的核心非常精簡,也更容易整合到各種不同的應用環境中。
Lua 的一大特色是它與 C/C++ 語言的無縫接軌。它提供了一個精巧的 C API,讓主應用程式能夠輕鬆地註冊 C 函數,讓 Lua 程式碼能夠呼叫。舉例來說,如果你的主程式需要讀取一個設定檔,你可以在 C 語言中寫一個 read_config_file
函式,然後透過 Lua 函式庫裡的 C API 將它暴露給 Lua 環境。
當 Lua 程式碼呼叫 read_config_file
時,控制權會回到主應用程式,執行 C 語言的函數。這個函數執行完畢後,再將結果返回給 Lua。這種機制讓 Lua 不僅是個腳本語言,更像是一個高度客製化、能與主程式深度互動的「組件」,可以用來處理邏輯、配置或甚至是遊戲插件等任務。
Lua 提供了 8 種資料型別:
nil
:表示「沒有」。 nil
在 if
的條件裡的話,會視為 false
值。boolean
:只有 true
和 false
。在 Lua 中,只有 nil
和 false
被視為假。所有其他的值,包括數字 0
、空字串 ""
或空的 Table {}
, 都被視為真。這與其他一些程式語言(如 C++ 或 JavaScript)的行為不同。number
:倍精度浮點數。string
:字串。table
:唯一的容器型別。thread
:雖然是用 thread 這個字,但它其實是協程 (coroutine)。function
:函數userdata
:如果某些型別是 C 語言傳回的,就用這種型別。在 Fennel 裡,可以用 (type $variable)
來檢查 $variable 的型別。
在 Lua 裡,有一些標準函式庫裡的函數,並不是內建在全域的環境裡,所以我們要使用時,要先引用函式庫之後才能使用。比方說,以下的 io.open
local io = require("io")
io.open("file.txt", "r")
另一方面,也有一些函數是直接內建在全域的環境裡,它們是全域函數 (global functions)。特別重要的有:
tonumber
:字串轉為數字tostring
:轉為字串print
:對它的所有引數都先呼叫 tostring
,然後印出並換行。印出時會在引數之間插入空格。type
:傳回型別pcall
:在保護模式 (protected mode) 下呼叫函數。因為是保護模式,出錯了,程式也不會立刻崩潰。它會回傳兩個值:第一個是布林值,表示呼叫是否成功;第二個是回傳值或錯誤訊息。error
:中止程式執行,產生錯誤,並且跳到最接近的 pcall
assert
:檢查條件,如果為假則拋出錯誤。ipairs
:走訪 Sequential Tablespairs
:走訪任何的 Tablesunpack
:將 Sequential Table 從單一 Table 展開,變成多個獨立的值。require
:載入並且傳回一個模組。若對上述的函數做個簡單的分類的話:
tonumber
、tostring
、print
、type
。pcall
、error
、assert
ipairs
、pair
、unpack
require
來看一個 unpack
在 Fennel 裡的用法:
(local t [:a :b :c])
(print (unpack t)) ;; a b c
如果你有寫過函數式編程的話,是否想起了一個巧妙的函數 apply
呢?由於 Fennel 採取極簡主義,既然已經從 Lua 那邊借到了 unpack
,所以 Fennel 就不提供 apply
函數了。
如果你有什麼需求,之前介紹過的函數還是無法處理,在開始引入第三方的函式庫之前,先參考一下 Lua 5.1 手冊。說不定 Lua 的標準函式庫就找得到了。
有時候叫 GenAI 產生的程式碼卻根本無法執行,連運算子的位置都錯了,這個很常見,因為 Fennel 是小眾語言, AI 可以利用的訓練資料還不夠多。變通方案也很簡單,你可以先叫 Gen AI 去產生 Lua 的程式碼,再透過編譯/反編譯器,編成 Fennel 的程式碼。
之前已經介紹過了,Lua 的 Table 可以當串列或是字典。但是,還不只是這樣子。Lua 的 Table 還有其它的用法:
在 Lua 中,**模組(module)**本質上就是一個 Table。這個 Table 包含了模組要向外部暴露的所有函數和變數。當你使用 require
載入一個模組時,它會返回這個 Table。這種設計非常簡潔,因為你不需要額外的關鍵字或特殊的語法來定義模組,只要建立一個 Table 並將其作為回傳值就可以了。
舉個例子,假設你有一個名為 aa.lua
的檔案,內容如下:
local aa = {}
function aa.ff()
return "This is a public function."
end
aa.vv = 100
return aa
在這個例子中,aa
就是一個 Table。我們將 ff
和 vv
設為這個 Table 的鍵值對,這樣它們就可以從外部存取。
Lua 的 Table 也可以用來實現 物件導向程式設計(Object-Oriented Programming, OOP),主要是透過 metatable 這個功能。
Lua 的 Table 裡可以包含一個 metatable ,而這個 metatable 雖然也是 Table ,但是它帶有特殊的語義。當包含 metatable 的 Table 被視為是物件且做方法呼叫時,一旦方法無法找到,就會自動到 metatable 裡去尋找,從而實現了物件導向的繼承與多型等關鍵特性。
Fennel 由於深受 Clojure 的影響,也把一些很不錯的設計自 Clojure 帶來,形成了一些 Table 的特殊用法。
容器的鍵如果是字串型態,且沒有包含空白 (space) 或是保留字元 (reserved characters) 的話,鍵就可以改用冒號開頭表示法 :shorthand
來寫。
{:key value :number 531}
這種冒號開頭表示法,特別常用於 Table 的鍵。實際上,在 Clojure 語言裡,冒號開頭表示法直接對應一個新的資料型別,叫做 keyword ,它就是專門給容器的鍵使用。
值得注意的是,在 Fennel 裡,這種冒號開頭表示法在編譯後會被轉換成 Lua 的字串型別,例如 :key 會變成 "key"。這點與 Clojure 的 keyword 是獨立的資料型別有所不同,但它提供了類似的方便性。
當 Table 的鍵使用冒號開頭表示法時,可以用 ${table}.${key}
的方式來取值,這比 (. ${table} ${key})
更簡潔。
(let [tbl {:x 52 :y 91}]
(+ tbl.x tbl.y)) ; -> 143
還有, ${table}.${key}
這種寫法也可以搭配 set
函數。
(let [tbl {}]
(set tbl.one 1)
(set tbl.two 2)
tbl) ; -> {:one 1 :two 2}
(let [data [1 2 3]
[fst snd thrd] data]
(print fst snd thrd)) ; -> 1 2 3
(let [pos {:x 23 :y 42}
{:x x-pos :y y-pos} pos]
(print x-pos y-pos)) ; -> 23 42
基於鍵的解構,如果要接受解構值的變數名稱,恰好跟鍵的名稱一樣的話,還可以再用更加縮寫的寫法,也就是鍵不再使用「冒號表示法」了,直接寫成「冒號」即可。
(let [pos {:x 23 :y 42}
{: x : y} pos]
(print x y)) ; -> 23 42
在 Fennel 的官網也有一分 Lua Primer 文件,專門講解 Fennel 作者認為 Lua 重要的部分,內容比本章還多一些。
我推斷 Fennel 作者的想法,也許他認為, developer 讀完了他寫的 Fennel Tutorials 和 Lua Primer 之後,就足以上手 Fennel 了吧?