트랙
/
Common Lisp
Common Lisp
/
연습 문제
/
Lilly의 남은 라자냐
Lilly의 남은 라자냐

Lilly의 남은 라자냐

학습 연습 문제

소개

람다 리스트

Common Lisp에서 함수의 인자 목록을 '람다 리스트'라고 해요. 람다 리스트에는 서로 다른 유형의 인자를 담을 수 있어요. 이런 유형은 모두 &로 시작하는 '람다 리스트 키워드'로 지정해요. 가장 흔히 쓰이는 유형은 선택적 인자, 키워드 인자, 나머지 인자 유형이에요. 특정 람다 리스트 키워드 뒤에 오는 람다 리스트의 매개변수는 모두 그 유형이 돼요. 람다 리스트 키워드는 람다 리스트 안에서 한 번만 사용할 수 있어요.

람다 리스트는 구조 분해나 매크로처럼 나중에 다룰 다른 구성 요소에서도 사용해요.

선택적 매개변수

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 외계인 릴리는 도와주신 덕분에 라자냐를 정말 많이 만들었어요. 고마운 마음을 담아 남은 라자냐를 나눠 주고 싶어 해요. 그런데 그전에, 함께 만든 함수 중 하나에 대해 전하고 싶은 이야기가 있대요.

이 연습 문제에서는 릴리의 라자냐 요리를 돕고, 남은 라자냐를 함께 나누는 일도 돕기 위해 함수를 몇 개 더 작성해요.

과제는 여섯 개예요. 세 개는 기존 라자냐 요리 함수를 개선하는 것이고, 나머지 세 개는 남은 라자냐를 나누는 것에 관한 거예요.

1. preparation-time-in-minutes 함수를 더 쓰기 편하게 만들기

릴리는 preparation-time-in-minutes 함수를 쓰려면 호출하기 전에 겹을 전부 세어야 해서 조금 불편했다고 해요. 그래서 겹을 몇 개든 인자로 받아서, 겹이 몇 개였는지에 따라 준비 시간을 계산하는 함수 버전을 만들어 줄 수 있겠느냐고 물어봐요. 참고로, 겹 하나를 준비하는 데는 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. 남은 라자냐 나누기

라자냐 요리 함수를 개선하는 걸 도와줬더니 이제 라자냐가 아주 많아요! 릴리는 그동안의 도움에 보답하려고 남은 라자냐를 나누고 싶어 해요. 여기서는 매개변수 세 개를 받는 함수를 작성해야 해요. 남은 라자냐의 총량, 릴리가 찬장에서 찾은 남은 라자냐 용기의 개수, 그리고 여러분 몫으로 똑같이 찾은 용기의 개수예요.

남은 라자냐 용기를 찾는 데 걸리는 시간이 저마다 다를 수 있고, 남은 라자냐의 정확한 양은 릴리와 함께 각자 찬장을 뒤지는 동안에 정해질 수 있기 때문에, 두 사람은 함수가 인자를 어떤 순서로든 받아야 한다는 데 의견을 모았어요. 그래야 숫자가 하나씩 정해질 때마다 적어 둘 수 있거든요. 세 숫자가 모두 모이면 함수 호출의 괄호를 닫고 그대로 실행하면 돼요.

이 함수는 남고 남은 라자냐의 양, 즉 남은 라자냐의 무게와 릴리와 여러분이 찾은 용기의 총 개수 사이의 차이를 반환해야 해요.

(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

Lilly의 남은 라자냐 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Common Lisp 트랙을 개념 27개연습 문제 92개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.