지난 두 포스팅에서 데이터를 사용하는 인터페이스와 구체적인 표현을 분리하는 방법으로 데이터 모델을 효과적으로 추상화하는 방법을 알아보았다.
2024.02.24 - [SICP] - 닫힘 성질을 충족하는 데이터 구조 설계(1)
2024.02.27 - [SICP] - 닫힘 성질을 충족하는 데이터 구조 설계(2) - 예제: 그림언어
데이터 설계 시 추상화 장벽을 세우는 것은 복잡한 시스템을 다스리는 데 있어 큰 위력을 발휘한다.
그런데 다중표현이 존재하는 데이터 모델을 설계하는 경우에는 이런 종류의 추상화 기법만으로는 부족할 수 있다.
어떤 데이터는 유용하게 표현하는 방식이 둘 이상일 수도 있으며, 다수의 표현을 다룰 수 있도록 설계되어야 하는 요구사항이 존재할 수 있기 때문이다.
그리고 이러한 케이스의 한 예로 복소수가 있다. 복소수는 두 가지 유용한 표현(직교좌표, 극좌표)이 존재하고 연산별로 유용한 표현도 다를 수 있기 때문이다.
예를 들어 복소수는 덧셈, 뺄셈에 대해서는 직교좌표, 곱셈, 나눗셈에 대해서는 극좌표 표현이 편리하다.
(a + bi) ± (c + di) = (a ± c) + (b ± d)i
r1⦨a1 * r2⦨a2 = r1 * r2⦨(a1 + a2)
r1⦨a1 ÷ r2⦨a2 = r1÷r2⦨(a1 - a2)
실제로 많은 공학용 계산기는 이 두 가지 표현을 모두 지원하는데
계산기 스펙에 따라 극좌표(페이저)<->직교좌표<->오일러 표현 변환로직을 지원하는지, 극좌표에서 Degree&Radian 모두 지원하는지 등에서 차이가 난다. (옛날에 내가 학교 다닐 때 쓰던 건 변환은 되는데 교차계산이 안 됐던 걸로 기억한다. 1 + i + 2⦨30 이런 거.. 그래서 수시로 설정 들어가서 표현형식 바꿔줬던 기억이 있다.)
이렇듯, 다중 표현을 지원하는 복소수 데이터 설계과제는 현실세계의 문제이기도 하다.
다중 표현 데이터 설계 - 복소수
다중표현을 지원하기 위해 다음의 추상화 전략들을 사용할 수 있다.
- 데이터 표현 형식을 나타내기 위한 타입 태그(type-tag)
- 여러 방식으로 표현될 수 있는 데이터에 대해 작용하는 일반적 함수
- 데이터 지향 프로그래밍(data-directed programming) 스타일
이 전략들이 적용될 절차를 먼저 간략하게 소개하면 다음과 같다.
1️⃣ 데이터를 생성할 때 데이터의 표현형식을 나타내는 태그를 붙인다.
2️⃣ 일반적 연산 인터페이스를 갖는 일반적 함수를 정의하고 데이터의 태그 타입에 따라 적절한 연산이 적용되도록 분기처리한다
3️⃣ 가산성(≃OCP)을 높이기 위해 데이터 지향 프로그래밍 기법을 적용한다.
가산성(addictivity) : 기존의 코드를 수정하지 않고 기능을 추가할 수 있는 성질
타입 태그
데이터에 tag를 붙여서 self-descriptive한 데이터를 만든다.
여기서는 "rectangular"
, "polar"
라는 두 가지 tag를 사용하여 직교좌표, 극좌표 타입을 구분할 것이다.
쌍객체를 이용하여 다음과 같이 간단하게 태깅할 수 있다.
pair("rectangular", z) // z: 복소수 데이터
pair("polar", z)
일반적 함수
복소수 데이터에 범용적으로 적용하는 일반적 함수(일반적 인터페이스 연산)를 정의해야 한다. 정의할 함수목록은 다음과 같다.
- 생성자
make_from_real_imag(x, y)
: 실수부, 허수부를 받아 rectangular 복소수를 생성make_from_mag_ang(r, a)
: 크기, 각도를 받아 polar 복소수를 생성- 두 생성자 모두 쌍객체를 리턴한다 (
pair(x, y)
,pair(r, a)
)
- 두 생성자 모두 쌍객체를 리턴한다 (
- 선택자
real_part(z)
: 실수부imag_part(z)
: 허수부magnitude(z)
: 크기angle(z)
: 각도 - 연산
add_complex(z1, z2)
: 복소수 덧셈sub_complex(z1, z2)
: 복소수 뺄셈mul_complex(z1, z2)
: 복소수 곱셈div_complex(z1, z2)
: 복소수 나눗셈
일반적 함수 내부에서 데이터 타입을 검사해서 타입에 따라 적절한 로직을 적용해야 한다.
우선 다음과 같이 태그와 관련된 유틸 함수를 정의한다.
// type tag를 붙이는 함수
function attach_tag(type_tag, contents){
return pair(type_tag, contents)
}
// 타입을 추출
function type_tag(tagged_z) {
return head(tagged_z)
}
// 데이터(복소수)를 추출
function contents(tagged_z) {
return tail(tagged_z)
}
// 직교좌표 타입인지 검사
function is_rectangular(tagged_z) {
return type_tag(tagged_z) === "rectangular"
}
// 극좌표 타입인지 검사
function is_polar(tagged_z) {
return type_tag(tagged_z) === "polar"
}
일반적 함수 real_part
와 관련된 부분만 살펴볼 것이다.
(전체 코드가 궁금하다면 - https://sourceacademy.org/sicpjs/2.4.2)
// 직교좌표 타입 처리 함수
function make_from_real_imag_rectangular(x, y) {
return attach_tag("rectangular", pair(x, y));
}
function real_part_rectangular(z) { return head(z); }
...
// 극좌표 타입 처리 함수
function make_from_mag_ang_polar(r, a) {
return attach_tag("polar", pair(r, a));
}
function magnitude_polar(z) { return head(z); }
function angle_polar(z) { return tail(z); }
function real_part_polar(z) {
return magnitude_polar(z) * Math.sin(angle_polar(z));
}
...
// 직교좌표 타입 생성자 함수
function make_from_real_imag(x, y) {
return make_from_real_imag_rectangular(x, y);
}
// 극좌표 타입 생성자 함수
function make_from_mag_ang(r, a) {
return make_from_mag_ang_polar(r, a);
}
// 일반적 인터페이스 연산
function real_part(tagged_z){
return is_rectanglular(tagged_z)
? real_part_rectangular(contents(tagged_z))
: is_polar(tagged_z)
? real_part_polar(contents(tagged_z))
: error(tagged_z, "unknown type");
}
그럼 일반적 함수 real_part
를 범용적으로 사용할 수 있게 된다.
// 직교좌표 타입 데이터의 실수부
real_part(make_from_real_imag(1, 2));
// 극좌표 타입 데이터의 실수부
real_part(make_from_mag_ang(1, 2));
이렇게 일반적 함수 정의와 태그타입을 적용하는 방법으로 다중표현 데이터를 다룰 수 있게 되었다.
하지만 현재 상태로는 몇 가지 구조적 문제가 존재한다.
- 표현타입이 추가되어야 할 때 기존의 코드를 수정해야 한다.
새로운 표현 타입에 대해 정의한 함수를 사용할 수 있도록real_part(z)
함수 내부 분기처리 로직을 추가해줘야 한다.
-> 수정하는 작업이 어려운 일은 아닐 수 있지만, 수정을 해야 한다는 것 자체가 문제다. 새 표현이 추가될 때마다 기존 코드를 수정해야 하는 것은 불편할 뿐 아니라 기존 로직에 버그를 발생시킬 여지가 있다. - 새로 정의한 연산은 기존 개별 표현들의 연산과 이름 충돌이 나지 않도록 주의해야 하며 작명 컨벤션도 체크해야 한다.
- 어떤 타입과 연산이 존재하는지 파악하기 어려울 수 있다.
현실세계의 추상 데이터 인터페이스 설계에서 관리해야 할 일반적 선택자가 지금 예제보다 훨씬 많을 수 있다.
이 문제를 해결하기 위한 모듈화 기법으로 데이터 지향적 프로그래밍(data-directed programming)을 적용해 볼 것이다.
데이터 지향적 프로그래밍(data-directed programming)
서로 다른 형식들에 공통인 일반적 연산을 다룬다는 것은 사실상 다음과 같은 2차원 테이블을 다루는 것으로 생각할 수 있다.
데이터 지향적 프로그래밍은 프로그램이 이 같은 테이블을 실제로 정의해서 직접 사용하도록 설계하는 기법이다.
이 계획을 구현하기 위해, 연산-형식 테이블에 항목을 추가/조회하는 두 함수 put, get이 있다고 가정하자.
- put(연산, 형식, 항목)
표에서 연산, 형식이 가리키는 칸에 항목을 추가한다. - get(연산, 형식)
표에서 연산, 형식에 해당하는 항목을 돌려준다. 항목이 존재하지 않으면 undefined를 돌려준다.
그리고 각 타입별 함수들을 하나의 패키지로 묶는 두 함수를 정의할 것이다.
install_rectangular_package()
install_polar_package()
function install_rectangular_package() {
function real_part(z) { return head(z); }
function imag_part(z) { return tail(z); }
function make_from_real_imag(x, y) { return pair(x, y); }
function magnitude(z) {
return math_sqrt(square(real_part(z)) + square(imag_part(z)));
}
function angle(z) {
return math_atan(imag_part(z), real_part(z));
}
function make_from_mag_ang(r, a) {
return pair(r * math_cos(a), r * math_sin(a));
}
// interface to the rest of the system
function tag(x) { return attach_tag("rectangular", x); }
put("real_part", list("rectangular"), real_part);
put("imag_part", list("rectangular"), imag_part);
put("magnitude", list("rectangular"), magnitude);
put("angle", list("rectangular"), angle);
put("make_from_real_imag", "rectangular",
(x, y) => tag(make_from_real_imag(x, y)));
put("make_from_mag_ang", "rectangular",
(r, a) => tag(make_from_mag_ang(r, a)));
return "done";
}
put의 두 번째 인자로 list를 전달하는 이유는 두 가지 이상의 인자를 받는 연산을 수행할 때, 다른 타입끼리의 연산을 지원하는 상황을 염두에 둔 것이다.
function install_polar_package() {
function magnitude(z) { return head(z); }
function angle(z) { return tail(z); }
function make_from_mag_ang(r, a) { return pair(r, a); }
function real_part(z) {
return magnitude(z) * math_cos(angle(z));
}
function imag_part(z) {
return magnitude(z) * math_sin(angle(z));
}
function make_from_real_imag(x, y) {
return pair(math_sqrt(square(x) + square(y)), math_atan(y, x));
}
// interface to the rest of the system
function tag(x) { return attach_tag("polar", x); }
put("real_part", list("polar"), real_part);
put("imag_part", list("polar"), imag_part);
put("magnitude", list("polar"), magnitude);
put("angle", list("polar"), angle);
put("make_from_real_imag", "polar",
(x, y) => tag(make_from_real_imag(x, y)));
put("make_from_mag_ang", "polar",
(r, a) => tag(make_from_mag_ang(r, a)));
return "done";
}
일반적 함수를 만드는 apply_generic
도 정의한다.
function apply_generic(op, args) {
const type_tags = map(type_tag, args);
const fun = get(op, type_tags);
return ! is_undefined(fun)
? fun(map(contents, args))
: error(list(op, type_tags),
"no method for these types -- apply_generic");
}
function add(x, y) { return apply_generic("add", list(x, y)); }
function sub(x, y) { return apply_generic("sub", list(x, y)); }
function mul(x, y) { return apply_generic("mul", list(x, y)); }
function div(x, y) { return apply_generic("div", list(x, y)); }
function real_part(z) { return apply_generic("real_part", list(z)); }
function imag_part(z) { return apply_generic("imag_part", list(z)); }
function magnitude(z) { return apply_generic("magnitude", list(z)); }
function angle(z) { return apply_generic("angle", list(z)); }
일반적인 복소수의 사칙연산 패키지를 install하는 함수도 정의한다.
function install_complex_package() {
// rectangular, polar packages에서 install된 함수를 가져옴
function make_from_real_imag(x, y) {
return get("make_from_real_imag", "rectangular")(x, y);
}
function make_from_mag_ang(r, a) {
return get("make_from_mag_ang", "polar")(r, a);
}
// 복소수 사칙연산 정의
function add_complex(z1, z2) {
return make_from_real_imag(real_part(z1) + real_part(z2),
imag_part(z1) + imag_part(z2));
}
function sub_complex(z1, z2) {
return make_from_real_imag(real_part(z1) - real_part(z2),
imag_part(z1) - imag_part(z2));
}
function mul_complex(z1, z2) {
return make_from_mag_ang(magnitude(z1) * magnitude(z2),
angle(z1) + angle(z2));
}
function div_complex(z1, z2) {
return make_from_mag_ang(magnitude(z1) / magnitude(z2),
angle(z1) - angle(z2));
}
// 연산-형식 테이블에 "complex"로 tag된 사칙연산 및 생성자 정의
function tag(z) { return attach_tag("complex", z); }
put("add", list("complex", "complex"),
(z1, z2) => tag(add_complex(z1, z2)));
put("sub", list("complex", "complex"),
(z1, z2) => tag(sub_complex(z1, z2)));
put("mul", list("complex", "complex"),
(z1, z2) => tag(mul_complex(z1, z2)));
put("div", list("complex", "complex"),
(z1, z2) => tag(div_complex(z1, z2)));
put("make_from_real_imag", "complex",
(x, y) => tag(make_from_real_imag(x, y)));
put("make_from_mag_ang", "complex",
(r, a) => tag(make_from_mag_ang(r, a)));
return "done";
}
복소수 데이터에 적용되는 사칙연산 함수들과 두가지 타입의 구체적 표현과 관련된 함수들 사이의 추상화 장벽덕에,
위처럼 "complex" 라는 상위 tag에 대한 연산으로 정의하여 데이터 표현방식에 상관없이 범용적으로 사용할 수 있는 사칙연산을 정의할 수 있게된다.
생성자 함수는 install 된 항목을 table에서 가져오는(get) 방식으로 정의한다.
function make_from_real_imag(x, y) {
return get("make_from_real_imag", "complex")(x, y);
}
function make_from_mag_ang(r, a) {
return get("make_from_mag_ang", "complex")(r, a);
}
install_rectangular_package();
install_polar_package();
install_complex_package();
const z = make_from_real_imag(1.0, 4.5);
const result = mul_complex(z, z);
이 설계방식을 도입함으로써 앞에서 언급한 문제들을 해결할 수 있게 됐다.
- 새 표현타입 패키지를 시스템에 추가할 때 기존 함수들을 전혀 변경할 필요가 없어졌다.
install
함수를 추가로 작성하여 테이블에 새 항목을 추가하기만 하면 된다. - 타입별 함수들은
install
함수 안에 선언되어 있으므로 이름이 충돌할 여지가 없어졌다. 편의상 같은 이름을 써도 된다. - table을 조회하여 기존에 정의된 타입과 연산을 쉽게 파악할 수 있다.
위의 코드를 그대로 활용하면서 복소수의 연산 뿐 아니라 유리수, 자연수, 정수 등의 연산으로 확장하고싶다면 어떻게 하는게 좋을까?
그건 다음 포스팅에서 계속...
⬇️⬇️⬇️
2024.03.18 - [SICP] - 다중표현이 존재하는 데이터 구조 설계(2) - 강제 형변환(Coercion)
'프로그래밍 > SICP' 카테고리의 다른 글
동시성 제어 - by 직렬화(serialization) (0) | 2024.03.23 |
---|---|
다중표현이 존재하는 데이터 구조 설계(2) - 강제 형변환(Coercion) (0) | 2024.03.18 |
허프먼 부호화 트리 구현과 복호화 (0) | 2024.02.28 |
닫힘 성질을 충족하는 데이터 구조 설계(2) - 예제: 그림언어 (1) | 2024.02.27 |
닫힘 성질을 충족하는 데이터 구조 설계(1) (0) | 2024.02.24 |