---
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]] — 아리티 첨부에서 시작된 레이어링 아이디어의 확장
