學習軌道
/
Common Lisp
Common Lisp
/
練習
/
莉莉的千層麵剩菜
莉莉的千層麵剩菜

莉莉的千層麵剩菜

學習練習

簡介

參數列表

在 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 做千層麵,也幫他們跟你一起把剩菜分一分。

一共有六個任務:三個是改良現有的千層麵烹調函式,三個是分配剩菜。

1. 讓 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.)

2. 讓你能更改預期的烘烤時間

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

3. Lilly 想起還有另一種偏好的烹調方式

你才剛完成那個修改,Lilly 就想起有些 Lisp 外星人喜歡_超級短_的烹調時間。基本上他們就是想吃生的。所以現在,如果引數是 NIL,函式就該回傳 0;除此之外,其他情況都跟上一個任務一樣回傳相同的值。

(remaining-minutes-in-oven)     ;; => 337
(remaining-minutes-in-oven nil) ;; => 0

4. 分配剩菜

你幫忙改良了千層麵烹調函式之後,現在有_好多千層麵_!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

5. 假設一下

在你們各自來回跑了幾趟櫥櫃之後,兩個人都同意,如果函式能直接_假設_你或 Lilly 有……比如說,在沒指定的情況下就有 10 個容器,那就太好了。

(split-leftovers :weight 20 :human 5) ;; => 5
(split-leftovers :weight 20 :alien 5) ;; => 5
(split-leftovers :weight 20)          ;; => 0

6. 標準的剩菜量

更棒的是,函式可以_假設_剩菜一定裝得進容器裡。所以如果沒有提供重量,函式應該回傳 :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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Common Lisp Exercism

準備好開始 莉莉的千層麵剩菜 了嗎?

註冊 Exercism,透過 27 個概念92 個練習 和真人引導來學習並精通 Common Lisp,全部免費。