오상우
오상우

Categories

  • lisp
  • scheme
  • sicp

하나의 프로그램 속에서, 한 종류의 데이터가 여러 표현 방식을 가지게 되는 경우가 있다. 이런 경우에 한 프로그램 속에서 다른 표현 방식이 어떻게 잘 작동하게 하는지를 generic procedure, data-directed programming 등의 방법을 통해 실습해보자.

여기서는 복소수를 직각 좌표 표현(실수부 값, 허수부 값)과 극좌표 표현(길이, 각) 두 가지로 표현해서, 이를 가지고 프로그래밍 하는 방법론에 대해 공부하려고 한다.

추상화 원칙에 따라, 어떤 표현 방식을 사용하던지 같은 프로시저로 사칙연산을 구할 수 있어야 한다.

(define (plus-complex z1 z2)
  (make-from-real-imag (+ (real-part z1) (real-part z2))
                       (+ (imag-part z1) (imag-part z2))))

(define (sub-complex z1 z2)
  (make-from-real-imag (- (real-part z1) (real-part z2))
                       (- (imag-part z1) (imag-part z2))))

(define (mul-complex z1 z2)
  (make-from-mag-ang (* (magnitude z1) (magnitude z2))
                     (+ (angle z1) (angle z2))))

(define (div-complex z1 z2)
  (make-from-mag-ang (/ (magnitude z1) (magnitude z2))
                       (- (angle z1) (angle z2))))

위의 사칙연산 프로시저가 직각 좌표 방식이나 극좌표 방식으로 표현된 데이터 모두에 잘 작동할 수 있도록 각각의 프로시저를 구현해보자.
먼저 직각 좌표 방식의 구현이다.

(define (make-from-real-imag x y) (cons x y))

(define (make-from-mag-ang r a)
  (cons (* r (cos a)) (* r (sin a))))

(define (real-part z) (car z))

(define (imag-part z) (cdr z))

(define (magnitude z)
  (sqrt (+ (square (real-part z)) (square (imag-part z)))))

(define (angle z)
  (atan (imag-part z) (real-part z)))

실수부와 허수부를 구하는 것은 너무 쉽지만, 길이와 각을 구하는 것은 삼각 함수를 사용해야 한다.

이번에는 극좌표 방식으로 구현해보자.

(define (make-from-mag-ang r a) (cons r a))

(define (make-from-real-imag x y)
  (cons (sqrt (+ (square x) (square y)))
        (atan y x)))

(define (magnitude z) (car z))

(define (angle z) (cdr z))

(define (real-part z)
  (* (magnitude z) (cos (angle z))))

(define (imag-part z)
  (* (magnitude z) (sin (angle z))))

역시 길이와 각은 바로 알 수 있지만, 실수부와 허수부를 구하려면 삼각 함수를 사용해야 한다.
이제 사칙 연산 계산을 구현하려고 보니, 문제가 있다. 현재 plus-complex 입장에서 들어온 데이터가 직각 좌표 방식인지, 극좌표 방식인지 알 방법이 없다는 점이다.
이 문제를 해결하는 방법으로 데이터 안에 tag를 달아서 표현 방식을 알려주는 방법이 있다. 이 방식을 ‘dispatching on type’이라고 부른다.

Dispatching on type 방식

먼저 데이터에 태그를 붙이는 프로시저, 태그가 붙은 데이터에서 태그/데이터를 골라내는 프로시저를 작성한다.

(define (attach-tag type-tag contents)
  (cons type-tag contents))

(define (type-tag datum)
  (if (pair? datum)
      (car datum)
      (error "Bad tagged datum -- TYPE-TAG" datum)))

(define (contents datum)
  (if (pair? datum)
      (cdr datum)
      (error "Bad tagged datum -- CONTENTS" datum)))

이제 두 표현 방식의 constructor에 표현 방식에 해당되는 태그를 붙이도록 수정한다. 또한 두 표현 방식의 프로시저들이 이름이 겹치지 않도록 수정해준다.

; 직각 좌표 표현

(define (make-from-real-imag-rectangular x y)
  (attach-tag 'rectangular (cons x y)))

(define (make-from-mag-ang-rectangular r a)
  (attach-tag 'rectangular (cons (* r (cos a)) (* r (sin a)))))

(define (real-part-rectangular z) (car z))

(define (imag-part-rectangular z) (cdr z))

(define (magnitude-rectangular z)
  (sqrt (+ (square (real-part-rectangular z)) (square (imag-part-rectangular z)))))

(define (angle-rectangular z)
  (atan (imag-part-rectangular z) (real-part-rectangular z)))
; 극 좌표 표현

(define (make-from-mag-ang-polar r a)
  (attach-tag 'polar (cons r a)))

(define (make-from-real-imag-polar x y)
  (attach-tag 'polar
              (cons (sqrt (+ (square x) (square y)))
                    (atan y x))))

(define (magnitude-polar z) (car z))

(define (angle-polar z) (cdr z))

(define (real-part-polar z)
  (* (magnitude-polar z) (cos (angle-polar z))))

(define (imag-part-polar z)
  (* (magnitude-polar z) (sin (angle-polar z))))

이제 일반화된 연산에서 들어온 데이터의 태그를 보고, 알맞은 프로시저를 불러서 사용하면 된다.

; 일반화된 selector 연산
(define (real-part z)
  (cond ((rectangular? z) (real-part-rectangular (contents z)))
        ((polar? z) (real-part-polar (contents z)))
        (else (error "Unknown type!!!" z))))

(define (imag-part z)
  (cond ((rectangular? z) (imag-part-rectangular (contents z)))
        ((polar? z) (imag-part-polar (contents z)))
        (else (error "Unknown type!!!" z))))

(define (magnitude z)
  (cond ((rectangular? z) (magnitude-rectangular (contents z)))
        ((polar? z) (magnitude-polar (contents z)))
        (else (error "Unknown type!!!" z))))

(define (angle z)
  (cond ((rectangular? z) (angle-rectangular (contents z)))
        ((polar? z) (angle-polar (contents z)))
        (else (error "Unknown type!!!" z))))

마지막으로 constructor를 쓸 때, 직각 좌표 표현을 쓸지 극좌표 표현을 쓸 지 정해야 하는데, 실수부와 허수부를 알면 직각 좌표 표현을 쓰고 길이와 각을 알면 극좌표 표현을 쓰는 것이 자연스럽다.

(define (make-from-real-imag x y)
  (make-from-real-imag-rectangular x y))

(define (make-from-mag-ang r a)
  (make-from-mag-ang-polar r a))

이렇게 하면 사칙 연산부를 그대로 놔 두어도 아무 문제없이 동작한다. 이때 사칙연산부를 generic procedure로 볼 수 있다.

Data-Directed Programming

인터페이스 레이어 사이에서 데이터가 이동할 때, 타입을 붙였다 뗐다 하면서 데이터를 구분하는 위와 같은 방식에는 심각한 약점 두가지가 있다.

  1. 일반화된 인터페이스 프로시저(real-part, imag-part, magnitude, angle)을 구현하고자 할 때, 모든 표현 방식을 알아야 한다.
  2. 각 표현 방식에 구현된 프로시저 이름이 다 달라야 한다.

위의 약점들은 결국 additivity가 없는 방식으로 일반화 인터페이스를 구현했기 때문에 생긴 문제점이다. 지금은 그리 큰 문제가 아닌것 같아 보여도, 만약 데이터 타입이 수십가지가 넘어간다면 유지보수에 심각한 문제점이 될 것이다.

이런 문제점을 해결하기 위해 등장한 방식이 ‘데이터 중심 프로그래밍(Data-directed Programming)’이다. 위의 구현 방식은 결국 연산/타입으로 이루어진 실제 프로시저들의 표로 나타낼 수 있는데, 데이터 중심 프로그래밍이란 이런 표를 바탕으로 연산/타입을 기준으로 알맞은 실제 프로시저를 돌려주는 하나의 프로시저로 구현하는 방식이다.
표에 실제 프로시저를 집어넣는 put과 연산/타입을 받아 알맞은 프로시저를 돌려주는 get 프로시저가 있다고 가정하자. 직각 좌표 표현방식은 다음과 같이 구현할 수 있다.

(define (install-rectangular-package)
  ; 로컬 프로시저
  (define (make-from-real-imag x y) (cons x y))
  (define (make-from-mag-ang r a)
    (cons (* r (cos a)) (* r (sin a))))
  (define (real-part z) (car z))
  (define (imag-part z) (cdr z))
  (define (magnitude z)
    (sqrt (+ (square (real-part z)) (square (imag-part z)))))
  (define (angle z)
    (atan (imag-part z) (real-part z)))
  
  ; 인터페이스 표에 삽입
  (define (tag x) (attach-tag 'rectangular x))
  (put 'real-part '(rectangular) real-part)
  (put 'imag-part '(rectangular) imag-part)
  (put 'magnitude '(rectangular) magnitude)
  (put 'angle '(rectangular) angle)
  (put 'make-from-real-imag 'rectangular
       (lambda (x y) (tag (make-from-real-imag x y))))
  (put 'make-from-mag-ang 'rectangular
       (lambda (r a) (tag (make-from-mag-ang r a))))
  'done)

위와 같이 구현하고 install-rectangular-package 프로시저를 실행히주면 표에 위에서 구현한 프로시저들이 모두 들어간다. 또한 실제 프로시저들의 이름은 위 정의부 안에서만 유효하므로 이름을 다르게 할 필요가 없다.

(define (install-polar-package)
  ; 로컬 프로시저
  (define (make-from-mag-ang r a) (cons r a))
  (define (make-from-real-imag x y)
    (cons (sqrt (+ (square x) (square y)))
          (atan y x)))
  (define (magnitude z) (car z))
  (define (angle z) (cdr z))
  (define (real-part z)
    (* (magnitude z) (cos (angle z))))
  (define (imag-part z)
    (* (magnitude z) (sin (angle z))))
  
  ; 인터페이스 표에 삽입
  (define (tag x) (attach-tag 'polar x))
  (put 'real-part '(polar) real-part)
  (put 'imag-part '(polar) imag-part)
  (put 'magnitude '(polar) magnitude)
  (put 'angle '(polar) angle)
  (put 'make-from-real-imag 'polar
       (lambda (x y) (tag (make-from-real-imag x y))))
  (put 'make-from-mag-ang 'polar
       (lambda (r a) (tag (make-from-mag-ang r a))))
  'done)

이제 generic selector 연산을 정의하면 된다. dispatching on type 방식의 단점이 모두 보완되었음을 확인하며 아래 코드를 보자.

(define (apply-generic op . args)
  (let ((type-tags (map type-tag args)))
    (let ((proc (get op type-tags)))
      (if proc
          (apply proc (map contents args))
          (error "no method for these types!!!" (list op type-tags))))))

(define (real-part z) (apply-generic 'real-part z))
(define (imag-part z) (apply-generic 'imag-part z))
(define (magnitude z) (apply-generic 'magnitude z))
(define (angle z) (apply-generic 'angle z))

이제 시스템에 새로운 연산/타입을 추가하더라도 이름을 다르게 할 필요가 없고, 기존에 존재하는 프로시저를 수정할 필요도 없다.
마지막으로 전역에서 새로운 복소수 데이터를 만들 수 있도록 다음과 같은 프로시저를 만든다.

(define (make-from-real-imag x y)
  ((get 'make-from-real-imag 'rectangular) x y))

(define (make-from-mag-ang r a)
  ((get 'make-from-mag-ang 'polar) r a))

메세지 패싱

위의 방법은 ‘똑똑한 연산’이 데이터 타입에 따라 일을 나누어 맡기는 방식이다. 위와는 달리 ‘똑똑한 데이터’ 자체로 연산에 따라 다른 일들 처리하는 방법도 있다. 이 경우에는 데이터 인스턴스가 단순 데이터가 아니라, 프로시저로 구현되어야 하겠다.
예컨대 make-from-real-imag 프로시저는 다음과 같이 정의된다.

(define (make-from-real-imag x y)
  (define (dispatch op)
    (cond ((eq? op 'real-part) x)
          ((eq? op 'imag-part) y)
          ((eq? op 'magnitude) (sqrt (+ (square x) (square y))))
          ((eq? op 'angle) (atan y x))
          (else (error "unknown op!!!" op))))
  dispatch)

실제 make-from-real-imag으로 생성되는 인스턴스는 dispatch 프로시저이다. 이 프로시저는 x y값에 대한 클로져를 가지고 있다.
이제 generic-apply와 generic selector 구현을 보자.

(define (apply-generic op arg) (arg op)) ; 인자가 여러개인 경우 구현할수 없다는 단점 존재

(define (real-part z) (apply-generic 'real-part z))
(define (imag-part z) (apply-generic 'imag-part z))
(define (magnitude z) (apply-generic 'magnitude z))
(define (angle z) (apply-generic 'angle z))

데이터 안에 자기 타입에 대한 연산이 구현되어 있으므로 generic selector와 apply 부분 코드가 몹시 간단함을 알 수 있다.
프로시저를 데이터로 사용하기 때문에 가능한 방법인 것 같다.