--- title: 컴비네이터 tags: [concept, SDF] sources: [SDF-ch2-dsl, SDF-ch3-generic-procedures, SDF-ch4-pattern-matching, SDF-ch5-evaluation] updated: 2026-04-30 --- # 컴비네이터 ## 한 줄 정의 원시 부품(primitives)과 조합 수단(combinators)으로 이루어진 언어 체계에서, 조합의 인터페이스 명세가 원시 부품의 인터페이스 명세와 동일하도록 설계된 부품 조합기. ## 핵심 내용 ### 컴비네이터 시스템의 정의와 핵심 성질 SDF의 정의: > "A *system of combinators* is a set of primitive parts and a set of means of combining parts such that the interface specifications of the combinations are the same as those of the primitives." 이 정의에서 핵심은 **인터페이스 동일성**이다. 조합의 결과가 원시 부품과 동일한 인터페이스를 가지므로, 재귀적 조합이 무제한으로 가능하다. 어떤 조합도 법적으로 유효한(legally correct) 프로그램을 만들어낸다. 네 가지 핵심 성질: 1. **믹스앤매치**: 임의의 부품 조합이 가능 2. **법적 정확성**: 모든 조합이 타입 오류 없이 유효 3. **컨텍스트 독립성**: 부품의 동작이 조합 맥락에 영향받지 않음 4. **비간섭성**: 새 부품·컴비네이터 추가가 기존 프로그램에 영향 없음 ### 기본 함수 컴비네이터 ```scheme ;; compose: g 다음 f (f ∘ g) (define (compose f g) (define (the-composition . args) (call-with-values (lambda () (apply g args)) f)) (restrict-arity the-composition (get-arity g))) ;; parallel-combine: f와 g를 같은 인수에 적용, 결과를 h에 전달 (define (parallel-combine h f g) (define (the-combination . args) (h (apply f args) (apply g args))) the-combination) ;; spread-combine: 인수를 두 그룹으로 나눠 f와 g에 각각 적용 (define (spread-combine h f g) (compose h (spread-apply f g))) ``` **아리티(arity) 관리**: 컴비네이터가 올바르게 작동하려면 각 함수의 인수 개수를 추적해야 한다. `restrict-arity`와 `get-arity`로 함수에 아리티를 메타데이터로 첨부한다(해시 테이블). 이는 레이어링 아이디어의 초기 형태이기도 하다. ### 컴비네이터로 DSL 만들기: 정규표현식 사례 SDF Ch2는 정규 표현식을 나쁜 설계의 예시로 들고, 컴비네이터 기반 대안을 제시한다: ```scheme ;; 나쁜 예: 정규표현식 문자열 "^[a-z].*[0-9]$" ; 컨텍스트에 따라 의미가 달라지고 조합이 어렵다 ;; 좋은 예: 컴비네이터 기반 DSL (r:seq (r:bol) (r:char-from "abcdefghijklmnopqrstuvwxyz") (r:repeat 0 #f (r:dot)) (r:char-from "0123456789") (r:eol)) ``` 컴비네이터 기반 DSL은 각 부품이 독립적이고, 조합이 안전하며, 의미가 맥락에 무관하다. ### 컴비네이터가 채택되는 영역들 SDF 전체에서 컴비네이터 패턴이 반복 등장한다: - **Ch2**: 함수 컴비네이터(`compose`, `parallel-combine`), 정규표현식 컴비네이터 - **Ch3**: 산술 패키지 컴비네이터(`extend-arithmetic`, `add-arithmetics`) - **Ch4**: 매처 컴비네이터 — 패턴이 매처들의 컴비네이터 조합으로 컴파일됨 - **Ch5**: 실행 프로시저들이 컴비네이터 시스템을 이룸 (분석/실행 분리 단계) ### 컴비네이터의 한계: 다이아몬드와 진흙 > "Systems built by combinators result in beautiful diamond-like systems. This is sometimes the right idea... but it is very hard to add to a diamond." 컴비네이터 시스템이 완성된 이후 확장하려면, 기존 컴비네이터 구조 *밖*에서 새 부품을 만들어 합치는 방법이 필요하다. 이것이 Ch3에서 제네릭 프로시저로 이행하는 이유다. 컴비네이터의 폐쇄성(closure)이 장점이자 단점이다. 조합이 원시 부품과 같은 타입이라는 성질이 무한 조합을 가능하게 하지만, 그 타입 체계 바깥으로 나가기 어렵게 만든다. 아키텍트 관점에서: 컴비네이터는 **설계 공간이 안정적이고 미리 알려진 도메인**에 적합하다. 요구사항이 변동적이거나 타입이 동적으로 추가되어야 하면 제네릭 프로시저가 낫다. ## SDF에서의 등장 - [[SDF-ch2-dsl]]: 핵심 챕터. 함수 컴비네이터(`compose`, `parallel-combine`, `spread-combine`) 구현, 아리티 관리, 정규표현식 DSL, 단위 래퍼, 체커 도메인 모델 - [[SDF-ch3-generic-procedures]]: 산술 패키지를 컴비네이터로 결합하는 시도와 그 한계 분석. 컴비네이터에서 제네릭 프로시저로의 이행 동기 설명 - [[SDF-ch4-pattern-matching]]: 매처들이 컴비네이터 방식으로 구성됨. 패턴 컴파일 = 매처 컴비네이터 조합 - [[SDF-ch5-evaluation]]: 실행 프로시저(execution procedures)들이 CPS 컴비네이터 시스템을 구성함 ## 실천 시 주의점 **아리티 추적의 부담**: 함수가 몇 개의 인수를 받는지를 런타임에 추적해야 컴비네이터가 올바르게 작동한다. 언어가 이를 지원하지 않으면 별도의 메타데이터 관리가 필요하다. **에러 메시지 품질**: 잘못된 조합에서의 에러가 어느 부품에서 발생했는지 추적하기 어렵다. 조합이 깊어질수록 스택 트레이스가 의미없어진다. **컴비네이터 수의 폭발**: 모든 유용한 조합을 컴비네이터로 제공하려다 보면 컴비네이터 수가 폭발적으로 늘어날 수 있다. 최소한의 직교 컴비네이터 집합을 설계하는 것이 중요하다. **맥락 독립성 보장**: 컴비네이터의 핵심 성질인 컨텍스트 독립성을 실제로 보장하기 위해서는 부품들이 부수 효과(side effect)나 전역 상태에 의존하지 않아야 한다. ## 관련 개념 - [[additive-programming]] — 컴비네이터는 가산적 프로그래밍의 기반 도구 - [[generic-procedures]] — 컴비네이터의 한계를 넘어서는 동적 확장 메커니즘 - [[domain-specific-language]] — 컴비네이터 시스템이 DSL의 구조적 기반 - [[layered-data]] — 아리티 첨부에서 시작된 레이어링 아이디어의 확장