在 Common Lisp 中,函式的引數列表稱為「參數列表」。
參數列表可以包含不同型別的引數。
這些不同的型別是透過「參數列表關鍵字」來標示的,它們全都以&開頭。
最常用的型別是選用引數、關鍵字引數與其餘引數這幾種。
在參數列表中,某個參數列表關鍵字之後的每個參數,都會屬於該型別。
每個參數列表關鍵字在一個參數列表中只能使用一次。
參數列表也會用在其他構造中,例如解構與巨集,這些之後會再討論。
在 Common Lisp 中,函式可以有一些選用的引數。
這些引數在參數列表中由&optional參數列表關鍵字標示。
參數如果沒有被指定,就會綁定到nil這個值。
如果有多個選用參數,它們會依序綁定。
選用參數可以指定預設值。
最後,每個選用參數還可以指定一個符號,這個符號會依函式呼叫者是否提供了該參數,而綁定到 true 或 false(這稱為「supplied-p 參數」)。
(defun optional-parameters (&optional x (y 'default) (z nil z-supplied-p))
(list x y (if z-supplied-p (list :z-was-supplied z)
(list :z-was-not-supplied z))))
(optional-parameters) ; => (NIL DEFAULT (:Z-WAS-NOT-SUPPLIED NIL))
(optional-parameters 5 nil 10) ; => (5 NIL (:Z-WAS-SUPPLIED 10))
在 Common Lisp 中,函式可以有具名參數(稱為「關鍵字參數」)。
這些參數在參數列表中由&key參數列表關鍵字標示。
關鍵字參數不是必要參數。
和選用參數一樣,它們可以指定預設值,也可以指定用來綁定其「是否被提供」狀態的符號。
呼叫帶有關鍵字參數的函式時,會把以關鍵字表示的參數名稱放在參數值前面。 函式呼叫者可以用任意順序指定關鍵字參數。
(defun keyword-parameters (&key x (y 'default) (z nil z-supplied-p))
(list x y (if z-supplied-p (list :z-was-supplied z)
(list :z-was-not-supplied z))))
(keyword-parameters) ; => (NIL DEFAULT (:Z-WAS-NOT-SUPPLIED NIL))
(keyword-parameters :y 5) ; => (NIL 5 (:Z-WAS-NOT-SUPPLIED NIL))
(keyword-parameters :z 10 :x 5) ; => (5 DEFAULT (:Z-WAS-SUPPLIED 10))
合併使用選用參數與關鍵字參數時要特別小心,因為關鍵字名稱和引數可能會被選用參數吃掉:
(defun could-be-confusing (&optional x y &key z) (list x y z))
(could-be-confusing :z 'huh?) ; => (:Z HUH? NIL)
在 Common Lisp 中,函式可以有一個參數,用來包含必要或選用參數都處理完之後剩下的引數。
這個參數由&rest參數列表關鍵字標示。
如果函式的所有引數都被其他型別的參數用掉了,其餘參數就會綁定到一個空列表。
如果有沒用到的引數,其餘參數就會綁定到由這些引數組成的列表。
(defun rest-of-it (req &optional opt &rest rest) (list req opt rest))
(rest-of-it 1) ; => (1 NIL NIL)
(rest-of-it 1 2) ; => (1 2 NIL)
(rest-of-it 1 2 3) ; => (1 2 (3))
(rest-of-it 1 2 3 4 5) ; => (1 2 (3 4 5))
Lisp 外星人 Lilly 在你的幫忙下,肯定做了很多千層麵。他們想送你一些剩菜來謝謝你。不過在那之前,他們想針對你幫忙寫過的其中一個函式給點回饋。
在這個練習裡,你會再寫幾個函式,幫 Lilly 做千層麵,也幫他們跟你一起把剩菜分一分。
一共有六個任務:三個是改良現有的千層麵烹調函式,三個是分配剩菜。
preparation-time-in-minutes 更好用Lilly 說,用你的 preparation-time-in-minutes 有點卡卡的,因為每次都得先數完所有層數才能呼叫函式。Lilly 想問,你能不能做一個版本的函式,把層(不管幾層)當成引數傳進去,再根據有幾層來算出準備時間?提醒一下,準備一層需要 19 分鐘。
(preparation-time 'sauce 'cheese 'right-handed-macaroni 'cheese 'sauce
'left-handed-macaroni 'sauce 'sauce 'cheese 'cheese)
;; => 190 (because there are 10 layers.)
Lilly 的家長烤千層麵總是要烤 337 分鐘,不過 Lilly 也承認,其他 Lisp 外星人喜歡烤久一點或短一點。
所以,如果 remaining-minutes-in-oven 不要用 expected-minutes-in-oven,而是改成接受一個_非必填_的參數,用來指定時間要長一點、很長、短一點還是很短,那就好了。長一點或短一點,是指在預設的 337 分鐘上加或減 100;很長或很短,則是加或減 200。如果沒有提供參數,或是引數是 'normal',就應該回傳預設值 337。
(remaining-minutes-in-oven) ;; => 337
(remaining-minutes-in-oven :normal) ;; => 337
(remaining-minutes-in-oven :shorter) ;; => 237
(remaining-minutes-in-oven :very-short) ;; => 137
(remaining-minutes-in-oven :longer) ;; => 437
(remaining-minutes-in-oven :very-long) ;; => 537
你才剛完成那個修改,Lilly 就想起有些 Lisp 外星人喜歡_超級短_的烹調時間。基本上他們就是想吃生的。所以現在,如果引數是 NIL,函式就該回傳 0;除此之外,其他情況都跟上一個任務一樣回傳相同的值。
(remaining-minutes-in-oven) ;; => 337
(remaining-minutes-in-oven nil) ;; => 0
你幫忙改良了千層麵烹調函式之後,現在有_好多千層麵_!Lilly 想報答你所有的幫忙,把剩菜跟你一起分。你需要寫一個函式,接受三個參數:剩菜的總量、Lilly 在櫥櫃裡找到的剩菜容器數量,以及你在自己那邊找到的數量。因為找剩菜容器可能要花上不同的時間,而且確切的剩菜量可能是在你和 Lilly 各自翻櫥櫃的時候才確定,所以你們兩個都同意,函式應該可以接受任意順序的引數,這樣每確定一個數字就能先寫下來。等到三個數字都齊了,只要把函式呼叫的括號關起來,再求值就好。
函式應該回傳真正剩下來的剩菜量,也就是剩菜重量和你與 Lilly 找到的容器總數之間的差。
(split-leftovers :weight 20 :human 10 :alien 5) ;; => 5
(split-leftovers :weight 20 :alien 10 :human 2) ;; => 8
(split-leftovers :alien 12 :weight 20 :human 4) ;; => 4
在你們各自來回跑了幾趟櫥櫃之後,兩個人都同意,如果函式能直接_假設_你或 Lilly 有……比如說,在沒指定的情況下就有 10 個容器,那就太好了。
(split-leftovers :weight 20 :human 5) ;; => 5
(split-leftovers :weight 20 :alien 5) ;; => 5
(split-leftovers :weight 20) ;; => 0
更棒的是,函式可以_假設_剩菜一定裝得進容器裡。所以如果沒有提供重量,函式應該回傳 :just-split-it。不過,如果有提供重量,而且它是 nil,那就應該回傳 :looks-like-someone-was-hungry。
(split-leftovers :human 5 :alien 5) ;; => :JUST-SPLIT-IT
(split-leftovers :weight NIL :human 5 :alien 5) ;; => :LOOKS-LIKE-SOMEONE-WAS-HUNGRY