§1.2 표시적 의미론
구문 트리는 아직 아무 뜻도 없다. x + 1에 해당하는 트리는 그냥 트리이지 숫자가 아니고,
숫자가 되려면 x가 얼마인지 알아야 한다.
그래서 뜻을 주는 함수가 트리 하나만 받지 않는다. 상태(state) 를 함께 받는다.
상태는 변수에 값을 붙여 주는 함수 σ : ⟨var⟩ → ℤ이다. Reynolds의 Σ는 이런 상태들이
이루는 공간이고, σ : Σ가 상태 하나다.
논리학에서는 assignment, 프로그래머에게는 메모리에 해당한다.
⟦-⟧intexp : ⟨intexp⟩ → Σ → ℤ
트리를 받고 상태를 받으면 정수를 낸다. ⟨intexp⟩와 Σ → ℤ를 분리해 적은 이 타입이
구문과 의미가 서로 다른 층에 있다는 사실을 보여 준다.
의미 함수(semantic function)는 구문의 절마다 방정식 하나씩이다. 구조적 재귀라서 정의가 곧 증명 도구가 된다 — 정의를 펼치는 것과 귀납법의 한 단계를 밟는 것이 같은 일이다. 단언 쪽은 이렇게 생겼다.
def Assert.eval {V : Type u} [DecidableEq V] : Assert V → State V → Prop
| .tru, _ => True
| .fls, _ => False
| .cmp c e₀ e₁, σ => c.denote (e₀.eval σ) (e₁.eval σ)
| .not p, σ => ¬ p.eval σ
| .bin op p q, σ => op.denote (p.eval σ) (q.eval σ)
| .quant .all v p, σ => ∀ n : Int, p.eval (σ[v := n])
| .quant .ex v p, σ => ∃ n : Int, p.eval (σ[v := n])
양화사 절에서 ∀v. p의 뜻은 "모든 정수 n에 대해, v를 n으로 덮은 상태에서 p가 참"
이다. 오른쪽에 쓴 ∀는 Lean의 것이고, 왼쪽의 ∀v. p는 객체 언어의 구다.
메타 수준의 기호로 객체 수준의 기호를 설명하고 있다.
이것이 Reynolds가 §1.2에서 계속 경계하는 자리다. 객체 언어의 기호와 메타 언어의 기호가
같은 모양이라 헷갈리기 쉽다. Background.lean이 그 구분만 따로 다루는 이유다.
결과 타입이 Prop인 것도 여기서 정해진다. 계산 가능한 Bool 평가기를 만들려면 모든
단언의 참·거짓을 판정하는 절차가 있어야 한다. 정수 양화가 있는 이 언어 전체에는 그런
절차가 없다. Prop은 판정 절차 없이 명제를 표현한다. 2장의 불 식은 양화사를 뺀 계산 가능한
조각이어서 Bool 평가기를 줄 수 있다.
정수 식 쪽은 양화사가 없으므로 지금도 계산된다. 돌려 보면 이렇다.
example : ⟦IntExp.bin .add (.var "x") (.num 1)⟧ₑ (State.const 41) = 42 := ⊢ ⟦IntExp.bin IntOp.add (IntExp.var "x") (IntExp.num 1)⟧ₑ (State.const 41) = 42 All goals completed! 🐙
1장이 쉬운 이유가 여기 있다. 의미 함수가 전함수(total function) 다. 술어 논리에는
비종료(nontermination)가 없으므로 모든 구에 뜻이 있고, 뜻이 있는지부터 따질 일이 없다.
2장에서 while이 들어오면 이 사정이 무너진다.
곁가지 — "타입"이라는 말이 가리키는 층
여기서 한 번 정리하고 가면 뒤가 편하다. 이 저장소를 읽다 보면 "타입"이라는 말이 서로 다른 것을 가리키며 여러 번 나오는데, 섞이면 §1.1과 §1.2의 구분 자체가 흐려진다.
지금까지 나온 것을 층으로 세워 보면 이렇다.
구문(syntax) ⟨intexp⟩ 구문 트리들의 집합
│
│ ⟦-⟧intexp (트리를 받아 값을 계산하는 함수)
▼
표시(denotation) Σ → ℤ 상태를 받는 의미 함수
│ σ를 적용
▼
값(value) ℤ 이 상태에서 얻은 값
⟨intexp⟩의 원소는 트리다. 1+1에 해당하는 원소는 c₊(c₁(), c₁())이라는 트리이지
정수 2 가 아니다. 트리를 정수로 보내는 것이 ⟦-⟧intexp이고, 그것이 §1.2에서 한 일이다.
Lean으로 옮기면 IntExp String이 위층이고 Int가 아래층이다. IntExp.eval이 화살표다.
IntExp String을 "정수 타입"이라고 부르면 안 되는 이유가 그것이다 — 그 타입의 원소는
정수가 아니라 트리다.
타입이 붙은 언어로 가면 층이 하나 더 생긴다
15장에서 타입 체계를 다루면 구문 쪽에 층이 하나 더 얹힌다. 타입 표현 int, bool,
τ → σ 자체가 하나의 구문이 되고, 그 구문에도 자기만의 의미 함수가 붙는다.
타입의 구문 ⟨type⟩ int, bool, →, × 로 만들어지는 트리
│ ⟦-⟧type
▼
타입의 의미 ⟦τ⟧ 값들이 사는 그릇 (⟦int⟧ = ℤ)
△ 값이 그 그릇 안에 있다
항의 의미 ⟦e⟧σ ∈ ⟦τ⟧
△ ⟦-⟧term
항의 구문 ⟨term⟩
여기서 자주 어긋나는 대응 하나를 짚어 둔다.
⟨intexp⟩와 같은 층에 있는 것은 ⟦τ⟧가 아니라 ⟨type⟩이다. 둘 다 구문의 집합이고, 둘 다 생성자와 세 조건으로 만들어지는
같은 종류의 대상이다. ⟨intexp⟩가 ⟦-⟧intexp를 거쳐 도달하는 곳이 ℤ = ⟦int⟧이고,
그쪽이 ⟦τ⟧에 해당한다.
Reynolds 1장을 억지로 타입이 붙은 언어로 다시 읽으면 이렇게 된다.
-
⟨intexp⟩ int타입을 갖는 항들의 구문. 타입이 하나뿐이라 타입 표현을 따로 둘 이유가 없었다.-
ℤ ⟦int⟧. 값들이 사는 그릇.-
⟨assert⟩와𝔹 같은 이야기의 진릿값 판이다. Reynolds의
𝔹는 실행 가능한Bool평가기를 뜻하지 않는다. 이 형식화는 객체언어의 양화를 Lean의 양화로 바로 옮기기 위해Prop을 쓴다.
집합, 도메인, 범주론의 역할
집합론, 도메인 이론, 범주론(category theory)은 같은 질문에 내놓은 세 가지 답이 아니다. 먼저 값이 어디에 사는지를 정한다.
- 집합론적(set-theoretic)
⟦τ⟧가 그냥 집합이다.⟦int⟧ = ℤ,⟦τ → σ⟧는 함수들의 집합. 1장이 이 관점이고, 비종료가 없으므로 이걸로 충분하다.- 도메인 이론적(domain-theoretic)
⟦τ⟧에 근사 순서를 주고, 모든 가산 증가 사슬의 최소 상계와 최소원⊥을 갖추면 이 책이 말하는 도메인이 된다. 2장에서while이 들어오면 이쪽으로 넘어간다. 보통 집합도Option같은 타입으로 비종료를 부호화할 수는 있다. 도메인의 순서와 연속성은 재귀 의미 방정식의 여러 고정점 가운데 최소 고정점을 골라 근사 사슬로 구성하게 해 준다.
도메인은 집합을 버리는 대안이 아니라 집합에 순서 구조를 더한 의미 공간이다.
범주론적 관점은 셋째 그릇이 아니다.
집합도 범주 Set의 대상이고, 도메인도 알맞은 범주의 대상이다. 범주론은 이런 대상과
그 사이의 함수를 한 언어로 설명한다. 예를 들어 Depth/SignatureFunctor.lean의 초기 대수는
여기서 구문을 재귀적으로 만드는 과정을 설명한다. 값이 사는 ⟦τ⟧까지 대신 정해 주지는 않는다.
Curry–Howard 대응도 여기서 선택할 의미론 하나를 더 보태는 말이 아니다. 명제를 타입으로,
증명을 그 타입의 항으로 읽는 대응이다. Assert.eval의 결과를 Prop으로 둔 직접적인 이유는
객체언어의 양화를 Lean의 ∀와 ∃로 그대로 표현하기 위해서다. 이 책은 1장에서 값을 보통
집합으로 다루고, 2장에서는 비종료와 재귀를 다루려고 근사 순서가 있는 도메인을 도입한다.
범주론 렌즈 — 의미는 유일한 준동형이다
IntExp.eval과 Assert.eval은 구문 대수에서 이 절의 의미 대수로 가는 준동형 조건을
만족하는 유일한 함수 쌍이다(Depth/Algebra.lean의 초기성 정리). 그 조건 자체가 합성성
(compositionality)의 다른 이름이다 — 생성자 하나의 값이 그 조각들의 값으로만 정해지기
때문이다. 합성성의 귀결, 곧 뜻이 같은 부분구를 바꿔 끼워도 전체 뜻이 바뀌지 않는다는
성질은 선택 심화: 합성성과 합동에서 더 본다.
선택 심화: 의미와 자유 변수는 어떤 접기인가는 이 유일한 함수 쌍을
접기(catamorphism)로 직접 구성해 본다.