1장에서는 프로시저로 복잡한 연산을 표현하고, 차수 높은 프로시저를 통해 복잡한 연산을 쉽게 이해할 수 있도록 만드는 방법에 대해 살펴보았다.
2장에서는 복잡한 데이터를 다루는데, 복잡한 데이터가 필요한 이유는 복잡한 프로시저를 설계하는 것과 같다. 연산 단계가 아니라 실제 문제 풀이의 눈높이에서 문제를 바라볼 수 있도록 데이터를 표현하는 방법을 알아보자.
정수 연산이 아닌 유리수 연산이 필요하다고 하자. 분모와 분자를 따로 나누어 연산하게 되면, 분모에 대응하는 분자를 기억해야할 뿐만 아니라, 계산 과정이 복잡해지면 프로세스가 뒤엉킬 가능성이 높다.
유리수 표현하기
다음과 같은 프로시저가 있다고 하자. make-rat n d (유리수 데이터 생성), numer rat(분자만 추출), denom rat(분모만 추출)
(define (add-rat a b)
(make-rat (+ (* (numer a) (denom b))
(* (numer b) (denom a)))
(* (denom a) (denom b))))
(define (mul-rat a b)
(make-rat (* (numer a) (numer b))
(* (denom a) (denom b))))
위와 같이 세 프로시저만 있으면 다양한 유리수 연산을 만들어낼 수 있다.
그러나, 정작 필요한 세가지 프로시저는 정의하지 않았다.
Pair
Lisp 에서는 pair 를 정의할 수 있는 데이터 구조가 존재한다.
(define pair (cons x y)) 와 같이 cons 를 사용하여 두 데이터를 묶어 새로운 이름을 할당할 수 있다.
또, car, cdr 을 사용해 첫 번째 인자, 두 번째 인자만 각각 추출할 수 있다.
따라서, 위 세가지 프로시저는 다음과 같이 정의할 수 있다.
(define (make-rat n d) (cons n d))
(define (numer x) (car x))
(define (denom x) (cdr x))
위 프로시저를 사용하여 연산하면 1/2 + 1/3 = 4/6 이라는 결과가 나온다. 정답이긴 하지만, 기약분수 형태로 표현하도록 앞서 만든 gcd를 사용해 make-rat를 수정해보자.
(define (make-rat n d)
(let ((g (gcd n d))
(cons (/ n g) (/ d g)))))
정의한 프로시저들을 사용하면 위 add-rat, mul-rat 를 활용하여 유리수 연산을 할 수 있다.
역할만 할당한 세가지 프로시저를 정의한 후에도 코드 수정없이 사용할 수 있다는 점을 기억해 두자.
위 예제를 다시 한 번 살펴보자. 사용하는 프로시저에 따라 문제를 다루는 수준이 다름을 알 수 있다.
add-rat, mul-rat 를 사용하여 문제의 영역에서 유리수를 다룰 수 있다.
make-rat, numer, denom 를 사용하여 분자와 분모의 묶음으로 유리수를 다룰 수 있다.
cons, car, cdr 을 사용하여 두 수를 묶은 쌍으로 유리수를 다룰 수 있다.
이렇게 수준을 나누면, 다양한 이점을 얻을 수 있는데, 그 중 하나는 프로그램을 관리하고 고치기가 쉽다는 것이다.
make-rat 을 gcd 를 사용하여 기약분수로 저장하는 것이 아니라, numer, denom 에서 꺼낼 때 약분한다고 해보자.
(define (make-rat n d) (cons n d))
(define (numer x)
(let ((g (gcd (car x) (cdr x))))
(/ (car x) g)))
(define (denom x)
(let ((g (gcd (car x) (cdr x))))
(/ (cdr x) g)))
위 프로시저를 사용한다면, 만들어지는 당시에는 gcd 연산을 하지 않기 때문에 조회가 적을 경우 연산에서 이점을 얻을 수 있다.
세 프로시저만 수정하면 데이터를 어떻게 연산하는지 변경할 수 있고, 윗단의 add-rat, mul-rat 와는 아무 연관이 없다.
이렇게 데이터 표현에 영향을 받는 것을 몇몇으로 제한하여 인터페이스로 사용하게 되면, 구현 방법을 갈아 끼울 수 있는 여유가 생긴다.
데이터를 요약하는 방법을 통해 구현 방법을 결정할 수 없더라도 시스템의 나머지 부분을 계속해서 만들 수 있다.