Files
softwaredesign/wiki/concepts/generic-procedures.md
minsung f868de1ce7 feat: wiki/concepts/ 핵심 개념 페이지 8개 생성
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>
2026-04-30 14:55:04 +09:00

6.3 KiB

title, tags, sources, updated
title tags sources updated
제네릭 프로시저
concept
SDF
SDF-ch3-generic-procedures
SDF-ch5-evaluation
SDF-ch6-layering
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!가 이를 잘 보여준다.

구현의 세 축

  1. generic-procedure-constructor: 디스패치 전략을 인자로 받아 제네릭 프로시저 생성기를 반환. 디스패치 전략 자체가 교체 가능한 컴포넌트다.

  2. dispatch-store: 핸들러 저장·검색 전략을 캡슐화. 단순 선형 스캔, 트라이(trie) 인덱스, 캐시 래핑 등 다양한 구현을 교체 가능.

  3. 메타데이터 테이블: 제네릭 프로시저에 규칙 목록을 "스티키 노트"처럼 첨부. 런타임 확장의 기반.

폐쇄성(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의 격언처럼, 제네릭 프로시저는 많은 연산이 하나의 공통 인터페이스를 통해 다양한 타입에 적용되는 구조를 지향한다. 반대로 타입별로 별도의 함수 집합을 만드는 것은 제네릭 프로시저의 이점을 살리지 못한다.

관련 개념