コンテンツにスキップ

プログラミング言語論:機械語から型システムとパラダイムまで

対象読者: 本テーマ第 1〜2 章(CPU の命令符号化、OS の役割)を読み、何らかのプログラミング言語で関数を書いた経験のある人。帰納法による証明は仮定しますが、型理論や意味論は仮定しません。

前提となる記事:OS の役割:抽象化・スケジューリング・仮想記憶はなぜ必要か

生 Markdown

プログラムを書くとき、私たちは「この書き方で本当に思ったとおり動くのか」「間違いを実行する前に見つけられないか」と考えます。この記事は、その問いに答えるために、プログラムの意味を小さな言語で厳密に決め、「型が付けば途中で止まらない」ことを証明する道筋を追います。読み終えると、コンパイラと型検査が何を保証し、何を保証できないのか、関数型とオブジェクト指向がなぜ違う形をしているのかを、理由つきで説明できるようになります。

  • CPU が実行できるのは固定長のビット列(機械語)だけです。アセンブリ言語はそのビット列に人間が読める名前を付けただけのもので、高級言語との間には「意味を保つ翻訳」という質的な断絶があります。
  • コンパイラとインタプリタの違いは、言語の性質ではなく実装戦略の違いです。同じ言語に両方の処理系を作れます。区別が本質的に効くのは、エラーを実行前に見つけるか実行時に見つけるかという点です。
  • 型システムの価値は「進行定理」(型の付いた式は値になるか、まだ先へ進めるか)と「保存定理」(1 ステップ進んでも型は変わらない)の 2 本に集約されます。この 2 つから、型の付いたプログラムは実行の途中で行き詰まらないという保証(型健全性)が従います。
  • 静的型検査は健全である限り必ず不完全です。これは実装の未熟さではなく、停止性問題の決定不能性から導かれる原理的な限界です。
  • 関数型プログラミングの中心概念は参照透過性であり、これは「同じ式は何度評価しても同じ値になる」という定理を成立させます。オブジェクト指向の中心概念は動的ディスパッチと部分型であり、関数型の部分型規則は引数について反変(部分型の向きが引数の側では逆になること)になります。

1. 動機:なぜ「言語」が必要なのか

Section titled “1. 動機:なぜ「言語」が必要なのか”

コンピュータアーキテクチャと CPU の構造 の 定義 6.1[コンピュータアーキテクチャとCPUの構造] で見たとおり、CPU が理解するのは 32 ビットや 64 ビットのビット列だけです。1940 年代の計算機は、実際にこのビット列を人間が紙に書き、スイッチやパンチカードで入力していました。

この作業は 2 つの意味で耐えがたいものでした。第 1 に、間違えます。1 ビット違えば別の命令になり、しかも大抵の場合それも「有効な」命令なので、計算機は何事もなかったかのように誤った計算を続けます。第 2 に、書き換えられません。プログラムの真ん中に命令を 1 つ挿入すると、それ以降のすべての分岐先アドレスがずれます。

そこで生まれた発想が「記号で書いて、機械に翻訳させる」ことでした。ここには 1 つの飛躍があります。翻訳をするのもまたプログラムだ、という自己言及です。1952 年に Grace Hopper が A-0 システムを、1957 年に IBM の John Backus のチームが FORTRAN のコンパイラを完成させたとき、多くの技術者は「機械が生成したコードが人間の手書きに勝てるはずがない」と考えていました。今日、その懐疑は完全に覆っています。

しかし翻訳を機械に任せた瞬間、新しい問いが生まれます。翻訳が正しいとは、どういうことか。これに答えるには、まず「プログラムの意味」を数学的に定義しなければなりません。この記事はその定義から始めて、型システムとパラダイムの設計原理までを見ていきます。

2. 抽象度の階段:機械語とアセンブリ言語

Section titled “2. 抽象度の階段:機械語とアセンブリ言語”

2.1. 機械語は命令の符号化である

Section titled “2.1. 機械語は命令の符号化である”

RISC-V の 32 ビット命令 addi(即値加算)を例に取ります。どの命令名がどのビット並びに対応するかを定める規約が 定義 8.1[コンピュータアーキテクチャとCPUの構造] の命令セットアーキテクチャです。この命令は I 形式と呼ばれる並びを持ち、上位から順に 12 ビットの即値、5 ビットのソースレジスタ番号 rs1、3 ビットの funct3、5 ビットのデスティネーションレジスタ番号 rd、7 ビットのオペコードが並びます。

例 2.1(addi 命令の符号化を手で計算する)

「レジスタ a0 の値に 1 を足して a0 に戻す」という命令を符号化します。RISC-V では a0 はレジスタ番号 10、すなわち 2 進で 01010 です。addi のオペコードは 0010011、funct3 は 000 です。即値 1 は 12 ビットで 000000000001 です。

これを上位から連結します。

000000000001 01010 000 01010 0010011
即値=1 rs1=10 funct3 rd=10 opcode

区切りを外して 4 ビットずつ切り直すと 0000 0000 0001 0101 0000 0101 0001 0011、16 進で 0x00150513 です。同じ手順で add a0, a0, a1(R 形式。R 形式の符号化は 例 8.2[コンピュータアーキテクチャとCPUの構造] でも計算しています)は 0x00B50533、関数からの復帰命令 ret(実体は jalr x0, 0(x1))は 0x00008067 になります。

つまり addi a0, a0, 1 というアセンブリの 1 行は、0x00150513 という 32 ビット整数に1 対 1 で対応する別表記にすぎません。

この対応が 1 対 1 であることが決定的です。アセンブラの仕事は、命令名をオペコードの表で引き、レジスタ名を番号に直し、ラベルをアドレスに解決してビットを詰めることだけです。新しい概念を導入しません。

2.2. 高級言語は何を追加したのか

Section titled “2.2. 高級言語は何を追加したのか”

一方、C 言語の 1 行はしばしば複数の命令に展開されます。

int add1(int x) { return x + 1; }

これを RISC-V 向けに最適化付きでコンパイルすると、次の 2 命令になります。

add1:
addi a0, a0, 1 # 0x00150513
ret # 0x00008067

ここで起きていることは、単なる記号の置換ではありません。「引数 x は第 1 引数レジスタ a0 に入っている」「返り値も a0 に置く」という呼び出し規約の知識、「int は 32 ビット 2 の補数である」という型と表現の対応、そして「局所変数はレジスタに割り付けてよい」という最適化の判断が使われています。高級言語が追加したのは、こうした決定を人間の手から取り上げる抽象化です。

ソースコードから実行までの階段。破線から上は人間が書く表記、下は機械が扱う表現です

定義 3.1(翻訳器と解釈器)↗

言語 SS(原始言語)、言語 TT(目的言語)、言語 II(実装言語)を考えます。

コンパイラとは、II で書かれたプログラム CC であって、SS のプログラム pp を入力すると TT のプログラム C(p)C(p) を出力し、任意の入力 dd について pp を dd に適用した結果と C(p)C(p) を dd に適用した結果が一致するものをいいます。

インタプリタとは、II で書かれたプログラム JJ であって、SS のプログラム pp と入力 dd の組を受け取り、pp を dd に適用した結果を直接出力するものをいいます。

この定義から読み取ってほしいのは、コンパイラかインタプリタかは言語の属性ではないということです。どちらも「pp の意味を実現する」という同じ目標を、出力の形を変えて達成しているだけです。実際、C++ にはインタプリタ(Cling など)があり、Python には機械語を生成する処理系(PyPy の JIT)があります。「Python はインタプリタ言語である」という言い方は、正確には「Python の主要な実装 CPython がバイトコードインタプリタである」という意味です。

観点コンパイル方式インタプリタ方式
誤りの発見時期翻訳時(実行前)に構文・型の誤りを検出該当行に到達したときに検出
実行速度最適化を翻訳時に済ませられるため速い命令ごとの解釈オーバーヘッドが載る
起動の速さ翻訳の時間が先に必要すぐ動く
移植性目的機械ごとに翻訳し直すインタプリタさえあれば同じコードが動く
実行時情報の活用静的にわかる範囲に限られる実際に通った経路に基づく最適化ができる

注意 3.2

最後の行が JIT(実行時コンパイル)の存在理由です。「この呼び出し箇所には過去 1 万回すべて同じ型のオブジェクトが来た」という事実は静的にはわからず、実行時にしか観測できません。JIT はこの観測に賭けて特化コードを生成し、賭けが外れたら解釈実行へ戻ります。コンパイルとインタプリタは対立ではなく、情報が使える時点の違いだと捉えるのが正確です。

4. プログラムの意味を厳密に述べる

Section titled “4. プログラムの意味を厳密に述べる”

「翻訳が意味を保つ」と言うためには意味の定義が要ります。ここでは最も広く使われる操作的意味論、すなわち「1 ステップずつどう書き換わるか」で意味を与える方法を、極小の言語で実演します。

次の定義は、真偽値と自然数だけを扱う言語を決め、「どの式がどの式に書き換わるか」を規則の一覧で与えます。規則は、横線の上に前提、下に結論を書く形で並びます。上が成り立つならば下が成り立つ、と読みます。「最小の関係」とは、これらの規則から導けるものだけを含む関係のことです。以後の定理は、この規則の表だけを根拠に証明されます。

定義 4.1(言語 L の構文と簡約関係)↗

項 tt を次の文法で定める。

t::=true∣false∣if t then t else t∣0∣succ t∣pred t∣iszero tt ::= \mathtt{true} \mid \mathtt{false} \mid \mathtt{if}\ t\ \mathtt{then}\ t\ \mathtt{else}\ t \mid \mathtt{0} \mid \mathtt{succ}\ t \mid \mathtt{pred}\ t \mid \mathtt{iszero}\ t

数値 nvnv と値 vv を次で定める。

nv::=0∣succ nv,v::=true∣false∣nvnv ::= \mathtt{0} \mid \mathtt{succ}\ nv, \qquad v ::= \mathtt{true} \mid \mathtt{false} \mid nv

1 ステップ簡約関係 t→t′t \to t' を、次の規則で生成される最小の関係とする。

(E-IfTrue)if true then t2 else t3→t2(E-IfFalse)if false then t2 else t3→t3(E-If)t1→t1′if t1 then t2 else t3→if t1′ then t2 else t3(E-Succ)t→t′succ t→succ t′(E-PredZero)pred 0→0(E-PredSucc)pred (succ nv)→nv(E-Pred)t→t′pred t→pred t′(E-IszeroZero)iszero 0→true(E-IszeroSucc)iszero (succ nv)→false(E-Iszero)t→t′iszero t→iszero t′\begin{aligned} &\text{(E-IfTrue)} && \mathtt{if}\ \mathtt{true}\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 \to t_2 \\ &\text{(E-IfFalse)} && \mathtt{if}\ \mathtt{false}\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 \to t_3 \\ &\text{(E-If)} && \frac{t_1 \to t_1'}{\mathtt{if}\ t_1\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 \to \mathtt{if}\ t_1'\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3} \\ &\text{(E-Succ)} && \frac{t \to t'}{\mathtt{succ}\ t \to \mathtt{succ}\ t'} \\ &\text{(E-PredZero)} && \mathtt{pred}\ \mathtt{0} \to \mathtt{0} \\ &\text{(E-PredSucc)} && \mathtt{pred}\ (\mathtt{succ}\ nv) \to nv \\ &\text{(E-Pred)} && \frac{t \to t'}{\mathtt{pred}\ t \to \mathtt{pred}\ t'} \\ &\text{(E-IszeroZero)} && \mathtt{iszero}\ \mathtt{0} \to \mathtt{true} \\ &\text{(E-IszeroSucc)} && \mathtt{iszero}\ (\mathtt{succ}\ nv) \to \mathtt{false} \\ &\text{(E-Iszero)} && \frac{t \to t'}{\mathtt{iszero}\ t \to \mathtt{iszero}\ t'} \end{aligned}

どの規則も適用できない項を正規形という。値でない正規形を行き詰まり項(stuck term)という。

行き詰まりが、この形式化における「実行時エラー」の定義です。たとえば pred false\mathtt{pred}\ \mathtt{false} は、E-PredZero も E-PredSucc も形が合わず、E-Pred を使おうにも false\mathtt{false} が簡約できないため、どの規則も適用できません。しかも値でもありません。実機なら「Boolean に対して pred は使えません」という例外が飛ぶ場面です。

例 4.2(評価列を最後まで追う)

項 if (iszero (pred (succ 0))) then 0 else succ 0\mathtt{if}\ (\mathtt{iszero}\ (\mathtt{pred}\ (\mathtt{succ}\ \mathtt{0})))\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{succ}\ \mathtt{0} を簡約します。使った規則を各行に添えます。

 if (iszero (pred (succ 0))) then 0 else succ 0→ if (iszero 0) then 0 else succ 0E-If の前提を E-Iszero、その前提を E-PredSucc(nv=0)で導出→ if true then 0 else succ 0E-If の前提を E-IszeroZero で導出→ 0E-IfTrue\begin{aligned} &\ \mathtt{if}\ (\mathtt{iszero}\ (\mathtt{pred}\ (\mathtt{succ}\ \mathtt{0})))\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{succ}\ \mathtt{0} \\ \to&\ \mathtt{if}\ (\mathtt{iszero}\ \mathtt{0})\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{succ}\ \mathtt{0} && \text{E-If の前提を E-Iszero、その前提を E-PredSucc(} nv = \mathtt{0} \text{)で導出} \\ \to&\ \mathtt{if}\ \mathtt{true}\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{succ}\ \mathtt{0} && \text{E-If の前提を E-IszeroZero で導出} \\ \to&\ \mathtt{0} && \text{E-IfTrue} \end{aligned}

0\mathtt{0} は値なので、ここで停止します。注意すべきは、2 行目で「まず条件式を最後まで評価する」と決めているのは E-If ただ 1 つの規則だという点です。この規則がなければ、条件が未評価のまま分岐する意味論も書けてしまいます。意味論とは、こうした選択を明示的に固定する作業です。

この規則集合はそのまま実行できるプログラムに書き写せます。以下は言語 L の完全なインタプリタです。

def is_numeric(t):
if t == ("zero",):
return True
if t[0] == "succ":
return is_numeric(t[1])
return False
class Stuck(Exception):
pass
def step(t):
"""項 t を 1 ステップ簡約する。規則が適用できなければ Stuck を送出する。"""
k = t[0]
if k == "if":
_, t1, t2, t3 = t
if t1 == ("true",):
return t2 # E-IfTrue
if t1 == ("false",):
return t3 # E-IfFalse
return ("if", step(t1), t2, t3) # E-If
if k == "succ":
return ("succ", step(t[1])) # E-Succ
if k == "pred":
u = t[1]
if u == ("zero",):
return ("zero",) # E-PredZero
if u[0] == "succ" and is_numeric(u):
return u[1] # E-PredSucc
return ("pred", step(u)) # E-Pred
if k == "iszero":
u = t[1]
if u == ("zero",):
return ("true",) # E-IszeroZero
if u[0] == "succ" and is_numeric(u):
return ("false",) # E-IszeroSucc
return ("iszero", step(u)) # E-Iszero
raise Stuck(t)
def evaluate(t):
while True:
try:
t = step(t)
except Stuck:
return t # 正規形に到達
term = ("if", ("iszero", ("pred", ("succ", ("zero",)))), ("zero",), ("succ", ("zero",)))
print(evaluate(term)) # ('zero',)
print(evaluate(("pred", ("false",)))) # ('pred', ('false',)) ← 行き詰まり

次の補題は、後の決定性の証明で使う下準備です。「値はこれ以上書き換わらない」という当たり前に見える事実を、規則の表と照らして確かめておきます。

補題 4.3(値の基本性質)✓

(1) nvnv が数値ならば、nv→t′nv \to t' となる項 t′t' は存在しない。 (2) vv が値ならば、v→t′v \to t' となる項 t′t' は存在しない。

証明(補題 4.3)

(1) を nvnv の構造に関する帰納法で示します。nv=0nv = \mathtt{0} のとき、定義 4.1 の規則のうち左辺が 0\mathtt{0} という形の項であるものは存在しません(E-PredZero の左辺は pred 0\mathtt{pred}\ \mathtt{0} であって 0\mathtt{0} ではありません)。よって簡約できません。

nv=succ nv1nv = \mathtt{succ}\ nv_1 のとき、左辺が succ\mathtt{succ} で始まる規則は E-Succ だけです。E-Succ を使うには前提 nv1→t1′nv_1 \to t_1' が必要ですが、帰納法の仮定より nv1nv_1 は簡約できないので、この前提は満たせません。よって succ nv1\mathtt{succ}\ nv_1 も簡約できません。

(2) 値は true\mathtt{true}、false\mathtt{false}、数値のいずれかです。true\mathtt{true} と false\mathtt{false} を左辺に持つ規則は存在せず(E-IfTrue の左辺は if\mathtt{if} で始まる項です)、数値については (1) が示しています。

次の定理は、どの式にも次の一手が高々 1 つしかないことを言います。証明は、式の形ごとに「適用できる規則は 1 つだけ」と確かめる場合分けです。「導出に関する帰納法」とは、ある 1 ステップの書き換えを規則から組み立てた手順の、前提にあたる部分から順に仮定を使う帰納法のことです。

定理 4.4(簡約の決定性)✓

言語 L の任意の項 tt について、t→t′t \to t' かつ t→t′′t \to t'' ならば t′=t′′t' = t'' である。

証明(定理 4.4)

t→t′t \to t' の導出に関する帰納法で示します。tt の形で場合分けします。

t=if t1 then t2 else t3t = \mathtt{if}\ t_1\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 のとき。t1=truet_1 = \mathtt{true} ならば適用できるのは E-IfTrue だけです。E-If を使うには前提 t1→t1′t_1 \to t_1' が要りますが、補題 4.3 (2) より true\mathtt{true} は簡約できないので E-If は使えません。よって t′=t′′=t2t' = t'' = t_2 です。t1=falset_1 = \mathtt{false} も同様です。t1t_1 が true\mathtt{true} でも false\mathtt{false} でもなければ E-IfTrue も E-IfFalse も形が合わないので、両方の簡約は E-If によるもので、それぞれ t1→s1t_1 \to s_1、t1→s1′t_1 \to s_1' を前提とします。帰納法の仮定より s1=s1′s_1 = s_1' となり、t′=t′′t' = t'' が従います。

t=pred t1t = \mathtt{pred}\ t_1 のとき。t1=0t_1 = \mathtt{0} ならば E-PredZero のみが適用でき(E-Pred の前提 0→⋅\mathtt{0} \to \cdot は 補題 4.3 (1) により成立しない)、t′=t′′=0t' = t'' = \mathtt{0} です。t1=succ nvt_1 = \mathtt{succ}\ nv(nvnv は数値)ならば E-PredSucc が適用でき、E-Pred は前提 succ nv→⋅\mathtt{succ}\ nv \to \cdot を要求しますが 補題 4.3 (1) よりこれは成立しません。よって t′=t′′=nvt' = t'' = nv です。ここで補題 (1) が本質的に効いています。t1t_1 が succ u\mathtt{succ}\ u の形でも uu が数値でない場合は、E-PredSucc の形が合わないので両方 E-Pred であり、帰納法の仮定で決まります。t1t_1 がそれ以外の形のときも E-Pred のみです。

t=iszero t1t = \mathtt{iszero}\ t_1 の場合は pred\mathtt{pred} と完全に同じ議論です。t=succ t1t = \mathtt{succ}\ t_1 のときは E-Succ のみが適用でき、帰納法の仮定から従います。tt が値のときは 補題 4.3 より簡約できないので、仮定に反し、この場合は起こりません。

決定性は「同じプログラムは何度動かしても同じ結果になる」ことの形式的な内容です。並行実行や乱数を導入するとこの定理は成り立たなくなり、その瞬間にデバッグの難しさが跳ね上がります(並行実行のときに順序を制御して結果を定める仕組みについては 定義 6.1[OS の役割] を参照してください)。

5. 型:静的型付けと動的型付け

Section titled “5. 型:静的型付けと動的型付け”

定義 4.1 の直後で見たように、L には行き詰まる項があります。型システムとは、実行する前に、行き詰まる項の一部を排除する仕組みです。

定義 5.1(言語 L の型付け関係)↗

型を T::=Bool∣NatT ::= \mathtt{Bool} \mid \mathtt{Nat} とする。関係 ⊢t:T\vdash t : T(項 tt は型 TT を持つ)を次の規則で生成される最小の関係とする。

(T-True)⊢true:Bool(T-False)⊢false:Bool(T-Zero)⊢0:Nat(T-If)⊢t1:Bool⊢t2:T⊢t3:T⊢if t1 then t2 else t3:T(T-Succ)⊢t:Nat⊢succ t:Nat(T-Pred)⊢t:Nat⊢pred t:Nat(T-Iszero)⊢t:Nat⊢iszero t:Bool\begin{aligned} &\text{(T-True)} && \vdash \mathtt{true} : \mathtt{Bool} \qquad &&\text{(T-False)} && \vdash \mathtt{false} : \mathtt{Bool} \qquad &&\text{(T-Zero)} && \vdash \mathtt{0} : \mathtt{Nat} \\[4pt] &\text{(T-If)} && \frac{\vdash t_1 : \mathtt{Bool} \quad \vdash t_2 : T \quad \vdash t_3 : T}{\vdash \mathtt{if}\ t_1\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 : T} &&\text{(T-Succ)} && \frac{\vdash t : \mathtt{Nat}}{\vdash \mathtt{succ}\ t : \mathtt{Nat}} \\[4pt] &\text{(T-Pred)} && \frac{\vdash t : \mathtt{Nat}}{\vdash \mathtt{pred}\ t : \mathtt{Nat}} &&\text{(T-Iszero)} && \frac{\vdash t : \mathtt{Nat}}{\vdash \mathtt{iszero}\ t : \mathtt{Bool}} \end{aligned}

T-If が t2t_2 と t3t_3 に同じ TT を要求している点に注目してください。この 1 箇所が、後で見る型システムの不完全性の主要な原因になります。

補題 5.2(正準形)✓

(1) vv が値で ⊢v:Bool\vdash v : \mathtt{Bool} ならば、v=truev = \mathtt{true} または v=falsev = \mathtt{false} である。 (2) vv が値で ⊢v:Nat\vdash v : \mathtt{Nat} ならば、vv は数値である。

証明(補題 5.2)

(1) 値は true\mathtt{true}、false\mathtt{false}、数値のいずれかです。vv が数値だと仮定すると、v=0v = \mathtt{0} なら型を与える規則は T-Zero だけなので ⊢v:Nat\vdash v : \mathtt{Nat} しか導けず、⊢v:Bool\vdash v : \mathtt{Bool} に矛盾します。v=succ nvv = \mathtt{succ}\ nv なら型を与える規則は T-Succ だけで、やはり結論は Nat\mathtt{Nat} です。よって vv は数値ではなく、true\mathtt{true} か false\mathtt{false} です。

(2) 同様に、v=truev = \mathtt{true} なら T-True により Bool\mathtt{Bool} しか導けず、v=falsev = \mathtt{false} でも T-False により Bool\mathtt{Bool} しか導けません。いずれも仮定 ⊢v:Nat\vdash v : \mathtt{Nat} に矛盾するので、vv は数値です。

進行定理の狙いは、「型が付けば、行き詰まる場面にそもそも出会わない」ことの片方の半分を示すことです。方針は、型の付いた式について、値でなければ必ず規則が 1 つ適用できることを、型付けの最後の規則ごとに確かめます。

定理 5.3(進行定理)✓

⊢t:T\vdash t : T なる項 tt は、値であるか、または t→t′t \to t' となる項 t′t' が存在する。すなわち、型の付いた項は行き詰まり項ではない。

証明(定理 5.3)

⊢t:T\vdash t : T の導出に関する帰納法で示します。

T-True、T-False、T-Zero が最後に使われた場合、tt はそれぞれ true\mathtt{true}、false\mathtt{false}、0\mathtt{0} であり、いずれも値です。

T-If の場合、t=if t1 then t2 else t3t = \mathtt{if}\ t_1\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 で ⊢t1:Bool\vdash t_1 : \mathtt{Bool} です。帰納法の仮定より、t1t_1 は値であるか簡約できます。簡約できるなら E-If により tt 全体も簡約できます。t1t_1 が値なら、⊢t1:Bool\vdash t_1 : \mathtt{Bool} と 補題 5.2 (1) より t1t_1 は true\mathtt{true} か false\mathtt{false} であり、E-IfTrue または E-IfFalse が適用できます。

T-Succ の場合、t=succ t1t = \mathtt{succ}\ t_1 で ⊢t1:Nat\vdash t_1 : \mathtt{Nat} です。帰納法の仮定より t1t_1 は値か簡約可能です。簡約可能なら E-Succ で tt も簡約できます。値なら 補題 5.2 (2) より t1t_1 は数値なので、succ t1\mathtt{succ}\ t_1 自体が数値、すなわち値です。

T-Pred の場合、t=pred t1t = \mathtt{pred}\ t_1 で ⊢t1:Nat\vdash t_1 : \mathtt{Nat} です。t1t_1 が簡約可能なら E-Pred を使います。t1t_1 が値なら 補題 5.2 (2) より数値であり、数値は 0\mathtt{0} か succ nv\mathtt{succ}\ nv の形です。前者は E-PredZero、後者は E-PredSucc が適用できます。この 2 通りで数値を尽くしていることが要点で、pred false\mathtt{pred}\ \mathtt{false} のような場合は ⊢false:Nat\vdash \mathtt{false} : \mathtt{Nat} が導けないため、そもそもこの場合分けに現れません。

T-Iszero の場合も同じ構造で、E-IszeroZero と E-IszeroSucc が数値のすべての形を覆っています。

保存定理はもう片方の半分です。1 ステップ進んだ後の式にも型が付いていれば、進行定理をもう一度使えます。証明は簡約規則を 1 つずつ取り上げ、それぞれで型が変わらないことを確かめる作業です。

定理 5.4(保存定理)✓

⊢t:T\vdash t : T かつ t→t′t \to t' ならば ⊢t′:T\vdash t' : T である。すなわち、簡約は型を変えない。

証明(定理 5.4)

t→t′t \to t' の導出に関する帰納法で、規則ごとに確かめます。以下、型付け規則は形で一意に決まるので、tt の形から最後に使われた型付け規則を逆に読み取れます(反転補題)。

E-IfTrue:t=if true then t2 else t3→t2t = \mathtt{if}\ \mathtt{true}\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 \to t_2。⊢t:T\vdash t : T の導出は T-If で終わるので、前提として ⊢t2:T\vdash t_2 : T があります。よって ⊢t′:T\vdash t' : T です。E-IfFalse も同様に ⊢t3:T\vdash t_3 : T が前提にあります。

E-If:t→if t1′ then t2 else t3t \to \mathtt{if}\ t_1'\ \mathtt{then}\ t_2\ \mathtt{else}\ t_3 で前提は t1→t1′t_1 \to t_1'。T-If の前提から ⊢t1:Bool\vdash t_1 : \mathtt{Bool}、⊢t2:T\vdash t_2 : T、⊢t3:T\vdash t_3 : T。帰納法の仮定より ⊢t1′:Bool\vdash t_1' : \mathtt{Bool}。この 3 つに T-If を適用して ⊢t′:T\vdash t' : T を得ます。

E-Succ:T-Succ より T=NatT = \mathtt{Nat} かつ ⊢t1:Nat\vdash t_1 : \mathtt{Nat}。帰納法の仮定より ⊢t1′:Nat\vdash t_1' : \mathtt{Nat}、T-Succ を適用して ⊢succ t1′:Nat\vdash \mathtt{succ}\ t_1' : \mathtt{Nat}。

E-PredZero:pred 0→0\mathtt{pred}\ \mathtt{0} \to \mathtt{0}。T-Pred より T=NatT = \mathtt{Nat} で、T-Zero より ⊢0:Nat\vdash \mathtt{0} : \mathtt{Nat} です。

E-PredSucc:pred (succ nv)→nv\mathtt{pred}\ (\mathtt{succ}\ nv) \to nv。T-Pred より T=NatT = \mathtt{Nat} かつ ⊢succ nv:Nat\vdash \mathtt{succ}\ nv : \mathtt{Nat}。これを導けるのは T-Succ だけなので、その前提として ⊢nv:Nat\vdash nv : \mathtt{Nat} があります。

E-Pred:T-Pred の前提 ⊢t1:Nat\vdash t_1 : \mathtt{Nat} に帰納法の仮定を適用し、T-Pred を再適用します。

E-IszeroZero:iszero 0→true\mathtt{iszero}\ \mathtt{0} \to \mathtt{true}。T-Iszero より T=BoolT = \mathtt{Bool} で、T-True より ⊢true:Bool\vdash \mathtt{true} : \mathtt{Bool}。E-IszeroSucc も同じく T-False を使います。

E-Iszero:T-Iszero の前提に帰納法の仮定を適用し、T-Iszero を再適用します。以上ですべての簡約規則を尽くしました。

系 5.5(型健全性)✓

⊢t:T\vdash t : T ならば、tt から始まる任意の簡約列 t→t1→⋯→tnt \to t_1 \to \cdots \to t_n について tnt_n は行き詰まり項ではない。すなわち、型の付いたプログラムは実行の途中で行き詰まらない。

証明(系 5.5)

nn に関する帰納法です。定理 5.4 を nn 回繰り返し適用すると ⊢tn:T\vdash t_n : T が得られます。この tnt_n に 定理 5.3 を適用すると、tnt_n は値であるか、さらに簡約できるかのいずれかです。行き詰まり項とは「値でない正規形」でしたから、tnt_n はそのどちらの条件も同時には満たせず、行き詰まり項ではありません。

「進行 + 保存 = 健全性」というこの 2 段構えは、Wright と Felleisen が 1994 年に定式化して以来、型システムを設計するときの標準的な証明義務になっています。新しい言語機能を足したら、この 2 つの定理を証明し直す。それが「型システムを設計する」ことの実質です。

5.1. 静的型検査は必ず不完全である

Section titled “5.1. 静的型検査は必ず不完全である”

系 5.5 で保証されるのは片側だけです。型が付けば安全ですが、逆は成り立ちません。if true then 0 else false\mathtt{if}\ \mathtt{true}\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{false} は E-IfTrue で 0\mathtt{0} に簡約されて何の問題もなく終わりますが、T-If が枝の型の一致を要求するため型が付きません。これは L という玩具言語の欠陥でしょうか。違います。

定理 5.6(健全な静的検査の不完全性)✓

L\mathcal{L} をチューリング完全なプログラミング言語とし、PP を「実行しても型エラーで停止しないプログラム」全体の集合とする。C⊆LC \subseteq \mathcal{L} が決定可能(あるプログラムが CC に属するか否かを必ず有限時間で判定できる)かつ健全(C⊆PC \subseteq P)ならば、C⊊PC \subsetneq P である。すなわち、安全であるのに CC が受理しないプログラムが必ず存在する。

証明(定理 5.6)

まず PP が決定不能であることを、停止性問題からの帰着で示します。チューリング機械 MM と入力 ww の組が与えられたとき、次のプログラム pM,wp_{M,w} を機械的に構成します。

p(M, w) の本体:
M を w 上でシミュレートする # 停止しないかもしれない
1 + true を評価する # ここに到達すれば必ず型エラー

L\mathcal{L} はチューリング完全なので MM のシミュレータを L\mathcal{L} で書けます。このとき、pM,wp_{M,w} が型エラーを起こすのは、シミュレーションが停止して 2 行目に到達したとき、かつそのときに限ります。よって

pM,w∈P  ⟺  M は w 上で停止しないp_{M,w} \in P \iff M \text{ は } w \text{ 上で停止しない}

が成り立ちます。PP が決定可能だとすると、この対応によって停止性問題も決定可能になり、チューリングの結果に矛盾します。よって PP は決定不能です。

さて CC は決定可能、PP は決定不能なので C≠PC \ne P です。仮定より C⊆PC \subseteq P ですから、C⊊PC \subsetneq P が従います。P∖CP \setminus C の元が、安全なのに受理されないプログラムです。

すべてのプログラム安全なプログラム(決定不能)型検査を通るif true then 0 else false(安全だが型が付かない)
健全な静的型検査が捉えられる範囲。太線の内側だけが「型が付く」プログラムで、その外にも安全なプログラムが必ず残ります
観点静的型付け(Java, OCaml, Rust, TypeScript)動的型付け(Python, Ruby, JavaScript)
誤りの検出実行前。到達しない経路の誤りも見つかる実行時。そのコードを通るテストが必要
拒否されるプログラム安全でも型が付かないものを拒否する拒否しない。動かして初めて分かる
実行性能型が確定するので値の表現を最適化できる実行時に型タグを検査する分の負荷がある
保守性型が機械検査される仕様書として働く仕様は文書とテストに依存する
記述の柔軟さ型を通すための記述が必要になることがある試作や探索的なコードを短く書ける
リファクタリング型検査器が呼び出し側の修正漏れを指摘する漏れは実行して初めて露見する

例 5.7(型推論を最後まで計算する)

静的型付けの「型を書く手間」は、型推論でかなり削れます。関数 twice(引数 f を 2 回適用する)の型を、制約を集めて解く方法で求めます。

twice=λf. λx. f (f x)\mathtt{twice} = \lambda f.\ \lambda x.\ f\ (f\ x)

ff の型を型変数 α\alpha、xx の型を β\beta と置きます。

  1. 内側の適用 f xf\ x が型付くには、ff は β\beta を受け取る関数でなければなりません。結果の型を γ\gamma と置くと、制約 α=β→γ\alpha = \beta \to \gamma が出ます。
  2. 外側の適用 f (f x)f\ (f\ x) では、ff は γ\gamma を受け取ります。結果を δ\delta と置くと、制約 α=γ→δ\alpha = \gamma \to \delta が出ます。
  3. 2 つの制約から β→γ=γ→δ\beta \to \gamma = \gamma \to \delta。関数型の等式は引数どうし・結果どうしの等式に分解できるので、β=γ\beta = \gamma かつ γ=δ\gamma = \delta を得ます。
  4. したがって γ=δ=β\gamma = \delta = \beta、α=β→β\alpha = \beta \to \beta です。

全体の型は (β→β)→β→β(\beta \to \beta) \to \beta \to \beta となり、β\beta に制約が残っていないので全称量化して ∀β. (β→β)→β→β\forall \beta.\ (\beta \to \beta) \to \beta \to \beta が得られます。型注釈を 1 つも書かずに、最も一般的な型が一意に決まりました。この手続きが Hindley–Milner 型推論であり、OCaml や Haskell、そして Rust の局所的な型推論の基礎になっています。

6. パラダイム:関数型とオブジェクト指向

Section titled “6. パラダイム:関数型とオブジェクト指向”

6.1. 関数型:参照透過性が定理を生む

Section titled “6.1. 関数型:参照透過性が定理を生む”

関数型プログラミングの理論的な母体は 1930 年代の λ\lambda 計算です。項は変数 xx、抽象 λx. t\lambda x.\ t、適用 t1 t2t_1\ t_2 の 3 つだけで、計算規則も β\beta 簡約 (λx. t) u→t[x:=u](\lambda x.\ t)\ u \to t[x := u] の 1 つだけです。この極小の体系がチューリング機械と同じ計算能力を持ちます。

定理 6.1(Church–Rosser の定理(合流性))※

λ\lambda 計算の項 tt について、t↠t1t \twoheadrightarrow t_1 かつ t↠t2t \twoheadrightarrow t_2(↠\twoheadrightarrow は β\beta 簡約の反射推移閉包)ならば、t1↠st_1 \twoheadrightarrow s かつ t2↠st_2 \twoheadrightarrow s となる項 ss が存在する。

注意 6.2

証明は Tait と Martin-Löf による平行簡約を使う方法が標準で、Barendregt の教科書の第 3 章に完全な形が載っています。ここでは主張の意味だけ押さえてください。定理 4.4 が「簡約の順序が 1 通りしかない」ことを言っていたのに対し、Church–Rosser は「順序は複数あってよいが、結果は合流する」ことを言っています。だから正規形は高々 1 つに定まり、式のどの部分から評価しても最終結果は変わりません。並列実行や遅延評価が意味を壊さない根拠がここにあります。

定義 6.3(参照透過性)↗

評価関係が決定的であり、式の評価結果が自由変数への束縛のみに依存し、評価が計算機の状態(変数の書き換え、入出力、時刻など)を変化させないとき、その言語は参照透過であるという。

命題 6.4(共通部分式除去の健全性)✓

参照透過な言語において、式 ee が環境 ρ\rho のもとで値 vv に評価されるとする。ee の 2 度の出現がいずれも環境 ρ\rho のもとにある(すなわち ee の自由変数がどちらの出現でも同じ束縛を受ける)とき、その 2 度の出現をどちらも vv で置き換えても、全体の評価結果は変わらない。

証明(命題 6.4)

ee の 1 度目の評価が値 vv を返したとします。定義 6.3 より評価は状態を変化させないので、1 度目の評価の前後で環境 ρ\rho は同一です。仮定より 2 度目の出現も ee の自由変数に同じ束縛を与える位置にあるので、2 度目の評価も同じ環境 ρ\rho のもとで行われます。同じ環境・同じ式に対する評価は、決定性の仮定より同じ値を返します。よって 2 度目の結果も vv です。両方の出現がいずれも vv に等しい値へ評価される以上、それらを vv で置き換えても全体の値は変わりません。

この命題が、ee を 1 度だけ計算して使い回す最適化(共通部分式除去)を正当化します。逆に、参照透過性がないとこの最適化は不正になります。

例 6.5(副作用があると最適化が壊れる)

次の Python コードで、f() + f() を「共通部分式」とみなして 1 回の呼び出しにまとめると結果が変わります。

counter = 0
def f():
global counter
counter += 1
return counter
print(f() + f()) # 1 + 2 = 3
counter = 0
a = f()
print(a + a) # 1 + 1 = 2

出力は 3 と 2 で一致しません。定義 6.3 の「評価が状態を変化させない」という仮定が破れているため、命題 6.4 の証明の第 1 段(環境が同一であること)が成立しないからです。関数型言語が副作用を型で隔離したり禁止したりするのは、禁欲のためではなく、この種の等式変形を安全に使うためです。

6.2. オブジェクト指向:動的ディスパッチと部分型

Section titled “6.2. オブジェクト指向:動的ディスパッチと部分型”

オブジェクト指向の中心は継承ではなく、呼ぶべきコードを実行時に受け手が決めるという動的ディスパッチです。その正体は、データと関数ポインタを束ねたレコードにすぎません。

def make_circle(r):
return {"area": lambda: 3.14159 * r * r, "name": lambda: "円"}
def make_square(s):
return {"area": lambda: s * s, "name": lambda: "正方形"}
shapes = [make_circle(1.0), make_square(2.0)]
for sh in shapes:
print(sh["name"](), sh["area"]()) # 円 3.14159 / 正方形 4.0

sh["area"] がどの関数を指すかは、sh の中身を見るまで決まりません。機械語のレベルでは、これは「関数ポインタをメモリから読んで、そのアドレスへ間接ジャンプする」という 2 命令に落ちます(C++ や Java の仮想関数表がまさにこの構造です)。分岐先が静的に決まらないため、コンピュータアーキテクチャと CPU の構造 で扱った分岐予測が効きにくく、これが仮想呼び出しのコストの正体です(予測が外れるとパイプラインの利得が失われます。命題 7.2[コンピュータアーキテクチャとCPUの構造] を参照)。

定義 6.6(部分型と包摂規則)↗

型の間の関係 S≤TS \le T(SS は TT の部分型)を、次の包摂規則が健全であるような関係として要求する。

Γ⊢t:SS≤TΓ⊢t:T\frac{\Gamma \vdash t : S \qquad S \le T}{\Gamma \vdash t : T}

すなわち、TT が期待される場所には SS の値をそのまま置いてよい。

これが Liskov の置換原則の型理論版です。では関数型どうしの部分型関係はどうなるでしょうか。

直感は次のとおりです。「猫を受け取る関数」を「動物を受け取る関数」の代わりに使うと、犬が渡されたときに困ります。逆に、「動物を受け取る関数」は猫が渡されても困りません。そのため、引数の側では部分型の向きが逆になります。下の図は、この向きの違いを並べたものです。

引数の型関数型(結果は Unit で共通)CatAnimalCat → UnitAnimal → Unit
引数の型の関係と、関数型の関係。矢印は「部分型である」向きを表します。引数の側では矢印の向きが逆になります

命題 6.7(関数型の変性)✓

関数型の部分型関係は

S1→S2≤T1→T2⟸T1≤S1 かつ S2≤T2S_1 \to S_2 \le T_1 \to T_2 \quad\Longleftarrow\quad T_1 \le S_1 \ \text{かつ}\ S_2 \le T_2

で与えられる。すなわち引数の位置では反変(向きが逆)、結果の位置では共変(向きが同じ)である。

証明(命題 6.7)

ff を型 S1→S2S_1 \to S_2 の関数とし、これを T1→T2T_1 \to T_2 が期待される場所で使ったとします。定義 6.6 の要求は「その使い方で型エラーが起きない」ことです。

引数について。呼び出し側は T1→T2T_1 \to T_2 だと信じているので、T1T_1 型の任意の値を渡してきます。ff が受け取れるのは S1S_1 型の値なので、T1T_1 型の値がすべて S1S_1 型として通用しなければなりません。これはまさに T1≤S1T_1 \le S_1 です。

結果について。ff は S2S_2 型の値を返します。呼び出し側はそれを T2T_2 型として扱うので、S2S_2 型の値がすべて T2T_2 型として通用する必要があります。これが S2≤T2S_2 \le T_2 です。

以上より、この 2 条件のもとで ff の使用は型エラーを起こしません。

例 6.8(共変にすると壊れる:Java の配列)

Java は配列を共変にしています。すなわち String が Object の部分型なら String[] も Object[] の部分型として扱われます。命題 6.7 の観点で言えば、配列への書き込みは引数の位置(Object を受け取る操作)なので、反変でなければ健全になりません。共変にした結果、次のコードはコンパイルを通ってしまいます。

String[] names = new String[1];
Object[] objs = names; // 共変なので許される
objs[0] = Integer.valueOf(42); // コンパイル時は Object[] なので通る

しかし実行すると 3 行目で ArrayStoreException が送出されます。これは 系 5.5 でいう型健全性が破れている状態で、Java は実行時に配列の要素型を検査する動的チェックを追加して穴を塞いでいます。ジェネリクス(List<String> と List<Object>)が非変に設計されているのは、同じ失敗を繰り返さないためです。

両者は同じ問題の異なる面を最適化しています。「図形の種類」と「図形に対する操作」からなる 2 次元の表を考えると、拡張の方向が直交していることが見えます。

新しいデータ(三角形を追加)新しい操作(周長を追加)
関数型(代数的データ型 + パターンマッチ)既存の全関数に分岐を書き足す必要がある新しい関数を 1 つ足すだけで済む
オブジェクト指向(クラス + 動的ディスパッチ)新しいクラスを 1 つ足すだけで済む既存の全クラスにメソッドを足す必要がある

これが Wadler の言う「式問題」です。片方が楽な方向は、もう片方では苦しい方向になります。近年の言語(Scala、Rust、Swift、Kotlin)が代数的データ型とトレイト・プロトコルの両方を備えているのは、対象の性質に応じて拡張しやすい側を選べるようにするためです。パラダイムは信条ではなく、拡張の軸をどちらに取るかという設計判断だと考えるのがよいでしょう。

演習 7.1易

言語 L の項 pred (succ (succ 0))\mathtt{pred}\ (\mathtt{succ}\ (\mathtt{succ}\ \mathtt{0})) を、定義 4.1 の規則名を添えながら正規形まで簡約してください。また、この項に 定義 5.1 で型が付くことを確かめてください。

解答

最外の項は pred u\mathtt{pred}\ u(u=succ (succ 0)u = \mathtt{succ}\ (\mathtt{succ}\ \mathtt{0}))の形です。uu は succ nv\mathtt{succ}\ nv で nv=succ 0nv = \mathtt{succ}\ \mathtt{0} は数値なので、E-PredSucc が適用できます。

pred (succ (succ 0))→succ 0(E-PredSucc, nv=succ 0)\mathtt{pred}\ (\mathtt{succ}\ (\mathtt{succ}\ \mathtt{0})) \to \mathtt{succ}\ \mathtt{0} \qquad (\text{E-PredSucc},\ nv = \mathtt{succ}\ \mathtt{0})

succ 0\mathtt{succ}\ \mathtt{0} は数値なので値であり、補題 4.3 (1) よりこれ以上簡約できません。1 ステップで正規形に到達しました。E-Pred を先に使おうとしても、その前提 u→u′u \to u' が同じ補題により成立しないので使えません。

型付けは、T-Zero で ⊢0:Nat\vdash \mathtt{0} : \mathtt{Nat}、T-Succ を 2 回適用して ⊢succ (succ 0):Nat\vdash \mathtt{succ}\ (\mathtt{succ}\ \mathtt{0}) : \mathtt{Nat}、最後に T-Pred で ⊢pred (succ (succ 0)):Nat\vdash \mathtt{pred}\ (\mathtt{succ}\ (\mathtt{succ}\ \mathtt{0})) : \mathtt{Nat} です。定理 5.4 の主張どおり、簡約後の succ 0\mathtt{succ}\ \mathtt{0} も T-Zero と T-Succ により Nat\mathtt{Nat} 型を持ちます。

演習 7.2標準

言語 L の項で、型が付かないにもかかわらず、簡約すると値に到達する(行き詰まらない)ものを 1 つ挙げ、両方を確認してください。この現象が 系 5.5 と矛盾しない理由も述べてください。

解答

t=if (iszero 0) then 0 else falset = \mathtt{if}\ (\mathtt{iszero}\ \mathtt{0})\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{false} を取ります。

型が付かないこと:tt に型を与えうる規則は T-If だけで、その前提は ⊢0:T\vdash \mathtt{0} : T と ⊢false:T\vdash \mathtt{false} : T を同じ TT について要求します。0\mathtt{0} に型を与える規則は T-Zero だけなので T=NatT = \mathtt{Nat}、false\mathtt{false} に型を与える規則は T-False だけなので T=BoolT = \mathtt{Bool} です。Nat≠Bool\mathtt{Nat} \ne \mathtt{Bool} なので両立せず、tt には型が付きません。

値に到達すること:E-If の前提を E-IszeroZero で導いて t→if true then 0 else falset \to \mathtt{if}\ \mathtt{true}\ \mathtt{then}\ \mathtt{0}\ \mathtt{else}\ \mathtt{false}、次に E-IfTrue で →0\to \mathtt{0}。0\mathtt{0} は値です。

矛盾しない理由:系 5.5 は「型が付く ⇒\Rightarrow 行き詰まらない」という一方向の含意であり、その逆「行き詰まらない ⇒\Rightarrow 型が付く」は主張していません。そして 定理 5.6 が示すとおり、逆向きが成り立つ決定可能な型システムは(チューリング完全な言語では)そもそも存在しません。false\mathtt{false} の枝は決して実行されませんが、型検査器は「どちらの枝も実行されうる」という保守的な前提で判断します。

演習 7.3標準

定理 4.4 の証明で、t=iszero t1t = \mathtt{iszero}\ t_1 の場合を、補題 4.3 をどこで使うかを明示しながら完全に書いてください。

解答

t1t_1 の形で場合分けします。

(a) t1=0t_1 = \mathtt{0} のとき。適用候補は E-IszeroZero と E-Iszero です。E-Iszero を使うには前提 0→t1′\mathtt{0} \to t_1' が必要ですが、0\mathtt{0} は数値なので 補題 4.3 (1) よりこの前提は成立しません。よって適用できるのは E-IszeroZero だけで、t′=t′′=truet' = t'' = \mathtt{true} です。

(b) t1=succ nvt_1 = \mathtt{succ}\ nv(nvnv は数値)のとき。t1t_1 自身が数値なので、同じく 補題 4.3 (1) より E-Iszero の前提は成立しません。適用できるのは E-IszeroSucc だけで、t′=t′′=falset' = t'' = \mathtt{false} です。

(c) t1=succ ut_1 = \mathtt{succ}\ u で uu が数値でないとき。E-IszeroSucc は右辺の nvnv が数値であることを要求するので形が合いません。E-IszeroZero も形が合いません。よって両方の簡約は E-Iszero によるもので、それぞれ前提 t1→st_1 \to s、t1→s′t_1 \to s' を持ちます。帰納法の仮定より s=s′s = s'、したがって t′=iszero s=iszero s′=t′′t' = \mathtt{iszero}\ s = \mathtt{iszero}\ s' = t'' です。

(d) t1t_1 が上のいずれでもないとき(true\mathtt{true}、false\mathtt{false}、あるいは if\mathtt{if} や pred\mathtt{pred} で始まる項)。E-IszeroZero も E-IszeroSucc も形が合わないので、両方 E-Iszero であり、(c) と同じ議論で t′=t′′t' = t'' を得ます。なお t1t_1 が true\mathtt{true} や false\mathtt{false} の場合は 補題 4.3 (2) より E-Iszero の前提も成立せず、そもそも tt は簡約できないため、t→t′t \to t' という仮定のもとでは起こりません。

演習 7.4難

関数型の部分型規則を「引数についても共変」、すなわち S1≤T1S_1 \le T_1 かつ S2≤T2S_2 \le T_2 ならば S1→S2≤T1→T2S_1 \to S_2 \le T_1 \to T_2 と定めたとします。型 Cat と Dog がともに Animal の部分型であるとして、この規則のもとで型検査を通るのに実行時に破綻するプログラムを構成してください。

解答

関数 gg を「猫を受け取り、その猫の爪を研がせる」ものとします。すなわち g:Cat→Unitg : \mathtt{Cat} \to \mathtt{Unit} で、本体では Cat にしかない操作(たとえば「爪を研ぐ」)を呼びます。

いま仮の共変規則を使うと、Cat≤Animal\mathtt{Cat} \le \mathtt{Animal} かつ Unit≤Unit\mathtt{Unit} \le \mathtt{Unit} より

Cat→Unit ≤ Animal→Unit\mathtt{Cat} \to \mathtt{Unit} \ \le\ \mathtt{Animal} \to \mathtt{Unit}

が導けます。よって 定義 6.6 の包摂規則により、gg を型 Animal→Unit\mathtt{Animal} \to \mathtt{Unit} の値として使えます。

そこで、Animal→Unit\mathtt{Animal} \to \mathtt{Unit} を引数に取る高階関数 hh に gg を渡し、hh の中で Dog\mathtt{Dog} の値を適用します。Dog≤Animal\mathtt{Dog} \le \mathtt{Animal} なのでこの適用は型検査を通ります。しかし実行時に gg の本体は渡された値に「爪を研ぐ」を要求し、Dog にはその操作がないので破綻します。型が付いたのに行き詰まったので、系 5.5 の型健全性が失われています。

正しい規則 命題 6.7 では、Cat→Unit≤Animal→Unit\mathtt{Cat} \to \mathtt{Unit} \le \mathtt{Animal} \to \mathtt{Unit} を導くには Animal≤Cat\mathtt{Animal} \le \mathtt{Cat} が必要ですが、これは成り立たないため、最初の一歩が塞がれます。例 6.8 の Java 配列は、まさにこの反変性を破った実例です。

  • Benjamin C. Pierce, Types and Programming Languages, MIT Press, 2002 — 第 3 章(算術式の意味論)、第 8 章(型付き算術式、進行定理と保存定理)、第 15 章(部分型)。この記事の言語 L と定理の骨格はこの本の構成に沿っています。
  • Robert Harper, Practical Foundations for Programming Languages, 2nd ed., Cambridge University Press, 2016 — 構造的操作的意味論と型安全性の一般的な扱い。
  • A. Wright and M. Felleisen, “A Syntactic Approach to Type Soundness”, Information and Computation 115 (1994), 38–94 — 進行と保存による型健全性証明の定式化。
  • H. P. Barendregt, The Lambda Calculus: Its Syntax and Semantics, revised ed., North-Holland, 1984 — 第 3 章に Church–Rosser の定理の完全な証明。
  • A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers: Principles, Techniques, and Tools, 2nd ed., Addison-Wesley, 2006 — 字句解析から最適化・コード生成までの標準的な教科書。
  • B. H. Liskov and J. M. Wing, “A Behavioral Notion of Subtyping”, ACM Transactions on Programming Languages and Systems 16(6) (1994), 1811–1841 — 置換原則の厳密な定式化。
  • RISC-V International, The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — https://riscv.org/technical/specifications/ 命令形式と符号化の一次資料。

このテーマの 100 本ノック(50 問)

執筆: Claude Code(Sonnet 5.5)・監査 3 回(Claude Code)・最終 2026-10-12

この記事の誤りを報告する ・運営: 夢現技研合同会社 ・料金 ・利用条件 ・特定商取引法に基づく表記

© 2026 夢現技研合同会社 ・コード例は MIT ライセンスです。