Bluefin을 효과 시스템이자 능력 시스템으로 바라보며, 능력과 효과의 관계 및 대규모 소프트웨어 설계에서의 의미를 살펴봅니다.
– Tom Ellis, 2026년 9월
Bluefin은 Haskell 세계에서 “효과 시스템”이라고 부르는 것입니다. 저는 2024년 초에 Andrzej Rybczak의 effectful를 매우 가깝게 바탕으로 한 첫 버전을 출시했습니다. effectful과 마찬가지로 Bluefin은 “IO 래퍼”(또는 “분석적”) 효과 시스템입니다. 즉, Haskell의 IO 타입을 가볍게 감싼 래퍼입니다1. effectful과 Bluefin의 주요 차이는 effectful에서는 효과의 존재를 타입 시스템의 제약 조건으로 나타낸다는 점(익숙하고 전통적인 접근법)인 반면, Bluefin에서는 효과의 존재를 값 수준 인수로 나타낸다는 점입니다.
컴퓨터 과학 문헌에는 수십 년 전부터 “능력 시스템”이라는 개념이 존재합니다. 이제 2026년 말인 지금, 저는 Bluefin을 우선 “능력 시스템”으로, 그다음 “효과 시스템”으로 부르기로 했습니다. 다시 말해 “전통적인 Haskell 효과 시스템의 이점을 얻는 데 사용할 수 있는 능력 시스템” 같은 것입니다. 왜일까요? 능력 시스템이 무엇인지 살펴보고, 효과 시스템이라는 개념을 더 면밀히 검토한 뒤, 무엇이 Bluefin을 가장 잘 설명하는지 알아봅시다.
_능력 시스템_은 “능력”을 다루기 위한 프로그래밍 언어 프레임워크입니다. 여기서 _능력_이란 프로그램 또는 시스템 자원을 참조하며, 더 나아가 그 자원에 접근할 권한을 부여하는 프로그램 개체입니다.
능력 시스템에서 각각의 능력은 주체에서 자원으로 향한다. 따라서 모든 능력은 접근할 자원이 무엇인지를 지정하는 용도와 그 접근을 수행할 권한을 제공하는 용도를 모두 겸할 수 있다.
– Capability Myths Demolished; Miller, Yee, Shapiro
대부분의 프로그래머가 접해 보았을 능력의 예는 파일 핸들입니다. Lampson의 표현에 따르면2:
운영 체제에서 친숙한 예는 열린 파일의 파일 기술자 또는 파일 핸들이다. 프로세스가 파일을 열면, 운영 체제는 [프로세스가 해당 파일에 접근할 충분한 권한이 있는지 확인한 후] 열린 파일을 위한 핸들을 생성한다.
능력 개념은 본래 운영 체제의 접근 제어와 보호를 위한 모델로서 보안 목적으로 도입되었습니다3. 능력 기반 커널의 예로는 seL4가 있습니다(흥미롭게도 Haskell로 작성된 실행 가능한 명세가 있습니다!). se4L에서 한 프로세스는 “사서함”(“종단점”이라 부름)에 메시지를 보낼 능력을 가질 수 있고, 다른 프로세스는 같은 사서함에서 메시지를 _수신_할 능력을 가질 수 있습니다. 이것이 두 프로세스 사이의 프로세스 간 통신(IPC)을 가능하게 합니다. 자세한 내용은 se4L FAQ를 참고하세요.
능력은 프로그래밍 언어에서도 사용됩니다(예: Pony). 이 경우 흔히 “객체 능력”이라고 부르며, 운영 체제(OS)의 개체보다는 언어의 런타임 시스템(RTS)에 있는 개체에 대한 접근을 허가합니다. 다만 RTS가 일반적으로 OS에 대한 접근을 제공하므로 그 경계는 모호합니다. Pony에서 능력의 예는 새 프로세스를 시작할 수 있는 능력인 StartProcessAuth입니다. Pony 프로그램은 새 프로세스를 시작하기 위해 StartProcess 함수를 호출할 수 있지만, 그 호출에 전달할 수 있는 StartProcessAuth 능력을 가진 경우에만 가능합니다. 자세한 내용은 Pony Process 패키지 문서를 참고하세요.
Bluefin을 작성한 뒤 저는 능력을 좁은 의미의 보안 맥락에서만 볼 이유가 없다는 것을 깨달았습니다. 효과 시스템도 능력 시스템으로 간주할 수 있습니다. 하지만 이것이 무엇을 의미하는지 이해하려면 “효과 시스템”이란 말의 의미를 더 자세히 살펴봐야 합니다.
Haskell에서 “효과 시스템”이라는 개념은 정확하게 정의되어 있지 않습니다. 그러나 이 용어의 모든 용례에는 중요한 공통점이 있습니다. 효과 시스템은 프로그래머가 프로그램 구성 요소의 외부에서 관찰 가능한 동작 범위를 제한할 수 있게 해 줍니다.
좁게 해석하면 “효과 시스템”은 extensible-effects4 스타일의 라이브러리를 의미할 수 있습니다. 여기서는 모든 효과가 특정 모나드(예를 들어 effectful의 “Eff”)에서 발생하며, 효과의 가능성은 제약 조건, 즉 타입 수준에서 표시됩니다. 예를 들어 다음 함수의 동작은 데이터베이스와 상호작용하거나 String 예외를 던지는 것만 가능하도록 제한됩니다(effectful의 타입을 사용):
operationEffectful ::
(Database :> es, Error String :> es) =>
Eff es ()
이러한 좁은 해석을 약간 확장하면 Bluefin은 효과 시스템입니다. Bluefin에서는 효과의 가능성이 값 수준 인수로 표시된다는 차이가 있습니다. 예를 들면 다음과 같습니다.
operationBluefin ::
Database es ->
Throw String es ->
Eff es ()
정의를 조금 더 넓히면 MTL도 효과 시스템으로 볼 수 있습니다.
operationMTL ::
(MonadDatabase m, MonadError String m) =>
m ()
또는 transformers까지도 가능합니다.
operationTransformers ::
DatabaseT (Except String) ()
아주 넓은 정의라면 ST(가변 상태 효과만 허용)와 심지어 IO(가변 상태, 예외 및 RTS와의 온갖 상호작용을 허용)도 효과 시스템으로 볼 수 있습니다.
(여기서 효과 시스템을 제시한 순서는 역사적으로 개발된 순서의 거의 _반대_입니다. 자세한 내용은 제 발표 “A History of Effect Systems”를 참고하세요.)
효과 시스템이 유용한 이유는 프로그래머가 효과를 수행하는 프로그램 구성 요소를 정의할 수 있기 때문이 아니라는 점을 강조하는 것이 중요합니다. 오히려 효과 시스템은 프로그래머가 지정된 집합 밖의 효과를 수행하는 것이 금지된 프로그램 구성 요소를 정의할 수 있게 해 주기 때문에 유용합니다.
극단적으로는 외부에서 관찰 가능한 모든 효과를 금지할 수 있으며, 그에 따라 타입 시스템을 이용해 연산이 때때로 “순수 함수” 또는 “수학 함수”라고 불리는 것이 되도록 강제할 수 있습니다. String을 받아 Int를 반환하고 외부에서 관찰 가능한 효과를 수행하지 않는 연산의 타입은 서로 다른 효과 시스템에서 다음과 같을 수 있습니다.
computeEffectful :: String -> Eff es Int
computeBluefin :: String -> Eff es Int
computeMTL :: Monad m => String -> m Int
computeTransformers :: String -> Identity Int
ST와 IO에서는 아예 사용하지 않는 것 외에는 외부에서 관찰 가능한 효과를 제한할 방법이 없으므로, 다음으로 만족할 수 있습니다(여기에는 효과 시스템이 전혀 없습니다).
computeFunction :: String -> Int
(실행 시간을 소모하거나 할당을 수행하는 것처럼 그러한 연산도 외부에서 관찰 가능한 효과를 _가진다_고 주장할 수 있습니다. 이는 “효과”를 유용한 방식으로 정의하기가 꽤 어렵다는 점을 시사합니다.)
능력 시스템에는 그 연산을 수행할 권한을 제공하는 능력에 접근할 수 있을 때에만 수행할 수 있는 연산들이 있습니다. 위에서 파일 핸들의 예를 보았습니다. 열린 파일의 핸들에 접근할 수 있을 때에만 그 파일에 쓸 수 있습니다. 마찬가지로 효과 시스템에는 대응하는 효과가 어떤 방식으로든 “범위 안에” 있을 때에만 수행할 수 있는 효과적 연산들이 있습니다. 예를 들어 어떤 종류의 오류, 예외 또는 던지기 효과가 범위 안에 있을 때에만 예외를 던질 수 있습니다. (extensible-effects 및 그 후속 시스템과 MTL에서 “범위 안에” 있다는 것은 “타입 수준에 있다”는 뜻이고, Bluefin에서는 “값 수준에 있다”는 뜻이며, transformers에서는 “모나드 변환자가 전달한다”는 뜻입니다.) 그렇다면 능력 시스템과 효과 시스템의 _차이_는 무엇일까요? 제 생각에는 차이가 없습니다!
가능한 대응 관계를 조금 더 자세히 살펴봅시다. 모든 능력을 효과로 다룰 수 있을까요? 위에서 능력 시스템을 정의할 때 파일에 접근할 권한은 흔히 “파일 기술자” 또는 “파일 핸들”이라 부르는 능력이 제공한다고 언급했습니다. 마찬가지로 “데이터베이스에 접근할 권한”을 가지고 있다면 그것은 능력으로 간주될 것입니다. 그러한 능력은 일반적으로 “연결 핸들”을 통해 제공됩니다. 위의 operation* 예시는 서로 다른 Haskell 효과 시스템이 그러한 능력을 어떻게 인코딩하는지 보여 줍니다. Haskell 효과 시스템은 이미 특정 프로그램 구성 요소를 사용하는 데 필요한 프로그램 자원을 암묵적 또는 명시적으로 전달하는 능력을 갖추고 있습니다.
그 반대는 어떨까요? 모든 효과를 능력으로 다룰 수 있을까요? 제 생각에는 그렇습니다. 특정 효과적 연산을 수행하려면 무언가가 “범위 안에” 있어야 합니다. 그것이 무엇이든 _그 효과를 수행할 능력_으로 해석할 수 있습니다.
능력의 관점에서 해석한 효과의 몇 가지 예를 살펴보겠습니다. 먼저 상태입니다. 상태 효과는 주어진 타입의 가변 값과 상호작용할 권한을 부여하는 능력으로 해석할 수 있습니다. 특히 STRef, IORef, Modify가 각각 전통적으로 “가변 참조”라 부르는 것을 값 수준에서 제공하는 ST, IO, Bluefin의 경우를 보는 것이 특히 흥미롭습니다. “가변 참조”는 앞서 본 능력의 정의(“프로그램 또는 시스템 자원을 참조하며, 더 나아가 그 자원에 접근할 권한을 부여하는 프로그램 개체”)와 정확히 일치합니다. 가변 참조는 어떤 가변 메모리 영역을 참조하는 포인터이며, 그 영역에 “접근 권한을 부여”합니다. 즉, 그러한 가변 참조를 가지고 있다면 이를 사용하여 가변 메모리 영역을 수정할 수 있습니다.
둘째로 예외를 살펴봅시다. 예외 효과는 특정 타입의 값을 특정 처리자에게 던질 권한을 부여하는 능력으로 해석할 수 있습니다. 다시 말해 Bluefin의 경우가 특히 흥미롭다고 생각합니다. Throw 타입의 값은 예외 메커니즘에 접근할 권한을 부여하여, try(또는 catch나 handle)가 설치한 처리자, 즉 Throw를 범위 안으로 들여온 바로 그 위치의 처리자에게 예외를 던질 수 있게 합니다.
다음은 예입니다. String 타입 예외는 능력 “th”를 사용해 던져지며, 해당 예외가 던져질 때 이를 처리하는 곳은 th를 범위 안으로 들여온 try입니다(다른 어떤 try 블록도 이를 볼 수 없습니다)5.
f n = try $ \th ->
if n > 5
then throw th "Too big"
else pure (2 * n)
> runPureEff (f 4)
Right 8
> runPureEff (f 10)
Left "Too big"
셋째로 리더 효과는 어떤 스레드 지역적이고 지역적으로 가변적인 상태와 상호작용할 수 있는 능력으로 해석할 수 있습니다. 현재 Haskell에는 그러한 원시 가변 상태 참조가 존재하지 않지만, 추가하기 위한 제안이 있습니다. (새 참조 타입의 이름으로 IOScopedRef가 마음에 듭니다6. Bluefin과 effectful은 이러한 종류의 참조를 각각 Ask와 Reader로 모사합니다.)
마지막으로 “입출력 효과”, 즉 RTS와 임의의 방식으로 상호작용하는 능력은 그러한 권한을 부여하는 능력으로 해석할 수 있습니다. 다시 말해 effectful의 타입 수준 IOE, Bluefin의 값 수준 IOE, MTL의 MonadIO 제약 조건 및 일반 IO 모나드 자체는 모두 RTS와 상호작용할 권한을 부여하는 능력으로 해석할 수 있습니다.
그렇다면 제가 Bluefin을 “능력 시스템”이라고 설명하기로 한 이유는 무엇일까요? “능력”이라는 용어가 “효과”라는 용어보다 더 잘 전달된다고 생각하기 때문입니다. 첫째, 후자는 “효과”가 _무엇인지_라는 난처한 질문을 불러일으킵니다. “순수”라는 개념과 대비하여 “효과”를 정의하려고 시도할 수는 있겠지만, 저는 설득력 있는 시도를 본 적이 없습니다. 사실 “순수”에 대한 설득력 있는 정의도 접한 적이 없습니다7.
둘째, 유감스럽게도 “효과 시스템”이라는 용어는 외부인에게 Haskell에 대해 잘못된 인상을 준다고 생각합니다. 구체적으로, 다른 언어와 달리 Haskell은 “효과”를 수행하기 위해 “특별한 시스템”이 필요하다는 인상입니다. 사실은 반대입니다. Haskell과 달리 다른 언어는 가능한 효과의 범위를 제한할 수 없습니다. Bluefin을 설명하는 데 “능력 시스템”이라는 용어를 사용하면 외부인의 Haskell에 대한 인식이 조금이나마 더 긍정적인 방향으로 움직이기를 바랍니다.
셋째, “효과 시스템”은 대규모 프로그램을 구조화하는 소프트웨어 공학 도구로 사용되지만, 그 이름은 사용 사례에 맞지 않습니다. Tom Wells의 “Encoding Architecture Into Your Code (So Agents Can’t Break It)” 발표는 추상화 경계를 정의하여 애플리케이션의 계층화를 강제하는 데 효과 시스템을 사용하는 방법을 설명합니다. 핵심 요약은 _효과 시스템을 사용해 아키텍처를 제약할 수 있다_는 것입니다. 이는 대규모 프로그래밍을 할 때 제가 발견한 Haskell의 장점을 간결하게 요약합니다. 하지만 이 표현은 제게 어색하게 들립니다. 아키텍처가 효과와 무슨 관련이 있을까요? “_능력 시스템_을 사용해 아키텍처를 제약할 수 있다”는 훨씬 더 자연스럽게 들립니다! 실제로 너무 자연스러워서 그 작동 방식이 거의 자명합니다. 아키텍처의 각 구성 요소에 필요한 작업만 수행할 능력을 주면 됩니다. (매우 흥미로운 미해결 질문이 하나 남습니다. 필요한 곳에 능력을 전파하기 위해 능력 시스템을 어떻게 사용할까요?)
Tom Wells의 발표는 특히 인공지능 에이전트가 코드를 생성할 때 좋은 아키텍처를 유지하는 주제에 관한 것이었습니다. 기술적으로 올바른 코드를 생성하는 비용은 빠르게 하락하지만 좋은 설계가 그에 상응하여 더 쉬워지지는 않는 세상에서, 효과 시스템이 결정적인 역할을 한다고 저는 믿습니다. 그 역할은 아키텍처에만 국한되지 않습니다. 대규모 코드에서 보안 속성을 강제하는 데에도 도움이 될 수 있습니다. 이를 “효과 시스템” 대신 “능력 시스템”이라고 부른다면, Haskell 공동체가 우리가 가장 좋아하는 언어가 그 점에서 어떻게 도움이 되는지 더 쉽게 설명하고 어쩌면 우리 공동체에 몇몇 전향자를 얻을 수 있을 것이라고 생각합니다.
다른 효과 시스템도 있으며, 저는 이를 “종합적” 효과 시스템이라고 부릅니다. “종합적” 효과 시스템의 예에는 tranformers, extensible-effects, polysemy가 있습니다. “IO 래퍼” 또는 “분석적” 효과 시스템과 “종합적” 효과 시스템, 그리고 일반적인 Haskell 효과 시스템의 역사에 관한 자세한 내용은 제 발표 A History of Effect Systems를 참고하세요↩︎
extensible-effects는 Kiselyov, Sabry, Swords의 Extensible Effects: an alternative to Monad Transformers에서 유래했습니다↩︎
“값으로 범위가 정해진 예외”라는 이 아이디어의 가장 이른 예는 아마도 Brachthäuser, Schuster, Ostermann의 Effects as Capabilities: Effect Handlers and Lightweight Effect Polymorphism일 것입니다.↩︎
제 글 Haskell’s missing mutable reference type도 참고하세요↩︎
그것이 단지 “‘순수’는 ‘참조 투명하다’는 뜻”인 경우가 아니라면 말입니다↩︎