갈루아의 반서재



Cool Tricks with Conditions


리스프의 몇 가지 반직관적인 트릭을 통해 우리는 좀 더 간결한 코드를 작성할 수 있게 해준다. 하나는 2개의 새로운 조건문 명령어이고, 다른 하나는 True와 False 에 대한 리스프만의 심플한 컨셉이다. 

 

Using the Stealth Conditionals and and or

and와 or 조건은 간단한 수학 연산자로 Boolean 연산을 도와준다. 예를 들면 다음과 같이 3개의 숫자가 모두 홀수인 경우에 T를 반환하는 것처럼 말이다. 

1
2
[75]> (and (oddp 5)(oddp 7)(oddp 9))
T
cs

or 를 이용하면 아래와 같이 하나만 홀수라도 T 를 반환한다.

1
2
[76]>  (or (oddp 4) (oddp 7) (oddp 8))
T
cs

위의 단순한 수학적 연산을 넘어서 and와 or를 다음과 같이 숫자가 짝수일 때만 True 를 반환하는 전역변수를 설정하는데도 활용할 수 있다.

1
2
3
4
5
6
[77]> (defparameter *is-it-even* nil)
*IS-IT-EVEN*
[77]> (or (oddp 4)(setf *is-it-even* t))
T
[77]> *is-it-even*
T
cs

홀수에 대해서도 마찬가지이다. 변수는 그대로이다. 

1
2
3
4
5
6
[79]> (defparameter *is-it-even* nil)
*IS-IT-EVEN*
[79]> (or (oddp 5) (setf *is-it-even* t))
T
[79]> *is-it-even*
NIL
cs

위의 예는 리스프는 앞의 구문이 true를 반환하면 더 이상 나머지 구문은 평가하지 않는 방식을 택했음을 보여준다. 마찬가지로, 앞선 구문이 false로 판명이 나도 나머지를 더 이상 평가하지 않고 멈춘다. 사소한 것 같지만, 실제 여러 상황에서 상당히 유용하다.

만약 디스크에 파일을 저장하기를 원한다고하자. 하지만 그 파일이 수정이 되었을 때만, 그리고 저장하고 싶을 때만 저장을 한다고 하자. 대략 다음과 같이 기술할 수 있을 것이다.

1
2
3
(if *file-modified* 
 (if (ask-user-about-saving) (save-file))
)
cs

여기서 ask-user-about-saving 함수는 사용자로 하여금 그 파일에 대해서 저장 여부를 확인한 후 사용자의 선택(T 또는 F)에 따라 해당되는 결과를 반환한다. 불린 연산을 통해 위의 코드를 다음과 같이 쓸수 있다.

1
(and *file-modified* (ask-user-about-saving) (save-file))
cs

또 다른 작성 방식은 다음과 같다. 경험많은 리스퍼들은 이 방식이 앞선 두 방식보다 더 분명하다고 얘기한다.

1
(if (and *file-modified* (ask-user-about-saving)) (save-file)))
cs


Using Functions That Return More than Just the Truth

True와 False에 대한 리스프만의 심플한 컨셉이 갖는 또 다른 장점에 대해 알아보자. 앞서 이미 다루었듯이 nil 을 제외한 나머지 값들은 모두 True 이다. 이 말은 조건문에 주로 사용되는 함수들은 T 이외의 값을 반환하는 옵션을 가지고 있다는 의미이다. 

예를 들면, member라는 명령은 아이템이 해당 리스트의 멤버인지 확인할 때 사용할 수 있다. 

1
2
3
4
[80]> (if (member 1 '(3 4 1 5))
'one-is-in-the-list
'one-is-not-in-the-list)
ONE-IS-IN-THE-LIST
cs

위의 결과는 꽤 분명해보인다. 하지만 아래와 같이 실행해보면 예상치 못했던 결과를 만날 수 있다.

1
2
[82]> (member 1 '(3 4 1 5))
(1 5)
cs

왜 (1 5)가 반환된 것일까? 리스퍼들이 true, false 를 반환하는 함수를 작성할 때마다, 스스로 다음과 같이 생각한다. 

“t 이외에 내가 반환할 수 있는 것이 있나?”

왜냐하면 모든 nil 아닌 값은 true로 평가되기 때문에, 또 다른 값을 반환하는 것은 분명히 덤이다. 

앞서 말했듯이 '(3 4 1 5) 라는 리스트는 (cons 3 (cons 4 (cons 1 (cons 5 nil)))) 와 동일하다. 이렇게 보면 왜 앞선 예에서 member 함수가 (cons 1 (cons 5 nil)) 를 반환하는지 분명해진다. 

그러면 왜 꼬리 대신에 발견한 값을 반환하는가? 같은 방식으로 원래 값을 다른 함수에 전달할 수 있으므로, member 함수를 정의할 때 효율적이기 때문이다. 하지만 아래의 독특한 예에서 이 계획은 무산된다. 

1
2
3
4
Break 1 [2]> (if (member nil '(3 4 nil 5))
                 'nil-is-in-the-list
                 'nil-is-not-in-the-list)
              NIL-IS-IN-THE-LIST
cs


위의 예에서 볼 수 있듯이, member 함수는 심지어 nil 을 멤버로 검색할 때조차 여전히 정답을 내고 있다. 만약 member 함수가 실제로 nil(즉, 다른 말로 우리가 찾고자 하는 원래값)을 반환한다면, false 로 평가를 할 것이고, 위의 예에서는 nil 이 리스트에 없다고 진술할 것이다. 하지만, member 함수는 아이템을 찾은 시점에 그 리스트의 꼬리를 반환할 것이기 때문에, true 로 평가받을 여지가 보장되는 셈이다. 

다음과 같이 rich return values 로부터 혜택을 보는 하나의 함수가 바로 find-if 이다. 다음을 보자. 

1
2
3
4
5
6
Break 2 [3]> (find-if #'oddp '(2 4 5 6))
5
Break 2 [3]> (if (find-if #'oddp '(2 4 5 6))
'there-is-an-odd-number
'there-is-no-odd-number)
THERE-IS-AN-ODD-NUMBER
cs

find-if 함수는 실제로 다른 함수를 포함한다. 위의 경우에는 oddp 라는 함수가 인수로 활용되었음을 볼 수 있다. 위의 예에서 find-if 는 oddp 가 true 를 반환하는 경우에 리스트의 첫 번째 값을 찾는다. 이 경우, 첫 번째 값이 존재한다면 그 값은 바로 홀수이다. 여러분은 분명히 find-if 가 어떻게 2가지 역할을 하는지 볼 수 있다. 하나는 특정 조건에 맞는 값을, 다른 하나는 조건 내에 포함된 true/false 값을 이끌어내준다.