bgl gwyng

@bgl@hackers.pub · 61 following · 63 followers

슈티를 함께 만들 팀을 만들고 있습니다. 관심 있으신 분, 또는 잘 모르겠지만 이야기를 나눠보고 싶은 분도 bgl@gwyng.com으로 편하게 연락주세요.

GitHub
@bglgwyng
shootee
www.shootee.io
1

엔드포인트 솔루션이나 네트워크 장비를 운영하다 보면 그 솔루션 본연의 역할을 지고지순(?) 하게 지키기보다는 뭔가 민원을 해결하는 예외 처리에 리소스를 투입할 때가 많은데 그럴 때마다 뭔가 법을 어긴 것 같고 마음이 안 좋다.

7
0

러스트가 어렵다는 이야기에는 러스트의 특징을 이질적으로 느끼는 경우가 많아서도 있겠지만…어렵다는 말의 재생산이 어렵다는 이미지를 더 굳히는 것 같기도 합니다. 일종의 악순환이라고도 생각하는 게…어렵다는 이미지를 가지고 보면 개념 익히는 것도 어렵고 컴파일 에러를 해결하는 과정도 좀 더 고되게 느낄 수 있거든요. 러스트에 대한 이미지를 어렵다고 생각할수록, 갖고있는 배경지식이 러스트와 이질적일수록 어렵다고 느끼는 사람들이 많은듯합니다

4

뒤늦은 서울숲하스켈 조교 후기: 왠지 모르겠는데, 다들 운동을 열심히 하시는지 몸이 굉장히 좋으셨다. 건강한 신체에 건강한 정신이 깃든다를 실천하고 계신 분들이었다.

...는 농담이고(근데 사실입니다), 커리큘럼이 내가 상상하던 방향이랑은 꽤 달라서 흥미로웠다.

마지막 회차에 하스켈로 웹서버를 띄우는 것을 목표로 진행중이었는데, 이를 위해 Monad Transformer(Monad는 진즉에 해치우고), Tagless Final, Lens를 모두 소개한 상태였다. 근데 저 개념들이 '왜 하스켈에선 이거 안 돼요? 왤케 불편해요?' 같은 질문을 회피하지 않으려면 꼭 가르쳐야 하는 부분들이긴 하다. 가령, 'Monad만 배우면 이제 하스켈에서 명령형 코딩 할수 있다'라는 이야기가 이론상은 맞는데, Monad Transformer나 Algebraic Effect 같은거 안쓰면 웹사이트등 실제로 쓸모있는 프로그램을 사실상 짤수가 없다. 그래서 가르치긴 해야한다.

문제는 저걸 다 가르치려면 상당히 빡셀테니, 나는 만약에 내가 하스켈 부트캠프를 한다면 일단은 저런걸 회피하고 하스켈의 멋진 부분에 집중하는 커리큘럼을 짜야겠다고 그동안 생각했었다. 근데 또 이건 어찌보면 기만이기도 하다. 그런데 서울숲하스켈에서는 어찌저찌 다들 따라오도록 구성을 잘하신것 같다 하스켈을 이질적인(긍정적으로든 부정적으로든) 프로그래밍 언어로 소개하는게 아니라, 언젠가 본인의 작업에 활용할 언어의 후보로 올리게끔 하려면 저런 내용들을 다 다뤘어야 할것이다.

암튼 그동안 수고많으셨습니다. @eunmin은민

7
0

@lionhairdino 들여쓰기를 제외한 토큰을 구분하는 용도로 쓰이는 공백을 그냥 공백이라고 했네요.

들여쓰기가 물론 가독성은 좋지요. 근데 편집할때 불편을 주는 문제가 있고, 에디터가 좀 힘을써주면 양쪽의 장점을 다 취할수있다는 얘기였습니다.

1

저도 비슷한 생각인데, Haskell이나 Rust는 코너 케이스를 다루지 않고는 컴파일도 못 하게 금지하는 경우들이 꽤 많고 (그래서 좋은 언어지요), 빠르게 해피 패스만을 검증하고 싶을 때는 Python 같은 널널한 언어(복잡하고 규모가 큰 소프트웨어를 만들 때는 나쁜 언어가 되지요)가 쉽게 느껴질 수 있다고 생각합니다. 즉, Haskell이나 Rust가 어렵다고 말할 때의 어려움은 개념적 이해의 난도라기 보다는 시행착오의 커브의 경사를 얘기하는 것 같아요.

비슷한 측면에서 저는 Python의 들여쓰기를 강제하는 문법이 프로그래밍 초심자에게 좋은 습관을 처음부터 정착시키는 데에는 일조할 수 있겠지만, 결코 쉽지는 않다고 생각합니다.



RE: https://hackers.pub/@bgl/01967f97-67ab-7a98-a6e5-16cb3ef31856

@hongminhee洪 民憙 (Hong Minhee) 약간 딴 얘긴데, 저는 들여쓰기가 그냥 안좋은 문법요소 같습니다. 코드의 복붙을 unreliable하게 만들어버려서요. 반대로 space sensitive한 문법은 괜찮다고 생각합니다. 복붙시 문제가 생겨도 스페이스 한번 치면 해결되니까요. 들여쓰기 대신에 {} 쌍을 쓰게 만들되, 에디터에서 보여줄때 어떻게 알아서 예쁘게 보여주는게 낫다고 생각해요.

2
develop :: Maybe Coffee -> IO Code
develop Nothing = pure Garbage
develop (Just fuel) = do
code <- think fuel >>= implement
filter (writtenIn Haskell) code

그때 하스켈학교 디스코드에서 코드 백일장 열어서 나온 것들을 조합해서 만들었다.



RE: https://hackers.pub/@bgl/01967fa1-dee1-76ca-93bc-1778c0dc9a75

3

서울숲하스켈보고 예전에 카카오 하스켈부트캠프했던게 생각났다. 그때 굿즈도 만들었다. hoxy 갖고싶은분 계시면 마플에 새로 주문넣어서 보내드립니다.

하스켈 로고```haskell
develop :: Maybe Coffee -> IO Code
develop Nothing = pure Garbage
develop (Just fuel) = do
  code <- think fuel >>= implement
  filter (writtenIn Haskell) code
```
3

Hackers' Pub 저장소에 보내주신 @perlmint 님과 @morealLee Dogeon 님의 CSS 및 PWA 관련 패치들이 모두 적용되어 배포까지 완료되었습니다.

여러분의 많은 기여 감사합니다. 🙏

참고로 현재 hackers.pub에 배포된 게 어떤 버전인지 알고 싶다면 https://hackers.pub/nodeinfo/2.1에 들어가셔서 software.version을 보시면 됩니다. 버전의 마지막 부분인 빌드 넘버가 Git 커밋 해시입니다.

2

러스트가 어렵다는 이야기가 숙고없이 재생산 되는거 같긴 합니다. 제가 러스트를 별로 안써봐서 실제로 얼마나 어려운진 모르겠습니다.

그런데 말씀하신 모나드, 트레잇, 오너십 등의 개념들과 클래스는 좀 차이가 있다고 생각합니다. 그러니까 자바에서 클래스 때문에 어떤 코드를 못짜게 되진 않잖아요? 자바를 하면서 클래스를 제대로 쓰지않고도 뭔가 만들순 있습니다. 반면 전자의 개념들은 잘못된 코드를 짜는걸 막고, 초보자 입장에서 뭔가 하고싶은게 있는데 그게 금지되는 상황에서 어렵다는 느낌을 (필요이상으로 크게) 받을수 있다고 생각합니다.



RE: https://yuri.garden/notes/a75i1vf42f

5
0
7

한달간 써본결과 Cursor >> >Windsurf 란 결론을 내렸다. 근데 Cursor는 프로젝트를 켜지를 못한다는(...왜???) 사소한 문제가 있어서 못쓰고 있다.

3

Rust로 작성한 JPEG XL 디코더, jxl-oxide의 버전 0.12.0을 릴리스했습니다. https://github.com/tirr-c/jxl-oxide/releases/tag/0.12.0

CMYK 프로파일 등 복잡한 ICC 프로파일을 지원하기 위해 기존에 사용하던 Little CMS 2 (lcms2) 에 더해, Rust로 작성된 색 관리 시스템인 moxcms 지원을 추가한 것이 주요 변경사항입니다. CLI 툴의 기본 CMS는 아직 lcms2이지만 --cms moxcms 옵션으로 moxcms를 사용할 수 있습니다.

8
4
2

Hackers' Pub에 GraphQL API를 추가하고 있습니다. https://hackers.pub/graphql가 GraphQL 엔드포인트입니다. 아직 인증 기능도 없고 노출된 노드도 얼마 없습니다만, 차차 추가해 나갈 예정입니다. 다음은 예시 쿼리입니다:

{
  actorByHandle(handle: "@hongminhee@hackers.pub") {
    uuid
    iri
    type
    handle
    instance {
      host
      software
      softwareVersion
    }
    name
    bio
    avatarUrl
    url
  }
}
4

Protected Route에 대해 좀 생각을 더 해봤는데, 난 처음엔 React Native Navigation에서만 쓰는 해괴한 방식인줄 알았다. 근데 좀더 생각해봐도 별로긴하다. 라우터란게 결국 전체 상태중에 path만 별도로 관리해주는 건데,

app :: State -> View
--- router 도입
app :: (Path, State) -> View
--- Protected Route
app :: State -> Path -> View

여기서 대충 말해서 Path -> View가 라우터라 할수 있겠다. Protected Route는 State로부터 라우팅 룰을 동적으로 바꿔버리는 방식이다. 근데 애초에, 라우팅 룰을 정적인 정보로 만들고(즉 ->가 아님) 활용할게 아니라면 라우터를 쓸 이유가 뭘까? 그냥 Path를 일반적인 다른 상태와 똑같이 취급해도 상관없을 것이다. 근데 react-router도 그렇고 애초에 라우팅 룰을 정적인 정보로 알수있는 설계로 되어있지가 않다. Tanstack Router는 예외다.

근데 이러면 그냥 XState 쓰면 되지 않나? 상태/전이에 대한 정적인 정보를 가지고 있고, 또 React Native에서의 라우팅에는 화면 전환이 단순한 Path 변경이 아닌 어떻게 화면을 전환 시킬지(애니메이션 등)도 기술해야하는데, 이 역시 XState 이벤트로 넣으면 된다.

2
2
0

bgl gwyng replied to the below article:

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

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

논리적인 말?

언제 어떤 문장이 "논리적이다"고 할 수 있을까? 일상적으로는 우리는 이를 남용해 의미가 있어 보이는 것을 "논리적이다"라고 수식하고는 한다. 그러나 필자는 이 남용이 "논리적"이라는 표현을 너무 가볍게 보고 있는 것이라 말하겠다. 어떤 것이 진정 논리적이기 위해서는 의미가 있는 것은 물론이거니와 최소한 다음의 두 조건을 더 만족해야한다.

  1. 몇몇 가정으로부터 그것을 증명해야 한다.
  2. 증명에 쓰인 체계가 모순을 보일 수 없어야 한다.

이 글에서는 이 중 1 번을 중점으로 설명하여 "좋은 가정 아래" 어떤 것이 논리적임을 증명하는 두 방법에 대해 다루어 볼 것이다. 두 방법 중 하나는 함수형 언어로 쓰인 프로그램과 유사한 구조를 가지며 나머지 하나는 일반적인 함수형 언어와는 상이한 구조를 가진다. 이 중 두번째 방법에 대해서는 (약간의 부정행위를 하면…\ldots) 2 번을 간단하게 보일 수 있기 때문에 이 또한 다룰 것이다.

논리적 증명의 대상

어떻게 어떤 것이 논리적이라는 걸 증명할 수 있는 지에 설명하기에 앞서 짚고 넘어가야 할 매우 중요한 요소가 하나 있다. 바로

질문

무엇이 논리적이냐?

는 것이다. 이 글에서는 철학적인 복잡도를 덜기 위해 이 대상을 다음과 같이 단순히 설명할 것이다.

논리적일 수 있는 대상은 명제(proposition)에 대한 판단(judgment)이다.

여기서 명제는 다음과 같이 정의되는 대상이다.

  1. 단위 명제(Atomic Proposition) AA는 명제이다.
  2. 참 명제(True Proposition) ⊤\top은 명제이다.
  3. 거짓 명제(False Proposition) ⊥\bot은 명제이다.
  4. 명제 PPQQ가 있을 때, P∧QP \land Q(PP 그리고 QQ)는 명제이다.
  5. 명제 PPQQ가 있을 때, P∨QP \lor Q(PP 또는 QQ)는 명제이다.
  6. 명제 PPQQ가 있을 때, P→QP \to Q(PP라면 QQ)는 명제이다.

여기서 단위 명제란 우리가 선택한, 더이상 세밀하게 분석하지 않을 논리적인 최소 단위이다. 예를 들어 1+1=21 + 1 = 20+2=30 + 2 = 3을 단위 명제로 두었을 때[1] 다음과 같은 것들이 명제이다.

  • ⊤\top
  • 1+1=21 + 1 = 2
  • (1+1=2)∧(0+2=3)(1 + 1 = 2) \land (0 + 2 = 3)
  • (0+2=3)→⊥(0 + 2 = 3) \to \bot
  • ⊤→⊥\top \to \bot

그렇다면 판단이란 무엇인가? 우리가 명제에 가지는 논리적 태도이다. 이 글에서는 다음과 같은 판단만을 이야기 할 것이다.

P trueP\ \texttt{true}, 즉 명제 PP가 참이다.

이를 "진리 판단"("truth judgment")이라고 한다. 좀 더 엄밀히는 다음과 같이 가정을 포함한 판단에 대해 다룰 것이다.

  • Γ⊢P true\Gamma \vdash P\ \texttt{true}

이 판단은 어떤 진리 판단의 나열 Γ\Gamma을 가정했을 때 명제 PP가 참이라고 말하는 판단이다. 이를 "가정적 판단"("hypothetical judgement")이라고 한다. 또한 여기서 Γ\Gamma를 "가정"("hypothesis") 혹은 "전건"("前件", "antecedent")이라고 하고, P trueP\ \texttt{true}를 "귀결("consequent") 혹은 "후건"("後件", "succedent")이라고 한다. 요약하자면 이 절의 핵심 질문

질문

무엇이 논리적일 수 있는 대상이냐?

에 대한 답은 다음과 같다.

명제의 진리에 대한 가정적 판단이 논리적일 수 있는 대상이다.

진짜 논리적이 되는 방법 1 - 자연 연역(Natural Deduction)

이제 어떤 가정적 판단이 논리적인지 증명하는 한 방법에 대해서 알아보자. 우선 PP가 참이라는 판단이 가정 중 하나라면 당연히 그 가정 아래에서는 PP가 참이라고 판단할 수 있을 것이다. 이를 간략하게 표현한 규칙(rule)이

P true∈ΓΓ⊢P truehyp \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\tJ P \in \Gamma} {\Gamma \vdash \tJ P} \text{hyp} \end{equation}

이다. 이 규칙 표현은 줄 위의 것들을 전제(premise)로 줄 밑의 결론(conclusion)을 추론(infer)해 낼 수 있음을 말한다. 전제에 쓰여있는 P true∈ΓP\ \texttt{true} \in \GammaP trueP\ \texttt{true}가 가정의 나열 Γ\Gamma에 포함되어 있음을 말한다. 또한 줄 옆에 써져있는 "hyp"는 이 추론 규칙(inference rule)의 이름으로서 가정(hypothesis)을 사용한다는 것을 나타낸다.

이런 추론 규칙을 명제를 만드는 각각의 방법에 대해 정의할 수 있다. 참 명제 ⊤\top에 대해서는

 Γ⊢⊤ trueI⊤ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\ } {\Gamma \vdash \tJ \top} \text{I}\top \end{equation}

라는 규칙을 줄 수 있다. 이 규칙 I⊤\text{I}\top은 어떤 Γ\Gamma를 가정하든 참 명제는 참이라고 판단할 수 있다는 규칙이다. 여기서 이름의 I\text{I}는 참 명제를 결론에 소개(Introduce)하는 규칙이라는 뜻이다. 대칭적으로 거짓 명제에 대해서는

Γ⊢⊥ trueΓ⊢P trueE⊥ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma \vdash \tJ \bot} {\Gamma \vdash \tJ P} \text{E}\bot \end{equation}

라는 규칙을 줄 수 있다. 이 규칙 E⊥\text{E}\bot은 어떤 가정들 Γ\Gamma로 부터 거짓 명제가 참이라고 판단했다면 그 가정들 아래에서는 아무 명제나 참이라고 판단할 수 있다는 규칙이다[2]. 여기서 이름의 E\text{E}는 우리가 거짓 명제가 참임을 결론으로 가지는 전제에 도달했을 때 그 결론을 제거(Eliminate)하여 다른 결론에 도달할 수 있도록 하는 규칙이라는 뜻이다.

⊤\top⊥\bot을 제외한 명제를 만드는 방법들은 이 두 종류의 규칙(I\text{I} 규칙과 E\text{E} 규칙)을 모두 가진다. P∧QP \land Q의 경우는 다음과 같다.

Γ⊢P trueΓ⊢Q trueΓ⊢P∧Q trueI∧Γ⊢P∧Q trueΓ,P true,Q true⊢R trueΓ⊢R trueE∧ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma \vdash \tJ P \qquad \Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \land Q}} \text{I}\land \qquad \cfrac {\Gamma \vdash \tJ{P \land Q} \qquad \Gamma, \tJ P, \tJ Q \vdash \tJ R} {\Gamma \vdash \tJ R} \text{E}\land \end{equation}

I∧\text{I}\landΓ\Gamma를 가정하여 PPQQ 각각이 참이라 판단했다면 같은 가정에서 P∧QP \land Q도 참이라고 판단할 수 있다는 규칙이다. 반대로 E∧\text{E}\landΓ\Gamma를 가정하여 P∧QP \land Q이 참이라 판단했고 Γ\Gamma에 더해 P trueP\ \text{true}Q trueQ\ \text{true}를 가정했을 때 RR이라는 명제가 참이라고 판단할 수 있다면 Γ\Gamma만 가정하고도 RR이 참이라고 판단할 수 있다는 뜻이다. P∨QP \lor QP→QP \to Q를 위한 규칙도 나열하면 다음과 같다.

Γ⊢P trueΓ⊢P∨Q trueI∨1Γ⊢Q trueΓ⊢P∨Q trueI∨2Γ⊢P∨Q trueΓ,P true⊢R trueΓ,Q true⊢R trueΓ⊢R trueE∨Γ,P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\Gamma \vdash \tJ P} {\Gamma \vdash \tJ{P \lor Q}} \text{I}\lor_1 \qquad \cfrac {\Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \lor Q}} \text{I}\lor_2 \newline\newline \cfrac {\Gamma \vdash \tJ{P \lor Q} \qquad \Gamma, \tJ P \vdash \tJ R \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma \vdash \tJ R} \text{E}\lor \newline\newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{array} \end{equation}

여기서 I∨1\text{I}\lor_1I∨2\text{I}\lor_2PPQQ 중 하나가 참이라고 판단했다면 P∨QP \lor Q가 참이라고 판단할 수 있다는 규칙이며 E∨\text{E}\lorP∨QP \lor Q가 참이라고 판단할 수 있고 PP가 참이라고 가정하든 QQ가 참이라고 가정하든 RR이 참이라고 판단할 수 있다면 그 가정 없이 RR이 참이라고 판단할 수 있다는 규칙이다. I→\text{I}\toPP가 참임을 가정해서 QQ가 참이라고 판단할 수 있다면 P→QP \to Q 또한 참이라고 판단할 수 있다는 규칙이며 E→\text{E}\toP→QP \to Q가 참이라고 판단할 수 있고 PP가 참이라고 판단할 수 있다면 QQ가 참이라고 판단할 수 있다는 규칙이다. 이렇게 소개 규칙과 제거 규칙의 쌍으로 논리적 증명을 하는 방식을 "자연 연역"("Natural Deduction")이라고 한다.

자연 연역으로 단순한 논리적 증명을 하는 방법의 예시를 들어보겠다. 어떤 단위 명제 AA, BB, CC에 대해서 A→BA \to BB→CB \to C가 참이라고 판단했다면 A→CA \to C 또한 참이라 판단할 수 있다. 이 증명은 다음과 같다. 우선 A→BA \to B 그리고 B→CB \to CAA가 참이라고 판단했을 때 BB가 참이라고 다음과 같이 판단할 수 있다.

A→B true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢A→B truehypA true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢A truehypA→B true,B→C true,A true⊢B trueE→ \global\def\tJ#1{#1\ \texttt{true}} \global\def\MyGamma{\tJ{A \to B}, \tJ{B \to C}, \tJ A} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\tJ{A \to B} \in (\MyGamma)} {\MyGamma \vdash \tJ{A \to B}} \text{hyp} \qquad \cfrac {\tJ A \in (\MyGamma)} {\MyGamma \vdash \tJ A} \text{hyp}} {\MyGamma \vdash \tJ B} \text{E}\to \end{array} \end{equation}

이를 써서 다음과 같이 증명을 끝낼 수 있다.

B→C true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢B→C truehypA→B true,B→C true,A true⊢B true⏞위의증명A→B true,B→C true,A true⊢C trueE→A→B true,B→C true⊢A→C trueI→ \global\def\tJ#1{#1\ \texttt{true}} \global\def\MyGamma{\tJ{A \to B}, \tJ{B \to C}, \tJ A} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\cfrac {\tJ{B \to C} \in (\MyGamma)} {\MyGamma \vdash \tJ{B \to C}} \text{hyp} \qquad \raisebox {-0.65em} {\(\overbrace {\MyGamma \vdash \tJ B} ^{위의 증명}\)}} {\MyGamma \vdash \tJ C} \text{E}\to} {\tJ{A \to B}, \tJ{B \to C} \vdash \tJ{A \to C}} \text{I}\to \end{array} \end{equation}

따라서 논리적으로 A→BA \to B 그리고 B→CB \to C가 참임을 가정했을 때 A→CA \to C가 참이라고 판단할 수 있다.

자연 연역과 함수형 언어

혹자는 위 규칙들에서 미묘한 친숙함을 발견했을지도 모른다. 이를 좀 더 구체적으로 살펴보기 위해 →\to의 규칙들을 다시 살펴보자.

Γ,P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{equation}

여기에 약간의 조작을 가해보자. 우선 Γ\Gamma를 단순한 판단의 나열로 취급하는 대신 이름 붙은 판단들로 취급하자. 예를 들어 위의 P trueP\ \texttt{true} 라는 판단에 xx라는 이름을 붙이면 다음과 같은 규칙을 얻는다.

Γ,x:P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, x {:} \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{equation}

더욱이 각각의 규칙들에 지금까지의 증명을 나타내는 항들을 더해보도록 하겠다. 예를 들어 I→\text{I}\to 규칙에서 Γ,x:P true⊢Q true\Gamma, x {:} P\ \texttt{true} \vdash Q\ \texttt{true}가 어떤 항 MM으로 나타내지는 증명이라면 Γ⊢P→Q true\Gamma \vdash P \to Q\ \texttt{true}의 증명은 fun (x:P)⇒M\mathtt{fun}\ (x \mathbin{:} P) \Rightarrow M이라는 항으로 나타내도록 하자. 마찬가지로 E→\text{E}\to 규칙에서 Γ⊢P→Q true\Gamma \vdash P \to Q\ \texttt{true}MM, Γ⊢P true\Gamma \vdash P\ \texttt{true}NN이라는 항으로 나타내진다면 Γ⊢Q true\Gamma \vdash Q\ \texttt{true}의 증명은 M NM\ N으로 나타내도록 하자. 이 항들을 판단에 끼워넣으면 다음과 같이 규칙을 바꿔 쓸 수 있다.

Γ,x:P true⊢M:Q trueΓ⊢fun (x:P)⇒M:P→Q trueI→Γ⊢M:P→Q trueΓ⊢N:P trueΓ⊢M N:Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, x {:} \tJ P \vdash M \mathbin{:} \tJ Q} {\Gamma \vdash \mathtt{fun}\ (x \mathbin{:} P) \Rightarrow M \mathbin{:} \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \to Q} \qquad \Gamma \vdash N \mathbin{:} \tJ P} {\Gamma \vdash M\ N \mathbin{:} \tJ Q} \text{E}\to \end{equation}

이는 (단순한) 함수형 언어의 형 검사 규칙과 동일하다! 함수 fun (x : P) => M이 형 P -> Q를 가지는지 검사하고 싶을 때는 x : P를 가정한 뒤 M이 형 Q를 가지는지 검사하면 된다. 또한 함수 M을 인자 N에 적용하는 것이 형 Q를 가지는지 알고 싶다면 함수 M이 형 P -> Q를 가질 때 인자 N이 형 P를 가지는지 검사하면 된다. 마찬가지 변화를 다음과 같이 모든 추론 규칙들에 적용할 수 있다.

x:P true∈ΓΓ⊢x:P truehyp Γ⊢unit:⊤ trueI⊤Γ⊢M:⊥ trueΓ⊢case M of {}:P trueE⊥Γ⊢M:P trueΓ⊢N:Q trueΓ⊢(M,N):P∧Q trueI∧Γ⊢M:P∧Q trueΓ,x:P true,y:Q true⊢N:R trueΓ⊢case M of (x,y)⇒N:R trueE∧Γ⊢M:P trueΓ⊢Left M:P∨Q trueI∨1Γ⊢M:Q trueΓ⊢Right M:P∨Q trueI∨2Γ⊢M:P∨Q trueΓ,x1:P true⊢N1:R trueΓ,x2:Q true⊢N2:R trueΓ⊢case M of Left x1⇒N1 ∣ Right x2⇒N2:R trueE∨ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {x{:}\tJ P \in \Gamma} {\Gamma \vdash x \mathbin{:} \tJ P} \text{hyp} \newline\newline\newline \cfrac {\ } {\Gamma \vdash \mathtt{unit} \mathbin{:} \tJ \top} \text{I}\top \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ \bot} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ \{\} \mathbin{:} \tJ P} \text{E}\bot \newline\newline\newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ P \qquad \Gamma \vdash N \mathbin{:} \tJ Q} {\Gamma \vdash (M, N) \mathbin{:} \tJ{P \land Q}} \text{I}\land \newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \land Q} \qquad \Gamma, x {:} \tJ P, y {:} \tJ Q \vdash N {:} \tJ R} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ (x, y) \Rightarrow N \mathbin{:} \tJ R} \text{E}\land \newline\newline\newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ P} {\Gamma \vdash \mathtt{Left}\ M \mathbin{:} \tJ{P \lor Q}} \text{I}\lor_1 \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ Q} {\Gamma \vdash \mathtt{Right}\ M \mathbin{:} \tJ{P \lor Q}} \text{I}\lor_2 \newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \lor Q} \qquad \Gamma, x_1{:}\tJ P \vdash N_1 \mathbin{:} \tJ R \qquad \Gamma, x_2{:}\tJ Q \vdash N_2 \mathbin{:} \tJ R} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ \mathtt{Left}\ x_1 \Rightarrow N_1\ |\ \mathtt{Right}\ x_2 \Rightarrow N_2 \mathbin{:} \tJ R} \text{E}\lor \end{array} \end{equation}

⊤\top은 단위(unit) 자료형에 대응되며 ⊥\bot은 빈 자료형에 대응되고 P∧QP \land QP 형과 Q 형의 쌍(pair) 자료형에 대응된다. 같은 방식으로 P∨QP \lor QP 형과 Q 형의 합(sum) 자료형[3]에 대응된다. 항에서부터 각각의 추론 규칙이 (I\texttt{I} 규칙의 경우) 각 자료형의 생성자(constructor)나 (E\texttt{E} 규칙의 경우) 패턴 맞추기(pattern matching)의 형 검사 규칙에 대응된다는 것을 볼 수 있을 것이다. 이를 처음 구체화시킨 두 사람의 이름을 따 이 대응을 "커리-하워드 대응"("Curry-Howard correspondence")이라고 부른다. 이 대응이 발견됨으로써 논리적인 사고와 프로그래밍 언어의 이해 간에 중요한 연결고리가 생겼고 이는 현재에도 프로그래밍 언어의 발전과 논리학의 발전 양측에 모두 지대한 영향을 끼치고 있다.

이 대응을 따랐을 때 어떤 판단을 증명한다는 것은 그 판단의 형을 가지는 프로그램(program)을 짜는 것과 동일하다. 이를 보다 구체적으로 이해하기 위해 앞서의 예시 증명을 다시 반복해보자. 증명의 너비를 줄이기 위해 Γ\Gammax:A→B true,y:B→C true,z:A truex{:}A \to B\ \texttt{true},\allowbreak y{:}B \to C\ \texttt{true}\allowbreak, z{:}A\ \texttt{true} 대신 사용하겠다.

x:A→B true∈ΓΓ⊢x:A→B truehypz:A true∈ΓΓ⊢z:A truehypΓ⊢x z:B trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {x{:}\tJ{A \to B} \in \Gamma} {\Gamma \vdash x \mathbin{:} \tJ{A \to B}} \text{hyp} \qquad \cfrac {z{:}\tJ A \in \Gamma} {\Gamma \vdash z \mathbin{:} \tJ A} \text{hyp}} {\Gamma \vdash x\ z \mathbin{:} \tJ B} \text{E}\to \end{array} \end{equation}

즉 함수 x : A -> B와 인자 z : A가 있을 때 함수 적용 x zB trueB\ \texttt{true}의 증명이다. 이어서,

y:B→C true∈ΓΓ⊢y:B→C truehypΓ⊢x z:B true⏞위의증명Γ⊢y (x z):C trueE→x:A→B true,y:B→C true⊢fun (z:A)⇒y (x z):A→C trueI→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\cfrac {y{:} \tJ{B \to C} \in \Gamma} {\Gamma \vdash y \mathbin{:} \tJ{B \to C}} \text{hyp} \qquad \raisebox {-0.65em} {\(\overbrace {\Gamma \vdash x\ z \mathbin{:} \tJ B} ^{위의 증명}\)}} {\Gamma \vdash y\ (x\ z) \mathbin{:} \tJ C} \text{E}\to} {x{:}\tJ{A \to B}, y{:}\tJ{B \to C} \vdash \mathtt{fun}\ (z \mathbin{:} A) \Rightarrow y\ (x\ z) \mathbin{:} \tJ{A \to C}} \text{I}\to \end{array} \end{equation}

따라서 fun (z : A) => y (x z)x : A -> By : B -> C가 주어졌을 때 A→C trueA \to C\ \texttt{true}의 증명이다.

지금까지 자연 연역에 대해서 알아보았다. 자연 연역은 함수형 언어를 쓰는 사람들에게는 어찌보면 매우 친숙할 수 있는 증명 방식이다. 그러나 자연 연역을 사용했을 때 거짓을 증명할 수 없다는 것을 보이는 것은 쉽지 않다. 자연 연역을 처음 도입한 게르하르트 겐첸(Gerhard Gentzen)은 같은 논문에서 거짓을 증명할 수 없다는 것을 보다 쉽게 보일 수 있는 다른 방식 또한 소개하였는데, 그것이 바로 논건 대수(論件 代數, Sequent Calculus)[4]이다.

진짜 논리적이 되는 방법 2 - 논건 대수(論件 代數, Sequent Calculus)

논건 대수 또한 명제의 진리에 대한 가정적 판단을 추론 규칙을 통해 이끌어낸다는 점은 자연 연역과 동일하다. 자연 연역과 논건 대수의 가장 큰 차이점은 명제를 만드는 각 방법에게 어떤 식으로 추론 규칙을 부여하는지에 있다. 자연 연역에서는 소개 규칙(I\text{I} 규칙)과 제거 규칙(E\text{E} 규칙)이라는 분류를 통해 ∧\land, ∨\lor, …\ldots에 추론 규칙을 부여했다. 논건 대수의 접근법은 이와는 살짝 다르다. 예를 들어 논건 대수에서의 P∧QP \land Q의 규칙에 대해 살펴보자.

Γ⊢P trueΓ⊢Q trueΓ⊢P∧Q trueR∧Γ,P true,Q true⊢R trueΓ,P∧Q true⊢R trueL∧ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\Gamma \vdash \tJ P \qquad \Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \land Q}} \text{R}\land \qquad \cfrac {\Gamma, \tJ P, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \land Q} \vdash \tJ R} \text{L}\land \end{array} \end{equation}

차이점을 눈치챘는가? 크게 두 차이점이 있다.

  1. 규칙의 이름이 다르다. 😅
  2. E∧\text{E}\land을 대체하는 L∧\text{L}\land의 꼴이 다르다.

먼저 이름의 차이를 설명하고 넘어가자. 자연 연역의 경우 I∧\text{I}\land 규칙은 ∧\land를 소개(introduce)하는 규칙, E∧\text{E}\land 규칙은 ∧\land를 제거(eliminate)하는 규칙이었다. 반면에 논건 대수의 R∧\text{R}\land 규칙은 ∧\land를 오른쪽(Right side)에 소개하는 규칙이고 L∧\text{L}\land 규칙은 ∧\land를 왼쪽(Left side)에 소개하는 규칙이다. E∧\text{E}\landL∧\text{L}\land의 꼴의 차이는 이 접근법의 차이에서부터 자연스럽게 따라나온 것이다.

명제를 만드는 다른 방법들에 있어서도 논건 대수의 방식을 따라서 추론 규칙을 마련해 줄 수 있다.

 Γ⊢⊤ trueR⊤ Γ,⊥ true⊢P trueL⊥Γ⊢P trueΓ⊢P∨Q trueR∨1Γ⊢Q trueΓ⊢P∨Q trueR∨2Γ,P true⊢R trueΓ,Q true⊢R trueΓ,P∨Q true⊢R trueL∨Γ,P true⊢Q trueΓ⊢P→Q trueR→Γ⊢P trueΓ,Q true⊢R trueΓ,P→Q true⊢R trueL→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\ } {\Gamma \vdash \tJ \top} \text{R}\top \qquad \cfrac {\ } {\Gamma, \tJ \bot \vdash \tJ P} \text{L}\bot \newline\newline\newline \cfrac {\Gamma \vdash \tJ P} {\Gamma \vdash \tJ{P \lor Q}} \text{R}\lor_1 \qquad \cfrac {\Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \lor Q}} \text{R}\lor_2 \newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ R \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \lor Q} \vdash \tJ R} \text{L}\lor \newline\newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{R}\to \qquad \cfrac {\Gamma \vdash \tJ P \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \to Q} \vdash \tJ R} \text{L}\to \end{array} \end{equation}

그리고 논건 대수가 자연 연역과 동등함을 (비교적) 쉽게 보이기 위해서는 명제를 만드는 각 방식의 R\text{R}L\text{L} 규칙에 더해 다음의 두 규칙을 추가해야한다.

P true∈ΓΓ⊢P trueinitΓ⊢P trueΓ,P true⊢Q trueΓ⊢Q truecut \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\tJ P \in \Gamma} {\Gamma \vdash \tJ P} \text{init} \qquad \cfrac {\Gamma \vdash \tJ P \qquad \Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ Q} \text{cut} \end{array} \end{equation}

여기서 init\text{init} 규칙은 자연 연역에서의 hyp\text{hyp} 규칙과 같은 꼴이다. 반면에 cut\text{cut} 규칙은 직접적으로 비교할만한 자연 연역에서의 규칙이 존재하지 않는데, 이는 논건 대수가 자연 연역과 전혀 다른 방식으로 증명을 전개하기 때문이다. 자연 연역에서는 예를 들어 I∧\text{I}\land 규칙으로 만들어진 P∧QP \land Q가 참이라는 판단을 바로 E∧\text{E}\land에 사용할 수 있었다. 함수형 언어로 말하자면

case (M, N) of
  (x, y) => L

같은 꼴의 사용이 가능했다. 그러나 논건 대수에서는 R∧\text{R}\land 규칙은 가정적 판단의 (오른쪽) 후건에, L∧\text{L}\land 규칙은 가정적 판단의 (왼쪽) 전건에 ∧\land를 새로 만들어낼 뿐이고 두 규칙이 상호작용할 수 있는 방법이 없다. 이 공백을 해결해주는 규칙, 즉 자연 연역의 I\texttt{I}/E\texttt{E} 규칙 쌍의 상호작용에 의한 증명을 논건 대수에서도 표현할 수 있게 해주는 규칙이 바로 cut\text{cut} 규칙이다.

그런데 이 상호작용은 과연 필요한 것인가? 위의 함수형 언어를 통한 예시에서 보다 분명히 볼 수 있는 것은 예시의 I∧\text{I}\landE∧\text{E}\land 규칙의 연달은 사용이 사실은 불필요하다는 점이다. 이는 자연 연역에서는 저런 식의 증명 대신 L에서 x가 등장하는 자리에 M을 치환해넣고, y가 등장하는 자리에 N을 치환해넣은 증명 또한 가능하기 때문이다. 이런 상호작용의 불필요성에 대한 대한 자연 연역에서의 직관을 이에 대응되는 논건 대수의 cut\text{cut} 규칙에 대해서 구체화한 것이 바로 다음의 cut\text{cut} 제거 정리이다.

정리

cut\text{cut} 제거 정리
cut\text{cut}을 사용해 증명 가능한 모든 판단은 cut\text{cut}을 사용하지 않고서도 증명할 수 있다.

cut\text{cut} 제거 정리의 의의는 단순히 필요없는 규칙을 제거하는 정도에 멈추지 않는다. cut\text{cut}을 제거하고 나면 논건 대수에서 가정 없이는 거짓을 증명하지 못한다는 것이 자명해지기 때문이다.

정리

cut\text{cut}을 제거한 논건 대수의 일관성
cut\text{cut}을 제거한 논건 대수는 판단 ⊢⊥ true\vdash \bot\ \texttt{true}를 증명하지 못한다.

증명은 다음의 네 문장이면 충분하다.

  1. 귀결이 ⊥ true\bot\ \texttt{true}R\text{R} 규칙이 존재하지 않기 때문에 어떤 R\text{R} 규칙도 쓸 수 없다.
  2. 가정이 없기 때문에 어떤 L\text{L} 규칙도 쓸 수 없다.
  3. 마찬가지로 가정이 없기 때문에 init\text{init} 규칙을 쓸 수 없다.
  4. 따라서 어떤 규칙도 판단 ⊢⊥ true\vdash \bot\ \texttt{true}을 결론으로 주지 않는다.

앞서 이 글의 서두에서 논건 대수의 일관성을 보이기 위해 "약간의 부정행위"를 저지를 것이라고 말했다. 오해가 없도록 명시하자면 위의 증명은 cut\text{cut}을 제거한 논건 대수에 있어서는 문제가 없다. 부정행위라고 부를 만한 부분은 바로 cut\text{cut} 제거 정리의 증명이 더 어렵다는 점이다. 이 증명은 단순화하기 쉽지 않기 때문에 글에서 직접적으로 다루지는 않을 것이고, 이것이 바로 서두에서 언급한 "부정행위"이다. 다만 이 어려운 증명도 수학적 귀납법(Induction) 이상의 지식은 필요로 하지 않기 때문에, 시도해보고자 하는 독자가 있다면 직접 시도해 볼 수 있을 것이다[5].

논건 대수의 소개를 앞서의 예시 증명을 논건 대수에서 반복하는 것으로 마치도록 하자. 우선 A→BA \to BAA가 참이라고 판단했을 때 BB가 참이라고 다음과 같이 판단할 수 있다.

A true∈(A true)A true⊢A trueinitB true∈(A true,B true)A true,B true⊢B trueinitA→B true,A true⊢B trueL→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\tJ A \in (\tJ A)} {\tJ A \vdash \tJ A} \text{init} \qquad \cfrac {\tJ B \in (\tJ A, \tJ B)} {\tJ A, \tJ B \vdash \tJ B} \text{init}} {\tJ{A \to B}, \tJ A \vdash \tJ B} \text{L}\to \end{array} \end{equation}

이를 써서 다음과 같이 증명을 끝낼 수 있다.

A→B true,A true⊢B true⏞위의증명C true∈(A→B true,A true,C true)A→B true,A true,C true⊢C trueinitA→B true,B→C true,A true⊢C trueL→A→B true,B→C true⊢A→C trueR→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\raisebox {-0.65em} {\(\overbrace {\tJ{A \to B}, \tJ A \vdash \tJ B} ^{위의 증명}\)} \qquad \cfrac {\tJ C \in (\tJ{A \to B}, \tJ A, \tJ C)} {\tJ{A \to B}, \tJ A, \tJ C \vdash \tJ C} \text{init}} {\tJ{A \to B}, \tJ{B \to C}, \tJ A \vdash \tJ C} \text{L}\to} {\tJ{A \to B}, \tJ{B \to C} \vdash \tJ{A \to C}} \text{R}\to \end{array} \end{equation}

앞서의 자연 연역 증명의 구조가 크게 다름에 주의해서 논건 대수 증명의 구조를 이해해보도록 하자. 앞서의 자연 연역 증명은

hyp규칙E규칙↓E규칙↑I규칙결론E규칙 \begin{array}{c} \texttt{hyp} 규칙 \qquad \phantom{\texttt{E} 규칙}\\ \downarrow \qquad \texttt{E} 규칙\\ \uparrow \qquad \texttt{I} 규칙\\ 결론 \qquad \phantom{\texttt{E} 규칙} \end{array}

의 구조를 가지고 있다. 즉 결론에 I\texttt{I} 규칙을 적용하고 어떤 가정에 E\texttt{E} 규칙을 적용해서 이 둘이 만나는 지점을 찾는 것이 증명의 구조이다. 이와는 다르게 논건 대수의 증명은

L규칙init규칙R규칙L규칙↓init규칙↓R규칙결론 \begin{array}{c} \phantom{\texttt{L} 규칙} \qquad \texttt{init} 규칙 \qquad \phantom{\texttt{R} 규칙}\\ \texttt{L} 규칙 \qquad \downarrow \phantom{\texttt{init} 규칙} \downarrow \qquad \texttt{R} 규칙\\ 결론 \end{array}

와 같이 (여러 개의) init\texttt{init} 규칙에서 시작해서 L\texttt{L} 규칙으로 가정, R\texttt{R} 규칙으로 귀결을 변경해 결론에 도달할 때까지 차례로 내려오는 구조를 가진다. 이런 구조의 차이가 바로 (자연 연역의) hyp\texttt{hyp} 규칙과 (논건 대수의) init\texttt{init} 규칙이 (같은 꼴임에도 불구하고) 다른 이름을 가지게 된 이유이자 논건 대수의 init\texttt{init} 규칙이 시작점(initial point)에 해당하는 이름을 가지게 된 이유이다.

논건 대수의 항

논건 대수를 좀 더 컴퓨터 공학적으로 이해할 수 있을까? 이를 위해 자연 연역의 경우와 마찬가지로 논건 대수에도 증명에 커리-하워드 대응을 통해 항을 주려고 시도할 수 있다. 그러나 앞서 언급한 것처럼 논건 대수 증명은 자연 연역의 증명, 그러니까 함수형 언어와 구조가 크게 다르다. 따라서 원본 그대로의 논건 대수에 주는 항은 상대적으로 복잡하고 다양한 개념들(생산자, 소비자, 소비자를 생산자로 바꾸기, 생산자와 소비자가 거래하게 하기, …\ldots)을 요구하고, 결과적으로 이는 컴퓨터 공학적인 직관성이 떨어지는 복잡한 항을 만들어 내게 된다. 이어질 2 편에서는 논건 대수에 약간의 변형을 가하면 직관적이고 쉽게 이해가 가능한 항을 줄 수 있으며, 항이 뜻하는 바가 저수준(low-level) 자료 표현(data representation)과 자연스럽게 연결될 수 있음을 설명할 것이다.

마치며

2 편의 "논리와 저수준 자료표현" 연작 중 1 편인 이 글에서는 논리적 증명을 하는 두 방법과 커리-하워드 대응에 대해서 설명했다. 1 편의 내용은 말하자면 2 편의 바탕 및 서두 역할을 하고 있다. 이 내용들이 2 편에서 다룰 본격적인 논리와 자료 표현의 관계에 대해 흥미를 불러일으켰기를 바라며 이만 마치도록 하겠다.


  1. 이 중 두번째 단위 명제는 일반적인 자연수 체계에서 거짓이나, 명제의 정의 자체는 명제가 참인지 거짓인지에 대해 논하지 않는다는 것을 강조하기 위해 단위 명제에 포함하였다. ↩︎

  2. 거짓도 참이라면 대체 무엇이 참이 아니겠는가? ↩︎

  3. 이는 Rust나 OCaml의 Result 형 혹은 Haskell의 Either형과 같은 형이다. ↩︎

  4. 전건(前件, antecedent)과 후건(後件, succedent)을 개별적으로 다룰 수 있다는 의미에서 "Sequent Calculus"를 논건 대수(論件 代數)라고 번역하였다. 보다 일반적으로 "시퀀트 대수"라는 표현이 쓰이나, 이는 "시퀀트"가 의미하는 바가 "논리에서 다루는 물건/사건/…\ldots"이라는 표현보다 직관성이 떨어진다고 보아 재번역을 하였다. ↩︎

  5. 증명에 대한 구체적인 질문이 있다면 답글을 남겨주기를 바란다. ↩︎

Read more →
9

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

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

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

0

bgl gwyng shared the below article:

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

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

논리적인 말?

언제 어떤 문장이 "논리적이다"고 할 수 있을까? 일상적으로는 우리는 이를 남용해 의미가 있어 보이는 것을 "논리적이다"라고 수식하고는 한다. 그러나 필자는 이 남용이 "논리적"이라는 표현을 너무 가볍게 보고 있는 것이라 말하겠다. 어떤 것이 진정 논리적이기 위해서는 의미가 있는 것은 물론이거니와 최소한 다음의 두 조건을 더 만족해야한다.

  1. 몇몇 가정으로부터 그것을 증명해야 한다.
  2. 증명에 쓰인 체계가 모순을 보일 수 없어야 한다.

이 글에서는 이 중 1 번을 중점으로 설명하여 "좋은 가정 아래" 어떤 것이 논리적임을 증명하는 두 방법에 대해 다루어 볼 것이다. 두 방법 중 하나는 함수형 언어로 쓰인 프로그램과 유사한 구조를 가지며 나머지 하나는 일반적인 함수형 언어와는 상이한 구조를 가진다. 이 중 두번째 방법에 대해서는 (약간의 부정행위를 하면…\ldots) 2 번을 간단하게 보일 수 있기 때문에 이 또한 다룰 것이다.

논리적 증명의 대상

어떻게 어떤 것이 논리적이라는 걸 증명할 수 있는 지에 설명하기에 앞서 짚고 넘어가야 할 매우 중요한 요소가 하나 있다. 바로

질문

무엇이 논리적이냐?

는 것이다. 이 글에서는 철학적인 복잡도를 덜기 위해 이 대상을 다음과 같이 단순히 설명할 것이다.

논리적일 수 있는 대상은 명제(proposition)에 대한 판단(judgment)이다.

여기서 명제는 다음과 같이 정의되는 대상이다.

  1. 단위 명제(Atomic Proposition) AA는 명제이다.
  2. 참 명제(True Proposition) ⊤\top은 명제이다.
  3. 거짓 명제(False Proposition) ⊥\bot은 명제이다.
  4. 명제 PPQQ가 있을 때, P∧QP \land Q(PP 그리고 QQ)는 명제이다.
  5. 명제 PPQQ가 있을 때, P∨QP \lor Q(PP 또는 QQ)는 명제이다.
  6. 명제 PPQQ가 있을 때, P→QP \to Q(PP라면 QQ)는 명제이다.

여기서 단위 명제란 우리가 선택한, 더이상 세밀하게 분석하지 않을 논리적인 최소 단위이다. 예를 들어 1+1=21 + 1 = 20+2=30 + 2 = 3을 단위 명제로 두었을 때[1] 다음과 같은 것들이 명제이다.

  • ⊤\top
  • 1+1=21 + 1 = 2
  • (1+1=2)∧(0+2=3)(1 + 1 = 2) \land (0 + 2 = 3)
  • (0+2=3)→⊥(0 + 2 = 3) \to \bot
  • ⊤→⊥\top \to \bot

그렇다면 판단이란 무엇인가? 우리가 명제에 가지는 논리적 태도이다. 이 글에서는 다음과 같은 판단만을 이야기 할 것이다.

P trueP\ \texttt{true}, 즉 명제 PP가 참이다.

이를 "진리 판단"("truth judgment")이라고 한다. 좀 더 엄밀히는 다음과 같이 가정을 포함한 판단에 대해 다룰 것이다.

  • Γ⊢P true\Gamma \vdash P\ \texttt{true}

이 판단은 어떤 진리 판단의 나열 Γ\Gamma을 가정했을 때 명제 PP가 참이라고 말하는 판단이다. 이를 "가정적 판단"("hypothetical judgement")이라고 한다. 또한 여기서 Γ\Gamma를 "가정"("hypothesis") 혹은 "전건"("前件", "antecedent")이라고 하고, P trueP\ \texttt{true}를 "귀결("consequent") 혹은 "후건"("後件", "succedent")이라고 한다. 요약하자면 이 절의 핵심 질문

질문

무엇이 논리적일 수 있는 대상이냐?

에 대한 답은 다음과 같다.

명제의 진리에 대한 가정적 판단이 논리적일 수 있는 대상이다.

진짜 논리적이 되는 방법 1 - 자연 연역(Natural Deduction)

이제 어떤 가정적 판단이 논리적인지 증명하는 한 방법에 대해서 알아보자. 우선 PP가 참이라는 판단이 가정 중 하나라면 당연히 그 가정 아래에서는 PP가 참이라고 판단할 수 있을 것이다. 이를 간략하게 표현한 규칙(rule)이

P true∈ΓΓ⊢P truehyp \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\tJ P \in \Gamma} {\Gamma \vdash \tJ P} \text{hyp} \end{equation}

이다. 이 규칙 표현은 줄 위의 것들을 전제(premise)로 줄 밑의 결론(conclusion)을 추론(infer)해 낼 수 있음을 말한다. 전제에 쓰여있는 P true∈ΓP\ \texttt{true} \in \GammaP trueP\ \texttt{true}가 가정의 나열 Γ\Gamma에 포함되어 있음을 말한다. 또한 줄 옆에 써져있는 "hyp"는 이 추론 규칙(inference rule)의 이름으로서 가정(hypothesis)을 사용한다는 것을 나타낸다.

이런 추론 규칙을 명제를 만드는 각각의 방법에 대해 정의할 수 있다. 참 명제 ⊤\top에 대해서는

 Γ⊢⊤ trueI⊤ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\ } {\Gamma \vdash \tJ \top} \text{I}\top \end{equation}

라는 규칙을 줄 수 있다. 이 규칙 I⊤\text{I}\top은 어떤 Γ\Gamma를 가정하든 참 명제는 참이라고 판단할 수 있다는 규칙이다. 여기서 이름의 I\text{I}는 참 명제를 결론에 소개(Introduce)하는 규칙이라는 뜻이다. 대칭적으로 거짓 명제에 대해서는

Γ⊢⊥ trueΓ⊢P trueE⊥ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma \vdash \tJ \bot} {\Gamma \vdash \tJ P} \text{E}\bot \end{equation}

라는 규칙을 줄 수 있다. 이 규칙 E⊥\text{E}\bot은 어떤 가정들 Γ\Gamma로 부터 거짓 명제가 참이라고 판단했다면 그 가정들 아래에서는 아무 명제나 참이라고 판단할 수 있다는 규칙이다[2]. 여기서 이름의 E\text{E}는 우리가 거짓 명제가 참임을 결론으로 가지는 전제에 도달했을 때 그 결론을 제거(Eliminate)하여 다른 결론에 도달할 수 있도록 하는 규칙이라는 뜻이다.

⊤\top⊥\bot을 제외한 명제를 만드는 방법들은 이 두 종류의 규칙(I\text{I} 규칙과 E\text{E} 규칙)을 모두 가진다. P∧QP \land Q의 경우는 다음과 같다.

Γ⊢P trueΓ⊢Q trueΓ⊢P∧Q trueI∧Γ⊢P∧Q trueΓ,P true,Q true⊢R trueΓ⊢R trueE∧ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma \vdash \tJ P \qquad \Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \land Q}} \text{I}\land \qquad \cfrac {\Gamma \vdash \tJ{P \land Q} \qquad \Gamma, \tJ P, \tJ Q \vdash \tJ R} {\Gamma \vdash \tJ R} \text{E}\land \end{equation}

I∧\text{I}\landΓ\Gamma를 가정하여 PPQQ 각각이 참이라 판단했다면 같은 가정에서 P∧QP \land Q도 참이라고 판단할 수 있다는 규칙이다. 반대로 E∧\text{E}\landΓ\Gamma를 가정하여 P∧QP \land Q이 참이라 판단했고 Γ\Gamma에 더해 P trueP\ \text{true}Q trueQ\ \text{true}를 가정했을 때 RR이라는 명제가 참이라고 판단할 수 있다면 Γ\Gamma만 가정하고도 RR이 참이라고 판단할 수 있다는 뜻이다. P∨QP \lor QP→QP \to Q를 위한 규칙도 나열하면 다음과 같다.

Γ⊢P trueΓ⊢P∨Q trueI∨1Γ⊢Q trueΓ⊢P∨Q trueI∨2Γ⊢P∨Q trueΓ,P true⊢R trueΓ,Q true⊢R trueΓ⊢R trueE∨Γ,P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\Gamma \vdash \tJ P} {\Gamma \vdash \tJ{P \lor Q}} \text{I}\lor_1 \qquad \cfrac {\Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \lor Q}} \text{I}\lor_2 \newline\newline \cfrac {\Gamma \vdash \tJ{P \lor Q} \qquad \Gamma, \tJ P \vdash \tJ R \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma \vdash \tJ R} \text{E}\lor \newline\newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{array} \end{equation}

여기서 I∨1\text{I}\lor_1I∨2\text{I}\lor_2PPQQ 중 하나가 참이라고 판단했다면 P∨QP \lor Q가 참이라고 판단할 수 있다는 규칙이며 E∨\text{E}\lorP∨QP \lor Q가 참이라고 판단할 수 있고 PP가 참이라고 가정하든 QQ가 참이라고 가정하든 RR이 참이라고 판단할 수 있다면 그 가정 없이 RR이 참이라고 판단할 수 있다는 규칙이다. I→\text{I}\toPP가 참임을 가정해서 QQ가 참이라고 판단할 수 있다면 P→QP \to Q 또한 참이라고 판단할 수 있다는 규칙이며 E→\text{E}\toP→QP \to Q가 참이라고 판단할 수 있고 PP가 참이라고 판단할 수 있다면 QQ가 참이라고 판단할 수 있다는 규칙이다. 이렇게 소개 규칙과 제거 규칙의 쌍으로 논리적 증명을 하는 방식을 "자연 연역"("Natural Deduction")이라고 한다.

자연 연역으로 단순한 논리적 증명을 하는 방법의 예시를 들어보겠다. 어떤 단위 명제 AA, BB, CC에 대해서 A→BA \to BB→CB \to C가 참이라고 판단했다면 A→CA \to C 또한 참이라 판단할 수 있다. 이 증명은 다음과 같다. 우선 A→BA \to B 그리고 B→CB \to CAA가 참이라고 판단했을 때 BB가 참이라고 다음과 같이 판단할 수 있다.

A→B true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢A→B truehypA true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢A truehypA→B true,B→C true,A true⊢B trueE→ \global\def\tJ#1{#1\ \texttt{true}} \global\def\MyGamma{\tJ{A \to B}, \tJ{B \to C}, \tJ A} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\tJ{A \to B} \in (\MyGamma)} {\MyGamma \vdash \tJ{A \to B}} \text{hyp} \qquad \cfrac {\tJ A \in (\MyGamma)} {\MyGamma \vdash \tJ A} \text{hyp}} {\MyGamma \vdash \tJ B} \text{E}\to \end{array} \end{equation}

이를 써서 다음과 같이 증명을 끝낼 수 있다.

B→C true∈(A→B true,B→C true,A true)A→B true,B→C true,A true⊢B→C truehypA→B true,B→C true,A true⊢B true⏞위의증명A→B true,B→C true,A true⊢C trueE→A→B true,B→C true⊢A→C trueI→ \global\def\tJ#1{#1\ \texttt{true}} \global\def\MyGamma{\tJ{A \to B}, \tJ{B \to C}, \tJ A} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\cfrac {\tJ{B \to C} \in (\MyGamma)} {\MyGamma \vdash \tJ{B \to C}} \text{hyp} \qquad \raisebox {-0.65em} {\(\overbrace {\MyGamma \vdash \tJ B} ^{위의 증명}\)}} {\MyGamma \vdash \tJ C} \text{E}\to} {\tJ{A \to B}, \tJ{B \to C} \vdash \tJ{A \to C}} \text{I}\to \end{array} \end{equation}

따라서 논리적으로 A→BA \to B 그리고 B→CB \to C가 참임을 가정했을 때 A→CA \to C가 참이라고 판단할 수 있다.

자연 연역과 함수형 언어

혹자는 위 규칙들에서 미묘한 친숙함을 발견했을지도 모른다. 이를 좀 더 구체적으로 살펴보기 위해 →\to의 규칙들을 다시 살펴보자.

Γ,P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{equation}

여기에 약간의 조작을 가해보자. 우선 Γ\Gamma를 단순한 판단의 나열로 취급하는 대신 이름 붙은 판단들로 취급하자. 예를 들어 위의 P trueP\ \texttt{true} 라는 판단에 xx라는 이름을 붙이면 다음과 같은 규칙을 얻는다.

Γ,x:P true⊢Q trueΓ⊢P→Q trueI→Γ⊢P→Q trueΓ⊢P trueΓ⊢Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, x {:} \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash \tJ{P \to Q} \qquad \Gamma \vdash \tJ P} {\Gamma \vdash \tJ Q} \text{E}\to \end{equation}

더욱이 각각의 규칙들에 지금까지의 증명을 나타내는 항들을 더해보도록 하겠다. 예를 들어 I→\text{I}\to 규칙에서 Γ,x:P true⊢Q true\Gamma, x {:} P\ \texttt{true} \vdash Q\ \texttt{true}가 어떤 항 MM으로 나타내지는 증명이라면 Γ⊢P→Q true\Gamma \vdash P \to Q\ \texttt{true}의 증명은 fun (x:P)⇒M\mathtt{fun}\ (x \mathbin{:} P) \Rightarrow M이라는 항으로 나타내도록 하자. 마찬가지로 E→\text{E}\to 규칙에서 Γ⊢P→Q true\Gamma \vdash P \to Q\ \texttt{true}MM, Γ⊢P true\Gamma \vdash P\ \texttt{true}NN이라는 항으로 나타내진다면 Γ⊢Q true\Gamma \vdash Q\ \texttt{true}의 증명은 M NM\ N으로 나타내도록 하자. 이 항들을 판단에 끼워넣으면 다음과 같이 규칙을 바꿔 쓸 수 있다.

Γ,x:P true⊢M:Q trueΓ⊢fun (x:P)⇒M:P→Q trueI→Γ⊢M:P→Q trueΓ⊢N:P trueΓ⊢M N:Q trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \cfrac {\Gamma, x {:} \tJ P \vdash M \mathbin{:} \tJ Q} {\Gamma \vdash \mathtt{fun}\ (x \mathbin{:} P) \Rightarrow M \mathbin{:} \tJ{P \to Q}} \text{I}\to \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \to Q} \qquad \Gamma \vdash N \mathbin{:} \tJ P} {\Gamma \vdash M\ N \mathbin{:} \tJ Q} \text{E}\to \end{equation}

이는 (단순한) 함수형 언어의 형 검사 규칙과 동일하다! 함수 fun (x : P) => M이 형 P -> Q를 가지는지 검사하고 싶을 때는 x : P를 가정한 뒤 M이 형 Q를 가지는지 검사하면 된다. 또한 함수 M을 인자 N에 적용하는 것이 형 Q를 가지는지 알고 싶다면 함수 M이 형 P -> Q를 가질 때 인자 N이 형 P를 가지는지 검사하면 된다. 마찬가지 변화를 다음과 같이 모든 추론 규칙들에 적용할 수 있다.

x:P true∈ΓΓ⊢x:P truehyp Γ⊢unit:⊤ trueI⊤Γ⊢M:⊥ trueΓ⊢case M of {}:P trueE⊥Γ⊢M:P trueΓ⊢N:Q trueΓ⊢(M,N):P∧Q trueI∧Γ⊢M:P∧Q trueΓ,x:P true,y:Q true⊢N:R trueΓ⊢case M of (x,y)⇒N:R trueE∧Γ⊢M:P trueΓ⊢Left M:P∨Q trueI∨1Γ⊢M:Q trueΓ⊢Right M:P∨Q trueI∨2Γ⊢M:P∨Q trueΓ,x1:P true⊢N1:R trueΓ,x2:Q true⊢N2:R trueΓ⊢case M of Left x1⇒N1 ∣ Right x2⇒N2:R trueE∨ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {x{:}\tJ P \in \Gamma} {\Gamma \vdash x \mathbin{:} \tJ P} \text{hyp} \newline\newline\newline \cfrac {\ } {\Gamma \vdash \mathtt{unit} \mathbin{:} \tJ \top} \text{I}\top \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ \bot} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ \{\} \mathbin{:} \tJ P} \text{E}\bot \newline\newline\newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ P \qquad \Gamma \vdash N \mathbin{:} \tJ Q} {\Gamma \vdash (M, N) \mathbin{:} \tJ{P \land Q}} \text{I}\land \newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \land Q} \qquad \Gamma, x {:} \tJ P, y {:} \tJ Q \vdash N {:} \tJ R} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ (x, y) \Rightarrow N \mathbin{:} \tJ R} \text{E}\land \newline\newline\newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ P} {\Gamma \vdash \mathtt{Left}\ M \mathbin{:} \tJ{P \lor Q}} \text{I}\lor_1 \qquad \cfrac {\Gamma \vdash M \mathbin{:} \tJ Q} {\Gamma \vdash \mathtt{Right}\ M \mathbin{:} \tJ{P \lor Q}} \text{I}\lor_2 \newline\newline \cfrac {\Gamma \vdash M \mathbin{:} \tJ{P \lor Q} \qquad \Gamma, x_1{:}\tJ P \vdash N_1 \mathbin{:} \tJ R \qquad \Gamma, x_2{:}\tJ Q \vdash N_2 \mathbin{:} \tJ R} {\Gamma \vdash \mathtt{case}\ M\ \mathtt{of}\ \mathtt{Left}\ x_1 \Rightarrow N_1\ |\ \mathtt{Right}\ x_2 \Rightarrow N_2 \mathbin{:} \tJ R} \text{E}\lor \end{array} \end{equation}

⊤\top은 단위(unit) 자료형에 대응되며 ⊥\bot은 빈 자료형에 대응되고 P∧QP \land QP 형과 Q 형의 쌍(pair) 자료형에 대응된다. 같은 방식으로 P∨QP \lor QP 형과 Q 형의 합(sum) 자료형[3]에 대응된다. 항에서부터 각각의 추론 규칙이 (I\texttt{I} 규칙의 경우) 각 자료형의 생성자(constructor)나 (E\texttt{E} 규칙의 경우) 패턴 맞추기(pattern matching)의 형 검사 규칙에 대응된다는 것을 볼 수 있을 것이다. 이를 처음 구체화시킨 두 사람의 이름을 따 이 대응을 "커리-하워드 대응"("Curry-Howard correspondence")이라고 부른다. 이 대응이 발견됨으로써 논리적인 사고와 프로그래밍 언어의 이해 간에 중요한 연결고리가 생겼고 이는 현재에도 프로그래밍 언어의 발전과 논리학의 발전 양측에 모두 지대한 영향을 끼치고 있다.

이 대응을 따랐을 때 어떤 판단을 증명한다는 것은 그 판단의 형을 가지는 프로그램(program)을 짜는 것과 동일하다. 이를 보다 구체적으로 이해하기 위해 앞서의 예시 증명을 다시 반복해보자. 증명의 너비를 줄이기 위해 Γ\Gammax:A→B true,y:B→C true,z:A truex{:}A \to B\ \texttt{true},\allowbreak y{:}B \to C\ \texttt{true}\allowbreak, z{:}A\ \texttt{true} 대신 사용하겠다.

x:A→B true∈ΓΓ⊢x:A→B truehypz:A true∈ΓΓ⊢z:A truehypΓ⊢x z:B trueE→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {x{:}\tJ{A \to B} \in \Gamma} {\Gamma \vdash x \mathbin{:} \tJ{A \to B}} \text{hyp} \qquad \cfrac {z{:}\tJ A \in \Gamma} {\Gamma \vdash z \mathbin{:} \tJ A} \text{hyp}} {\Gamma \vdash x\ z \mathbin{:} \tJ B} \text{E}\to \end{array} \end{equation}

즉 함수 x : A -> B와 인자 z : A가 있을 때 함수 적용 x zB trueB\ \texttt{true}의 증명이다. 이어서,

y:B→C true∈ΓΓ⊢y:B→C truehypΓ⊢x z:B true⏞위의증명Γ⊢y (x z):C trueE→x:A→B true,y:B→C true⊢fun (z:A)⇒y (x z):A→C trueI→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\cfrac {y{:} \tJ{B \to C} \in \Gamma} {\Gamma \vdash y \mathbin{:} \tJ{B \to C}} \text{hyp} \qquad \raisebox {-0.65em} {\(\overbrace {\Gamma \vdash x\ z \mathbin{:} \tJ B} ^{위의 증명}\)}} {\Gamma \vdash y\ (x\ z) \mathbin{:} \tJ C} \text{E}\to} {x{:}\tJ{A \to B}, y{:}\tJ{B \to C} \vdash \mathtt{fun}\ (z \mathbin{:} A) \Rightarrow y\ (x\ z) \mathbin{:} \tJ{A \to C}} \text{I}\to \end{array} \end{equation}

따라서 fun (z : A) => y (x z)x : A -> By : B -> C가 주어졌을 때 A→C trueA \to C\ \texttt{true}의 증명이다.

지금까지 자연 연역에 대해서 알아보았다. 자연 연역은 함수형 언어를 쓰는 사람들에게는 어찌보면 매우 친숙할 수 있는 증명 방식이다. 그러나 자연 연역을 사용했을 때 거짓을 증명할 수 없다는 것을 보이는 것은 쉽지 않다. 자연 연역을 처음 도입한 게르하르트 겐첸(Gerhard Gentzen)은 같은 논문에서 거짓을 증명할 수 없다는 것을 보다 쉽게 보일 수 있는 다른 방식 또한 소개하였는데, 그것이 바로 논건 대수(論件 代數, Sequent Calculus)[4]이다.

진짜 논리적이 되는 방법 2 - 논건 대수(論件 代數, Sequent Calculus)

논건 대수 또한 명제의 진리에 대한 가정적 판단을 추론 규칙을 통해 이끌어낸다는 점은 자연 연역과 동일하다. 자연 연역과 논건 대수의 가장 큰 차이점은 명제를 만드는 각 방법에게 어떤 식으로 추론 규칙을 부여하는지에 있다. 자연 연역에서는 소개 규칙(I\text{I} 규칙)과 제거 규칙(E\text{E} 규칙)이라는 분류를 통해 ∧\land, ∨\lor, …\ldots에 추론 규칙을 부여했다. 논건 대수의 접근법은 이와는 살짝 다르다. 예를 들어 논건 대수에서의 P∧QP \land Q의 규칙에 대해 살펴보자.

Γ⊢P trueΓ⊢Q trueΓ⊢P∧Q trueR∧Γ,P true,Q true⊢R trueΓ,P∧Q true⊢R trueL∧ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\Gamma \vdash \tJ P \qquad \Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \land Q}} \text{R}\land \qquad \cfrac {\Gamma, \tJ P, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \land Q} \vdash \tJ R} \text{L}\land \end{array} \end{equation}

차이점을 눈치챘는가? 크게 두 차이점이 있다.

  1. 규칙의 이름이 다르다. 😅
  2. E∧\text{E}\land을 대체하는 L∧\text{L}\land의 꼴이 다르다.

먼저 이름의 차이를 설명하고 넘어가자. 자연 연역의 경우 I∧\text{I}\land 규칙은 ∧\land를 소개(introduce)하는 규칙, E∧\text{E}\land 규칙은 ∧\land를 제거(eliminate)하는 규칙이었다. 반면에 논건 대수의 R∧\text{R}\land 규칙은 ∧\land를 오른쪽(Right side)에 소개하는 규칙이고 L∧\text{L}\land 규칙은 ∧\land를 왼쪽(Left side)에 소개하는 규칙이다. E∧\text{E}\landL∧\text{L}\land의 꼴의 차이는 이 접근법의 차이에서부터 자연스럽게 따라나온 것이다.

명제를 만드는 다른 방법들에 있어서도 논건 대수의 방식을 따라서 추론 규칙을 마련해 줄 수 있다.

 Γ⊢⊤ trueR⊤ Γ,⊥ true⊢P trueL⊥Γ⊢P trueΓ⊢P∨Q trueR∨1Γ⊢Q trueΓ⊢P∨Q trueR∨2Γ,P true⊢R trueΓ,Q true⊢R trueΓ,P∨Q true⊢R trueL∨Γ,P true⊢Q trueΓ⊢P→Q trueR→Γ⊢P trueΓ,Q true⊢R trueΓ,P→Q true⊢R trueL→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\ } {\Gamma \vdash \tJ \top} \text{R}\top \qquad \cfrac {\ } {\Gamma, \tJ \bot \vdash \tJ P} \text{L}\bot \newline\newline\newline \cfrac {\Gamma \vdash \tJ P} {\Gamma \vdash \tJ{P \lor Q}} \text{R}\lor_1 \qquad \cfrac {\Gamma \vdash \tJ Q} {\Gamma \vdash \tJ{P \lor Q}} \text{R}\lor_2 \newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ R \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \lor Q} \vdash \tJ R} \text{L}\lor \newline\newline\newline \cfrac {\Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ{P \to Q}} \text{R}\to \qquad \cfrac {\Gamma \vdash \tJ P \qquad \Gamma, \tJ Q \vdash \tJ R} {\Gamma, \tJ{P \to Q} \vdash \tJ R} \text{L}\to \end{array} \end{equation}

그리고 논건 대수가 자연 연역과 동등함을 (비교적) 쉽게 보이기 위해서는 명제를 만드는 각 방식의 R\text{R}L\text{L} 규칙에 더해 다음의 두 규칙을 추가해야한다.

P true∈ΓΓ⊢P trueinitΓ⊢P trueΓ,P true⊢Q trueΓ⊢Q truecut \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\tJ P \in \Gamma} {\Gamma \vdash \tJ P} \text{init} \qquad \cfrac {\Gamma \vdash \tJ P \qquad \Gamma, \tJ P \vdash \tJ Q} {\Gamma \vdash \tJ Q} \text{cut} \end{array} \end{equation}

여기서 init\text{init} 규칙은 자연 연역에서의 hyp\text{hyp} 규칙과 같은 꼴이다. 반면에 cut\text{cut} 규칙은 직접적으로 비교할만한 자연 연역에서의 규칙이 존재하지 않는데, 이는 논건 대수가 자연 연역과 전혀 다른 방식으로 증명을 전개하기 때문이다. 자연 연역에서는 예를 들어 I∧\text{I}\land 규칙으로 만들어진 P∧QP \land Q가 참이라는 판단을 바로 E∧\text{E}\land에 사용할 수 있었다. 함수형 언어로 말하자면

case (M, N) of
  (x, y) => L

같은 꼴의 사용이 가능했다. 그러나 논건 대수에서는 R∧\text{R}\land 규칙은 가정적 판단의 (오른쪽) 후건에, L∧\text{L}\land 규칙은 가정적 판단의 (왼쪽) 전건에 ∧\land를 새로 만들어낼 뿐이고 두 규칙이 상호작용할 수 있는 방법이 없다. 이 공백을 해결해주는 규칙, 즉 자연 연역의 I\texttt{I}/E\texttt{E} 규칙 쌍의 상호작용에 의한 증명을 논건 대수에서도 표현할 수 있게 해주는 규칙이 바로 cut\text{cut} 규칙이다.

그런데 이 상호작용은 과연 필요한 것인가? 위의 함수형 언어를 통한 예시에서 보다 분명히 볼 수 있는 것은 예시의 I∧\text{I}\landE∧\text{E}\land 규칙의 연달은 사용이 사실은 불필요하다는 점이다. 이는 자연 연역에서는 저런 식의 증명 대신 L에서 x가 등장하는 자리에 M을 치환해넣고, y가 등장하는 자리에 N을 치환해넣은 증명 또한 가능하기 때문이다. 이런 상호작용의 불필요성에 대한 대한 자연 연역에서의 직관을 이에 대응되는 논건 대수의 cut\text{cut} 규칙에 대해서 구체화한 것이 바로 다음의 cut\text{cut} 제거 정리이다.

정리

cut\text{cut} 제거 정리
cut\text{cut}을 사용해 증명 가능한 모든 판단은 cut\text{cut}을 사용하지 않고서도 증명할 수 있다.

cut\text{cut} 제거 정리의 의의는 단순히 필요없는 규칙을 제거하는 정도에 멈추지 않는다. cut\text{cut}을 제거하고 나면 논건 대수에서 가정 없이는 거짓을 증명하지 못한다는 것이 자명해지기 때문이다.

정리

cut\text{cut}을 제거한 논건 대수의 일관성
cut\text{cut}을 제거한 논건 대수는 판단 ⊢⊥ true\vdash \bot\ \texttt{true}를 증명하지 못한다.

증명은 다음의 네 문장이면 충분하다.

  1. 귀결이 ⊥ true\bot\ \texttt{true}R\text{R} 규칙이 존재하지 않기 때문에 어떤 R\text{R} 규칙도 쓸 수 없다.
  2. 가정이 없기 때문에 어떤 L\text{L} 규칙도 쓸 수 없다.
  3. 마찬가지로 가정이 없기 때문에 init\text{init} 규칙을 쓸 수 없다.
  4. 따라서 어떤 규칙도 판단 ⊢⊥ true\vdash \bot\ \texttt{true}을 결론으로 주지 않는다.

앞서 이 글의 서두에서 논건 대수의 일관성을 보이기 위해 "약간의 부정행위"를 저지를 것이라고 말했다. 오해가 없도록 명시하자면 위의 증명은 cut\text{cut}을 제거한 논건 대수에 있어서는 문제가 없다. 부정행위라고 부를 만한 부분은 바로 cut\text{cut} 제거 정리의 증명이 더 어렵다는 점이다. 이 증명은 단순화하기 쉽지 않기 때문에 글에서 직접적으로 다루지는 않을 것이고, 이것이 바로 서두에서 언급한 "부정행위"이다. 다만 이 어려운 증명도 수학적 귀납법(Induction) 이상의 지식은 필요로 하지 않기 때문에, 시도해보고자 하는 독자가 있다면 직접 시도해 볼 수 있을 것이다[5].

논건 대수의 소개를 앞서의 예시 증명을 논건 대수에서 반복하는 것으로 마치도록 하자. 우선 A→BA \to BAA가 참이라고 판단했을 때 BB가 참이라고 다음과 같이 판단할 수 있다.

A true∈(A true)A true⊢A trueinitB true∈(A true,B true)A true,B true⊢B trueinitA→B true,A true⊢B trueL→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\tJ A \in (\tJ A)} {\tJ A \vdash \tJ A} \text{init} \qquad \cfrac {\tJ B \in (\tJ A, \tJ B)} {\tJ A, \tJ B \vdash \tJ B} \text{init}} {\tJ{A \to B}, \tJ A \vdash \tJ B} \text{L}\to \end{array} \end{equation}

이를 써서 다음과 같이 증명을 끝낼 수 있다.

A→B true,A true⊢B true⏞위의증명C true∈(A→B true,A true,C true)A→B true,A true,C true⊢C trueinitA→B true,B→C true,A true⊢C trueL→A→B true,B→C true⊢A→C trueR→ \global\def\tJ#1{#1\ \texttt{true}} \begin{equation} \begin{array}{c} \cfrac {\cfrac {\raisebox {-0.65em} {\(\overbrace {\tJ{A \to B}, \tJ A \vdash \tJ B} ^{위의 증명}\)} \qquad \cfrac {\tJ C \in (\tJ{A \to B}, \tJ A, \tJ C)} {\tJ{A \to B}, \tJ A, \tJ C \vdash \tJ C} \text{init}} {\tJ{A \to B}, \tJ{B \to C}, \tJ A \vdash \tJ C} \text{L}\to} {\tJ{A \to B}, \tJ{B \to C} \vdash \tJ{A \to C}} \text{R}\to \end{array} \end{equation}

앞서의 자연 연역 증명의 구조가 크게 다름에 주의해서 논건 대수 증명의 구조를 이해해보도록 하자. 앞서의 자연 연역 증명은

hyp규칙E규칙↓E규칙↑I규칙결론E규칙 \begin{array}{c} \texttt{hyp} 규칙 \qquad \phantom{\texttt{E} 규칙}\\ \downarrow \qquad \texttt{E} 규칙\\ \uparrow \qquad \texttt{I} 규칙\\ 결론 \qquad \phantom{\texttt{E} 규칙} \end{array}

의 구조를 가지고 있다. 즉 결론에 I\texttt{I} 규칙을 적용하고 어떤 가정에 E\texttt{E} 규칙을 적용해서 이 둘이 만나는 지점을 찾는 것이 증명의 구조이다. 이와는 다르게 논건 대수의 증명은

L규칙init규칙R규칙L규칙↓init규칙↓R규칙결론 \begin{array}{c} \phantom{\texttt{L} 규칙} \qquad \texttt{init} 규칙 \qquad \phantom{\texttt{R} 규칙}\\ \texttt{L} 규칙 \qquad \downarrow \phantom{\texttt{init} 규칙} \downarrow \qquad \texttt{R} 규칙\\ 결론 \end{array}

와 같이 (여러 개의) init\texttt{init} 규칙에서 시작해서 L\texttt{L} 규칙으로 가정, R\texttt{R} 규칙으로 귀결을 변경해 결론에 도달할 때까지 차례로 내려오는 구조를 가진다. 이런 구조의 차이가 바로 (자연 연역의) hyp\texttt{hyp} 규칙과 (논건 대수의) init\texttt{init} 규칙이 (같은 꼴임에도 불구하고) 다른 이름을 가지게 된 이유이자 논건 대수의 init\texttt{init} 규칙이 시작점(initial point)에 해당하는 이름을 가지게 된 이유이다.

논건 대수의 항

논건 대수를 좀 더 컴퓨터 공학적으로 이해할 수 있을까? 이를 위해 자연 연역의 경우와 마찬가지로 논건 대수에도 증명에 커리-하워드 대응을 통해 항을 주려고 시도할 수 있다. 그러나 앞서 언급한 것처럼 논건 대수 증명은 자연 연역의 증명, 그러니까 함수형 언어와 구조가 크게 다르다. 따라서 원본 그대로의 논건 대수에 주는 항은 상대적으로 복잡하고 다양한 개념들(생산자, 소비자, 소비자를 생산자로 바꾸기, 생산자와 소비자가 거래하게 하기, …\ldots)을 요구하고, 결과적으로 이는 컴퓨터 공학적인 직관성이 떨어지는 복잡한 항을 만들어 내게 된다. 이어질 2 편에서는 논건 대수에 약간의 변형을 가하면 직관적이고 쉽게 이해가 가능한 항을 줄 수 있으며, 항이 뜻하는 바가 저수준(low-level) 자료 표현(data representation)과 자연스럽게 연결될 수 있음을 설명할 것이다.

마치며

2 편의 "논리와 저수준 자료표현" 연작 중 1 편인 이 글에서는 논리적 증명을 하는 두 방법과 커리-하워드 대응에 대해서 설명했다. 1 편의 내용은 말하자면 2 편의 바탕 및 서두 역할을 하고 있다. 이 내용들이 2 편에서 다룰 본격적인 논리와 자료 표현의 관계에 대해 흥미를 불러일으켰기를 바라며 이만 마치도록 하겠다.


  1. 이 중 두번째 단위 명제는 일반적인 자연수 체계에서 거짓이나, 명제의 정의 자체는 명제가 참인지 거짓인지에 대해 논하지 않는다는 것을 강조하기 위해 단위 명제에 포함하였다. ↩︎

  2. 거짓도 참이라면 대체 무엇이 참이 아니겠는가? ↩︎

  3. 이는 Rust나 OCaml의 Result 형 혹은 Haskell의 Either형과 같은 형이다. ↩︎

  4. 전건(前件, antecedent)과 후건(後件, succedent)을 개별적으로 다룰 수 있다는 의미에서 "Sequent Calculus"를 논건 대수(論件 代數)라고 번역하였다. 보다 일반적으로 "시퀀트 대수"라는 표현이 쓰이나, 이는 "시퀀트"가 의미하는 바가 "논리에서 다루는 물건/사건/…\ldots"이라는 표현보다 직관성이 떨어진다고 보아 재번역을 하였다. ↩︎

  5. 증명에 대한 구체적인 질문이 있다면 답글을 남겨주기를 바란다. ↩︎

Read more →
9

@bglbgl gwyng vscode 쓰시죠? 예전 기억을 더듬어보면nix-ld만 잘 설정해서 필요하면 extension들을 ad-hoc하게 깔아서 쓰는덴 큰 문제가 없었습니다. (제 경험상 vscode-fhs는 비추합니다) 저는 그러다 한번씩 pkgs/applications/editors/vscode/extensions/update_installed_exts.sh 스크립트를 이용해서 깔려있는 extension들을 nix-expression으로 뽑아서 vscode-with-extensions로 다시 빌드하곤 했습니다.

0

@bglbgl gwyng vscode 쓰시죠? 예전 기억을 더듬어보면nix-ld만 잘 설정해서 필요하면 extension들을 ad-hoc하게 깔아서 쓰는덴 큰 문제가 없었습니다. (제 경험상 vscode-fhs는 비추합니다) 저는 그러다 한번씩 pkgs/applications/editors/vscode/extensions/update_installed_exts.sh 스크립트를 이용해서 깔려있는 extension들을 nix-expression으로 뽑아서 vscode-with-extensions로 다시 빌드하곤 했습니다.

2
2
3
0
1
1
2

I'm exploring a new idea called FediOTP (codename): an authentication system that uses DMs to deliver one-time passwords, allowing any account to authenticate with web services. Unlike current solutions that rely on specific APIs (, ), this would work with any ActivityPub-compatible server, increasing interoperability across the fediverse. Would love to hear your thoughts on potential challenges or use cases for this approach.

1
5
0
2

많은 파일을 대상으로 텍스트를 대량 치환해야 할 일이 꽤 있는데, 파일 탐색과 치환을 인터랙티브하게 수행할 수 있는 툴이 없어서 ised(interactive sed)를 만들었다. 월요일부터 개밥먹기 할 예정. github.com/parksb/ised

3
1

@zust 플러그인은 오히려 Yarn이 잘 만든 소프트웨어라는 생각을 하게 된 계기예요 ㅎㅎ 저는 '공격적인 수정' 중 하나가 PnP라고 생각하는데요, 최근에 겪은 문제로는...

(1) Node v22.12에서 require(esm)이 가능해졌음에도 PnP는 이를 제대로 지원하지 못했습니다. (github.com/yarnpkg/berry/issue)

(2) Go로 재작성하고 있는 타입스크립트도 PnP와의 호환이 문제가 됩니다. (github.com/microsoft/typescrip)

Yarn만의 문제는 아니고, 호환성과 혁신성을 트레이드오프할 때 일어날 수 있는 일인 것 같습니다.

0
3

# Ask Hackers Pub : 이번 주말에 뭐 하시나요?

이번 주말에 뭘 하려고 계획 중인지 편하게 얘기해 보아요.
읽을 책, 가볼 곳, 해볼 것.. 어떤 것이든 좋습니다.
도움 요청이나 피드백 요청도 좋습니다.
물론! 아무것도 하지 않고 쉬는 것도 훌륭합니다.

* 지난 주말에 계획하셨던 일의 회고도 한 번 남겨보면 좋을 것 같아요.

5

bgl gwyng shared the below article:

간단한 rpmbuild의 사용과 private rpm package 배포

Perlmint @perlmint@hackers.pub

마지못해 패키지를 만들어야 할 것 같은 사람을 위한 설명입니다. 제대로된 패키지를 만들고 싶은 경우에는 부족한 점이 많습니다.

대부분의 경우에는 프로그램을 직접 소스에서 빌드하는 일도 적고, 그걸 시스템 전역에 설치하는 일도 흔치는 않을 것입니다. 좋은 패키지매니저와 관리가 잘되는 패키지 저장소들을 두고 아주 가끔은 직접 빌드를 할 일이 생기고, 흔치 않게 시스템 전역에 설치할 일이 생길 수 있습니다. 어지간한 프로그램들은 요즈음의 장비에서는 별 불만 없이 빌드 할 만한 시간이 소요되나, 컴파일러처럼 한번 빌드했으면 다시는 하고 싶지 않은 프로그램을 다시 설치해야하는 경우도 있을 수 있습니다. 하필 이런 프로그램들은 결과물도 덩치가 매우 큽니다. 이럴 때는 최대한 간단하고 필요한 항목만 패키지에 넣어서 만들어두고 다시 활용하면 좋을 것이기에 이런 경우를 위한 rpmbuild에 대한 나름 최소한의 사용 방법을 정리해봅니다.

rpmbuild

rpmbuild는 rpm-build 패키지로 설치가 가능하며, 나름 단순하게 rpm으로 패키징을 할 수 있는 유틸리티입니다. spec파일에 패키지 정보, 빌드 명령, 설치 명령, 패키지가 포함해야 할 파일 목록을 작성해서 rpmbuild에 입력으로 넣어주면 빌드부터 시작해서 rpm패키지를 만들어줍니다. native 프로그램의 경우 디버그 심볼을 알아서 분리해서 별도의 패키지로 만들어주고, 필요한 의존성도 추정해서 명시해줍니다. 또한, 필요한 경우 하나의 spec 명세로 연관된 서브 패키지도(ex. 실행파일 패키지인 curl과 라이브러리 패키지 libcurl, 라이브러리를 사용하기 위한 개발 패키지 libcurl-devel) 같이 만들 수 있습니다.

작업 환경

rpmbuild는 기본으로 ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS,BUILDROOT}의 경로에서 동작하며 각 경로의 용도는 다음과 같습니다.

  • SOURCES에는 압축된 소스코드가 위치합니다.
  • SPECS에는 패키지 정의인 spec파일을 둡니다.
  • BUILD밑에서 빌드 작업이 진행됩니다.
  • RPMS에 바이너리 rpm결과물이 생성됩니다.
  • SRPMS에는 소스 rpm결과물이 생성됩니다.
  • BUILDROOT는 패키징 하기 위해 빌드 결과물을 모으는 경로입니다.

spec파일

spec파일은 패키지를 어떻게 빌드하고 어떤 항목들이 패키지에 포함될지, 패키지의 이름, 설명 및 의존성 등의 메타데이터, 패키지 설치, 삭제시의 스크립트를 정의할 수 있습니다. 보통 시작 부분에는 메타데이터 정의로 시작하며, 다음과 같은 기본적인 형태를 취합니다. 나름 단순하게 만든 python을 위한 spec을 예시로 들어보겠습니다.

Summary: Python %{version}
Name: python-alternative
Version: %{version}
Release: 1%{?dist}
Obsoletes: %{name} <= %{version}
Provides: %{name} = %{version}
URL: https://www.python.org
Requires: libffi openssl
AutoReq: no
License: PSFL

Source: https://www.python.org/ftp/python/%{version}/Python-%{version}.tgz

BuildRequires: libffi-devel openssl-devel
BuiltRoot: %{_tmppath}/%{name}-%{version}-%{release}-root

%define major_version %(echo "%{version}" | sed -E 's/^([0-9]+)\\..+/\1/' | tr -d)
%define minor_version %(echo "%{version}" | sed -E 's/^[0-9]+\\.([0-9]+)\\..+/\1/' | tr -d)

%description
Python

%package devel
Summary: python development files
Requires: %{name} = %{version}-%{release}

%description devel
Python development package

%prep
%setup -q -n Python-%{version}

%build
./configure --prefix=%{_prefix}

%install
%{__make} altinstall DESTDIR=%{buildroot}
%{__ln_s} -f %{_bindir}/python%{major_version}.%{minor_version} %{buildroot}/%{_bindir}/python%{major_version}

%clean
%{__rm} -rf %{buildroot}

%files
%{_bindir}/python*
%exclude %{_bindir}/idle*
%{_bindir}/pip*
%{_bindir}/pydoc*
%exclude %{_bindir}/2to3*
%{_libdir}/libpython*
%{_prefix}/lib/libpython*
%{_prefix}/lib/python*
%{_mandir}/man1/python*

%files devel
%{_includedir}/python*
%{_prefix}/lib/pkgconfig/python*

%로 매크로를 사용할 수 있으며, %package, %description, %files 같은 매크로는 인자를 주어서 서브 패키지를 정의하는데도 쓸 수 있습니다. 앞선 예제처럼 devel 이라고작성하면 메인 패키지이름 뒤에 붙여서 python-alternative-devel가 되며, curl - libcurl과 같은 경우에는 메인의 이름은 curl이고, 딸린 패키지를 정의할 때는 %package -n libcurl과 같이 -n옵션을 추가해서 지정할 수 있습니다. 몇몇 매크로는 단계를 정의하는 것과 같은 동작을 하며 다음과 같습니다.

%package

유사성을 보면 spec파일의 맨 첫부분은 메인 패키지의 %package에 해당하는 것이 아닌가 싶습니다. <Key>: <Value>의 형태로 메타정보를 작성합니다. 대부분은 Key를 보면 무슨 값인지 추측 할 만합니다. 나중에 설명할 %files에서 나열한 파일을 rpmbuild가 분석하여 자동으로 패키지가 필요로 하는 의존성을 추정해서 추가 해 줍니다. python 스크립트, perl 스크립트, native 실행파일 등을 분석해서 알아서 추가해주는 것 같은데, 경우에 따라서는 틀린 의존성을 추가해주기도 합니다. 이 때는 AutoReq: no를 설정하여 자동 의존성 추가를 막을 수 있습니다. 이 python-alternative 패키지는 /usr/local/bin/python%{version}을 설치하는데 아마도 같이 포함되는 python 스크립트에 의해서 /bin/python을 의존성으로 추정하여 요구합니다. 패키지 스스로가 제공하는 의존성은 미리 설치 되어있기를 요구하지 않게 동작하는 것 같으니 보통은 문제가 없습니다만, 이 경우에는 스스로 제공을 하지 않기 때문에 python을 설치하기 위해서 python이 필요한 경우가 발생하므로 AutoReq를 껐습니다.

%prep

준비단계로 소스코드의 압축을 해제하고 필요한경우 패치를 적용합니다. %setup 매크로를 이 안에서 보통 사용하며, %setupSource에 명시된 파일명의 압축 파일을 SOURCES 밑에서 찾아서 압축을 풉니다. 그리고 동일한 이름의 디렉토리로 이동을 합니다. 앞선 예제에서는 SOURCES/Python-%{version}.tgz의 압축을 풀고 Python-%{version}으로 이동을 합니다. 패치가 필요한 경우 보통 이 뒤에 패치를 적용하는 명령들을 추가 합니다.

%build

설정, 컴파일 등을 수행하는 단계입니다. 이곳에서 자주 하는 매크로로 %configure, %make_build 등이 있습니다. %configure는 configure를 prefix 및 기타 몇가지 일반적으로 쓰이는 옵션을 추가하여 실행해주며, %make_buildmake와 비슷하게 모든 타겟을 빌드 합니다. 예제에서는 둘다 안쓰고 있고, 심지어 실제 빌드는 안하는데 어쨌든 이후의 %install까지 지나고나서 빌드 결과물만 맞는 위치에 만들어지면 대충 패키지를 만드는데는 별 문제는 없는 것 같습니다.

%install

여기서 빌드 결과물을 설치하는 명령을 작성합니다. 일반적으로 %make_install을 사용하여 make install DESTDIR=%{buildroot}와 비슷한 명령을 수행하여 %{buildroot}밑에 빌드 결과물이 prefix를 유지하여 설치되게 합니다. 예제의 %{__ln_s} -f %{_bindir}/python%{major_version}.%{minor_version} %{buildroot}/%{_bindir}/python%{major_version}을 보면 추정 할 수 있듯이, 패키지에 포함시킬 파일들을 %{buildroot}밑에 생성을 하면 되며, 추가적인 심볼릭 링크는 패키지를 빌드하는 시점에는 존재하지 않지만, 패키지를 설치하게되면 존재하게 될 %{_bindir}/python%{major_version}.%{minor_version}를 향하는 것을 %{buildroot} 밑인 %{buildroot}/%{_bindir}/python%{major_version}에 만듭니다.

%files

패키지에 포함될 파일 목록을 작성합니다. glob 양식으로 파일 목록을 작성할 수 있습니다. %{buildroot} 밑에 생성 되었지만 어느 %files에도 포함되지 않은 파일이 있는 경우에는 빌드를 실패합니다. 그러므로 %exclude를 사용해서 명시적으로 제외해줘야 합니다.

기타 매크로

rpmbuild에서는 기본으로 다양한 매크로를 제공하고 있습니다. --define "_libdir %{_prefix}/lib64"와 같은 옵션을 실행시에 주어서 실행시점에 매크로를 덮어 쓸 수도 있고, 앞선 spec파일 내의 %define major_version 와 같이 다른 매크로와 셸 명령을 활용하여 매크로를 정의 할 수도 있습니다. 원하는 동작을 안하는 것 같은 경우에는 --show-rc옵션을 사용하여 매크로가 어떻게 정의되어있는지 확인해 볼 수 있습니다.

빌드

rpmbuild의 매뉴얼을 보면 자세하게 나와있지만 가장 단순하게는 rpmbuild -bb <specfile>로 바이너리 패키지를 빌드할 수 있습다. 이 때, 압축된 소스코드는 미리 SOURCES밑에 두어야 합니다.

private rpm package 배포

직접 비공개 패키지 저장소 프로그램을 실행하여 제공하는 방법도 있겠지만, 최대한 간단하게 할 수 있는 방법으로, rpm관련 패키지 설치 명령이 입력으로 http등의 URL도 받는 것을 활용하여 적당한 장비에서 http로 서빙을 해주면 됩니다.

Read more →
4
5

요즘 Server-side rendering인데 차트를 streaming으로 incremental update 할수있는 좋은 방법을 찾고 있다. Solid Start의 Server Signal이란게 원하는거랑 가까워 보이긴한데...

3

거꾸로 상태 모나드로 강화 학습 하기 (1/2) 2편을 마저 써야하는데, 먼저 하스켈 세미나 시간에 전체 내용으로 동료들한테 발표를 했다. 근데 발표하면서 글에 해결해야하는 문제점이라고 써놓은게 되게 얼렁뚱땅이란걸 깨달았다. 실제로 코드를 짠지가 시간이 좀 되어서 정확히 뭐가 문제였는지 좀 까먹어서 실제로 쓰기가 힘들어서 중간에 좀 놨었다ㅋㅋ 암튼 발표를 해보는게 내용 점검에 도움이 많이 된단걸 알았다.

거꾸로 상태 모나드로 강화 학습 하기 (1/2)

기계 학습을 전혀 모르고 살면 안 되겠다 싶어, 얼마전부터 하스켈로 공부하기 시작했다. Hasktorch라고 하스켈용 Torch 바인딩을 사용한다. 이전에 도전했을땐 MNIST까지 하고 다음에 뭘 해야할지 모르겠어서 그만뒀는데, 이번엔 강화 학습으로 이어나가 보기로 했다. 강화 학습은 주어진 환경에서 보상을 최대화하는 에이전트를 학습시키는 것으로, 데이터가 필요없다는게 장점이다. 대신 환경을 만들어야 하는데, 간단한 게임도 환경이 될 수 있다. 게임 만들기는 데이터 모으기와 달리 즐거운 일이니 마다할 필요가 없다. 첫번째로 도전으로 스네이크 게임을 골랐는데, 다들 한번쯤은 해봤을 것이다. 뱀을 조종해 먹이를 최대한 많이 먹으면 되는데, 뱀의 머리가 벽이나 아니면 자기 몸통에 부딪히면 죽는다. 여차여차 학습시켜서 이정도까지 하는데에는 성공했다. 강화 학습 지식이 일천해서 게임을 클리어하는 수준까지는 못 만들겠다. 이 글은 학습을 잘 시키는 방법이 아니라, 강화학습 코드를 어떻게 잘 짜느냐에 대한 것이다.<일단 강화 학습이란걸 형식화 해보자. 앞서 언급한 환경과 에이전트란 단어를 어떻게 정의할수 있을까? 먼저 에이전트의 정의는 이렇다.type Agent = Observation -> Action<관측 Observation 에 따라 행동 Action 을 선택한다. 나는 눈앞에 콜라가 보이면 마신다. 환경은 에이전트를 실행하는 무언가이다. 일상적인 표현으로 쓰자면, 에이전트를 둘러싼 무언가이다.runAgent :: Agent -> [Action]<이 runAgent 함수가 환경의 역할을 수행한다. Agent를 인자로 받아 죽을 때까지 선택한 행동들을 반환한다. 그런데 이건 너무 외연적인 정의고, 환경과 에이전트가 보통 만족할만한 조건을 나열해보자면 이렇다.환경은 상태를 가지고 있고 시간이 지남에 따라 변경된다상태는 에이전트가 무엇을 관측할지를 결정한다에이전트의 행동은 다음 상태에 영향을 끼친다<이 조건들을 바탕으로 환경을 좀더 구체적으로 정의하면 다음과 같다.class Environment e where type State e type Observation e type Action e update :: (State e, Action e) -> State e -- 조건 1, 3 observe :: State e -> Observation e -- 조건 2<스네이크 게임에서 상태는 뱀의 모양과 먹이의 위치, 관측은 상태와 같고(플레이어는 전체 게임 화면을 볼 수 있다), 행동은 좌회전과 우회전이 된다. 그런데 뱀이 아무렇게나 좌회전 우회전 한다고 박수 쳐줄순 없고, 우리는 얘한테 바라는 게 있다. 죽지않고 더 많은 먹이를 먹어야 한다. 이를 위해 뱀이 제때 방향을 바꿔서 먹이를 지나치지 않고 먹으면 잘 했다고 보상을 주자. 그러면 뱀은 보상을 더 많이 받을 방법을 학습한다.type RewardFunction = (Observation, Action) -> Float<보상 함수는 관측와 행동에 따라 보상을 결정한다. 가령 뱀이 먹이 하나를 냠냠하면 보상을 1 주면 된다. 지나쳐버리면 0점이고, 죽었을 때는 -1점을 줄 수도 있다. 당근과 채찍이라 생각하고 정의하면 된다. 또, 보상은 더해서 누적이 되어야 하므로 대충 Float으로 고른다. 학습을 시키려면 보상을 계산해야하고, 그러기 위해 관측과 행동을 짝지어야한다.trainAgent :: Agent -> [(Observation, Action)]<runAgent와 달리 Observation도 포함되어 있다. 이때 [(Observation, Action)]을 에피소드 Episode 라고 한다. 에피소드로부터 보상을 구하자.rewards = fmap (uncurry rewardFn) (trainAgent agent)rewardFn :: RewardFunctionagent :: Agent<...해치웠나? 에피소드가 어떻게 기록될지를 살펴보자.0123456789행동→↓→↓←↑→↓←↓보상🍎🍎🍎💀<위의 rewards의 값이 이런 의미일거라고 보인다. 실제로 Float 값을 표시해보자.0123456789행동→↓→↓←↑→↓←↓보상0010011000<혹시 이상한 걸 찾으셨나요? 이런식으로 보상하는게 틀린 건 아니다. 다만 에이전트가 장기적인 계획을 세우도록 학습시키지 못한다. 먹이를 먹는 순간에만 보상을 받기 때문에, 멀리 떨어진 먹이를 향해 다가가게끔 유도할 수가 없다. 이를 해결하려면 보상을 뒤에서부터 누적시켜야 한다.0123456789행동→↓→↓←↑→↓←↓보상3332221000rewards = scanr (+) 0 (fmap (uncurry rewardFn) (trainAgent agent))<코딩 테스트용 코드를 짜야할것 같은 느낌이 들었는데, 다행히 scanr 함수 덕분에 쉽게 해결했다. 좀더 개선해볼까. 지금의 보상 체계에서 에이전트는 먼 미래를 고려하며 선택하는 것을 배울 수 있다. 그런데, 사실 뱀이 맨 처음에 좌회전을 하던 우회전을 하던, 죽기 전까지 먹이를 총 몇개 먹는지에 엄청난 영향을 끼칠거 같진 않다. 어떤 시점에서의 선택의 영향은 시간이 지날 수록 점점 희미해진다. 이 점을 반영해서 뱀이 잘못된 편견을 갖지 않도록 도와주자. 미래의 보상을 누적하되, 감쇠율 0.9를 적용하는 것이다.0123456789행동→↓→↓←↑→↓←↓보상1.932.152.391.541.711.901.000.000.000.00rewards = scanr (\x y -> x + 0.9 * y) 0 (fmap (uncurry rewardFn) (trainAgent agent))<이제 나머지는 GPU한테 맡기면 된다. 조금만 기다리면 위의 동영상에서처럼 움직이는 뱀을 볼 수 있다.<여기서 GPU를 100장 더 사서 뱀 대신에 좀더 우리 삶에 도움되는 에이전트를 학습시킨다면, 그걸 10년전에 했으면, 난 지금 부자가 되어있을지도 모르겠다. 하지만 기회는 이미 떠났고, 난 지금 돈은 없지만 대신 시간은 많다. 그 시간을, 지금의 그럭저럭 볼만한 코드를 쥐꼬리만큼 개선하는데 낭비해보려 한다. 지금 코드에서 뭐가 마음에 안드냐면,<보상이 사실 서로 다른 두 개를 가리킨다 먹이를 먹자마자 즉각적으로 주는 보상과, 그것을 누적한 보상, 이렇게 두 개가 있다. 실제로 학습에 사용하는 것은 후자이다. 그런데 막상 보상 함수의 정의는 전자에 대한 것이다. 이는 보상 함수를 계산할 때 미래에 어떤 일이 일어나는지 알수가 없어서 그렇다. 정의를 두 개로 나눈 이유가 의미를 명쾌하게 하기 위해서가 아니라, 그냥 한번에 계산을 할수 없기 때문이다.<환경의 정의 위에서 살펴본 runAgent의 정의가 일견 우아해보일 수 있다. 문제는 그 정의는 환경과 에이전트가 모두 순수 함수일 것을 강요한다는 것이다. Environment의 정의도 마찬가지다. 환경과 에이전트는 각자의 부수효과 Side effect 를 가질 수 있어야 한다. 가령 온라인 게임을 환경으로 삼는다면, 환경은 네트워킹을 할 수 있어야 한다. 또, 에이전트는 매번 똑같은 선택이 아닌 확률적 선택을 하고 싶을텐데, 이는 순수 함수로써는 불가능하다.<그러면 부수효과를 허용하면 되는거 아냐?<맞다. 나처럼 시간이 많다면 직접 해보는 것도 나쁘지 않다. 하지만 정의가 점점 지저분해지는 것을 보게될 것이고... 시간을 아껴주기 위해 정답을 알려주겠다. 환경은 에이전트가 실행될 수 있는 모나드여야 한다. 딱 거기까지여야 한다. 환경과 에이전트 사이에 그 이상의 관계는 부적절하다.<글이 생각했던 것보다 길어져버려서, 오늘은 여기까지 해야겠다. 이어지는 글에서 거꾸로 상태 모나드가 이걸 어떻게 해결하는지 소개한다. 사실 아까 환경이니 에이전트니 어쩌고 할때부터, 진작에 강화 학습을 마스터한 학생들이 이미 다 아는 내용에 지겨워 졸기 시작하는게 보였다. 다음 수업은 재밌을테니, 대신 그때까지 모나드를 배워오세요.

hackers.pub · Hackers' Pub

Link author: bgl gwyng@bgl@hackers.pub

8

@basix 님이 알려주신 DaisyDisk를 깔고 UI 때깔부터 심상치않길래 바로 10달러 결제하고 디스크 정리를 시작했다.

각종 캐시들을30분간 열심히 지워서100GB+를 확보했는데, 근 한달동안 가장 보람차고 충만한 30분이었다ㅠㅠ



RE: https://hackers.pub/@basix/019665ed-f91c-7649-9ffd-0f460c440132

3

@bglbgl gwyng 이거 DaisyDisk로 잡으면 macOS 시스템에서 이상하게 리저빙해둔 것까지 잘 잡히더라고요. 전 타임머신이 오작동해서 디스크 반쪽짜리로 살다가 DaisyDisk로 감지해서 diskutil로 열심히 제거했습니다...

0
3

왜 맥북으로 개발을 하면 상시로 저장공간이 모자란 걸까요? 500GB 쓰는데 그렇습니다. OmniDisk를 가끔 돌려보는데 한 300GB정도가 어딘가 숨어있어요...

2