알제브라적 이펙트가 필요한 이유
(antelang.org)- 알제브라적 이펙트(effect handlers)는 다양한 언어 기능(예외 처리, 제너레이터, 코루틴 등)을 라이브러리 수준에서 구현할 수 있게 해주는 유연한 제어 흐름 도구임
- 함수형 프로그래밍에서 흔한 컨텍스트 관리, 의존성 주입, 글로발 상태 대체 등에도 적용 가능함
- API 설계의 간결성과 코드 내에서의 상태/환경 전달의 자동화에 기여함
- 함수형 순수성 보장, 리플레이어빌리티, 보안 감사 등도 지원하는 장점이 있음
- 최근 컴파일러 기술 발전으로 성능 이슈도 많이 개선됨
알제브라적 이펙트(Algebraic Effects)의 개요
알제브라적 이펙트(일명 effect handlers)는 최근 각광받는 프로그래밍 언어 기능임. Ante와 여러 연구 언어(Koka, Effekt, Eff, Flix 등)의 핵심 기능 중 하나로 빠르게 확장 추세를 보임. 많은 자료가 이펙트 핸들러의 개념을 설명하지만, 실제로 "왜" 필요한지에 대한 심층적 설명은 부족한 상황임. 이 글은 알제브라적 이펙트의 실질적인 용처와 이점을 최대한 폭넓게 소개함.
문법과 의미론 빠른 이해
- 알제브라적 이펙트는 "재개 가능한 예외"와 유사한 개념임
-
effect SayMessage
와 같이 이펙트 함수 선언 가능 -
foo () can SayMessage = ...
처럼, 함수에서 해당 이펙트 사용 가능성 명시 -
handle foo () | say_message () -> ...
로 예외의 try/catch처럼 핸들링 가능
이와 같은 기본 구조를 통해 이펙트 호출 및 제어가 가능함
사용자 정의 제어 흐름 확장
알제브라적 이펙트의 가장 큰 이유는 한 언어 기능만으로 원래 각각 별도의 언어 기능(제너레이터, 예외, 코루틴, 비동기 등) 필요하던 기능을 라이브러리로 구현할 수 있음.
- 함수에 다형적 이펙트 변수 (
can e
)를 두면, 다양한 이펙트를 함수 인자로 전달 및 조합 가능함 - 예시로,
map
함수는 인자로 받은 함수가 임의의 이펙트e
를 쓸 수 있도록 선언, 다양한 효과(출력, 비동기 등)와 자연스럽게 결합 가능함
예외와 제너레이터 구현 예시
-
예외 구현: 이펙트 발생 후
resume
호출 없이 처리하면 예외와 동일하게 동작함 -
제너레이터 구현:
Yield
이펙트 정의, 값을 yield할 때마다 외부 핸들러가 개입하여 조건에 따라 흐름 제어 가능, 필터링 등 고급 패턴도 간단한 수준의 코드로 작성 가능함
여러 이펙트를 조합해서 사용할 수 있다는 점도 기존 효과 추상화 기법에 비해 큰 장점임
추상화 계층으로서의 활용
알제브라적 이펙트는 단순히 코어 프로그래밍 기능 확장 외에도 다양한 비즈니스 애플리케이션 시나리오에서 활용도가 높음
의존성 주입 (Dependency Injection)
- 데이터베이스, 출력 등 의존 객체를 이펙트로 추상화하여 핸들러로 관리 가능
- 테스트용 목(mock) 객체 대체, 출력 리디렉션 등도 유연하게 구현 가능
조건부 로깅 또는 출력 관리
- 로깅 레벨에 따라 로그 메시지 출력 여부를 중앙에서 제어 가능
API 설계 간결화 및 Context 전달 자동화
상태(State) 이펙트 활용
- Context 객체 혹은 환경 정보 전달이 필요한 상황에서, 이펙트 기반으로
get/set
만 사용하도록 구현 시, 명시적 전달 없이 상태 관리 자동화 가능 - 기존에는 모든 함수에 context를 인자로 넘겨야 하지만, state effect로 이 부분을 은닉 가능
글로벌 객체 대체
- 난수 발생기, 메모리 할당 등 전역 객체로 관리하던 상태도 effect로 추상화해 코드의 명확성, 테스트 편의성, 동시성 지원 측면에서 유리함
- 핸들러만 교체하면 실제 난수 소스를 유연하게 변경 가능
직접 스타일(Direct Style) 작성 지원
- 기존에는 옵션 타입, 에러 래핑 등으로 여러 객체를 중첩해서 다뤄야 했음
- 이펙트는 이러한 래핑 없이도 에러나 부수효과 경로를 깨끗하게 표현할 수 있음
순수성 보장과 보안 감사
부작용(부수효과) 명시
- 대부분의 이펙트 핸들러 언어에서는, 부수효과가 발생하는 함수에 반드시
can IO
,can Print
등 효과를 타입 시그니처에 명시함 - 쓰레드 생성, 소프트웨어 트랜잭셔널 메모리(STM) 등에서는 반드시 순수 함수 필요함
로그 리플레이 및 결정적 네트워킹
- 순수성을 기반으로
record
,replay
와 같은 핸들러를 만들어 실행 결과를 재현 가능 - 디버깅, 데이터베이스, 게임 네트워크 등에 결정적 결과 및 롤백 지원 가능
Capability-based Security 지원
- 함수 타입 시그니처에 처리하지 않은 모든 이펙트가 노출되어, 외부 라이브러리 보안 감사시 효과적임
- 만약 기존에 부수효과가 없던 함수가 업데이트되어
can IO
가 붙으면, 이를 호출하는 코드에서 즉시 감지 가능함
다만, 모든 이펙트가 자동으로 전파되기에 무심결에 효과가 처리되는 부작용 발생 가능성도 있음
효율성 관점과 결론
- 예전엔 실행 효율 문제가 약점이었으나, 최근엔 tail-resumptive 효과 등 다수의 경우에서 최적화가 매우 진전됨
- 다양한 언어별로 각각 효과적인 컴파일 전략(closure call, evidence passing, 핸들러 특수화 등) 적용됨
알제브라적 이펙트는 미래의 프로그래밍 언어에서 훨씬 더 핵심적인 위치를 차지할 것으로 기대됨.
Hacker News 의견
-
저는 두 가지 단점이 있다고 봅니다
주어진 코드 조각을 보면, foo나 bar가 실패할 수 있다는 표시가 전혀 없다는 점이 첫 번째
이런 호출이 에러 핸들러를 유발할 수 있다는 걸 알기 위해선 타입 시그니처를 직접 찾아봐야 하고, 상황에 따라 IDE의 도움 없이 수작업이 필요
두 번째는, foo와 bar가 실패할 수 있음을 파악한 후 실제 실패 시 어떤 코드가 실행되는지 찾으려면 호출 스택을 따라 한참 위로 올라가 'with' 표현을 찾아내야 하고, 그 이후엔 해당 핸들러를 따라 내려가야 하는 구조
정적으로 이 동작을 따라가거나 IDE에서 바로 정의로 점프하는 게 불가능하며, my_function이 여러 곳에서 다양한 핸들러로 호출될 수 있기 때문
이런 개념은 매우 신선하다고 생각하지만, 결과적으로 코드의 가독성이나 디버깅 측면에서는 우려를 가지고 있음-
실행 실패 시 어떤 코드가 동작하는지 찾는 문제에 대해, 이것이 바로 동적 코드 인젝션의 핵심이라 설명
shallow-binding, deep-binding 등 다양한 동적 기능과 동일하게 호출 스택을 따라 바인딩이 이뤄지는 구조
정적 분석이나 IDE 점프가 불가능한 것도 동적 특성 때문
하지만 이 과정에서 실제로 신경 쓸 필요가 크게 없다고 생각
왜냐하면 순수 코드에 효과만 추가하는 방식이라, 상황에 따라 순수 효과든 비순수 효과든 테스트용 목(mock)이나 프로덕션 환경 등 다양한 문맥에서 연결될 수 있기 때문
의존성 주입과 비슷한 원리
전통적인 모나드에서도 비슷하게 구현할 수 있지만, 실제로 모나드가 어디 인스턴스화되는지 찾기 위해서는 역시 호출 스택을 살펴야 함
이런 기술들이 제공하는 이점이 존재하지만, 동시에 대가도 분명히 있음
테스트와 샌드박싱 등에는 유리하지만, 코드에서 어떤 일이 벌어지는지 명확히 보이진 않는 특성 -
렉시컬 효과와 핸들러에 대한 IDE 지원 관련으로 학사 논문을 쓴 경험을 공유
위에서 지적된 모든 점들이 충분히 실현 가능하다고 생각
논문 링크 -
.NET 기반에서 인터페이스를 과도하게 사용하는 경향이 있어, 메소드 구현체로 바로 점프하기 위해 여러 단계를 거치는 번거로움이 있다는 얘기
종종 구현체가 다른 어셈블리에 있으면 IDE 기능이 무용지물이 되곤 함
고급 Dependency Injection(대표적으로 Autofac)에서는 LISP의 동적 스코프 변수처럼 계층적으로 스코프를 구축해 런타임에 서비스가 어떤 인스턴스에 바인딩되는지 결정
이 점에서, 효과를 ISomeEffectHandler 같은 인터페이스 인스턴스로 주입해서 효과 발생 시 해당 메소드 호출로 대표
핸들러의 구체 동작(예외 발생, 로깅 등)은 DI 설정에 따라 동적으로 결정
기존에는 예외를 throw하는 패턴을 썼지만, 인터페이스를 기반으로 효과를 명시하고 처리 방식을 전적으로 DI에 맡기는 설계로 전환 가능
yield 등 반복자 관련까지는 깊이 파보지 못함 -
foo와 bar가 실패할 수 있다는 표시가 없는 부분이 핵심 포인트라고 생각
직접적인 스타일로 효과적 맥락을 신경 쓰지 않고 코드 작성이 가능함
실패 시 어떤 코드가 동작하는지 찾는 것도 추상화의 본질
실행 시점에 실제로 어떤 효과 핸들러가 결합될지는 나중에 결정
마치 f : g:(A -> B) -> t(A) -> B 에서 g가 수행될 때 어떤 코드가 실행될지 미리 알 수 없는 것 같은 원리 -
호출 스택 따라 올라가며 핸들러를 찾는 정적 분석 불가능성 주장에 동의하지 않음
실제로는 정적 분석이 가능하며, IDE에서 "caller로 이동" 같은 기능을 활용해 어떤 핸들러가 사용되는지 선택할 수 있다는 의견
-
-
Ante의 "의사코드"가 매우 인상적
Haskell의 특성과 Elixir의 표현력, 실용성이 절묘하게 합쳐진 느낌
개발자를 위한 Haskell이라는 인상
컴파일러 성숙해지기를 기대
Ante로 앱 개발을 해보고 싶다는 희망 -
AE(Algebraic Effects)가 제어 흐름을 일반화해 코루틴도 구현할 수 있다는 주장에 대해
실제로 새로운 언어 런타임에서 AE를 구현하는 가장 단순한 방법이 코루틴을 활용해 yield/resume 기본 구조에 효과를 문법적으로 입히는 것이라고 생각
혹시 제가 놓치고 있는 부분이 있는지 질문-
AE가 코루틴과 다른 대표적인 점으로 타입 안전성을 꼽음
AE에서는 함수가 소스 코드상에서 어떤 효과를 사용할 수 있는지 명시 가능
예를 들어, query_db(): User can Database 형태라면 데이터베이스에 접근할 수 있고, 호출 시 Database 핸들러를 반드시 제공해야 함
어떤 일을 할 수 있고 없는지에 대한 제약이 매우 명확히 드러나는 구조
NextJS 등에서 서버 컴포넌트가 클라이언트 기능을 직접 쓸 수 없듯이, 이런 안전성 제약이 다양한 분야에서 인기 -
Effect-TS가 JavaScript에서 이 방식(코루틴 활용)에 근접하지만, 결과적으로 좋은 아이디어인지 확신은 없음
Spring 프레임워크의 DI와 유사하게, AE가 코드 전체에 확산되어 오히려 복잡성만 야기할 수 있음을 우려
실제로 EffectDays에서 프론트엔드 효과 사용법을 소개하는 발표는 대부분 무의미한 보일러플레이트뿐이었다는 비판
AE가 매혹적인 개념이긴 하지만, 많은 작업을 함수로 감싸야 하는 부담은 JS 특유의 간편한 코드 작성성을 저해할 수 있다고 생각
반면, motioncanvas와 같이 코루틴만으로 복잡한 2D 그래픽 시나리오를 쉽게 표현하는 접근도 큰 장점
관련 영상 EffectDays
MotionCanvas -
스레드 내에선 AE 핸들러가 call/cc처럼 코드를 여러 번 재개(resume)할 수 있다는 주장이 있음
반면, 코루틴의 경우 yield될 때마다 한 번씩만 재개 가능
이런 불확실한 실행 흐름이 오히려 예측을 어렵게 하므로, 여러 번 호출할 수 있는 함수를 명시적으로 반환하거나 이터레이터 등 다른 구조로 대체하는 방식을 선호
-
-
코딩 추상화로서 이 개념이 굉장히 매력적으로 느껴진다는 입장
Sun에서 커널 프로그래밍을 하며 sleep(foo)와 같이 호출한 뒤, foo에 의해 다시 깨어날 때 코드를 간결하게 작성할 수 있다는 점이 큰 장점이라고 느낌
각종 엣지 케이스를 제어 흐름으로 일일이 처리하는 부담감이 줄어듦
메모리 지역성 관련 이슈만 유의하면, 여러 함수들을 미리 대기 상태로 초기화시키고, 알고리즘을 각 유닛의 변이로 직접 표현하는 것에 즐거움이 있을 것 같음 -
"대수적 효과는 예외를 재개할 수 있는 예외와 같다"라는 주장에 대해
ApplicativeError나 MonadError 타입 클래스와 실질적으로 어떤 점이 다른지 질문
함수에서 사용할 수 있는 효과를 명시하는 방식은 checked exceptions과 비슷하고, handle 표현식으로 효과를 처리하는 것도 try/catch와 거의 동일
이런 타입 클래스들은 이미 handleError/handleErrorWith 등으로 예외를 잡는 방식을 지원
대수적 효과는 "미래"의 언어에 적용될 장점이 있다지만, 실제론 오늘날도 충분히 쓰이고 있는 개념
cats 설명 링크-
단일 효과만을 다룬다면 큰 차이가 없을 수 있지만, 여러 효과가 동시에 필요한 경우에는 직접적인 효과 지원이 명시적으로 monad를 중첩하는 방식보다 훨씬 더 깔끔하고 직관적
monad를 조합할 경우 순서 지정이나 일부 함수 결과가 기대하는 monad 세트와 일치하지 않을 때 순서 변경 등 골치 아픈 문제가 있음 -
개인적으로 모나드와 효과는 경쟁 구도가 아니라 서로 보완적인 해석 방식이 더 적합하다고 생각
관련 논문(예: Koka 논문) 참고 -
대수적 효과는 delimited continuation처럼 프로그램 스택에서 동작
단순한 모나드 트릭만으로는 5번 위 스택 프레임에 있는 효과 핸들러로 즉시 점프했다가, 해당 프레임에서 로컬 변수만 변경하고 다시 5번 아래로 돌아오는 것이 불가능 -
차이점은 정적 vs 동적 동작
모나드로 프로그래밍 할 땐 모든 관련 메소드를 직접 구현해야 하지만, 효과 시스템에선 어떤 시점에서든 동적으로 효과 핸들러를 설치해 기존 핸들러를 유연하게 오버라이드 가능
예를 들어, 테스팅을 위해 IO 특성을 가진 전용 모나드를 하위에서 사용하고, 그 아래에서만 효과 핸들러를 설치하는 복합적인 구조도 가능 -
유사점은 크지만, 사용성에서 차이가 큼
대수적 효과는 "free" monad와 비슷한 구조이나, 내장되어 있어서 문법이 더 쉽고 합성(composability)도 뛰어남
Haskell 등 모나드 중심 언어에서는 타입 클래스 추론(mtl 스타일)과 내장된 bind(syntax) 덕분에 얼핏 비슷한 효과를 낼 수 있긴 함
-
-
원래 대수적 효과가 정적 타입 시스템에서만 다뤄진다고 오해했으나, 그 외에도 동적 구조가 있음을 최근 알게 됨
예전에 Eff의 동적 버전에 관한 두 개의 글(첫번째, 두번째)이 특히 인상적
"일반화된 arity의 매개변수화 연산"과 같은 개념도 추상화를 프로그래밍에 연결할 때 흥미로운 부분으로 느껴짐- 정적 타입 시스템의 어떤 점이 싫은지 궁금
-
오래된 개념이 최근 새로운 이름과 틀로 다시 등장함을 언급
LISP Condition System 소개
Algebraic Effects 체험기 -
OCaml 5 알파에서 effects로 protohackers를 했던 경험
재밌긴 했지만 당시엔 툴체인이 다소 불편
Ante가 비슷한 느낌이라, 앞으로의 발전 기대- OCaml 5.3 이후의 effects는 예전보다 훨씬 개선된 상태
아직 타입 시스템이 붙진 않았지만 지금은 확실히 깔끔해짐
- OCaml 5.3 이후의 effects는 예전보다 훨씬 개선된 상태
-
Prolog에서 많은 시간을 보내며, 비결정성 함수 합성과 컴파일 타임 타입 체크를 쉽게 할 수 있게 해주는 언어를 찾고 있는 중
Ante도 그 후보 중 하나로 관심
LSP, tree-sitter 같은 개발자 도구와 에디터 플러그인도 잊지 말아야 한다는 코멘트- Ante 저자로서, 이미 (아주 기본적이지만) LSP 지원 중
새 언어엔 초기부터 툴링이 필수라 생각
디버깅 경험도 중시하고 있으므로, 최소한 debug mode에서 리플레이(replayability) 기능도 기본 제공할 수 있을지 검토 중
- Ante 저자로서, 이미 (아주 기본적이지만) LSP 지원 중
-
"대수적 효과는 예외를 재개할 수 있는 예외와 같다"는 주장에 대해
Common Lisp conditions와 비슷하냐는 질문
오래된 개념이 이름만 바꿔 다시 등장한다는 점에서 흥미를 느낌-
대수적 효과는 LISP condition system보다 훨씬 포괄적
continuations가 multi-shot 가능한 점에서 Scheme의 call/cc와 유사
이런 병렬성이 오히려 없는 것보다 더 나쁜 결과를 초래할 수 있다는 점도 언급 -
Smalltalk엔 "재개 가능 예외(resumable exceptions)"가 있음
-
단순히 효과를 오래된 condition 시스템의 이름 바꾸기 정도로 간주하면 논의 진전이 어렵다고 생각
현재 논의되고 있는 대수적 효과는 단순 개념 이상의 차이점 존재 -
Dependency Injection 역시 유사한 맥락에서 언급할 수 있음
-