@bglbgl gwyng 메시지 패싱이 함수 호출보다 더 근본적인 요소라고 저는 생각합니다. RPC 프레임워크만 봐도 함수 호출을 메시지 패싱으로 구현할 수 있는 것은 자명하죠. 그래서 함수 호출은 메시지 패싱의 특수한 형태로 보아야 한다 생각합니다.
도막도
@domatdo@hackers.pub · 42 following · 34 followers
규칙을 만들고, 부수고, 살펴보고, 고치기를 좋아합니다. 논리를 다루는 일이 즐거워 프로그래밍을 업으로 삼았습니다. 현재는 프론트엔드 플랫폼 엔지니어로 일하고 있습니다.
다음 주제에 특히 관심이 많습니다.
- 프로그래밍 언어론
- 타입 이론
- 소프트웨어 아키텍쳐
- 글쓰기 그리고 맥주와 와인
GitHub
- @ENvironmentSet
blog
- overcurried.com
@bglbgl gwyng 한편으로는 단순 함수 호출로 모델링하기 어려운 상호작용도 있습니다. 일대 다 통신이나 다대 다 통신, 스트리밍 등이 그러하지요. 함수를 사용해서 이들을 모델링하는게 불가능하지는 않겠지만 메시지 패싱으로 모델링하는 것보다 단순하지는 않으리라 생각합니다.
@bglbgl gwyng 가장 원하시는 답은 함수 호출과 메시지 패싱이 근본적으로 어떻게 다르냐?는 것이 될 텐데요. 이따 퇴근하면서 덧붙여보도록 하겠습니다.
@bglbgl gwyng 메시지 패싱이 함수 호출보다 더 근본적인 요소라고 저는 생각합니다. RPC 프레임워크만 봐도 함수 호출을 메시지 패싱으로 구현할 수 있는 것은 자명하죠. 그래서 함수 호출은 메시지 패싱의 특수한 형태로 보아야 한다 생각합니다.
이론적으론 자바는 반쪽짜리 OOP인가 봅니다. 모든 건 객체고, 객체들끼리 함수 호출이 아닌 메시지 패싱을 해야되고, 이 메시지들 처리 방식을 객체가 능동적으로 결정해야 Pure OOP라 부르나 봅니다. 어디까지나 이론적인 얘기입니다. 스몰토크나, 아니면 얼랭등을 만져봤으면 더 쉽게 이해할 것 같은데요. 낮에 객체 지향 얘기가 보여 교양 공부 해봤습니다.
@lionhairdino 자바에서도 기본적인 것은 다 됩니다. 객체지향 프로그래밍을 가능하게 하는 최소 요건을 맞추는 것은 어렵지 않아요. ‘잘‘ 갖추는 것과 사용자들이 그것을 적절히 쓰게 만드는 게 어려운 거고 많은 자바 레거시는…후략
@bglbgl gwyng 월드 와이드 웹에 누구든 웹 서비스를 연결하고 수정하고 분리할 수 있는 것은, 그 과정에서 웹이 절대 망가지지 않는 것은, 상당 부분 월드 와이드 웹이 기초한 프로토콜이 메시지 패싱에 기반하고 있기 때문입니다.
@bglbgl gwyng 가장 원하시는 답은 함수 호출과 메시지 패싱이 근본적으로 어떻게 다르냐?는 것이 될 텐데요. 이따 퇴근하면서 덧붙여보도록 하겠습니다.
@domatdo도막도 넵 그렇습니다. 제가 처음에 올린 단문도 객체지향이 설계에 실질적으로 도움이 되는 사례를 나열해보면 상속을 벗어나지 않는다는 주장이었습니다. 메시지 패싱으로 어떤 설계 문제를 해결했다,는 사례를 저는 아직 모르고, 최소한 잘 알려지지 않은거 같습니다.
@bglbgl gwyng 월드 와이드 웹에 누구든 웹 서비스를 연결하고 수정하고 분리할 수 있는 것은, 그 과정에서 웹이 절대 망가지지 않는 것은, 상당 부분 월드 와이드 웹이 기초한 프로토콜이 메시지 패싱에 기반하고 있기 때문입니다.
@domatdo도막도 그렇다면 저는 메시지 패싱을 잘 활용한 코드의 예시를 보면 설득이 될거 같네요. 저는 그것이 메소드 호출 이상의 의미를 갖는지에 대해 아직 회의적입니다.
@bglbgl gwyng 함수 호출 이상의 의미를 갖는지 의문이라는 말씀이신 거죠?
@domatdo도막도 메시지 패싱은 잘은 모르지만 그런게 있다는건 알고는 있습니다. 그렇다면 Java가 메시지 패싱을 어떻게 지원한다고 할수 있을까요? 또 그걸로 어떻게 설계의 이점을 얻을까요?
@bglbgl gwyng 취급할 수 있는 메시지를 정의하는 방법으로 인터페이스(그리고 메서드 시그니처 선언)을, 메시지 처리 동작을 정의하는 방법으로 메서드 정의를 제공합니다. 메시지 패싱은 복잡계가 외부와 상호작용(~협력)하는 방법으로 작동하고, 이러한 복잡계에게 적절한 역할과 자율성을 부여하면 1) 덜 복잡해서 이해하기 쉬운 구조적인 프로그램을 만들 수 있고 2) 최소한(단순히 코드의 양을 이야기한다기보다는 모듈 수를 비롯한 변경점의 복잡도를 이야기한다 봐주시는게 정확할 겁니다.)의 수정으로 변화에 대응할 수 있는 프로그램을 만들 수 있습니다.
@bglbgl gwyng 객체지향 프로그래밍의 핵심이 메시지 패싱이라는 개념이라는 것은 여러 객체지향 언어를 비교해보면 명확하게 확인할 수 있습니다. 상속, 상태, 접근자, 접근 제한자, 클래스, 프로토타입, 멤버 변수, this 바인딩, 메서드 디스패치 등등 다양한 개념이 여러 언어에서 서로 다르게 나타나는 한편 메시지 패싱이라는 아이디어는 변치 않습니다.
@bglbgl gwyng 객체지향 프로그래밍의 잘못된 적용례로 단순 구조체에 불과한 객체를 양산하는 상태 주도 개발을 꼽는 것도 동일한 맥락에서입니다. 메시지 패싱 없는 객체지향 프로그래밍은 객체지향 프로그래밍이 아니어요.
@bglbgl gwyng (답문이 너무 짧아서 노파심에 덧붙이지는 말입니다만 오해의 여지를 안 만들기 위해 최대한 드라이하게 이야기하려고 노력중이라 여겨주시면 좋겠습니다..ㅎㅎ)
@bglbgl gwyng 객체지향 프로그래밍의 핵심이 메시지 패싱이라는 개념이라는 것은 여러 객체지향 언어를 비교해보면 명확하게 확인할 수 있습니다. 상속, 상태, 접근자, 접근 제한자, 클래스, 프로토타입, 멤버 변수, this 바인딩, 메서드 디스패치 등등 다양한 개념이 여러 언어에서 서로 다르게 나타나는 한편 메시지 패싱이라는 아이디어는 변치 않습니다.
@bglbgl gwyng 메서드에 기반한 메시지 패싱 매커니즘과 인터페이스라는 개념을 제공합니다.
@bglbgl gwyng (답문이 너무 짧아서 노파심에 덧붙이지는 말입니다만 오해의 여지를 안 만들기 위해 최대한 드라이하게 이야기하려고 노력중이라 여겨주시면 좋겠습니다..ㅎㅎ)
@domatdo도막도 좋은 책 추천 감사합니다! 그런데 제가 의심하는게 사실 '객체와 그것들의 협력에 대한 이야기' 요 부분입니다. 가령 Java와 같은 객체지향 패러다임의 언어 프로그래밍 언어가 실제로 객체들의 협력을 위해 언어적으로 어떤 기능을 지원하나요? 저는 그런 기능이 딱히 없다고보고, 그래서 1. Java는 객체지향 언어가 아니다 2. 객체지향 패러다임은 (실질적으로) 그 문제와 별로 관련이 없다. 두 결론 중 하나를 택해야 한다고 생각합니다.
@bglbgl gwyng 메서드에 기반한 메시지 패싱 매커니즘과 인터페이스라는 개념을 제공합니다.
함수형 프로그래밍과 객체지향 프로그래밍은 상호 배제적인 패러다임이 아니다.
@bglbgl gwyng 저는 반대로 상속을 빼놓고도 객체지향을 이야기할 수 있고, 더 나아가서는 응당 그래야 한다고 생각합니다. 상속은 객체지향 프로그래밍을 추구하는 프로그래밍 언어들이 자주 채택한 구현 기법 그 이상 그 이하도 아니지요. 중요한 것은 복잡계를 객체로 모델링하고, 또 그 복잡계를 적절히 조합해 복잡도를 제어하는 일입니다. 객체지향은 객체와 그것들의 협력에 대한 이야기가 되어야 하지 그 외에 어떤 구현 디테일에 대한 이야기가 되어서는 안 된다고 생각해요.
@bglbgl gwyng 조영호님의 <오브젝트>를 강하게 추천드려요.
방금 하스켈 학교에서 객체지향 vs 함수형 떡밥이 n번째로 돌았는데, 나는 그냥 객체지향 = 상속(서브타이핑) 이라고 생각한다. 객체지향에서 상속을 빼면 뭐 남는게 없다. 그래서 객체지향이란 단어를 의미있게 사용하려면 상속이랑 동치시켜 사용할수 밖에 없다고 본다.
근데 상속은 코드를 합성하는 수많은 방법중 하나일 뿐이다. Java같은 언어는 그 수많은 방법중 딱 하나 상속만을 언어 자체에서 지원하는거고, 거기서 벗어나는 다른 유용한 추상화들은 죄다 디자인 패턴이라고 퉁쳐서 부른다. 그래서 객체지향 vs 함수형(= 상속 vs 함수형)은, 나에겐 Monoid vs 타입클래스 같은 비교처럼 들린다. 좌변과 우변이 체급이 안 맞아서 대결이 불성립한다.
@bglbgl gwyng 저는 반대로 상속을 빼놓고도 객체지향을 이야기할 수 있고, 더 나아가서는 응당 그래야 한다고 생각합니다. 상속은 객체지향 프로그래밍을 추구하는 프로그래밍 언어들이 자주 채택한 구현 기법 그 이상 그 이하도 아니지요. 중요한 것은 복잡계를 객체로 모델링하고, 또 그 복잡계를 적절히 조합해 복잡도를 제어하는 일입니다. 객체지향은 객체와 그것들의 협력에 대한 이야기가 되어야 하지 그 외에 어떤 구현 디테일에 대한 이야기가 되어서는 안 된다고 생각해요.
문제에서 복잡한 부분을 잘 발견하는 건지, 그냥 문제를 복잡하게 만드는 것을 잘 하는 건지 헷갈린다. 둘을 유의미하게 구별하고 싶은데.
논리와 low-level data representation을 다뤄볼지, 아니면 함수형 추상 기계들(Turing Machine같은 것이지만 함수형을 위한 것들)을 다뤄볼지
@ailrunAilrun (UTC-5/-4) 저도 함수형 추상 기계가 더 궁금해요. 다양한 시선에서 코드를 이해해보고 싶네요.
사람이라는 게 참 묘하다. 서비스 개발을 시키면 플랫폼 엔지니어링을 하고 싶고, 플랫폼 엔지니어링을 시키면 서비스 개발을 하고 싶어한다. 둘 다 해야 할 때는 이론 공부를 하고 싶다고 하고, 이론 공부를 시키면 서비스 개발을 하고 싶다고 한다.
주어가 계속 바뀌는 이상한 문장인데, 그래서인지 더 마음에 들어서 올렸다. 역시 사람 마음이라는 것은 "내 이야기는 아니고 아는 친구의 친구 이야기인데" 같은 걸로는 쉽게 숨기기 어렵다.
사람이라는 게 참 묘하다. 서비스 개발을 시키면 플랫폼 엔지니어링을 하고 싶고, 플랫폼 엔지니어링을 시키면 서비스 개발을 하고 싶어한다. 둘 다 해야 할 때는 이론 공부를 하고 싶다고 하고, 이론 공부를 시키면 서비스 개발을 하고 싶다고 한다.
For my library and CLI projects, I've been writing documentation before writing any code. It helps me imagine the final interface early on, which usually leads to better design. Sure, sometimes I miss implementation details and have to revise later, but hey—it's just docs. Docs are easy to change.
This tiny habit has surprisingly big payoffs. When I focus on how things will be used rather than how they'll be built, I end up with interfaces that actually make sense.
Anyone else do this? Curious about your experience with documentation-first approaches.
RE: https://hollo.social/@hongminhee/01964c76-ef1e-7994-b3f0-57f967742566
@hongminhee洪 民憙 (Hong Minhee) I never start writing code before I gather enough context for the problem. Listening to possible customer's voices and collecting prior arts are one of the most important part of designing solutions.
@domatdo도막도 그 둘은 같습니다만
thunk
에 congruence rule이 없습니다. 즉 M = L
이어도 thunk(M) = thunk(L)
인 것은 아닙니다. (M
을 계산한 결과가 L
인 경우, thunk(M)
은 지연된 계산이기 때문에 더이상 평가가 진행되지 않아 thunk(L)
이 될 수 없습니다.)
@ailrunAilrun (UTC-5/-4) 아!! 이 지점을 놓치고 있었네요. 감사합니다.
@ailrunAilrun (UTC-5/-4) (force . thunk) m = m이라 볼 수 있지 않았나요?
@ailrunAilrun (UTC-5/-4) 정확한 의미론을 보면 알 수 있겠네요. 아침에 일어나서 한 번 살펴봐야겠습니다 ㅎㅎ
@domatdo도막도
thunk . force
가 좀 난해한 녀석이죠. thunk
를 LISP의 quasi quotation으로, force
를 back quotation으로 이해하면 thunk . force
는 항등입니다만, thunk
를 그냥 지연된 계산으로 이해하면 thunk (M)
은 지연된 M
계산이고 thunk (force (thunk (M)))
은 지연된 force (thunk (M))
계산이라 같다고 하기 미묘합니다. 이 맥락에서 같다는 두 프로그램이 실행되어서 문법적으로 동일한 결과 값을 준다는 이야기일 때요.
@ailrunAilrun (UTC-5/-4) (force . thunk) m = m이라 볼 수 있지 않았나요?
@domatdo도막도 다른 말로는
Up (Down a)
는 모나드(Monad)고 Down (Up a)
는 코모나드(Comonad)가 되는데요, 이를 이용해 CBPV는 Effect 또한 자연스럽게 다룰 수 있습니다. 글에서 다루기에는 소개할 내용이 많아서 생략했지만요.
@ailrunAilrun (UTC-5/-4) 어쩐지 effect system 문서를 보면 CBPV 언급을 자주 하던데 이런 배경이 있었군요. 한 번 찾아봐야겠어요.
@domatdo도막도 그리고
force . thunk
는 항등사상이 맞습니다. M
은 M
이라는 계산을 하는 녀석이고, force (thunk (M))
도 정확히 M
이라는 계산을 하는 녀석이죠.
@ailrunAilrun (UTC-5/-4) 아, thunk . force라고 해야 했는데 force . thunk라 했네요..! 그리고 다시 생각하니 thunk . force도 항등 사상이 맞군요…
PostHog 뉴스레터는 중소규모 팀 맞춤형(즉, 사이드프로젝트 물주는 사람들한테도 좋은..)으로 좋은 내용이 많아서 새 글이 올라올때마다 여기에도 자주 공유하고 싶음
@kodingwarriorJaeyeol Lee 회사에서 posthog을 적극적으로 도입하면서 퀄리티에 많이 놀랐는데 마인드셋이 대단한 팀이었네요. 자주 공유해주시면 큰 도움이 되겠어요. 물론 저도 지금 구독하러 갑니다..~
@ailrunAilrun (UTC-5/-4) 생각해보면 return도 두 언어를 오가는 대응에 해당하네요. 모든 값을 계산으로 바꾼다는 점에서 force보다도 더 thunk의 짝이 되기 좋아보이나 싶지만 조금만 더 생각해보면 둘을 합성해서 항등 사상을 얻을 수가 없으니 아주 잘 어울리는 한 쌍은 못 되는군요. 뭔가 to의 역할을 잘 생각해보면 보이는 게 있을 거 같은데… 고민해봐야겠어요.
@ailrunAilrun (UTC-5/-4) 재밌게도 force . thunk랑 return . thunk 모두 항등 사상이 못 되네요. 이건 값이 ‘~인 것‘이기 때문일까요? ~인 것을 갖고 무언가를 더 하기 위해서 return과 to가 있는 것인가 싶어졌어요.
@ailrunAilrun (UTC-5/-4) 아하. CBPV의 thunk와 force를 일반화하면 두 언어 사이에서 항을 변환하는 사상을 떠올릴 수 있겠네요. 메타프로그래밍 관점에서 생각하면 항을 메타 언어의 데이터로 바꾸는 생성자(lisp의 quote 같은)와 메타 언어의 데이터를 대상 언어의 항으로 되돌리는 eval 생성자를 생각할 수도 있겠어요. 더 흥미로워지는데요?
@ailrunAilrun (UTC-5/-4) 생각해보면 return도 두 언어를 오가는 대응에 해당하네요. 모든 값을 계산으로 바꾼다는 점에서 force보다도 더 thunk의 짝이 되기 좋아보이나 싶지만 조금만 더 생각해보면 둘을 합성해서 항등 사상을 얻을 수가 없으니 아주 잘 어울리는 한 쌍은 못 되는군요. 뭔가 to의 역할을 잘 생각해보면 보이는 게 있을 거 같은데… 고민해봐야겠어요.
@domatdo도막도 수반 논리(Adjoint Logic)에 기반한 언어간 상호운용성(interoperability)입니다. CBPV를 이 관점에서 이해하면 계산 언어와 값 언어간의 상호운용성을 특정한 방식으로 구현했다고 볼 수 있습니다. CBPV가 수반 논리보다는 제한적인 대신 그 제한이 기계 수준에서 이해하는데는 유용한 점이 있다고 비교해 볼 수 있겠네요.
@ailrunAilrun (UTC-5/-4) 아하. CBPV의 thunk와 force를 일반화하면 두 언어 사이에서 항을 변환하는 사상을 떠올릴 수 있겠네요. 메타프로그래밍 관점에서 생각하면 항을 메타 언어의 데이터로 바꾸는 생성자(lisp의 quote 같은)와 메타 언어의 데이터를 대상 언어의 항으로 되돌리는 eval 생성자를 생각할 수도 있겠어요. 더 흥미로워지는데요?
@domatdo도막도 저 개인적으로는 CBPV랑 경쟁하는(?) 체계를 연구하고 있다보니 CBPV의 약진이 맘에는 안 듭니다만
@ailrunAilrun (UTC-5/-4) 연구하고 계신 체계는 어떤 체계인가요?
도막도 replied to the below article:
함수형 언어의 평가와 선택

Ailrun (UTC-5/-4) @ailrun@hackers.pub
함수형 언어(Functional Language)의 핵심
함수형 언어가 점점 많은 매체에 노출되고, 더 많은 언어들이 함수형 언어의 특징을 하나 둘 받아들이고 있다. 함수형 언어, 적어도 그 특징이 점점 대세가 되고 있다는 이야기이다. 하지만, 함수형 언어가 대체 무엇이란 말인가? 무엇인지도 모르는 것이 대세가 된다고 할 수는 없지 않은가?
함수형 언어란 아주 단순히 말해서 함수가 표현식[1]인 언어를 말한다. 다른 말로는 함수가 값이기 때문에 다른 함수를 호출해서 함수를 얻어내거나 함수의 인자로 함수를 넘길 수 있는 언어를 말한다. 그렇다면 이 단순화된 핵심만을 포함하는 언어로 함수형 언어의 핵심을 이해할 수 있지 않을까? 이게 바로 람다 대수(Lambda Calculus)의 역할이다.[2]
람다 대수는 딱 세 종류의 표현식만을 가지고 있다.
- 변수 (xx, yy, …\ldots)
- 매개변수 xx에 인자를 받아 한 표현식 MM(함수의 몸체)을 계산하는 함수 (λx→M\lambda x\to M)
- 어떤 표현식 LL의 결과 함수를 인자 NN으로 호출 (L NL\ N)
이후의 설명에서는 MM 과 NN, 그리고 LL이라는 이름을 임의의 표현식을 나타내기 위해 사용할 것이다. 람다 대수가 어떤 것들을 표현할 수 있는가? 앞에서 말했듯이 람다 대수는 함수의 인자와 함수 호출의 결과가 모두 함수인 표현식을 포함한다. 예를 들어 λx→(λy→y)\lambda x \to (\lambda y \to y) 는 매개변수 xx에 인자를 받아 함수 λy→y\lambda y \to y를 되돌려주는 함수이고, λx→(x (λy→y))\lambda x \to (x\ (\lambda y \to y))는 매개변수 xx에 함수인 인자를 받아 그 함수를 (λy→y\lambda y \to y를 인자로 사용하여) 호출하는 함수이다.
람다 대수(Lambda Calculus)의 평가(Evaluation)
이제 문제는
그래서 람다 대수의 표현식이 하는 일이 뭔데?
이다. 위의 표현식에 대한 소개는 산수로 말하자면 x+yx + y와 같이 연산자(++)와 연산항(xx와 yy)로부터 얻어지는 문법만을 설명하고 있고, 3+53 + 5와 같은 구체적인 표현식을 계산해서 88이라는 결과 값을 내놓는 방식을 설명하고 있지 않다. 이런 표현식으로부터 값을 얻어내는 것을 언어의 "평가 절차"("Evaluation Procedure")라고 한다. 람다 대수의 평가 절차를 설명하는 것은 어렵지 않다. 적어도 표면적으로는 말이다.
- 함수는 이미 값이다.
- 함수 λx→M\lambda x \to M을 NN으로 호출하면 MM에 등장하는 모든 xx을 NN으로 치환(Substitute)하고 결과 표현식의 평가를 계속한다.
이는 겉으로 보기에는 말이 되는 설명처럼 보인다. 하지만 이 설명을 실제로 해석기(Interpreter)로 구현하려고 시도한다면 이 설명이 사실 여러 세부사항을 무시하고 있다는 점을 깨닫게 될 것이다.
- 함수 호출 L NL\ N에서 LL이 (아직) λx→M\lambda x \to M 꼴이 아닐 때는 어떻게 해야하지?
- 함수 호출 (λx→M) N(\lambda x \to M)\ N에서 NN을 먼저 평가하는 게 낫지 않나? xx가 MM에 여러번 등장한다면 NN을 여러번 평가해야 할텐데?
첫번째 문제는 비교적 간단히 해결할 수 있다. LL을 먼저 평가해서 λx→M\lambda x \to M 꼴의 결과 값을 얻어낸 뒤에 호출을 실행하면 되기 때문이다. 반면에 두번째 질문은 좀 더 미묘한 문제를 가지고 있다. 함수 호출의 평가에서 발생하는 이 문제에 구체적인 답을 하기 위해서는 값에 의한 호출(Call-By-Value, CBV)와 이름에 의한 호출(Call-By-Name, CBN)이 무엇인지 이해해야 한다.
값에 의한 호출(Call-By-Value)? 이름에 의한 호출(Call-By-Name)?
앞에서 말한 함수 호출에서부터 발생하는 문제는 사실 함수형 언어에서만 발생하는 문제는 아니다. C와 같은 명령형 언어에서도 함수를 호출할 때 인자를 먼저 평가해야하는지를 결정해야하기 때문이다. 즉 이 문제는 함수를 가지고 있고 함수를 호출해야하는 모든 언어들이 가지고 있는 문제이다.
그렇다면 이 일반적인 문제를 어떻게 해결하는가? 대부분의 언어가 취하는 가장 대표적인 방식은 "값에 의한 호출"("Call-By-Value", "CBV")이라고 한다. 이 함수 호출 평가 절차에서는 함수의 몸체에 인자를 치환하기 전에[3] 인자를 먼저 평가한다. 이 방식을 사용하면 인자를 여러번 평가해야하는 상황을 피할 수 있다.
또 다른 방식은 "이름에 의한 호출"("Call-By-Name", "CBN")이라고 한다. 이 방식에서는 함수의 몸체에 인자를 우선 치환한 후 몸체를 평가한다. 몇몇 언어의 매크로(Macro)와 같은 기능이 이 방식을 사용한다. 얼핏 보기에는 CBN은 장점이 없어보인다. 그러나 함수가 인자를 사용하지 않을 경우는 CBN이 장점을 가진다는 것을 볼 수 있다. 극단적으로 평가가 종료되지 않는 표현식(Non-terminating expression)이 있다면[4] CBV는 종료하지 않고 CBN만이 종료하는 경우가 있음을 다음 예시를 통해 살펴보자. 표현식 (λx→(λy→y)) N(\lambda x \to (\lambda y \to y))\ N이 있다고 할 때, NN이 평가가 종료되지 않는 표현식이라고 하자. 이 경우 CBV를 따른다면 종료하지 않는 NN 평가를 먼저 수행하느라 이 표현식의 값을 얻어낼 수 없지만, CBN을 따른다면 λy→y\lambda y \to y라는 값을 손쉽게 얻어낼 수 있다. 바로 이런 상황 때문에
CBN은 CBV보다 일반적으로 더 많은 표현식들을 평가할 수 있다
고 말한다.
모호한 선택을 피하는 방법
두 방식의 장점을 모두 가질 수는 없을까? 다시 말해서, 어떤 상황에서는 이름에 의한 호출을 사용하고, 어떤 상황에서는 값에 의한 호출을 사용할 수 없을까? 이 질문에 답한 수많은 선구자들 가운데 폴 블레인 레비(Paul Blain Levy)가 내놓은 답인 "값 밀기에 의한 호출"("Call-By-Push-Value", "CBPV")은 함수형 언어의 평가를 기계 수준(Machine level)에서 이해하는데에 있어 강력한 도구를 제공한다. CBPV는 우선 "계산"("Computation")과 "값"("Value")을 구분한다.
- 계산 MM, NN, LL, …\ldots = 함수 λx→M\lambda x \to M 또는 함수 호출 L VL\ V
- 값 VV, UU, WW, …\ldots = 변수 xx
잠깐, 앞서서 함수형 언어에서 함수는 값이라고 하지 않았던가? 이는 값 밀기에 의한 호출에서 함수와 함수 호출을 종전과 전혀 다르게 이해하기 때문이다. 함수 λx→M\lambda x \to M는 스택(Stack)에서 값을 빼내어(Pop) xx라는 이름을 붙인 후 MM을 평가하는 것이고, 함수 호출 L VL\ V는 스택에 값 VV를 밀어넣고(Push)[5] LL을 평가하는 것이다. 따라서 함수 λx→M\lambda x \to M는 평가의 결과가 아닌 추가적인 평가가 가능한 표현식이 된다. 이 구분을 간결하게 설명하는 것이 다음의 CBPV 표어이다.
값은 "~인 것"이다. 계산은 "~하는 것"이다.
그렇지만 함수형 언어이기 위해서는 함수를 값으로 취급할 수 있어야 한다고 했지 않은가? 그렇다. 이를 위해 CBPV는
계산을 강제한다면(force\mathbf{force}) 계산 MM를 하는 지연된 계산인 값 thunk(M)\mathbf{thunk}(M)
을 추가로 제공한다. 이 둘 (force(V)\mathbf{force}(V)와 thunk(M)\mathbf{thunk}(M))을 다음과 같이 문법에 추가할 수 있다.
- 계산 = λx→M\lambda x \to M 또는 L VL\ V 또는 force(V)\mathbf{force}(V)
- 값 = xx 또는 thunk(M)\mathbf{thunk}(M)
CBPV를 완성하기 위해 필요한 마지막 조각은 계산을 끝내는 법이다. 현재까지 설명한 λx→M\lambda x \to M와 L VL\ V 그리고 force(V)\mathbf{force}(V) 는 모두 다음 계산을 이어서 하는 표현식이고, 계산을 끝내는 방법을 제공하지는 않는다. 예를 들어 λx→M\lambda x \to M의 평가는 스택에서 값을 빼내고 계산 MM의 평가를 이어한다. 그렇다면 계산의 끝은 무엇인가? 결과 값을 제공하는 것이다. 이를 위해 return(V)\mathbf{return}(V)를 계산에 추가하고, 이 결과 값을 사용할 수 있도록 M to x→NM\ \mathbf{to}\ x \to N (계산 MM을 평가한 결과 값을 xx라고 할 때 계산 NN을 평가하는 계산) 또한 계산에 추가하면 다음의 완성된 CBPV를 얻는다.
- 계산 = λx→M\lambda x \to M 또는 L VL\ V 또는 force(V)\mathbf{force}(V) 또는 return(V)\mathbf{return}(V) 또는 M to x→NM\ \mathtt{\mathbf{to}}\ x \to N
- 값 = xx 또는 thunk(M)\mathbf{thunk}(M)
이제 CBPV를 얻었으니 원래의 목표로 돌아가보자. 어떻게 CBV 호출과 CBN 호출을 CBPV로 설명할 수 있을까?
- CBV 함수 λx→M\lambda x \to M와 호출 L NL\ N이 있다면, 이를 return(thunk(λx→M))\mathbf{return}{(\mathbf{thunk}(\lambda x \to M))}과 L to x→N to y→force(x) yL\ \mathbf{to}\ x \to N\ \mathbf{to}\ y \to \mathbf{force}(x)\ y로 표현할 수 있다. 즉, CBPV의 관점에서 CBV의 함수는 지연된 원래 계산 λx→M\lambda x \to M을 값으로 되돌려주는 계산으로 이해할 수 있고, 함수 호출 L NL\ N은 함수 부분 LL을 먼저 평가하고 NN을 평가한 뒤 NN의 계산 결과 yy를 스택에 밀어넣고 지연된 계산인 함수 부분 xx의 계산을 강제하는(force(x)\mathbf{force}(x)) 것으로 이해할 수 있다.
- CBN 함수 λx→M\lambda x \to M와 호출 L NL\ N이 있다면, 이를 λx→M\lambda x \to M(단, 변수 xx의 모든 사용을 force(x)\mathbf{force}(x)로 치환함)과 L thunk(N)L\ \mathbf{thunk}(N)로 표현할 수 있다. 즉, CBPV의 관점에서 함수 호출은 L NL\ N은 지연된 NN을 스택에 밀어넣은 뒤 LL의 계산을 이어가는 것으로 볼 수 있다. 이 지연된 NN은 이후에 스택에서 빼내어져 어떤 이름 xx가 붙은 뒤, 이 변수가 사용될 때에야 비로소 계산된다.
다소 설명이 복잡할 수 있으나, 단순하게 말해서 CBPV는 CBV에 따른 상세한 평가 순서와 CBN 따른 상세한 평가 순서를 세부적으로 설명할 수 있는 충분한 기능을 모두 갖추고 있으며, 이를 통해 CBV 함수 호출과 CBN 함수 호출을 모두 설명할 수 있다는 이야기이다.
기계 수준(Machine level)에서의 Call-By-Push-Value의 장점
앞에서는 CBPV가 CBV와 CBN를 모두 설명할 수 있음을 다뤘다. 그러나 CBPV는 프로그래머(Programmer)가 직접 사용하기에는 과도하게 자세한 세부사항들을 포함하고 있기에, 프로그래머가 직접 CBPV를 써서 CBV와 CBN의 구분을 조율하기에는 적합하지 않다. 그렇다면 어느 수준에서 CBV와 CBN을 혼합해 사용할 때 도움을 줄 수 있을까? 바로 람다 대수를 기계 수준으로 컴파일(Compile)할 때이다. 이때는 CBPV가 가진 자세한 세부사항의 표현력이 굉장히 유용해진다.
예를 들어 람다 대수를 기계 수준으로 변환할 때 흔히 필요한 것 중 하나인 항수 분석(Arity analysis)에 대해 이야기해보자. 항수 분석은 함수가 하나의 인자를 받은 뒤 실행되어야 하는지, 혹은 두 인자를 모두 받아 실행되어야 하는지 등을 확인하여 이후에 그에 걸맞는 최적화된 기계어(Machine language)를 생성할 수 있게 도와주는 분석 작업이다. 평범한 람다 대수에서는 항수 분석의 결과를 직접적으로 표현하기 어렵다. 예를 들어 람다 대수의 λx→(λy→y)\lambda x \to (\lambda y \to y)의 경우 이 함수가 xx와 yy를 모두 받아 yy를 되돌려주는 함수인지 (항수가 2인 함수인지), 혹은 xx를 받아 λy→y\lambda y \to y라는 함수를 되돌려주는 함수인지 (항수가 1인 함수인지) 구분할 수 없다. 그러나 이를 CBPV로 변환한 λx→(λy→return(y))\lambda x \to (\lambda y \to \mathtt{return}(y))나 λx→return(thunk(λy→return(y)))\lambda x \to \mathtt{return}(\mathtt{thunk}(\lambda y \to \mathtt{return}(y)))는 각각이 무엇을 뜻하는지 분명히 이해할 수 있다.
- λx→(λy→return(y))\lambda x \to (\lambda y \to \mathtt{return}(y))는 두 변수 xx와 yy를 스택에서 빼낸 뒤 yy의 값을 되돌려주는 함수(항수가 2인 함수)이다.
- λx→return(thunk(λy→return(y)))\lambda x \to \mathtt{return}(\mathtt{thunk}(\lambda y \to \mathtt{return}(y)))는 변수 xx를 스택에서 빼낸 뒤 지연된 계산 λy→return(y)\lambda y \to \mathtt{return}(y)를 돌려주는 함수(항수가 1인 함수)이다.
이런 장점을 바탕으로 CBPV를 더 발전시킨 "언박싱한 값에 의한 호출"("Call-By-Unboxed-Value")을 GHC 컴파일러의 중간 언어(Intermediate language)로 구현하는 것에 대한 논의가 현재 진행되고 있으며 앞으로 더 많은 함수형 컴파일러들이 관련된 중간 언어를 채용하기 시작할 것으로 보인다.
마치며
이 글에서는 함수형 언어의 핵인 람다 대수를 간단히 설명하고 람다 대수를 평가하는 방법에 대해서 다루어보았다. 특히 그 중 값 밀기에 의한 평가(Call-By-Push-Value, CBPV)가 무엇이며 CBPV가 다른 대표적인 두 방법(CBV, CBN)을 어떻게 표현할 수 있는지, 그리고 CBPV의 장점이 무엇인지에 대해서도 다루어 보았다. 이 글에서 미처 다루지 못한 중요한 주제는 CBPV를 기계에 가까운 언어로 번역해보는 것이다. 여기에서는 글이 너무 복잡해지는 것을 피하기 위해 제했으나, CBPV의 장점에서 살펴봤듯 이는 CBPV에 있어 핵심 주제 중 하나이기 때문에 이후에 다른 글을 통해서라도 이 주제를 소개할 기회를 가지고자 한다. 이 글이 CBPV에 대한 친절한 소개글이었기를 바라며 이만 줄이도록 하겠다.
결과 값(Value)을 가지는 언어 표현을 말한다. 예를 들어 1+11 + 1은 22라는 값을 가지는 표현식이지만 (JavaScript의)
let x = 3;
나 (Python의)def f(): ...
은 그 자체로는 값이 없기 때문에 표현식이 아니다. ↩︎다만 실제 역사에서는 람다 대수의 이해와 발견이 함수형 언어의 개발보다 먼저 이루어졌다. 이런 역사적 관점에서는 (이미 많은 수학자들이 이해하고 있던) 람다 대수에 여러 기능을 추가한 것이 바로 함수형 언어라고 볼 수 있다. ↩︎
프로그래밍 언어(Programming Language)는 실제로는 치환을 사용하지 않고 환경(Environment)을 사용하는 경우가 더 많지만 설명의 편의를 위해 다른 언어들 또한 환경 대신 치환에 기반해 평가한다고 가정하겠다. ↩︎
앞서 설명한 람다 대수에서는 이를 쉽게 얻을 수 있다. 오메가(Ω\Omega)라고 부르는 표현식인 (λx→x x) (λx→x x)(\lambda x \to x\ x)\ (\lambda x \to x\ x)의 평가는 값에 의한 호출을 따르든 이름에 의한 호출을 따르든 종료되지 않는다. ↩︎
바로 이 함수 호출을 값 밀기에 기반해 해석하는 데에서 CBPV의 이름이 유래했다. ↩︎
@ailrunAilrun (UTC-5/-4) 람다 대수 스펙이 있다면 축약 방법은 Unspecified behavior라고 볼 수 있겠네요. 이곳저곳에서 Call-By-Push-Value를 자주 언급해서 궁금하던 찰나였는데 친절하게 설명해주신 덕분에 편하게 이해할 수 있었습니다. 늘 감사합니다.
Yacc와 같은 파서 제네레이터에 BNF를 넣으면 파서 코드가 자동으로 생성된다. 그런데 HTTP나 ActivityPub 등의 프로토콜 스펙을 입력으로 넣으면 자동으로 코드를 구현해주는 도구 어디 없나?
@curry박준규 스펙을 입력으로 넣으면 자동으로 코드를 구현해주는 도구가 있긴 합니다. '컴파일러'라고 하죠. (농담입니다 ㅋㅋㅋ)
Facebook에서 특히 자주 발생하는 문제지만 그렇다고 다른데에서 안 터지는 것도 아니라 참 답답
@ailrunAilrun (UTC-5/-4) 저는 그래서 긴 글을 쓸 때에는 메모장에 적어서 옮겨요. 페이스북 같은 환경에서 앱이 고장나거나 잘못 눌러 발생한 네비게이션으로 작성한 컨텐츠가 날라가는 일도 피할 수 있어서 좋아요.