암시적 매개변수와 동적 스코핑의 차이를 코이펙트/이펙트 관점에서 살펴보고, Haskell, Reader 모나드, Koka의 implicit values/functions 및 reflection과의 관계를 논의한다.
오랫동안 나는 암시적 매개변수(implicit parameters)와 동적 스코핑(dynamic scoping)이 사실상 같은 것이라고 생각해 왔다. 둘 다 비슷한 문제(예: 이른바 “설정(configuration) 문제”, 즉 어떤 설정 값을 일일이 명시적으로 인자로 넘기지 않고, 중첩된 함수 정의들 깊숙한 곳까지 전달해야 하는 문제)를 해결하는 데 쓸 수 있기 때문이다. 하지만 암시적 매개변수는 되도록 쓰지 말아야 할 것이라는 평판이 있고(대신 reflection을 쓰라는 식으로), 반면 리더 모나드(reader monad)를 통한 동적 스코핑은 (모든 것을 모나드화해야 한다는 점만 빼면) 유용하고 잘 이해된 구성 요소이다. 왜 이런 차이가 날까?
Oleg은 암시적 매개변수가 사실 진짜 동적 스코핑이 아니라고 지적하며, Lisp과 Haskell이 서로 다른 동작을 보이는 예시를 제시한다. 그리고 Haskell에서조차 Lisp식 동작을 원하지 않을 것이다. 동적 스코핑의 연산적 개념(스택을 거슬러 올라가며 동적 변수의 바인딩 지점을 찾는다)을 떠올려 보면, 이 방식은 게으른 계산(laziness)과 잘 맞지 않는다. 어떤 쓰렁크(thunk, 아직 평가되지 않은 식)가 동적 변수를 참조하고 있다면, 프로그램 실행 중 어느 예측 불가능한 시점에 강제(force)될 수 있다. 동적 변수들이 어떻게 바인딩될지를 알기 위해 “그 쓰렁크가 정확히 어디에서 실행될지”까지 추적해야 한다면, 그 길은 곧 광기로 이어진다. 그런데도 엄격 평가(strict) 언어에서는 동적 스코핑으로 무슨 일이 일어나는지 파악하는 데 별문제가 없다(음, 대체로는—이에 대해서는 곧 더 이야기하겠다).
연구 커뮤니티에서는, 이 차이의 핵심이 암시적 매개변수가 코이펙트(coeffect)라는 점이라는 것을 밝혀냈다. 이 점을 처음 관찰한 논문은 아마도 Coeffects: Unified static analysis of context-dependence일 것이다(보다 현대적인 정리는 Coeffects: A calculus of context-dependent computation에 있고, Haskell에 좀 더 초점을 맞춘 정리는 Embedding effect systems in Haskell에서 볼 수 있다). 사실 Tomas는 2012년에 내 블로그 댓글에서 비슷한 아이디어를 이야기하고 있었으므로, 이 연구는 그 전부터 진행되고 있었던 듯하다.
핵심 포인트는, 어떤 코이펙트(바로 암시적 매개변수 같은)에서는 콜바이네임(call-by-name) 축약이 타입과 코이펙트를 보존하므로, 암시적 매개변수가 이펙트로서의 동적 스코핑처럼 폭주하지 않는다는 점이다. 이 둘은 필연적으로 다르게 동작한다! 타입 클래스는 코이펙트이기도 한데, 현대 Haskell에서 암시적 매개변수를 사용할 때 이 점을 명시적으로 인정하고 있다(예: reflection 패키지에서).
올해 ICFP에서, Koka에서의 implicit values and functions에 관한 흥미로운 기술 보고서를 소개받았다. Koka는 동적 스코핑에 새롭게 변주를 준 언어다. 이를 보며, Haskell의 암시적 매개변수가 이 작업에서 뭔가 배울 수 있지 않을까 하는 생각이 들었다. Implicit value는 전역적으로, 즉 top-level에서 정의되도록 한 선택이 좋은데, 이 덕분에 일반적인 모듈 네임스페이싱에 참여할 수 있다. 이름공간이 전혀 없는 “동적으로 스코프되는 이름들의 잡동사니 집합”으로 두는 대신에 말이다(이는 reflection이 암시적 매개변수에 비해 개선한 점이기도 하다). 그런데 곰곰이 생각해 보면, implicit function 쪽은 오히려 암시적 매개변수에서 아이디어를 가져온 것처럼 보인다!
암시적 함수(implicit function)의 큰 혁신은, 함수 안의 모든 동적 참조를(렉시컬하게만이 아니라, 이후의 모든 동적 호출에 대해) 그 함수가 정의되던 시점의 렉시컬 스코프(즉, 정의 시점의 동적 스코프)에 결부시킨다는 점이다. 그 결과, 해당 함수는 implicit value에 전혀 의존하지 않는 함수가 된다(다시 말해, “이 함수를 호출하는 시점에 implicit value가 정의되어 있어야 한다”는 _이펙트_가 사라진다). 이것은 정확히, Haskell의 let ?x = ... 같은 암시적 매개변수 바인딩이 했을 법한 일이다. 즉, 암시적 함수를 정의할 때, 나중까지 미루지 않고 그 자리에 바로 사전(dictionary)을 채워 넣는 셈이다. 아주 컨텍스트적이다! (물론 Koka는 이것을 대수적 이펙트(algebraic effects)를 사용해 구현하고, 매우 단순한 변환만으로도 올바른 의미론을 얻어낸다.) 결과적으로 이건 전통적인 의미에서의 동적 스코핑은 아니지만, 기술 보고서에서 말하듯 더 나은 추상화를 가능하게 한다.
암시적 값/함수가 Haskell로 역수입될 수 있을지는 잘 보이지 않는다. 최소한 어딘가에 순서를 강제하는 구성요소(예: 모나드)가 숨어 있지 않고서는 말이다. 암시적 함수가 암시적 매개변수와 매우 비슷하게 동작하기는 하지만, 나머지 동적 스코핑(암시적 함수 자체의 바인딩을 포함해서)은 전통적인, 이펙트 기반(코이펙트가 아닌) 동적 스코핑에 불과하다. 그런데 Haskell에서는 이걸 그대로 할 수 없다. 베타 축약 및 에타 전개에서의 타입 보존을 깨뜨리지 않고는 말이다. Haskell은 끝까지 밀고 갈 수밖에 없고, 암시적 매개변수의 명백한 문제점들(이를 reflection이 해결해 준다)을 넘어서고 나면, 대부분의 것들이 꽤 그럴듯하게 잘 돌아가는 듯하다.