トラック
/
Common Lisp
Common Lisp
/
演習
/
リリーのラザニアの残り
リリーのラザニアの残り

リリーのラザニアの残り

学習演習

はじめに

ラムダリスト

Common Lispでは、関数の引数リストはラムダリストと呼ばれます。ラムダリストには、さまざまな種類の引数を書くことができます。これらの種類は、すべて&で始まるラムダリストキーワードを使って指定します。もっともよく使われるのは、オプショナル引数、キーワード引数、レスト引数です。あるラムダリストキーワードより後ろにあるラムダリスト内の仮引数は、すべてその種類になります。ラムダリストキーワードは、1つのラムダリストの中で1回しか使えません。

ラムダリストは、あとで説明するデストラクチャリングやマクロなど、ほかの構文でも使われます。

オプショナル仮引数

Common Lispでは、関数に省略可能な引数を持たせることができます。これは、ラムダリストの中で&optionalというラムダリストキーワードを使って指定します。指定されなかった仮引数には、nilが束縛されます。オプショナル仮引数が複数ある場合、それらは順番に束縛されます。オプショナル仮引数には、デフォルト値を指定できます。最後に、各オプショナル仮引数にはシンボルを指定できます。このシンボルには、その仮引数が関数の呼び出し側から渡されたかどうかに応じて、真偽が束縛されます(これを「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エイリアンのリリーは、あなたの助けのおかげで、たくさんのラザニアを作ることができました。そのお礼に、残りものを少し分けてあげたいと思っています。でもその前に、あなたが手伝ってくれた関数のひとつについて、いくつかフィードバックがあります。

この演習では、リリーのラザニア作りを手伝い、さらに残りものをあなたと分け合えるようにするために、関数をいくつか追加で書いていきます。

タスクは6つあります。3つは既存のラザニア作りの関数を改善するもの、残りの3つは残りものを分けるものです。

1. preparation-time-in-minutesを使いやすくする

リリーによると、preparation-time-in-minutesは、関数を使う前にすべての層を数えなければならないので、少し使いにくかったそうです。リリーは、層を(何層でも)入力として受け取り、層の数に応じて準備時間を計算する、そういうバージョンの関数を作れないかと考えています。念のため、1層を準備するには19分かかります。

(preparation-time 'sauce 'cheese 'right-handed-macaroni 'cheese 'sauce
                  'left-handed-macaroni 'sauce 'sauce 'cheese 'cheese)
                  ;; => 190 (because there are 10 layers.)

2. 予想されるオーブン時間を変更できるようにする

リリーの親はいつもラザニアを337分焼いていましたが、リリーも、他の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. リリーがもうひとつの好みの調理スタイルを思い出す

その変更を終えた直後、リリーは、_とても短い_調理時間を好むLispエイリアンもいることを思い出します。実質的に、生で食べたいのです。そこで、前のタスクと同じ値に加えて、入力がNILだった場合は0を返すようにします。

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

4. 残りものを分ける

ラザニア作りの関数の改善を手伝ってもらったおかげで、_たくさんのラザニア_があります! リリーは、あなたの助けに報いるために、残りものを分け合いたいと思っています。3つの入力を受け取る関数を書く必要があります。残りものの総量、リリーが食器棚で見つけた残りもの用の容器の数、そしてあなたが見つけた容器の数です。残りもの用の容器を探すのにかかる時間は人によって違い、また残りものの正確な量は、あなたとリリーがそれぞれの食器棚を探している間に決まるかもしれません。そこで二人は、関数が入力の順序を問わずに受け取れるようにしようと決めます。そうすれば、それぞれの数値が決まった時点で書き留めておけるからです。3つの数値がそろったら、関数呼び出しの括弧(())を閉じて評価するだけです。

関数は、余った残りものの量、つまり残りものの重さと、あなたとリリーが見つけた容器の合計数との差を返します。

(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. 仮定を置く

それぞれの食器棚に何度か往復したあと、二人は、入力が指定されなかった場合に、あなたまたはリリーが…たとえば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を学んでマスターできます。すべて無料です。