갈루아의 반서재

The Conditionals: if and Beyond


true 나 false 에 따라 서로 다른 값이 반환되도록 할 때 if 를 사용한다.

1
2
3
4
[57]> (if (= (+ 1 2) 3) 'yup 'nope)
YUP
 [57]>(if (= (+ 1 2) 4) 'yup 'nope)
NOPE
cs

물론 리스트가 비었는지 체크할 때도 사용할 수 있다.

1
2
3
4
[58]> (if '(1) 'the-list-has-stuff-in-it 'the-list-is-empty)
THE-LIST-HAS-STUFF-IN-IT
[58]> (if '() 'the-list-has-stuff-in-it 'the-list-is-empty)
THE-LIST-IS-EMPTY
cs

아래 예에서는 5가 홀수인지 살펴본다. 5는 홀수이므로 odd-number을 반환한다.

1
2
3
4
[59]> (if (oddp 5)
          'odd-number
          'even-number)
ODD-NUMBER
cs


2가지 중요한 점을 관찰할 수 있다. 

- if  다음의 오직 하나의 표현식만 평가된다

- if 문에서는 오직 한 가지만 수행할 수 있다


보통은 리스프에서 함수가 평가될 때, 함수 뒤에 위치한 모든 표현식이 평가의 대상이지만, if 문에서는 위의 룰을 따르지 않는다. 다음의 경우를 보자. 

1
2
3
4
[60]> (if (oddp 5)
         'odd-number
         (/ 1 0))
ODD-NUMBER
cs

1을 0으로 나눠야하기 때문에 일반적인 경우라면 위의 구문은 실행이 되지 않을 것이다. 하지만 if 문에서는 앞서 관찰한대로 if 다음의 하나의 표현식만 평가되므로, 이 경우 5는 홀수이고, 홀수인 경우에는 odd-number를 반환하기만 하면 되므로 1을 0으로 나눈다는 것 자체는 아무 문제가 되지 않는다. 하지만 6을 넣는 경우는 사정이 달라진다. 이 경우에는 6이 홀수가 아니므로 (/ 1 0)을 평가하게 되고, 다음과 같이 에러가 발생한다. 

1
2
3
4
5
[60]> (if (oddp 6)
         'odd-number
         (/ 1 0))
 
*** - /: division by zero
cs

앞서 말했듯이 if 문의 경우 하나의 표현식만 평가받기 때문에, 둘 이상의 독립된 조건을 걸 수 없습니다. 둘 이상의 분기를 두고자 하는 경우, 특별한 명령어인 progn 을 사용할 수 있습니다. progn 을 사용하면 마지막 평가만 전체 표현식의 값으로 반환됩니다. 다음의 예를 봅시다. 

1
2
3
4
5
6
7
[62]> (defvar *number-was-odd* t)
*NUMBER-WAS-ODD*
[64]> (if (oddp 5)
       (progn (setf *number-was-odd* t) 'odd-number) 'even-number)
ODD-NUMBER
[64]> *number-was-odd*
T
cs


Going Beyond if: The when and unless Alternatives


매번 progn 을 사용하는 것은 쉽지 않은 일이다. 리스프는 이를 위해 암묵적인 progn 을 포함하는 몇 가지 명령어를 가지고 있다. 일반적인으로 다음과 같이 사용한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[65]> (defvar *number-is-odd* nil)
*NUMBER-IS-ODD*
[65]> (when (oddp 5)
         (setf *number-is-odd* t)
         'odd-number)
ODD-NUMBER
[65]> *number-is-odd*
T
[65]> (unless (oddp 4)
         (setf *number-is-odd* nil)
         'even-number)
EVEN-NUMBER
[65]>  *number-is-odd*
NIL
cs

when 을 사용할 때는 조건이 true 일때 표현식이 평가되고, unless 의 경우에는 조건이 false 일 때 표현식이 평가된다. 


The Command That Does It All: cond


cond 폼은 리스프에서 브랜칭을 처리하는 전통적인 방법이다. 다음의 예를 보자. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[66]> (defvar *arch-enemy* nil)
*ARCH-ENEMY*
 
[66]> (defun pudding-eater (person)
          (cond ((eq person 'henry) (setf *arch-enemy* 'stupid-lisp-alien) '(curse you lisp alien – you ate my pudding))
                ((eq person 'johnny) (setf *arch-enemy* 'useless-old-johnny) '(i hope you choked on my pudding johnny))
                (t '(why you eat my pudding stranger ?))))
PUDDING-EATER
 
[66]>  (pudding-eater 'johnny)
(I HOPE YOU CHOKED ON MY PUDDING JOHNNY)
 
[66]>  *arch-enemy*
USELESS-OLD-JOHNNY
 
[66]> (pudding-eater 'george-clooney)
(WHY YOU EAT MY PUDDING STRANGER ?)
cs

cond 내부에서는 서로 다른 브랜치를 처리하기 위해 괄호를 이용한다. 그리고 괄호 내의 첫 번째 표현식이 해당 분기를 실행시키는 조건을 포함하고 있다. 위의 예에서, 각각의 경우에 이름별로 서로 다른 분기를 가지고 있는 것을 볼 수 있다. 하나는 Henry, 다른 하나는 Johnny, 그리고 마지막은 나머지 모두를 위한 경우이다.

제공된 사람의 이름을 비교하기 위해 eq 함수를 사용한다. 모든 조건은 위에서 아래로 비교된다. 마지막에 나오는 t (for true) 라는 조건은 최소한 맨 마지막 라인이라도 실행되도록 하기 위한 조치이다. 

when 과 unless 를 가지고, 암묵적인 progn이 존재하므로 트리거된 분기는 하나 이상의 명령을 가질 수 있다.  위의 경우첫 2개의 분기는 반환값 뿐만 아니라 추가적인 *arch-enemy* 변수를 설정한다. 


Branching with case


그럼 마지막으로 case 에 대해서 살펴보자. eq 함수를 사용하고, case를 통해 값을 비교한다. case 를 사용하면 앞의 예제는 다음과 같이 된다. 

1
2
3
4
5
6
[70]> (defun pudding-eater (person)
          (case person 
             ((henry) (setf *arch-enemy* 'stupid-lisp-alien) '(curse you lisp alien – you ate my pudding))
             ((johnny) (setf *arch-enemy* 'useless-old-johnny) '(i hope you choked on my pudding johnny))
          (otherwise '(why you eat my pudding stranger ?))))
PUDDING-EATER
cs

각각의 case 문에 다루는 사람의 이름이 명시되기 때문에 앞의 코드보다 훨씬 더 보기 편해졌다. 다만, case는 비교를 위해 eq를 사용하기 때문에 심볼에 대해서만 사용되는 점에 주의할 필요가 있다.