3P by GN⁺ | ★ favorite | 댓글 1개
  • 알제브라적 이펙트는 재개 가능한 예외처럼 제어 흐름을 잡아 처리하는 언어 기능으로, Ante의 핵심 기능이며 Koka, Effekt, Eff, Flix 같은 연구 언어에서도 중심적으로 쓰임
  • 같은 메커니즘으로 제너레이터, 예외, async, 코루틴, 자동 미분을 라이브러리 수준에서 만들 수 있고, 효과 다형성 덕분에 map 같은 함수도 효과 종류와 무관하게 한 번만 작성 가능함
  • 데이터베이스 접근, 출력, 로깅, 상태 전달 같은 의존성 주입과 컨텍스트 전달을 효과로 바꾸면 테스트 mock, 출력 수집, 로그 필터링을 핸들러 교체로 처리할 수 있음
  • 함수 시그니처에 can IO, can Print, can Fail 같은 효과가 드러나면 순수성 보장, 기록/재생, 보안 감사에 유리하지만, 이미 허용된 효과는 의도치 않게 기존 핸들러로 전파될 수 있음
  • 전통적인 약점은 효율성 우려였으나, 최근 언어들은 tail-resumptive 효과 최적화, evidence passing, 단일 resume 제한, 핸들러 특수화로 비용을 줄이고 있음

알제브라적 이펙트의 기본 모델

  • 알제브라적 이펙트는 effect handlers라고도 불리며, “재개 가능한 예외”라는 모델로 이해할 수 있음
  • Ante 의사코드에서는 효과 함수를 선언하고, 함수 시그니처에 해당 효과를 사용할 수 있음을 can으로 표시함
    • say_message: Unit -> Unit 같은 효과 함수를 호출하면 효과를 “던지는” 형태가 됨
    • 호출 함수는 foo () can SayMessage처럼 해당 효과 사용 가능성을 시그니처에 드러냄
  • handle 표현식은 try/catch와 비슷하게 효과를 잡고, resume 호출로 중단된 계산을 이어감
    • say_message 핸들러가 print "Hello World!"를 실행한 뒤 resume ()을 호출하면 원래 계산이 계속되어 42를 반환함
  • “algebraic”이라는 이름은 대부분 잔존적인 용어이며, 실제로는 effect handlers가 더 정확한 표현에 가깝지만 사용자에게 익숙한 이름으로 알제브라적 이펙트를 사용함

사용자 정의 제어 흐름

  • 알제브라적 이펙트는 여러 언어 기능을 하나의 메커니즘으로 구현하게 해줌
  • 효과 다형성은 what color is your function 문제를 줄임
    • map (input: Vec a) (f: a -> b can e): Vec b can e는 입력 함수 f가 어떤 효과 e를 수행하든 map도 같은 효과를 수행한다고 표현함
    • 같은 map을 stdout 출력, 비동기 함수 호출, 스트림 yield 등과 함께 사용할 수 있음
    • 많은 효과 핸들러 언어는 효과 변수 e를 생략해 익숙한 map (input: Vec a) (f: a -> b): Vec b 형태로 쓸 수 있음
  • 예외는 효과를 처리할 때 resume을 호출하지 않는 방식으로 구현 가능함
    • Throw a 효과의 throw: a -> never_returns를 정의함
    • 0으로 나누는 경우 throw "error: Division by zero!"를 호출하고, 핸들러는 메시지를 출력한 뒤 계산을 재개하지 않음
  • 제너레이터는 Yield a 효과의 yield: a -> Unit로 구현할 수 있음
    • 벡터 요소를 순회하며 yield elem을 호출함
    • filter 핸들러는 yield된 값이 조건을 만족하면 다시 yield x를 호출하고 resume ()으로 다음 요소를 이어감
    • my_for_each 핸들러는 yield된 값마다 함수 f를 실행하고 resume ()으로 계속 진행함
  • 협력적 스케줄러도 yield: Unit -> Unit 효과로 만들 수 있으며, 핸들러가 제어를 받아 다른 함수 실행으로 전환함
  • 여러 효과는 서로 잘 합성되며, 이 점이 다른 효과 추상화보다 사용성을 높이는 장점으로 다뤄짐

의존성 주입과 테스트 가능성

  • 효과는 일반 비즈니스 애플리케이션에서도 의존성 주입에 사용할 수 있음
  • 데이터베이스 객체를 함수 인자로 직접 넘기는 대신 Database 효과를 정의할 수 있음
    • 기존 형태는 business_logic (db: Database) (x: I32)처럼 DB 객체를 인자로 받음
    • 효과 기반 형태는 business_logic (x: I32) can Database가 되고, 내부에서 query "..."를 호출함
  • 구체적인 데이터베이스 선택은 호출 스택 상위의 핸들러가 맡음
    • 운영 DB를 다른 DB로 바꾸거나 테스트용 mock DB로 교체할 수 있음
    • mock_database 핸들러는 query 메시지를 무시하고 항상 DbResponse.Ok를 반환하도록 resume할 수 있음
  • 출력도 효과로 다루면 테스트 중 stdout을 직접 쓰지 않고 문자열로 수집 가능함
    • print_to_string 핸들러는 print msg 호출을 잡아 all_messages 문자열에 줄바꿈과 함께 누적함
    • output_messages는 실제 출력 없이 반환값 1234와 메시지 문자열을 검증할 수 있음
  • 로깅은 Log 효과와 LogLevel을 사용해 조건부 출력으로 바꿀 수 있음
    • log_handler는 메시지의 레벨이 설정된 기준 이상이면 print msg를 호출함
    • foo () with log_handler Error는 에러 로그만 출력함

더 깔끔한 API와 컨텍스트 전달

  • 알제브라적 이펙트는 프로그램이나 라이브러리 전반에 전달되는 Context 객체 패턴을 효과로 표현할 수 있음
  • Use a 효과는 상태 효과로 볼 수 있으며 get: Unit -> a, set: a -> Unit을 제공함
    • state 핸들러는 초기 상태를 보관하고, get에는 현재 컨텍스트를 반환하며, set에는 새 컨텍스트로 갱신함
    • 예시 state 정의는 소유권 규칙을 무시하며, 실제 구현에서는 Copy a 제약이 필요할 수 있음
  • 벡터 내부에 문자열을 저장하고 인덱스를 키처럼 넘기는 예시는 컨텍스트 전달 비용을 보여줌
    • 효과를 쓰지 않으면 push_string, get_string, append_with_separator, example 등이 strings를 계속 인자로 받아야 함
    • 효과 기반 구현에서는 원시 연산인 push_string, get_stringget/set을 호출하고, 상위 코드는 strings를 직접 넘기지 않아도 됨
  • 이 방식은 내부 컨텍스트 전달을 라이브러리가 감싸는 경우에 잘 맞음
    • 라이브러리 사용자는 컨텍스트 전달 방식의 내부 세부사항을 신경 쓰지 않아도 됨
    • 특정 컨텍스트 타입에 묶이지 않으려면 필요한 함수들을 인터페이스로 추상화할 수 있음

전역 변수 대체와 직접 스타일

  • 난수 생성이나 메모리 할당처럼 겉으로는 무상태처럼 보이지만 실제로는 상태가 필요한 API는 전역 변수 대신 효과로 표현할 수 있음
  • 난수 생성 예시는 Prng 객체를 프로그램 전반에 직접 전달해야 하는 부담을 보여줌
    • 전역 Prng는 편하지만 스레드 안전성이 필요해지는 등 전역 값의 단점이 생김
    • Random 효과의 random: Unit -> U8을 사용하면 사용자는 상위 호출 스택 어딘가에서 핸들러로 초기화만 명시하면 됨
    • 이후 /dev/urandom이나 다른 난수 원천으로 바꾸려면 핸들러만 교체하면 되고 호출 스택의 나머지 코드는 바꿀 필요가 없음
  • 메모리 할당도 Allocate 효과로 표현할 수 있음
    • allocate: (size: Usz) -> Alignment -> Ptr a
    • free: Ptr a -> Unit
    • 대부분의 호출에는 전역 allocator를 쓰고, tight loop 안에서는 루프 본문에 핸들러를 추가해 arena allocator로 바꿀 수 있음
  • 효과는 전용 값으로 감싼 결과를 넘기는 방식보다 직접 스타일을 가능하게 함
    • Maybe t를 쓰면 and_then, map으로 성공 경로를 이어야 함
    • Rust의 ? 같은 문법 설탕은 좋은 경로에 집중하기 위한 장치임
    • 효과 기반 get_line_from_stdin (): String can Fail, IOparse (s: String): U32 can Fail은 일반 순차 코드처럼 line = ..., x = ..., x * 2로 작성됨
  • 실패 처리는 핸들러를 적용해 좋은 경로에서 벗어나는 방식으로 다룰 수 있음
    • get_line_from_stdin () with default "42"Fail 효과를 기본값으로 처리함
  • 서로 다른 오류 타입도 효과 목록으로 자연스럽게 합성됨
    • LibraryA.foo (): U32 can Throw LibraryA.Error
    • LibraryB.bar (): U32 can Throw LibraryB.Error
    • my_functionThrow LibraryA.Error, Throw LibraryB.Error, Throw MyError를 함께 선언할 수 있음
    • 반복이 길어지면 AllErrors = can Throw ... 같은 타입 별칭을 만들 수 있음
    • 같은 Throw String 효과는 하나로 합쳐지며, 분리하고 싶다면 MyError 같은 래퍼 타입이 필요함

순수성, 재실행성, 보안 감사

  • 대부분의 효과 핸들러 언어는 OCaml 정도를 제외하면 부작용이 발생할 수 있는 곳에 효과를 사용함
    • Ante에서는 can Print, can IO처럼 표시하지 않으면 부작용을 사용할 수 없음
    • extern 정의는 컴파일러가 검사할 수 없어 타입 정의를 신뢰해야 함
    • 디버그 모드에서만 IO 효과를 수행해 릴리스 모드의 효과 안전성을 유지하는 방식은 계획된 기능임
  • 어떤 함수들은 입력으로 순수 함수를 요구함
    • 스레드를 생성할 때 생성된 스레드가 현재 스레드 소유의 핸들러로 호출할 수 없어야 함
    • spawn_all (functions: Vec (Unit -> a pure)): Vec a can IO는 순수 함수만 받아 모든 함수를 스레드로 실행하고 완료를 기다리는 형태임
  • Software Transactional Memory(STM)는 순수 함수가 필요한 동시성 기법임
    • 여러 함수를 동시에 실행하다가 트랜잭션 중 값이 다른 스레드에 의해 변경되면 해당 트랜잭션을 다시 시작함
    • Effekt의 개념 증명 구현은 effekt-stm에 있음
  • 순수성은 rr 디버깅 유틸리티와 비슷한 기록/재생 가능성을 제공할 수 있음
    • recordreplay 두 핸들러가 main이 내보내는 최상위 효과, 보통 IO, 를 처리함
    • record는 효과 발생과 결과를 기록하고, 실제 처리를 위해 내장 IO 핸들러로 다시 올림
    • replay는 실제 IO를 수행하지 않고 효과 로그의 결과를 사용함
    • 디버그 빌드에서 기본으로 기록하면 결정적 디버깅을 얻을 수 있음
  • 함수 시그니처의 효과 목록은 Capability Based Security와 유사하게 보안 감사에 도움이 됨
    • get_pi: Unit -> F64는 백그라운드에서 몰래 IO를 하지 않는다고 알 수 있음
    • 라이브러리 업데이트 후 get_pi: Unit -> F64 can IO가 되면 호출하는 쪽 함수가 이미 IO를 요구하지 않는 한 코드에서 오류를 받게 됨
    • 최소 효과만 선언하는 편이 바람직하며, 예를 들어 전체 IO보다 Print만 선언하는 식이 좋음
    • 새 효과 추가는 semantic versioning을 깨는 변경으로 다뤄짐
    • 관련 자료로 Capability Based SecurityDesigning with Static Capabilities and Effects가 있음

한계와 구현 전략

  • 효과 접근의 한계 중 하나는 의도치 않은 처리가 가능하다는 점임
    • 어떤 함수가 새로 IO를 요구하게 되어도 호출 함수가 이미 IO를 허용하면 오류가 나지 않을 수 있음
    • Fail 효과도 마찬가지로, 이전에는 실패하지 않던 라이브러리 함수가 나중에 Fail할 수 있게 되면 기존 Fail 핸들러로 전파될 수 있음
    • 이 동작은 상황에 따라 괜찮을 수 있지만, 기본값 제공처럼 별도 처리를 원했을 경우에는 의도와 다를 수 있음
  • 전통적인 주요 단점은 효율성 우려였지만, 최근 효과의 컴파일 출력은 크게 개선됨
  • 많은 알제브라적 이펙트 언어는 tail-resumptive 효과를 일반 클로저 호출로 최적화함
    • tail-resumptive 효과는 핸들러가 마지막에 resume을 호출하는 효과임
    • 실제 효과의 대부분이 여기에 해당하고, 본문 예시 대부분도 이 범주에 들어감
    • 예외는 resume을 전혀 호출하지 않기 때문에 예외적 사례로 분류됨
  • 언어별 최적화 전략도 다름
    • Kokaevidence passing을 사용하고 효과를 핸들러까지 끌어올려 런타임 없이 C로 컴파일함
    • AnteOCamlresume을 최대 한 번만 호출하도록 제한함
      • 이 제한은 비결정성 같은 일부 효과를 배제함
      • 대신 리소스 처리를 단순화하고 segmented stacks 같은 방식으로 내부 continuation을 더 효율적으로 구현할 수 있음
    • Effekt는 핸들러를 프로그램에서 완전히 특수화해 제거함
      • 이 방식은 대부분의 함수를 second-class로 만드는 제한이 있음
      • boxed 형태로 first-class 함수를 얻고 pay-as-you-go 방식으로 전환할 수 있음
      • 관련 자료로 Effekt captures 문서논문이 있음

댓글과 토론

Hacker News 의견들
  • 이 코드 조각을 보면 단점이 두 가지 보임
    첫째, foobar가 실패할 수 있다는 표시가 없어서, 타입 시그니처를 찾아보거나 IDE에서 hover해야 오류 처리기가 호출될 수 있음을 알 수 있음
    둘째, 실패했을 때 실행될 코드를 찾으려면 호출 스택을 위로 따라가 with 표현식을 찾고 다시 처리기 안으로 내려가야 함. my_function이 여러 곳에서 각기 다른 처리기로 호출될 수 있으니 IDE가 정적으로 정의로 이동하기도 어려움
    개념 자체는 멋지지만, 결과 코드의 가독성디버깅 가능성에는 큰 우려가 듦

    • 그게 핵심 중 하나임. 이것은 동적 코드 주입이고, 다른 동적 기능처럼 얕은 바인딩이나 깊은 바인딩 전략으로 구현할 수 있음
      동적이라는 건 호출자나 호출자의 호출자 같은 호출 프레임이 바인딩을 도입한다는 뜻이라, 개념상 스택을 따라 올라가야 함
      IDE가 정적으로 정의로 이동할 수 없는 것도 맞고, 동적 기능이기 때문임. 다만 사용자는 신경 쓰지 않아도 된다고 기대함. 효과를 호출한다는 점만 제외하면 순수 코드를 쓰는 것이고, 그 효과는 문맥에 따라 순수하거나 비순수할 수 있음
      그래서 운영 환경에서는 실제 I/O에 연결하고, 테스트에서는 mock으로 연결해 실제 I/O 대신 다른 효과를 끼워 넣을 수 있음. 결국 의존성 주입
      일반적인 모나드로도 가능하지만, 그 경우도 실제로 쓰는 모나드가 어디서 인스턴스화되는지 보려면 호출 스택을 꽤 위까지 봐야 함. 테스트와 샌드박싱을 가능하게 하는 코드 주입이라는 이점을 얻는 대신, 무슨 일이 벌어지는지 덜 명확해지는 비용을 치르는 셈임
    • 렉시컬 효과와 처리기에 대한 IDE 지원을 학사 논문으로 썼음: https://se.cs.uni-tuebingen.de/teaching/thesis/2021/11/01/ID...
      말한 것들은 모두 충분히 가능함
    • .NET 세계에서는 개발자들이 구현이 하나뿐이어도 모든 것을 인터페이스로 만드는 나쁜 습관이 많고, 심지어 DTO에도 그렇게 하는 경우가 있음
      메서드의 “구현으로 이동”을 눌러도 인터페이스 선언으로 가서 실제 구현까지 추가로 찾아가야 하고, 구현이 다른 어셈블리에 있으면 더 난감함. 직접 참조라면 IDE가 디컴파일할 수는 있어도 찾아주지는 못해서, 결국 디버거로 step into 해야 할 때가 있음
      더 강력한 의존성 주입 컨테이너인 Autofac 같은 것은 계층적 스코프를 만들 수 있고, 새 스코프가 등록을 재정의할 수 있음. Lisp의 동적 스코프 변수와 비슷하게, 런타임에 서비스가 무엇으로 해석되는지는 현재 DI 스코프 계층에 달려 있음
      그래서 효과는 어느 정도 ISomeEffectHandler 인스턴스를 클래스나 메서드에 주입하고 그 메서드를 호출하는 방식으로 흉내 낼 수 있음. 효과 처리는 현재 ISomeEffectHandler 등록이 결정하고, 프로그램 전반에서 동적으로 바뀔 수 있음
      예외를 던지는 대신 IErrorConditions 인터페이스로 오류 프로토콜을 만들고 ec.Report(...)를 호출할 수 있음. 현재 설치된 구현은 throw, log 등 원하는 일을 할 수 있음. yield 같은 것까지는 아직 완전히 밀어붙여 보지 않았음
    • foobar가 실패할 수 있다는 표시가 없는 것이 오히려 핵심의 일부라고 봄. 직접 스타일로 쓰면서 효과 문맥을 전혀 신경 쓰지 않을 수 있음
      실패했을 때 실행될 코드를 찾기 어렵다는 점도 핵심으로 이해함. 효과 처리 방식의 특정 구현에서 추상화할 수 있고, 실패 시 실행될 코드는 나중에 실행 방식을 결정할 때 정해짐
      f : g:(A -> B) -> t(A) -> B에서 g가 실행될 때 실행될 “그” 코드를 찾을 방법이 없는 것과 같음. 특정 g 구현에서 추상화하고 있기 때문임
    • 이것은 정적으로 할 수 있다고 봄. 그게 대수적 효과의 핵심 중 하나임
      본질적으로 “호출자로 이동”과 비슷하게 동작해서, IDE가 선택지를 보여주고 그중 관심 있는 호출자나 처리기를 찾게 할 수 있음
  • “대수적 효과는 재개할 수 있는 예외라고 생각하면 된다”는 설명은 ApplicativeErrorMonadError 타입 클래스와 실질적으로 무엇이 다른지 모르겠음
    효과를 던지는 건 위 타입 클래스의 선언된 오류 타입과 raiseError 메서드에 해당하고, handle 표현식은 필요에 따라 handleErrorhandleErrorWith를 쓰는 것과 사실상 같음
    “내일의 프로그래밍 언어에서 인기가 급증할 기능”이라기보다, 이미 오늘의 프로그래밍 언어에서도 쓰이고 있음: https://typelevel.org/cats/typeclasses/applicativemonaderror...

    • 효과가 하나뿐이라면 큰 차이가 없을 수 있음. 하지만 여러 효과가 동시에 있으면, 모나드를 중첩하는 것보다 명시적 지원이 더 좋아짐
      모나드 중첩은 순서를 골라야 하고, 어떤 함수의 출력이 호출 함수가 쓰는 모나드 집합이나 순서와 정확히 맞지 않아 재배열이 필요할 때가 있음
    • 모나드와 효과는 경쟁 관계라기보다, 계산 문맥을 추론하는 상호 보완적 접근으로 보는 편이 좋아 보임
      예를 들어 https://goto.ucsd.edu/~nvazou/koka/padl16.pdf 참고
    • 대수적 효과는 구분된 연속(delimited continuation) 영역에 있고, 프로그램 스택 위에서 동작함
      어떤 모나드 꼼수를 써도 호출 스택 5단계 위의 효과 처리기로 즉시 점프해서 그 스택 프레임의 지역 변수를 갱신한 뒤, 다시 5단계 아래의 같은 실행 지점으로 돌아오는 일은 할 수 없음
    • 정적 동작과 동적 동작의 차이로 봄
      모나드 프로그래밍에서는 관련 메서드를 모두 모나드에 구현해야 하지만, 효과에서는 현재 적용 중인 처리기를 덮어써야 하는 지점마다 효과 처리기를 동적으로 설치할 수 있음
      두 시스템을 결합하면 유용할 수 있음. 예를 들어 테스트와 샌드박싱에는 맞춤형 I/O 호환 모나드를 쓰고, 그 아래의 효과 처리기는 여전히 그 I/O 비슷한 모나드만 호출하게 만들 수 있음
    • 둘은 꽤 비슷하지만 사용성이 다름. 대수적 효과는 어떤 “free” 모나드 기법을 언어에 내장한 것과 비슷함
      내장되어 있으면 문법이 더 좋고 합성성도 더 나은 경우가 많음. Haskell처럼 모나드 접근에 충분히 맞춰진 언어에서도 같은 일을 할 수 있지만, mtl식 합성을 주는 타입 클래스 추론과 Haskell의 do나 Scala의 for 같은 내장 바인드 문법이 있으면 도움이 됨
  • 대수적 효과는 매우 흥미로워 보임. 예전에도 들어봤지만 정적 타입 시스템 영역에 속한다고 생각했고, 정적 타입 시스템을 좋아하지 않아 더 살펴보지 않았음
    그런데 이전의 동적 버전 Eff에 대한 글 두 개가 타입이나 범주를 도입하지 않고 아이디어를 잘 설명함. “free algebra”와 “unique homomorphism”은 각각 “terms”와 “evaluation” 정도로 생각하면 됨
    Andrej Bauer가 “parameterised operation with generalised arity”라고 부르는 것을 나는 [0, 1] 모양의 추상화라고 부를 것 같다는 점이 특히 흥미로움. 대수적 효과 개념을 써서 추상화 대수를 프로그래밍 언어로 바꾸는 데 도움이 될 수 있음
    [1] https://math.andrej.com/2010/09/27/programming-with-effects-...
    [2] http://abstractionlogic.com

    • 정적 타입 시스템의 무엇이 문제인가?
  • 대수적 효과는 흥미롭고 글도 좋지만, 큰 프로젝트에서의 사용성은 주로 “여기저기 점프” 때문에 걱정됨
    “더 깔끔한 API 설계에 도움이 된다”는 말은 논쟁적임. 간접 계층을 하나 추가하며, 물론 대수적 효과가 없는 실제 코드베이스에도 그런 간접성은 많음
    중단점을 찍었을 때 내가 다루는 객체가 어디서 생성됐는지 어떻게 알아내는지가 가장 걱정됨. 명시적으로 전달하면 스택 추적을 위아래로 오가며 찾을 수 있지만, 효과 합성에서는 인스턴스화 출처를 찾기 어려워져 요요 문제로 이어질 수 있음: https://en.wikipedia.org/wiki/Yo-yo_problem
    대수적 효과 자체 경험은 없지만, 글에서 같다고 설명한 Python 생성기는 써봤음. 크고 복잡한 생성기 표현식을 따라가는 일은 매우 지루하고 오류가 나기 쉬웠음
    관련 함수들은 여전히 시그니처에 can Use Strings를 써야 함. 실용적으로는 문자열을 명시적으로 전달하는 것과 can Use Strings 시그니처를 추가하는 차이를 잘 모르겠음. 기존 함수에 추가 문맥 전달을 넣고 싶으면 결국 모두 찾아가 적절한 배관을 추가해야 함
    낮은 수준에서는 대수적 효과가 재개를 위해 레지스터 처리를 곁들인 longjmp 명령으로 구현된다고 이해함. 그렇다면 대수적 효과가 많이 쓰이고 여러 방식으로 합성되는 코드베이스에서는 심한 요요 문제가 생기고, 코드가 실제로 무엇을 하는지 길을 잃기 쉬워 보임
    1인 프로젝트에서는 덜 심하겠지만, 코드베이스가 머릿속에 없는 큰 팀에서는 큰 효율 문제가 될 수 있음. 재개를 위한 메모리 할당을 대수적 효과가 어떻게 다루는지 좋은 자료가 있으면 보고 싶음

    • setjmp/longjmp와는 조금 다름. 적어도 C의 그것들은 호출 스택 위로 점프할 수는 있지만 다시 아래로 내려갈 수는 없음
      글 끝에도 썼듯이 언어마다 대수적 효과 구현이 다르고, 최근 몇 년 사이 효율도 좋아졌음. 언어는 처리기 정의 방식에 따라 효과를 다르게 최적화할 수 있음
      꼬리 재개(tail-resumptive) 처리기는 일반 클로저 호출로 구현할 수 있음. resume을 호출하지 않는 처리기는 예외처럼 구현하거나, 함수가 처리기를 빠져나갈 때까지 매 단계 오류 값을 반환하는 식으로 구현할 수 있음
      resume 호출 뒤에 작업을 수행하는 처리기, 예를 들어 | my_effect x -> foo (); resume (); bar ()는 분할 호출 스택 등으로 구현할 수 있음. resume을 여러 번 호출하는 처리기는 구분된 연속에 해당하는 것이 필요함
      일반 구현 방식으로 효과를 모나드로 변환할 수도 있음. 어떤 효과 집합이든 각 효과를 자체 모나드로 하는 모나드 변환자로 번역할 수 있고, free monad도 사용할 수 있음. 이 접근의 비용은 보통 bind 함수에 넘기는 클로저 박싱에서 나옴
      Koka는 효과를 capability passing으로 번역한 뒤 처리기까지 위로 올리는 자체 접근을 씀. 몇 가지 제약만 두면 효과와 처리기를 프로그램에서 완전히 특수화해 제거할 수도 있고, Effekt가 그렇게 함
      선택지가 정말 많음. 관심 있으면 글 끝 각주에 Koka와 Effekt 논문 링크를 넣어 두었고, 위 접근들을 구현함
    • 배관은 타입 추론으로 크게 줄일 수 있지 않을까? 효과를 호출하는 함수에는 시그니처가 필요하겠지만, 호출 체인 위쪽의 호출자들은 필요 없을 수 있음
      설령 모든 시그니처를 고쳐야 하더라도, 적어도 호출 지점마다 매번 문맥을 넘기도록 수정할 필요는 없음
  • 이 Ante “의사코드”는 훌륭함. Haskell에 Elixir의 표현력, 맛, 실용성을 더한 느낌이고, 개발자를 위한 Haskell 같음
    컴파일러가 성숙해지기를 기다리고 있으며, Ante로 앱을 개발해보고 싶음

  • 몇 년 전 OCaml 5 alpha에서 효과를 써서 protohackers를 해봤음. 재미있었지만 당시 툴체인은 조금 투박했음
    이것도 매우 비슷해 보이고 느껴지며, 발전이 기대됨

    • OCaml 5.3의 효과는 몇 년 전보다 꽤 깔끔해졌음. 그래도 아직 타입은 붙지 않음
  • “대수적 효과는 재개할 수 있는 예외라고 생각하면 된다”라면, Common Lisp의 조건(condition) 아닌가? 오래된 아이디어에 이름을 계속 새로 붙이는 순환이 참 좋음

    • 아님. 대수적 효과는 LISP의 조건 시스템보다 더 많은 경우를 지원하는 일반화이고, 연속이 multi-shot이기 때문임
      가장 가까운 것은 Scheme의 call/cc임. 이런 평행 비교는 하지 않는 것보다 더 해로울 때가 있음
    • Smalltalk에도 문자 그대로 재개 가능한 예외가 있음
    • 아이디어를 다루는 방식이 사고를 멈추게 만듦. 효과는 단순히 이름만 바꾼 조건이 아니고, 여기에는 그 한 문장보다 훨씬 자세히 설명하는 글 전체가 있으니 차이점도 직접 볼 수 있음
    • 의존성 주입이기도 함
  • 왜 좋은지에 초점 맞춘 설명이 부족함. 대수적 효과가 없는 언어에도 의존성 주입이 있는데, 그게 왜 이점인지 모르겠음
    이 의존성 주입이 실행이나 컴파일에서 더 빠른지 등도 설명하지 않음

    • 모나드와 같음
      첫째는 테스트임. “효과”가 있는 순수 코드를 쓰되, 운영에서는 효과가 실제 세계와의 상호작용이고 테스트에서는 mock으로 대체됨. I/O를 하지 않는 순수 코드와 그 바깥의 절차적 껍데기 대신, I/O를 하는 순수 코드를 쓸 수 있어 더 많은 코드를 테스트할 수 있음
      둘째는 샌드박싱임. mock이 아니라 코드가 할 수 있는 일을 제한하는 방화벽인 경우임
      이 둘째는 매우 바람직한 사용 사례이고, 공급망 취약점 완화로 생각할 수 있음. log4j를 떠올리면 됨
      둘 다 이미 모나드로 가능함. 효과는 더 쓰기 편할 수 있지만 더 동적이기도 해서 구현이 복잡해짐. 동적 기능은 항상 정적 기능보다 비용이 큼
    • 주류 언어에서 의존성 주입을 구현하는 방식은 대개 언어와 함께 작동하기보다, 메타프로그래밍으로 언어를 우회함
      의존성 주입 코드에서 일반 코드라면 불가능한 오류가 나는 일도 드물지 않음
      언어 자체가 처음부터 의존성 주입을 지원하도록 설계되면 어떻게 동작할 수 있는지 보는 점이 흥미롭고, 대수적 효과는 그 방법 중 하나임
    • 대수적 효과가 낯설기 때문에, 대부분의 개발자에게 익숙한 것으로 일부를 설명하는 것임
      장님과 코끼리 이야기와 비슷함. 코끼리인 대수적 효과는 완전히 다른 짐승이지만, 초보자가 처음 이해하도록 몸통은 뱀 같은 것, 귀는 부채 같은 것이라고 말하는 셈임
      대수적 효과는 새로운 제어 흐름 유형임. 효과가 발생하면 호출 스택 위쪽에 정의된 처리기는 효과를 처리하고 발생 지점에서 계산을 재개할 수 있고, 여러 번 재개할 수도 있으며, 처리하지 않고 빠져나갈 수도 있음. 이 경우 실행은 스택 위의 처리기가 호출된 시점으로 돌아가고 그 이후 실행은 없었던 것처럼 되어, 백트래킹을 할 수 있음
      “대수적”이라는 점은 보통 효과들이 그룹으로 묶이고, 서로 다른 방식으로 안전하게 합성할 수 있는 합성 대수를 갖도록 설계된다는 뜻임. 흔한 성질은 교환법칙이라 실행 순서가 중요하지 않게 됨. 물론 모든 대수적 효과가 교환법칙을 갖는 것은 아니고 구현자가 그렇게 설계해야 함
      모나드는 모나드 변환자 없이는 잘 합성되지 않는 경우가 많아 까다로울 수 있음
      이런 것들을 합치면 예외, 코루틴, 생성기, async/await, 확률적 프로그래밍, 백트래킹 검색, 의존성 주입처럼 보통 언어에 내장되는 제어 흐름을 구현할 수 있음. 직접 새로운 것도 만들 수 있음
      가장 흔히는 부수효과를 하려는 의도와 실제 부수효과 실행을 분리하는 데 사용함. 그렇게 하면 부수효과가 통제되고, 코드를 추론하기 쉬워짐
  • 범주론에서 온 듯한 새 아이디어를 볼 때마다, 이것이 정말 주류 언어에 들어갈지 궁금해짐
    경험상 언어의 철학적 수준에서 응집성이 있어야, 프로그래밍과 비즈니스 문맥 모두에 능숙한 개발자 팀에서 일하기 좋음
    어떤 문제를 푸는 프로그래밍 패턴 묶음은, 코드의 품질 속성과 비즈니스 문제 해결 능력이 같은 다른 불연속적인 패턴 묶음으로 대체될 수 있는 경우가 많음
    궁금한 것은 주류 언어가 대수적 효과와 처리기를 깊은 혼란 없이 받아들일 수 있는지, 아니면 이런 추상화 위에 처음부터 새 언어를 만들어야 하는지임

    • 대수적 효과는 의존성 주입을 언어로 형식화한 변형 또는 강화판임
      의존성 주입은 라이브러리 구현만으로도 오랫동안 현실에서 엄청나게 많이 쓰여 왔음
    • 약한 형태의 대수적 효과는 이미 매우 흔함. React hooks가 그렇음
      React hooks는 완전한 대수적 효과와 몇 가지 점에서 다름. hook의 처리기는 이미 구현되어 있고 다른 처리기로 교체할 수 없음. 예를 들어 useState 구현은 고정되어 있음
      multi-shot 재개도 아님. hook이 발생하면 처리기는 한 번만 처리하고 재개할 수 있음. 완전한 대수적 효과에서는 같은 발생 효과를 여러 번 재개할 수 있음
      대수적 효과는 보통 묶음으로 제공되고, 그 효과들 사이에는 특정한 합성 규칙이 있음. 그래서 대수적임
    • React hooks가 기본적으로 그것임. 언어 수준은 아니지만 널리 채택되고 이해되고 있음
  • 대수적 효과가 제어 흐름을 일반화해서 예를 들어 코루틴을 구현할 수 있다는 주장을 자주 봄
    그런데 언어 런타임에서 대수적 효과를 구현한다면 가장 뻔한 방식은 코루틴이고, 효과는 yield/resume 주변의 문법 설탕일 것 같음
    무엇을 놓치고 있는 걸까?

    • 대수적 효과가 코루틴에는 없는 타입 안전성을 제공함. 더 구체적으로, 함수가 할 수 있는 일과 할 수 없는 일을 코드의 렉시컬 범위 안에서 지정할 수 있음. 생성기나 코루틴은 그럴 수 없음
      예를 들어 query_db(): User can Datebase라고 주석이 붙은 함수는 데이터베이스를 호출할 수 있고, 호출자는 query_db를 호출하려면 Database 처리기를 제공해야 한다는 뜻임
      무엇을 할 수 있고 없는지의 제약은 다른 프로그래밍 분야에서도 꽤 흔함. 대표적으로 NextJS에서는 서버 컴포넌트가 클라이언트 기능을 쓸 수 없고, 클라이언트 컴포넌트는 서버 DB에 접근할 수 없음
    • JavaScript의 Effect-TS가 문법 설탕을 뺀 채 이렇게 하고 있지만, 결국 좋은 생각인지는 모르겠음
      Spring framework가 떠오름. DI도 대수적 효과의 한 형태인데 코드 전체로 암처럼 퍼짐
      얼마 전 EffectDays의 프런트엔드 효과 사용 강연[1]을 봤는데, 발표 내내 실제로는 아무것도 하지 않는 보일러플레이트를 보여주는 내용이었음
      대수적 효과는 매혹적인 아이디어이고 함수형 언어에서 명령형 코드를 표현하게 해주지만, JS 같은 언어에서는 모든 것을 함수로 감싸는 추가 작업이 상상 가능한 가장 단순한 JS보다 퇴보처럼 느껴짐
      반례로 canvasmotion[2]도 2D 그래픽 시나리오를 표현하는 데 코루틴을 쓰는데, 이것은 어려운 일을 더 쉽게 만드는 느낌임
      [1] https://www.youtube.com/watch?v=G_jp87gxILE
      [2] https://motioncanvas.io/
    • 이 스레드의 다른 사람들은 대수적 효과 처리기가 call/cc처럼 코드를 여러 번 재개할 수 있다고 했음. 코루틴은 yield할 때마다 한 번만 재개할 수 있다는 점과 다름
      개인적으로는 실행이 얼마나 예측 불가능해질 수 있는지 생각하면 거기에 큰 가치는 잘 못 느끼겠음. 여러 번 호출할 함수를 명시적으로 반환하는 함수나, 그에 준하는 반복자를 반환하는 함수를 쓰는 편이 더 낫다고 봄