那我們究竟如何用 Fennel 來做事呢?就像所有的高階語言一樣,我們至少需要:
(fn print-and-add [a b c]
(print a)
(+ b c))
fn
是定義函數的關鍵字,方括弧 [...]
裡,是函數的引數,最後一個表達式會是函數的傳回值。
(fn print-and-add [a b c]
"purpose/context of fn here."
(print a)
(+ b c))
; We use `;` as comment
上頭例子裡的 "purpose/context of fn here."
是 doc string ,它可加可不加。他的位置在方括弧後的下一行。
分號 ;
用來表示註解。
let
用來創造 let 區塊內有效的區域變數,且創造之後,值不可修改。(let [x (+ 89 5.2)
f (fn [abc] (print (* 2 abc)))]
(f x))
在上頭的例子裡,在最後一個 )
之後的程式碼,x
與 f
這兩個變數就不再存在了。
(let [x 19]
;; (set x 88) <- not allowed!
(let [x 88]
(print (+ x 2))) ; -> 90
(print x)) ; -> 19
let
建立的變數,無法用 set
去修改,但是,你可以在裡頭再疊加第二個 let
,且在第二個 let
區域裡,建立同樣名稱的變數去遮蔽原先的變數。
local
用來創造單一檔案內有效的區域變數,且創造之後,值不可修改。(local tau-approx 6.28318)
var
用來創造單一檔案內有效的區域變數,且創造後,值還可以透過 set
修改。(var x 19)
(set x (+ x 8))
(print x) ; -> 27
基本的算術運算都有支援:+, -, *, /
。當然,運算子寫在表達式的開頭處。
特殊之處是數字的型別:除了 Lua 5.3 之後,有 integer 的型別之外,其它的 Lua 版本的數字都是 number 型別, number 是雙倍精度的浮點數。Neovim 使用的 Lua 是 Luajit ,它相容於 Lua 5.1 版。
字串是不可修改 (immutable) 的型別。 ..
用來做字串的拼接。
(.. "hello" " world") ; -> "hello world"
Python 有 list, dict 容器型別;在 Java 則有 ArrayList, HashMap 容器型別;Golang 則有 slice, map 容器型別。然而,無論是在 Lua 或是 Fennel 裡,Table 都是唯一的容器型別。
但是,雖然只有一種,字典與串列兩種用途,它都可以滿足。
做字典用法時,使用大括弧來宣告字典。以下的 "number"
是鍵;531
是值。
{"key" value
"number" 531
"f" (fn [x] (+ x 2))}
.
用來從 Table 取得「鍵」對應的「值」。(let [tbl (function-which-returns-a-table)
key "a certain key"]
(. tbl key))
tset
用來對 Table 寫入新的「鍵/值」或是修改「鍵/值」(let [tbl {}
key1 "a long string"
key2 12]
(tset tbl key1 "the first value")
(tset tbl key2 "the second one")
tbl)
; -> {"a long string" "the first value" 12 "the second one"}
當 Table 要用來存儲帶有線性、循序語意的資料時,我們需要把 Table 的所有鍵值,都設定為數值,並且從 1
開始遞增。
在這種用途,Fennel 提供了一種新的宣告語法,使用方括弧 [
來宣告。而這時,就不用寫鍵的值了,它會自動從 1
開始產生。
["abc" "def" "xyz"]
; equivalent to {1 "abc" 2 "def" 3 "xyz"}
table.insert
有兩種用法:
table.insert
會把新的值插入在串列的尾端。table.insert
會把新的值插入到索引值對應的位置,這種時候,串列裡所有的其它元素的索引值會自動做對應的調整。table.remove
也跟 table.insert
一樣,有用或不用索引值的兩種用法。
(local ltrs ["a" "b" "c" "d"])
(table.remove ltrs) ; Removes "d"
(table.remove ltrs 1) ; Removes "a"
(table.insert ltrs "d") ; Appends "d"
(table.insert ltrs 1 "a") ; Prepends "a"
(. ltrs 2) ; -> "b"
;; ltrs is back to its original value ["a" "b" "c" "d"]
在 Fennel 裡,我們通常會把字典用途的 Table 稱為 General Table ,而把串列用途的 Table 稱為 Sequential Table。
length
可以用來傳回串列容器或是字串的長度。
(let [tbl ["abc" "def" "xyz"]]
(+ (length tbl)
(length (. tbl 1)))) ; -> 6
如果不知道要做幾次,只知道要一直做下去,這通常使用迴圈語法:while
。
(while (keep-looping?)
(do-something))
也有 for
迴圈。
(for [i 1 10]
(print i))
;; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
(for [i 1 10 2]
(print i))
;; 1, 3, 5, 7, 9
然而,多數時候,迭代可以更清晰地表達我們的意圖:
each
搭配 pairs
可以迭代走訪 General Table 。each
搭配 ipairs
可以迭代走訪 Sequential Table 。(each [key value (pairs {"key1" 52 "key2" 99})]
(print key value))
(each [index value (ipairs ["abc" "def" "xyz"])]
(print index value))
想表達函數式編程裡的 map
或是 filter
的語意時,可以使用 collect
和 icollect
。(這兩者的語意最接近於 Python 的 Dictionary Comprehension 與 List Comprehension 。)
collect
可以生成 General Table 。icollect
可以生成 Sequential Table 。(collect [_ s (ipairs [:greetings :my :darling])]
s (length s))
;; -> {:darling 7 :greetings 9 :my 2}
(icollect [_ s (ipairs [:greetings :my :darling])]
(if (not= :my s)
(s:upper)))
;; -> ["GREETINGS" "DARLING"]
注意:範例裡的底線 _
,它用來佔位一個變數。因為 ipair
左邊會有兩個變數,但是因為該變數之後不會使用,就使用底線符號。
另外,想表達函數式編程裡的 reduce
的語意時,則可以使用 accumulate
。參考資料。
Fennel 的 if
,它有兩種用法:
if
。(if (pass-the-exams?)
(graduate)
(repeat-the-grade))
if elseif elseif … else
,但是寫法省事得多。(let [x (math.random 64)]
(if (= 0 (% x 2))
"even"
(= 0 (% x 9))
"multiple of nine"
"I dunno, something else"))
如果某個條件判斷並沒有 else 分支時,通常使用 when
。注意,通常 when
裡包含的表達式也常常是帶有副作用的表達式。(註)
(when (currently-raining?)
(wear "boots")
(deploy-umbrella))
我們探討了 Fennel 的核心語法,而很多核心語法都是從 Lua 直接搬過來用。
本章節借用了大量官網教學的程式碼範例,但官網教學卻涵蓋了更多細節,有興趣的讀者不妨也搭配參考著閱讀。
註:軟體和程式設計中的副作用 (side effect),指的是在一個函式、方法或運算式執行時,除了回傳一個值之外,還對外部狀態造成了改變。此一概念與函數式編程很有關,之後再細談。