SDF 챕터들에서 추출한 핵심 개념: - additive-programming: 가산적 프로그래밍 (전체 관통 테마) - generic-procedures: 제네릭 프로시저 (Ch3, Ch5, Ch6) - combinators: 컴비네이터 (Ch2~Ch5) - partial-information: 부분 정보 (Ch1, Ch4, Ch6, Ch7) - degeneracy: 퇴화성 (Ch1, Ch7) - layered-data: 레이어드 데이터 + 의존성 추적 (Ch2, Ch3, Ch6, Ch7) - propagation: 전파 모델 (Ch1, Ch5, Ch6, Ch7) - domain-specific-language: DSL (Ch2~Ch5) wiki/index.md Concepts 섹션 등록, wiki/log.md 기록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.3 KiB
title, tags, sources, updated
| title | tags | sources | updated | |||||
|---|---|---|---|---|---|---|---|---|
| 제네릭 프로시저 |
|
|
2026-04-30 |
제네릭 프로시저
한 줄 정의
인수의 타입(혹은 임의의 술어 조합)에 따라 적절한 핸들러를 동적으로 선택·호출하며, 기존 구조를 수정하지 않고 새 타입 핸들러를 추가함으로써 가산적으로 확장 가능한 프로시저.
핵심 내용
컴비네이터에서 제네릭 프로시저로의 이행
Ch2의 컴비네이터는 아름다운 폐쇄 구조를 만들지만, 완성된 "다이아몬드"에 새 면을 추가하기는 어렵다:
"Systems built by combinators result in beautiful diamond-like systems... but it is very hard to add to a diamond. If a system is built as a ball of mud, it is easy to add more mud."
제네릭 프로시저는 이 균형점을 다르게 설정한다. 구조 자체는 개방적(open)이고, 핸들러는 언제든지 추가 가능하다. "진흙 덩어리"가 아니라 핵심 디스패치 인프라는 단단하되 확장 지점은 열려있는 구조다.
술어-디스패치 메커니즘
SDF의 제네릭 프로시저는 술어-디스패치(predicate dispatch) 방식을 사용한다. 각 핸들러는 적용 가능성 명세(applicability specification), 즉 인수 각각에 대한 술어 리스트와 함께 등록된다:
;; 기본 수치 연산
(define-generic-procedure-handler plus
(all-args 2 number?)
(lambda (a b) (+ a b)))
;; 기호 산술 확장
(define-generic-procedure-handler plus
(any-arg 2 symbolic? number?)
(lambda (a b) (list '+ a b)))
단일 디스패치(OOP의 메서드)가 수신자 하나의 타입만 보는 것과 달리, 이 방식은 모든 인수의 조합에 따라 핸들러를 결정한다. 어드벤처 게임 예시(Ch3.5)에서 4인수 generic-move!가 이를 잘 보여준다.
구현의 세 축
-
generic-procedure-constructor: 디스패치 전략을 인자로 받아 제네릭 프로시저 생성기를 반환. 디스패치 전략 자체가 교체 가능한 컴포넌트다.
-
dispatch-store: 핸들러 저장·검색 전략을 캡슐화. 단순 선형 스캔, 트라이(trie) 인덱스, 캐시 래핑 등 다양한 구현을 교체 가능.
-
메타데이터 테이블: 제네릭 프로시저에 규칙 목록을 "스티키 노트"처럼 첨부. 런타임 확장의 기반.
폐쇄성(Closure): 제네릭 산술의 자기 참조
제네릭 프로시저의 폐쇄성 속성을 제대로 활용하면, 확장들이 자기 자신을 기반으로 구축될 수 있다:
(let ((g (make-generic-arithmetic make-simple-dispatch-store)))
(add-to-generic-arithmetic! g numeric-arithmetic)
(extend-generic-arithmetic! g symbolic-extender)
(extend-generic-arithmetic! g function-extender)
(install-arithmetic! g))
이 구조에서 function-extender가 추가하는 함수 산술은 기호 산술을 포함한 전체 제네릭 산술을 재귀적으로 사용한다. 컴비네이터 방식에서는 이런 자기 참조 구조를 만들기 어렵다.
자동 미분: 제네릭 프로시저의 킬러 애플리케이션
Ch3.3의 자동 미분은 제네릭 프로시저의 힘을 가장 인상적으로 보여준다. 미분 객체 [x, δx]를 새 타입으로 도입하고, 기존 산술 연산들에 이 타입을 처리하는 핸들러만 추가하면 연쇄 법칙이 자동으로 적용된다. 기존 수치·기호 산술 코드는 한 줄도 수정하지 않는다.
;; sqrt 연산에 미분 핸들러 추가
(define diff:sqrt
(diff:unary-proc sqrt (lambda (x) (/ 1 (* 2 (sqrt x))))))
이것이 가산적 프로그래밍의 이상적인 형태다.
효율적 디스패치
핸들러가 많아지면 단순 선형 스캔은 느려진다. 두 가지 최적화:
- 트라이(Trie) 인덱스: 인수 시퀀스를 트라이로 인덱싱하여 첫 번째 인수 타입으로 후보 집합을 좁힘
- 캐시 래핑: 인수 타입 태그 조합을 키로 이전 디스패치 결과를 캐시.
cache-wrapped-dispatch-store로 임의의 디스패치 전략에 투명하게 추가 가능
SDF에서의 등장
- SDF-ch3-generic-procedures: 핵심 챕터. 술어-디스패치 제네릭 프로시저 전 구현, 자동 미분, 트라이 디스패치, 추상 술어, 어드벤처 게임 예시
- SDF-ch5-evaluation:
g:eval이 제네릭 프로시저로 구현됨. 새 표현식 타입을 핸들러 추가만으로 인터프리터에 도입 - SDF-ch6-layering: 레이어드 프로시저는 제네릭 프로시저를 기반으로 구현됨. "레이어드 데이텀의 decoration은 제네릭 연산을 지원하는 태깅의 일반화"
실천 시 주의점
규칙 충돌과 순서 의존성: 적용 가능성이 겹치는 두 핸들러가 있을 때, 어느 것이 선택되는지는 추가 순서에 의존할 수 있다. 이 암묵적 의존성은 시스템이 커질수록 관리하기 어려워진다. 의식적으로 술어들 사이의 포함 관계(set-predicate<=!)를 명시적으로 선언해 우선순위를 명확히 해야 한다.
디스패치 비용: 모든 호출에서 인수 타입을 동적으로 확인하는 비용이 발생한다. 캐싱으로 많이 줄일 수 있지만, 성능이 중요한 내부 루프에는 직접 호출을 고려해야 한다.
디버깅 어려움: 어떤 핸들러가 선택됐는지 추적하기 어렵다. 디스패치 로직을 투명하게 볼 수 있는 디버깅 도구 없이는 버그 원인 파악이 힘들다.
"100 functions on 1 data structure": Alan Perlis의 격언처럼, 제네릭 프로시저는 많은 연산이 하나의 공통 인터페이스를 통해 다양한 타입에 적용되는 구조를 지향한다. 반대로 타입별로 별도의 함수 집합을 만드는 것은 제네릭 프로시저의 이점을 살리지 못한다.
관련 개념
- additive-programming — 핸들러 추가가 가산적 확장의 핵심 메커니즘
- combinators — 컴비네이터의 한계를 극복하는 상보적 접근
- layered-data — 제네릭 프로시저 위에 구축된 레이어링
- domain-specific-language — g:eval을 제네릭 프로시저로 만들어 DSL 확장성 확보
- partial-information — 추상 술어를 통한 타입 태깅이 부분 정보 처리와 연결