Ailrun (UTC-5/-4)

@ailrun@hackers.pub · 2 following · 33 followers

소개 - Who Am I?

  • 개발자/연구원
  • Haskell Language Server Admin
  • PL Theorist
  • Logician
  • 중증 맥덕

근황 - Recent Interests

  • 언어간 상호운용성(Interoperability) 연구 중
  • 의존적 형이론(dependent type theory) 연구 중
  • 효율적인 레이텍 조판(LaTeX Typesetting) 공부 중
  • Québec 에서 제일 맛있는 Stout 마시는 중
GitHub Page
ailrun.github.io/ko
GitHub
@Ailrun

주의: 프로그래머에게도 수학도에게도 쓸모 있지 않은 그저 생각 놀이로, 어제 모임에서 제가 떠들었던 잡담입니다.

튜링 완전한 프로그램은 따로 메모리를 두어 관리하며 돌아가는데, 람다 산법은 이런 메모리가 없는데도 불구하고, 튜링 완전이 할 수 있는 일은 모두 할 수 있다고 합니다. 왜 그럴 수 있는지, 시작 아이디어가 뭘까 생각해 봤습니다. (슬쩍 보기엔, 학문적으로 긴 여정이 있는데, 그 걸 모두 따라가기엔 벅찬 일이라, 절대 따라가고 싶진 않고, 그저 아이디어 정도만 알고 싶습니다.)

함수형에선, 정보를 "기억memory"하는 역할도 역시 함수가 담당합니다. 기억이 필요할 땐 함수 구조를 주어, 기억 공간을 만든다고 볼 수 있습니다. 함수 합성에서 다음 함수의 인자로 새로 바인딩하며 기억의 역할을 합니다. 애초 메모리 모델이 없는 게 아니라, 다른 구조로 메모리 모델을 구현했다고 볼 수 있는 것 아닌가 싶습니다. (이렇게 말하는 곳은 없습니다.)

람다 함수는, "따로 호출할 일이 없어 이름 없는 함수로 정의한다" 쯤으로 넘어가기엔, 숨어 있는 의미가 너무 큽니다. 분명, 이렇게 넘어갈 일이 아닌데, 역시나 친절히 설명해주는 자료를 아직은 못봤습니다. (많이 찾아 보 거나, 깊게 공부한 건 아니라서, 어딘가에는 있지 않을까 싶어요)

람다 함수로 만들어,

  • 실행 시점 제어
  • 함수 합성 체인 참여
  • 필요한 정보들을 모아 두는 단위
  • 외부와 소통하는 길을 만들어 둘 수 있고, ...

여기에 기억이라는 중요한 역할도 담당하게 합니다.

람다 산법은 매개 변수, 함수 몸체, 적용, 이렇게 3가지 요소만으로 모든 걸 해결합니다. 알론조 처치 아저씨는 어찌 이런 구조를 떠올렸을까 싶습니다. 애초에 위와 같은 식으로 볼 수 있는 함수 통찰의 눈이 먼저 있었던 상태에서 만든 거겠지요?

2

"AI[1] 덕분에 ***가 민주적이 된다"는 말이 자주 나오고는 한다. 나는 이에 매우 회의적이다. 다른 것은 다 받아들여서 AI가 많은 사람들이 어떤 일을 매우 싸게 (거의 공짜로) 할 수 있게 해준다고 쳐도, AI로 만들어내기 힘든 수준의 "고급" 결과물은 여전히 존재하고, 이는 특정 직군의 고경력 전문가에게 넘어가게 된다. 문제는 이 "경력"을 어떻게 쌓느냐이다. 해당 직군의 초보자들은 AI에 밀려 경쟁력을 잃기 십상이고, 그들을 위한 여러 자리를 (AI를 활용하여 생산성을 높인) 전문가 한 명이 대체하기도 한다. 어떤 직군을 가질 수 있는 조건이 말하자면 이미 엄청난 실력을 가질 것이 되는 환경의 조성을 "민주적이다"라고 할 수 있을까?


  1. 정확히는 최근 많이 사용되는 미디어(Media) 생성 인공지능들과 대형 언어 모형(Large Language Model, LLM)들 ↩︎

1

"AI[1] 덕분에 ***가 민주적이 된다"는 말이 자주 나오고는 한다. 나는 이에 매우 회의적이다. 다른 것은 다 받아들여서 AI가 많은 사람들이 어떤 일을 매우 싸게 (거의 공짜로) 할 수 있게 해준다고 쳐도, AI로 만들어내기 힘든 수준의 "고급" 결과물은 여전히 존재하고, 이는 특정 직군의 고경력 전문가에게 넘어가게 된다. 문제는 이 "경력"을 어떻게 쌓느냐이다. 해당 직군의 초보자들은 AI에 밀려 경쟁력을 잃기 십상이고, 그들을 위한 여러 자리를 (AI를 활용하여 생산성을 높인) 전문가 한 명이 대체하기도 한다. 어떤 직군을 가질 수 있는 조건이 말하자면 이미 엄청난 실력을 가질 것이 되는 환경의 조성을 "민주적이다"라고 할 수 있을까?


  1. 정확히는 최근 많이 사용되는 미디어(Media) 생성 인공지능들과 대형 언어 모형(Large Language Model, LLM)들 ↩︎

4

원숭이가 키보드를 쳐도 수 억번 치면 확률상 실제 동작하는 프로그램이 나오는데, LLM이 로컬에서 브루트포스를 할 수 있는 때가 오면, 테스트 통과 조건만 깔짝대면 완성된 프로그램이 나오는 때가 얼마 안남았네요.

@lionhairdino 사실 테스트 통과 조건만 가지고 프로그램을 완성하는 건 (더 잘 정립된 방법으로) 현재에도 이곳저곳에 활용되고 있습니다. 대표적으로 엑셀(Excel)의 플래쉬 필(Flash Fill)이 있지요. 이와 관련된 분야를 프로그램 합성(Program Synthesis)라고 하고, 수십년 전부터 (AI보다는 투명한 방법으로) 꽤 활발하게 연구되어왔습니다.

4

논리와 메모리 - 논리와 저수준(Low-level) 자료 표현(Data representation) (2 편 중 2 편)

Ailrun (UTC-5/-4) @ailrun@hackers.pub

이 글은 "논리적"이 되는 두 번째 방법인 논건 대수를 재조명하며, 특히 컴퓨터 공학적 해석에 초점을 맞춥니다. 기존 논건 대수의 한계를 극복하기 위해, 컷 규칙을 적극 활용하는 반(半)공리적 논건 대수(SAX)를 소개합니다. SAX는 추론 규칙의 절반을 공리로 대체하여, 메모리 주소와 접근자를 활용한 저수준 자료 표현과의 커리-하워드 대응을 가능하게 합니다. 글에서는 랜드(∧)와 로어(∨)를 "양의 방법", 임플리케이션(→)을 "음의 방법"으로 구분하고, 각 논리 연산에 대한 메모리 구조와 연산 방식을 상세히 설명합니다. 특히, init 규칙은 메모리 복사, cut 규칙은 메모리 할당과 초기화에 대응됨을 보여줍니다. 이러한 SAX의 컴퓨터 공학적 해석은 함수형 언어의 저수준 컴파일에 응용될 수 있으며, 논리와 컴퓨터 공학의 연결고리를 더욱 강화합니다. 프랭크 페닝 교수의 연구를 바탕으로 한 SAX는 현재도 활발히 연구 중인 체계로, ML 계열 언어 컴파일러 개발에도 기여할 수 있을 것으로 기대됩니다.

Read more →
12
3
4

생각보다 훨씬 자세하게 명문화된 규칙이 있군요.! 공부하면서 이 예시 저 예시 보다 보면, 버전이 널뛰고, 그러다 보면 HLS 지원이 오락 가락하게 되어, 사실 각 잡고 프로젝트에 쓰는 것 아니면, 의존도가 높지 않게 되어버리더라고요. 그 이유가 버전 지원 때문이란 생각에, 미리 HLS 친화적인 버전을 골라낼 수 있나 했는데, 그러긴 어려울 것 같네요.

0
1
1
1

@ailrunAilrun (UTC-5/-4) 정말 좋은 글 정말 감사합니다. 아직도 CS관련 글을 보다가 가로로 긴 직선만 나오면 "이거 내가 읽어도 되나?" 싶은 생각이 들면서 읽기를 포기하곤 하는데 기초적인 부분부터 너무 잘 설명해주셔서 끝까지 읽을 수 있었습니다. (사실 두번 읽었습니다.)

자연 연역의 E/I 룰에서는 항상 가정의 목록이 유지되거나 줄어들어 "가정의 소비(?)"된 정도를 기준으로 증명을 순차적으로 구성하거나 파악하는게 유리한 대신 전건과 후건을 다루는데 있어서 그 대칭성이 보이지 않는다. 대신 이와 동등한 논건대수에서는 추론 규칙에 전건과 후건 사이의 대칭성을 명백히 드러냄으로써 추론 시스템 자체의 특정 구조적인 성질을 이해하는데 유리할 수 있다.

라는 생각이 들었는데 이게 올바른 관찰일까요?

"이 내용들이 2 편에서 다룰 본격적인 논리와 자료 표현의 관계에 대해 흥미를 불러일으켰기를 바라며" -> 2편 정말 기대됩니다. ㅎ

@jhhuhJi-Haeng Huh

자연 연역의 E/I 룰에서는 항상 가정의 목록이 유지되거나 줄어들어 "가정의 소비(?)"된 정도를 기준으로 증명을 순차적으로 구성하거나 파악하는게 유리한 대신 전건과 후건을 다루는데 있어서 그 대칭성이 보이지 않는다. 대신 이와 동등한 논건대수에서는 추론 규칙에 전건과 후건 사이의 대칭성을 명백히 드러냄으로써 추론 시스템 자체의 특정 구조적인 성질을 이해하는데 유리할 수 있다.

  • 증명을 위에서 아래로 읽으면 자연 연역의 경우 가정이 줄어들기만 하는 것이 맞습니다. 다만 프로그램적인 측면에서는 아래에서 위로 (가정이 늘어나는 방향으로) 읽는 것이 더 자연스러운데요, 이는 가정이 늘어나는 것이 프로그램에서 깊은 스코프(scope)로 들어갈 수록 더 많은 변수를 소개하는 것과 같은 개념이기 때문입니다.
  • "증명을 순차적으로 구성하기 쉽다"는 사실 약간 애매하기는 합니다. 둘 다에 익숙해지면 논건 대수의 증명이 (기계적으로 찾기에) 더 쉽기 때문에요. 실제로 증명 검색 알고리즘(proof search algorithm, 어떤 판단을 증명하는 증명을 찾는 알고리즘)도 논건 대수에 기반하는 경우가 더 많습니다. 다만 이미 만들어진 증명을 "파악"(혹은 이해)하는 데에 있어서는 (프로그래머로서는) 자연 연역의 함수형 프로그램스러운 증명이 훨씬 쉽지요.
  • "대칭성"과 관련한 관찰은 논건 대수의 발전에 있어서 핵심이라고 볼 수 있습니다. 뛰어난 직관을 가지고 계시네요.
3

"여론에 따라" 함수형 추상 기계 관련 글을 쓰고 있었는데, 쓰다 보니 논리와 low-level data representation 이 보였다는 핑계 말씀이지요? 생각할 엄두조차 내지 못했던 주제들, 다 이해는 못하지만 감사히 보고 있습니다. SPJ, 와들러 교수님 등의 교양 수준 강의 활동들을 보다 보면, 왜 국내 교수님(고인물-댓가없이 입문자들에게 도움을 준다는 의미)들은 없나 아쉬운데, 엘룬님 글로 조금 달래지네요. @ailrunAilrun (UTC-5/-4)

0

@lionhairdino 예를 들면 "다양한 시선에서 코드를 이해한다"가 지금 공개한 연작의 2 편에서 논건 대수를 통한 계산의 이해와도 밀접한 연관이 있다고 읽혔습니다. 원래 두번째 주제에 대해서 쓰다가 방향성을 틀었네요.

1

@bglbgl gwyng 자연 연역, 즉 함수형 언어에서 (치환을 통해) "더 간단한 증명"을 해서 본문에 등장하는 문제를 피할 수 있는 것처럼 논건(論件) 대수에서도 (cut이 없는) "더 간단한 증명"을 할 수 있다는 이야기였습니다. 본문에서 설명이 조금 부족했던 것 같네요.

1

@ailrunAilrun (UTC-5/-4) 좋은 글 감사합니다!

저런 식의 증명 대신 L에서 x가 등장하는 자리에 M을 치환해넣고, y가 등장하는 자리에 N을 치환해넣은 증명 또한 가능하기 때문이다. 이 직관을 구체화한 것이 바로 다음의 cut 제거 정리이다.

요부분이 좀 헷갈리는데요, 글에서 소개된 Sequent Calculus에서는 치환에 대한 설명이 없는 것으로 보입니다. 함수형 언어에서의 치환에 해당하는 Sequent Calculus에서의 규칙이 추가로 필요한데 생략하신 걸까요, 아니면 글의 내용만으로 설명가능한데 제가 뭔가 놓친걸까요?

@bglbgl gwyng 자연 연역, 즉 함수형 언어에서 (치환을 통해) "더 간단한 증명"을 해서 본문에 등장하는 문제를 피할 수 있는 것처럼 논건(論件) 대수에서도 (cut이 없는) "더 간단한 증명"을 할 수 있다는 이야기였습니다. 본문에서 설명이 조금 부족했던 것 같네요.

0

논리와 저수준(Low-level) 자료 표현(Data representation)에 대한 글 2 편 중 첫째 편입니다. "논리적이 되는 두 가지 방법"이라는 부제로 논리적 증명을 하는 두 방법론에 대해서 다뤄보았습니다. 이 중 하나의 방법론이 둘째 편의 핵심이 될 예정입니다.



RE: https://hackers.pub/@ailrun/2025/%EB%85%BC%EB%A6%AC%EC%99%80-%EC%A0%80%EC%88%98%EC%A4%80-low-level-%EC%9E%90%EB%A3%8C-%ED%91%9C%ED%98%84-data-representation-1-%ED%8E%B8

4
1

논리적이 되는 두 가지 방법 - 논리와 저수준(Low-level) 자료 표현(Data representation) (2 편 중 1 편)

Ailrun (UTC-5/-4) @ailrun@hackers.pub

이 글은 어떤 문장이 "논리적"이라고 할 수 있는지에 대한 심도 있는 탐구를 시작합니다. 일상적인 오용을 지적하며, 진정으로 논리적인 주장은 증명 가능성과 체계의 무모순성이라는 두 가지 핵심 조건을 충족해야 한다고 주장합니다. 특히, "좋은 가정 아래" 논리성을 증명하는 두 가지 방법, 즉 함수형 언어와 유사한 구조를 가진 자연 연역과, 약간의 "부정행위"를 통해 무모순성을 쉽게 보일 수 있는 논건 대수를 소개합니다. 글에서는 명제와 판단의 개념을 명확히 정의하고, 자연 연역을 통해 논리적 증명을 구축하는 방법을 상세히 설명합니다. 특히, 자연 연역과 함수형 언어 간의 놀라운 유사성, 즉 커리-하워드 대응을 통해 논리적 사고와 프로그래밍 언어 이해 사이의 연결고리를 제시합니다. 또한, 자연 연역의 한계를 극복하고 무모순성을 보다 쉽게 증명할 수 있는 논건 대수를 소개하며, 자연 연역과의 구조적 차이점을 강조합니다. 이 글은 논리적 사고의 깊이를 더하고, 프로그래밍 언어와 논리 간의 관계에 대한 흥미로운 통찰을 제공합니다. 특히, 커리-하워드 대응을 통해 논리와 프로그래밍이 어떻게 연결되는지 이해하고 싶은 독자에게 유익할 것입니다.

Read more →
12

말씀하신 것들을 보아 몇몇 분들은 실제 내용 측면에서는 논리와 저수준 자료표현에 더 관심을 가질 분들이 있는 것 같아 전자를 (먼저?) 다뤄보도록 하겠습니다. 2 편으로 나눠져 올라갈 예정입니다.

6

완전한 검정 배경에 하얀 글자는 대비가 높아, 눈에 잔상이 남습니다. 눈의 피로를 덜기 위해 다크 모드를 주 톤으로 선택했다면, 대비를 적당 수준으로 낮춰야 합니다. 나이가 올라갈 수록 영향을 받습니다. 모르시는 분들이 많은 것 같습니다.

2
3
9
1

답 댓글이 아니라, 질문 댓글입니다. 레코드 업데이트 하는 동안에 반드시 레코드 타입을 먼저 알아야 한다는 게 "정상"이라는 거지요?

bar :: T Int
-- bar = emptyT --- 허용
bar = emptyT { x = [3] } --- 레코드 업데이트 중에는 타입 specialize를 못하니 불가

@bglbgl gwyng

1
1

https://gitlab.haskell.org/ghc/ghc/-/wikis/migration/9.6#superclass-expansion-is-more-conservative

내가 9.4 -> 9.6 마이그레이션에서 겪고 있는 문제가 이거랑 관련이 있는거 같은데(확실치 않음)... 9.4에서는 c :: Type -> Constraint 일때 forall c. c Int 뭐 이런 조건이 있으면, 모든 c에 대해 c Int가 존재하는게 말이 안되는데도 실제로 c Int 꼴로 쓰이는 c만 고려해서 타입체크를 통과시켜줬던거 같다(이것도 확실하지 않음). 근데 9.6에선 당연히 거부당한다.

위의 내 이해가 맞다면 9.4의 constraint solving 완전 무근본이었단건데, 이건 또 믿기 어렵다(하스켈의 설계 결정에 대한 신뢰 유지한다고 하면). 어디서 내가 잘못 파악한거지.

@bglbgl gwyng 이 변화는 Paterson-smaller 제약들(Constraints), 즉 어떤 정렬 순서(Well-order)에 의해 더 작은 제약들만 확장하겠다는 확장 순서의 변화고, 이를 제외하면 양상자(quantifier)와의 상호작용은 크게 변하지 않았습니다. 따라서 설명하신 것과는 다른 방식으로 오류가 발생하게 되지 않았나 싶습니다.

3

adjoint도 두 개가 짝을 이뤄 뭔가를 완성하는 느낌이 있었는데, dual도 뭔가 시스템이 완성?안정?된 느낌도 들고 그러네요.
아직 콕 짚지는 못하지만, 프로그램 설계할 때, 기계적으로 따르기만 해도 도움이 될 것 같은 요소가 있을 것만 같아서, 가끔 이해하고 싶은 욕구가 생기는데, "화살표를 싹다 뒤집으면 듀얼"이야 같은 말로는 응용할 수 있을만한 이해에 도달을 못하고 있습니다. 인문학스런 해석 질문은 그만 드려야겠지요. @ailrunAilrun (UTC-5/-4)

@lionhairdino 쌍대는 너무 일반적인 개념이라 쌍대 자체를 응용하기에는 구체성이 모자라고요, 어떤 것의 쌍대를 이용할 것인가를 살펴봐야지요. 이를테면 A의 쌍대를 A -> Void로 생각할 때에는 쌍대의 쌍대가 (A -> Void) -> Void, 즉 Continuation Passing Style에 해당하는 변환을 주지요.

1

아하!하고 답 달려고 고민했는데, 역시나 어렵네요. 둘이 서로 반쪽같은 것들로, 둘을 가지고 있으면 뭔가 되는 그런 "느낌"이라고 뿐이 감이 안오네요. 0에서 +1로 갔다가 -1로 오면 "역"이지만, 0에서 +1로 가고, 0에서 -1로 가는 건 듀얼로 볼 수 있겠지요? 똑 떨어지지 않는 인문학 같아요. @ailrunAilrun (UTC-5/-4)

2

얼마 전에 LaTeX에 2020년에(;;) 추가된 NewDocumentCommand에 대해 알게됐는데, 왜 이제야 알았는지 억울할 지경이다. 진작부터 편하게 선택 인자의 순번이랑 선택인자 구분자(기본은 []인 것)를 바꿔가면서 썼다면 얼마나 좋았을까\ldots

3
3
2
1

@lionhairdino @domatdo도막도 함수의 경우에는 그냥 완전히 다른 해석을 하고 있기 때문이라고 보시면 됩니다. 원래의 람다 함수를 기계 수준에서 이해하려고 한다면 함수 정의로 jump하는 개념을 항상 포함하고 있다고 볼 수 있습니다. 이와는 다르게 CBPV는 값과 계산을 나눔으로서 람다 함수를 그냥 빼내기 연산(Pop) 이후 다른 연산을 이어하는 것으로 바꾼 거지요.

1

(비전공자가 볼 내용은 아닌 것 같긴 한데요.) 취미 공부하는 사람의 질문입니다.
"인자를 받는다"는 행위를
"인자를 스택에 넣는다" 와 "인자를 스택에서 빼낸다" 둘 로 쪼개어
보는 아이디어에서 시작하는 것으로 보면 되나요?
그래서 adjoint란 용어가 아른 거리는 건가 싶습니다.

람다식을, 인자가 아직 들어오지 않아 reduce할 게 없는 으로 볼 때는
[1.인자를 스택에 push ---2.인자를 스택에서 pop] 이 없어 reduce할 게 없는 상태로 봤는데,
CBPN에선 [2.인자를 스택에서 pop]은 있고, 이 걸로 reduce할 게 있는 상태.
로 본다는 얘기인가요?

위 설명에서 질문하고 싶은 것도 한 가득이고, @domatdo도막도 님의 이어지는 질문도 어렵긴 한데, 뭔가 전부는 아니더라도 "제가 필요한 정도"의 것은 건져갈 게 보이는 것 같아 질문드립니다.
@ailrunAilrun (UTC-5/-4)

@lionhairdino @domatdo도막도 함수의 경우에는 그냥 완전히 다른 해석을 하고 있기 때문이라고 보시면 됩니다. 원래의 람다 함수를 기계 수준에서 이해하려고 한다면 함수 정의로 jump하는 개념을 항상 포함하고 있다고 볼 수 있습니다. 이와는 다르게 CBPV는 값과 계산을 나눔으로서 람다 함수를 그냥 빼내기 연산(Pop) 이후 다른 연산을 이어하는 것으로 바꾼 거지요.

2

@domatdo도막도 그 둘은 같습니다만 thunk에 congruence rule이 없습니다. 즉 M = L이어도 thunk(M) = thunk(L)인 것은 아닙니다. (M을 계산한 결과가 L인 경우, thunk(M)은 지연된 계산이기 때문에 더이상 평가가 진행되지 않아 thunk(L)이 될 수 없습니다.)

1

@domatdo도막도 thunk . force가 좀 난해한 녀석이죠. thunk를 LISP의 quasi quotation으로, force를 back quotation으로 이해하면 thunk . force는 항등입니다만, thunk를 그냥 지연된 계산으로 이해하면 thunk (M)은 지연된 M 계산이고 thunk (force (thunk (M)))은 지연된 force (thunk (M)) 계산이라 같다고 하기 미묘합니다. 이 맥락에서 같다는 두 프로그램이 실행되어서 문법적으로 동일한 결과 값을 준다는 이야기일 때요.

1
1
1
1

@domatdo도막도 수반 논리(Adjoint Logic)에 기반한 언어간 상호운용성(interoperability)입니다. CBPV를 이 관점에서 이해하면 계산 언어와 값 언어간의 상호운용성을 특정한 방식으로 구현했다고 볼 수 있습니다. CBPV가 수반 논리보다는 제한적인 대신 그 제한이 기계 수준에서 이해하는데는 유용한 점이 있다고 비교해 볼 수 있겠네요.

3
2
2

Hackers' Pub의 문제는 아니고 Firefox랑 Linux 한글 입력기랑 JS기반 입력기의 환장의 콜라보같은데, 긴 글을 치다보면 한글 입력을 가끔 씹는다. 이를테면 "우주로 가자."고 치는데 "우주로 가." 같이 쳐지는 상황\ldots 써진 걸 확인하면 될 일이지만 약간 "Zone"에 들어가서 정신없이 치다가 한 서너줄 전에 글자 하나 빠진 거 발견하면 아주 돌아버린다\ldots

0

Hackers' Pub의 문제는 아니고 Firefox랑 Linux 한글 입력기랑 JS기반 입력기의 환장의 콜라보같은데, 긴 글을 치다보면 한글 입력을 가끔 씹는다. 이를테면 "우주로 가자."고 치는데 "우주로 가." 같이 쳐지는 상황\ldots 써진 걸 확인하면 될 일이지만 약간 "Zone"에 들어가서 정신없이 치다가 한 서너줄 전에 글자 하나 빠진 거 발견하면 아주 돌아버린다\ldots

2

함수형을 공부하기 전, 함수 작업?의 대상을 {함수, 인자}로만 인식했었는데, {함수, 인자, 적용}으로 인식하기 시작하면서 편해지는 게 많아졌습니다. 그런데 이게 편한 이유가 다른 배경 지식이 생겨서 그런 건지, 이 길로 가는게 쉬운 길이어서 그런 건지는 잘 모르겠습니다. 여기서 시작하면 $<$>가 자연스러워 보였습니다. 혼자 결론에 도달하고는, 누군가가 처음 공부할 때 알려줬으면 좋았겠는데 싶었습니다. 그냥 혼자 생각입니다.

꾸벅 꾸벅 조는 건 완전 동의입니다. 제가 혼자 그 공부하면서 졸았습니다. @xtjuxtapose

2

함수형 언어의 평가와 선택

Ailrun (UTC-5/-4) @ailrun@hackers.pub

함수형 언어(Functional Language)의 핵심

함수형 언어가 점점 많은 매체에 노출되고, 더 많은 언어들이 함수형 언어의 특징을 하나 둘 받아들이고 있다. 함수형 언어, 적어도 그 특징이 점점 대세가 되고 있다는 이야기이다. 하지만, 함수형 언어가 대체 무엇이란 말인가? 무엇인지도 모르는 것이 대세가 된다고 할 수는 없지 않은가?

함수형 언어란 아주 단순히 말해서 함수가 표현식[1]인 언어를 말한다. 다른 말로는 함수가 이기 때문에 다른 함수를 호출해서 함수를 얻어내거나 함수의 인자로 함수를 넘길 수 있는 언어를 말한다. 그렇다면 이 단순화된 핵심만을 포함하는 언어로 함수형 언어의 핵심을 이해할 수 있지 않을까? 이게 바로 람다 대수(Lambda Calculus)의 역할이다.[2]

람다 대수는 딱 세 종류의 표현식만을 가지고 있다.

  1. 변수 (xx, yy, …\ldots)
  2. 매개변수 xx에 인자를 받아 한 표현식 MM(함수의 몸체)을 계산하는 함수 (λx→M\lambda x\to M)
  3. 어떤 표현식 LL의 결과 함수를 인자 NN으로 호출 (L NL\ N)

이후의 설명에서는 MMNN, 그리고 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와 같이 연산자(++)와 연산항(xxyy)로부터 얻어지는 문법만을 설명하고 있고, 3+53 + 5와 같은 구체적인 표현식을 계산해서 88이라는 결과 값을 내놓는 방식을 설명하고 있지 않다. 이런 표현식으로부터 값을 얻어내는 것을 언어의 "평가 절차"("Evaluation Procedure")라고 한다. 람다 대수의 평가 절차를 설명하는 것은 어렵지 않다. 적어도 표면적으로는 말이다.

  • 함수는 이미 값이다.
  • 함수 λx→M\lambda x \to MNN으로 호출하면 MM에 등장하는 모든 xxNN으로 치환(Substitute)하고 결과 표현식의 평가를 계속한다.

이는 겉으로 보기에는 말이 되는 설명처럼 보인다. 하지만 이 설명을 실제로 해석기(Interpreter)로 구현하려고 시도한다면 이 설명이 사실 여러 세부사항을 무시하고 있다는 점을 깨닫게 될 것이다.

  1. 함수 호출 L NL\ N에서 LL이 (아직) λx→M\lambda x \to M 꼴이 아닐 때는 어떻게 해야하지?
  2. 함수 호출 (λx→M) N(\lambda x \to M)\ N에서 NN을 먼저 평가하는 게 낫지 않나? xxMM에 여러번 등장한다면 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 ML 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)의 경우 이 함수가 xxyy를 모두 받아 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))는 두 변수 xxyy를 스택에서 빼낸 뒤 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에 대한 친절한 소개글이었기를 바라며 이만 줄이도록 하겠다.


  1. 결과 (Value)을 가지는 언어 표현을 말한다. 예를 들어 1+11 + 122라는 값을 가지는 표현식이지만 (JavaScript의) let x = 3;나 (Python의) def f(): ...은 그 자체로는 값이 없기 때문에 표현식이 아니다. ↩︎

  2. 다만 실제 역사에서는 람다 대수의 이해와 발견이 함수형 언어의 개발보다 먼저 이루어졌다. 이런 역사적 관점에서는 (이미 많은 수학자들이 이해하고 있던) 람다 대수에 여러 기능을 추가한 것이 바로 함수형 언어라고 볼 수 있다. ↩︎

  3. 프로그래밍 언어(Programming Language)는 실제로는 치환을 사용하지 않고 환경(Environment)을 사용하는 경우가 더 많지만 설명의 편의를 위해 다른 언어들 또한 환경 대신 치환에 기반해 평가한다고 가정하겠다. ↩︎

  4. 앞서 설명한 람다 대수에서는 이를 쉽게 얻을 수 있다. 오메가(Ω\Omega)라고 부르는 표현식인 (λx→x x) (λx→x x)(\lambda x \to x\ x)\ (\lambda x \to x\ x)의 평가는 값에 의한 호출을 따르든 이름에 의한 호출을 따르든 종료되지 않는다. ↩︎

  5. 바로 이 함수 호출을 값 밀기에 기반해 해석하는 데에서 CBPV의 이름이 유래했다. ↩︎

Read more →
13
3
0

@curry박준규 더 primitive한 의미론을 포착해서 문법 요소로 만들고 그걸로 다양한 정의를 하게 하는게 낫다는 점에선 동의하는데요. 말씀하신 기준을 그대로 적용해버리면, 모든 언어들이(하스켈도 포함) 발전할수록 점점 키워드가 추가되는데, 그때마다 점점 더 안 좋아졌다고 하면 좀 이상하죠.

@bglbgl gwyng @curry박준규 적은 키워드로 표현 가능한 상황이 더 많다면 "디자인(Design)이 더 잘 되었다"고는 할 수 있다고 봅니다. 언어가 발전한다는게 디자인을 개선한다는 것과 직결되어있지 않은 경우가 더 많으니 말입니다. (많은 경우 표현력의 증대나 자주 사용되는 표현의 단순화에 더 집중하죠)

3

@bglbgl gwyng 이게 생각해보니 ‘문법이 복잡하다’, ‘문법 요소가 적다’라는 표현이 구체적이지 않은 것 같습니다. 갑자기 언어 확장이 떠오르면서 ‘문법 안 복잡한 거 맞나?’ 싶네요. 그래서 표현을 좀 바꾸면 저는 사전에 정의된 키워드가 적을 수록 좋다고 생각합니다.

3