Theorem Proving in Lean 4

by Jeremy Avigad, Leonardo de Moura, Soonho Kong and Sebastian Ullrich, with contributions from the Lean Community

このテキストは読者がLean 4を使うことを前提にしています。Lean 4をインストールするには、Lean 4 Manualの節Quickstartをご覧ください。このテキストの最初のバージョンはLean 2用に書かれました。Lean 3用のバージョンはこちらで入手可能です。

この翻訳について

translated by aconite(2章~12章), Haruhisa Enomoto(1章)

この翻訳は有志による非公式翻訳です。翻訳に際して、表現を大きく変えた箇所や、分かりやすさを期すため記述やコード例を追加した箇所があります。また、用語の訳が一般的でない可能性があります。誤りを含む可能性もあります。必要に応じて原文Theorem Proving in Lean 4 (GitHub)もご覧ください。

原文のライセンスはApache License 2.0であり、それに基づいて原文を翻訳・公開しています。

この翻訳のソースはGitHubリポジトリから入手可能です。また、ページ右上端のGitHubマークを押してGitHubリポジトリに移動することもできます。この翻訳の全ページとソースはApache License 2.0の下で公開されています。

誤字脱字、内容の誤りの指摘、フォークからのPull Request、フォークによる翻訳の改変等は歓迎いたします。基本的に、ご指摘はGitHubリポジトリのIssuesで受け付けます。

翻訳に際して、機械翻訳サービスDeepL翻訳を参考にしました。

バージョン情報

この翻訳は原文のcommit 81b028359684646f2db48e3909ee81b4fad51dfb (Date: Fri Mar 1 14:54:59 2024 -0600)に基づいています。

Introduction (イントロダクション)

Computers and Theorem Proving (コンピュータと定理証明)

Formal verification(形式検証)では、論理的・計算的手法を使って、厳密な数学用語で表現された主張を立証する。この主張には、通常の数学の定理の他にも、ハードウェアやソフトウェアの一部、 ネットワークプロトコル、機械的・ハイブリッドシステムがその仕様を満たしているという主張も含まれる。実際には、数学的主張の検証をすることと、システムの正しさを検証することの間には、明確な区別はない。というのも、形式検証では、ハードウェアやソフトウェアのシステムを数学的な言葉で記述する必要があり、その時点で、その正しさに関する主張を立証することは、定理証明の一形態となる。逆に、数学の定理の証明には長い計算が必要な場合があり、その場合、定理の真偽を検証するには、その計算が想定通りに実行されることを検証する必要がある。

数学的な主張を裏付けるための最も信頼性のある方法は証明を与えることであり、20世紀の論理学の発展により、従来の証明方法のすべてではないにせよ、そのほとんどが、いくつかの基礎的な体系における小さな公理と規則の集合に還元できることが明らかになった。この還元のもとで、コンピューターが主張の立証に役立つ方法は2つある: 1つは、そもそも証明を「発見」する手助けをすること、もう1つは証明とされるものが正しいかどうかを「検証」する手助けをすることである。

Automated theorem proving(自動定理証明)は、この「発見」の側面に焦点を当てている。resolution theorem provers(導出原理に基づく証明器)、tableau theorem provers(タブロー定理証明器)、fast satisfiability solvers(高速充足可能性ソルバー)などは、命題論理や一階論理における公式の正しさを証明する手段を提供する。また、整数や実数に対する線形式や非線形式など、特定の言語や領域に対する探索手続きや決定手続きを提供するシステムもある。SMT(satisfiability modulo theories, 充足可能性モジュロ理論)のようなアーキテクチャは、領域一般的な探索手法と領域固有の手続きを組み合わせたものである。数式処理システムや高度な数学ソフトウェア・パッケージは、数学的計算を実行したり、数学的上界または下界を確立したり、数学的対象を見つけたりする手段を提供する。計算は証明と見なすこともでき、これらのシステムも数学的主張の立証に役立つ。

自動推論システムは、そのパワーと効率性を追求する結果、しばしば健全性の保証を犠牲にしている。このようなシステムにはバグがあることもあり、システムが導いた結果が正しいことを保証するのは難しい。対照的に、interactive theorem proving(対話型定理証明)は、定理証明の「検証」の側面に焦点を当て、全ての主張が適切な公理的基礎づけにおける証明によって保証されることを要求する。その基準は非常に高く、推論の全てのルールと計算の全てのステップは、それ以前の定義や定理に訴えることで正当化されなければならず、それら全ては最終的には基本的な公理や規則にまで遡る。実際、このようなシステムのほとんどは、他のシステムに伝達したり、独立にチェックしたりできる、完全に精緻化された「証明オブジェクト」を提供する。このような証明を構築するためには、通常ユーザーからの入力やインタラクションがより多く必要となるが、それによってより深く複雑な証明を得ることができる。

Lean Theorem Prover(Lean定理証明器)は、これら対話型定理証明と自動定理証明との間のギャップを埋めることを目的としている。このために、ユーザーとのインタラクションと完全に明記された公理的証明の構築が可能であるフレームワークの内部で、自動化されたツールと方法が使えるようにする。そのゴールは、数学的推論と複雑なシステムについての推論の両方をサポートし、両方の領域における主張を検証することである。

Leanの基礎となる論理は計算的解釈を持ち、Leanはプログラミング言語とみなすこともできる。もっと言えば、Leanは厳密な意味論を持つプログラムを書くためのシステムであり、プログラムが計算する関数について推論を行うためのシステムである。Leanはまた、独自のmetaprogramming language(メタプログラミング言語)として機能するメカニズムを持つ。つまり、Leanそのものを使って自動化を実装したり、Leanの機能を拡張したりすることができる。Leanのこのような側面は、無料のオンラインテキストFunctional Programming in Leanで解説されるが、システムの計算的側面はここでも登場するだろう。

About Lean (Leanについて)

Leanプロジェクトは、2013年にMicrosoft Research RedmondのLeonardo de Mouraによって立ち上げられた、現在進行中の長期的な取り組みであり、自動化への潜在的可能性の多くは時間をかけて徐々に実現されるだろう。LeanはApache License 2.0の下でリリースされている。これは寛容なオープンソースライセンスであり、他の人がコードと数学ライブラリを自由に利用し、拡張することを許可している。

Leanをあなたのコンピュータにインストールするには、Quickstartにある指示を参照するのがよいだろう。LeanのソースコードとLeanのビルド方法は、https://github.com/leanprover/lean4/から入手できる。

このチュートリアルは、Lean 4として知られるLeanの現在のバージョンについて記述する。

About this Book (この本について)

この本は、Leanで証明を記述し検証する方法を学ぶために意図されている。そのために必要な背景情報の多くは、Lean特有のものではない。まず最初に、Leanがベースにしている論理システムを学ぶ。これはdependent type theory(依存型理論)の一種で、通常の数学の定理のほとんどを証明するのに十分強力であり、またそれを自然な方法で行うのに十分な表現力を持っている。より具体的には、LeanはCalculus of Constructionsとして知られるシステムのうち、帰納型を持つものの一種に基づいている。Leanは依存型理論で数学的対象を定義し数学的主張を表現できるだけでなく、依存型理論を証明を書くための言語としても使用できる。

完全に詳述された公理的証明は非常に複雑であるため、定理証明の課題は、できるだけ多くの細部をコンピュータに埋めさせることである。2章 Dependent Type Theory (依存型理論)では、これを行う様々な方法を学ぶことができる。例えば、項の書き換えや、項や式を自動的に単純化するLeanの自動化手法などである。同様に、elaborationtype inference(型推論)の方法も、柔軟な代数的推論をサポートするために使うことができる。

最後に、システムとのコミュニケーションに使用する言語や、複雑な理論やデータを管理するためにLeanが提供するメカニズムなど、Lean特有の機能について学ぶ。

本文中には、以下のようなLeanのコード例が見られる:

theorem and_commutative (p q : Prop) : p ∧ q → q ∧ p :=
  fun hpq : p ∧ q =>
  have hp : p := And.left hpq
  have hq : q := And.right hpq
  show q ∧ p from And.intro hq hp

(訳者注: 以下の文章は、文章中にあるようにVS Codeのコマンドを用いて翻訳元英語版を開いた際の話だと思われます。) この本をVS Codeの中で読んでいると、"try it!"というボタンが表示される。このボタンを押すと、コード例を正しくコンパイルするのに十分な周囲の非表示コードとともに、エディタにコード例がコピーされる。読者はコード例を自由に修正することができ、それに応じてLeanは読者が入力した結果をチェックし、継続的にフィードバックを提供する。この後の章を読み進めながら、自分でコード例を実行し、自分自身で書いたコードを試してみることをお勧めする。本書はVS Code上で「Lean 4: Open Documentation View」コマンドで開くことができる。

Acknowledgments (謝辞)

This tutorial is an open access project maintained on Github. Many people have contributed to the effort, providing corrections, suggestions, examples, and text. We are grateful to Ulrik Buchholz, Kevin Buzzard, Mario Carneiro, Nathan Carter, Eduardo Cavazos, Amine Chaieb, Joe Corneli, William DeMeo, Marcus Klaas de Vries, Ben Dyer, Gabriel Ebner, Anthony Hart, Simon Hudon, Sean Leather, Assia Mahboubi, Gihan Marasingha, Patrick Massot, Christopher John Mazey, Sebastian Ullrich, Floris van Doorn, Daniel Velleman, Théo Zimmerman, Paul Chisholm, Chris Lovett, and Siddhartha Gadgil for their contributions. Please see lean prover and lean community for an up to date list of our amazing contributors.

Dependent Type Theory (依存型理論)

Dependent Type Theory(依存型理論)は強力で表現力の高い言語であり、複雑な数学的主張を表現したり、複雑なハードウェアやソフトウェアの仕様を記述したり、これら両方について自然で統一された方法で推論したりすることができる。LeanはCalculus of Constructionsとして知られる依存型理論の一種に基づいている。そして、Leanは「非累積的宇宙の可算無限階層」と「帰納型」を備えている。この章が終わるころには、これらが意味するところを理解できているだろう。

Simple Type Theory (単純型理論)

「型理論」という名前は「全てのexpression(式 あるいは 項)はそれに関連した型を持つ」という事実に由来する。例えば、適切な文脈の下で項 x + 0 は自然数型を持ち、項 f は自然数を受け取り自然数を返す関数型を持つ。正確な定義を述べると、Leanにおいて「自然数」とは「任意精度の符号なし整数」のことである。

Leanでは項を次のように宣言する。

-- def <項の名前(識別子)> : <項の型> := <項の定義式>
def m : Nat := 1           -- `m` は自然数型を持つ
def b1 : Bool := true      -- `b1` はブール型を持つ
def b2 : Bool := false

項の型をチェックするには次のようにする。

def m : Nat := 1
def b1 : Bool := true
def b2 : Bool := false
-- #check <項>
#check m                   -- output: Nat
#check b1 && b2            -- `&&` は「かつ」 output: Bool
#check b1 || b2            -- `||` は「または」 output: Bool

項を評価する(項の値を計算する)には次のようにする。

def m : Nat := 1
def b1 : Bool := true
def b2 : Bool := false
-- #eval <項>
#eval 5 * 4                -- 20
#eval m + 2                -- 3
#eval b1 && b2             -- false

/--/ はコメントブロックを形成し、その中のテキストは無視される。同様に、-- から行末までもコメントとみなされ、コメントは無視される。コメントブロックは入れ子にすることができる。そのため、多くのプログラミング言語と同じように、コードの塊を「コメントアウト」することができる。

def キーワードは現在の作業環境で新しい名前付きの項を宣言(定義)する。上の例では、def m : Nat := 1 は値が 1 である Nat 型の新しい項 m を定義している。#check <項の名前> コマンドはその項が持つ型を報告するようLeanに要求する。Leanでは、システムに情報を問い合わせる補助コマンドは基本的にハッシュ記号(#)から始まる。#eval <項の名前> コマンドはその項を評価するようLeanに要求する。いくつかの項を自分で宣言し、いくつかの項を自分で型チェックしてみてほしい。def を使って新しいオブジェクトを宣言することは、システム上で実験するための良い方法である。

Simple Type Theory(単純型理論)が強力なのは、他の型から新しい型を作ることができるからである。例えば、ab が型なら、a -> ba から b への関数の型を表し、a × ba の要素と b の要素からなるペアの型(ab の直積型)を表す。ここで、× はUnicode記号であることに注意してほしい。LeanではUnicodeを使用する。Unicodeを適切に使用することで読みやすさを向上させることができる。また、全ての現代的なエディタはUnicodeをサポートしている。Leanの標準ライブラリでは、型を表すギリシャ文字や、-> をよりコンパクトにしたUnicode記号 をよく見かける。

× などを使った例を掲載する。

#check Nat → Nat                   -- `→` は "\to" あるいは "\r" と打つと入力できる
#check Nat -> Nat                  -- `->` は `→` のASCII表記

#check Nat × Nat                   -- `×` は "\times" と打つと入力できる
#check Prod Nat Nat                -- `Prod Nat Nat` は `Nat × Nat` のASCII表記

#check Nat → Nat → Nat
#check Nat → (Nat → Nat)           -- これは1つ上と同じである。つまり、`→` は右結合的である
#check Nat × Nat → Nat
#check (Nat → Nat) → Nat           -- 関数を受け取る関数の型

#check Nat.succ                    -- Nat → Nat
#check (0, 1)                      -- Nat × Nat
#check Nat.add                     -- Nat → Nat → Nat

#check Nat.succ 2                  -- Nat
#check Nat.add 3                   -- Nat → Nat
#check Nat.add 5 2                 -- Nat
#check (5, 9).1                    -- Nat
#check (5, 9).2                    -- Nat

#eval Nat.succ 2                   -- 3
#eval Nat.add 5 2                  -- 7
#eval (5, 9).1                     -- 5
#eval (5, 9).2                     -- 9
#eval Nat.add (10, 7).1 (10, 7).2  -- 17

今一度、自分でいくつかの例を作り、これらのコマンドを試してほしい。

基本的な構文を見てみよう。Unicodeの矢印 \to\r\-> と打つことで入力できる。ASCIIの代替記号 -> も使用できる。したがって、Nat -> NatNat → Nat は同じ意味である。どちらの式も、入力として自然数を受け取り、出力として自然数を返す関数の型を表す。直積型を表すUnicode記号 ×\times と打つと入力できる。一般に、αβγ のような小文字のギリシャ文字を使って型を表す。これらは \a\b\c と打つと入力できる。

以下に上述した記号の入力方法をまとめる。

入力記号
\to
\r
->
\times×
\aα
\bβ
\gγ

上記の例について注意すべきことがさらにいくつかある。まず、項 x に対する関数 f の適用は f x と表記される(例: Nat.succ 2)。次に、型の記述において は右結合的である。例えば、Nat.add の型は Nat → Nat → Nat であり、これは Nat → (Nat → Nat) と等価である。したがって、Nat.add を、自然数を受け取り「自然数を受け取り自然数を返す関数」を返す関数とみなすことができる。型理論においては、Nat.add を自然数のペアを入力として受け取り、自然数を出力として返す関数として表現するよりも、一般的にこちらの方が便利である。つまり、Nat.add の型を Nat × Nat → Nat とするよりも、Nat → Nat → Nat とする方が便利である。これにより、例えば Nat.add 関数を「部分適用」することができる。上記の例は Nat.add 3Nat → Nat 型を持つことを示している。すなわち、Nat.add 3 は2番目の引数 n を「待つ」関数を返す。したがって、Nat.add 3Nat.add 3 n と書くのと等価である。

m : Natn : Nat があれば、(m, n)Nat × Nat 型を持つ mn の順序対を表すことが分かっただろう。この記法は自然数のペアを作る方法を与えてくれる。逆に、p : Nat × Nat とすると、p.1 : Natp.2 : Nat となる。この記法を使うとペアの2つの成分を取り出すことができる。

Types as objects (項としての型)

型そのもの(NatBool など)も項であるとみなすことは、Leanの依存型理論が単純型理論を拡張するのに使う手法の1つである。そうみなすためには、NatBool などの各型も型を持っていなければならない。

#check Nat               -- Type
#check Bool              -- Type
#check Nat → Bool        -- Type
#check Nat × Bool        -- Type
#check Nat → Nat         -- ...
#check Nat × Nat → Nat
#check Nat → Nat → Nat
#check Nat → (Nat → Nat)
#check Nat → Nat → Bool
#check (Nat → Nat) → Nat

上の各式が Type 型の項であることがわかるだろう。型を表す新しい定数を宣言することもできる:

def α : Type := Nat
def β : Type := Bool
def F : Type → Type := List
def G : Type → Type → Type := Prod

#check α        -- Type
#check F α      -- Type
#check F Nat    -- Type
#check G α      -- Type → Type
#check G α β    -- Type
#check G α Nat  -- Type

上の例が示すように、Type → Type → Type 型を持つ関数の例、すなわち直積 Prod はすでに見た:

def α : Type := Nat
def β : Type := Bool

#check Prod α β       -- Type
#check α × β          -- Type

#check Prod Nat Nat   -- Type
#check Nat × Nat      -- Type

任意の型 α が与えられたとき、型 List α は型 α の項からなるリストの型を表す。

def α : Type := Nat

#check List α    -- Type
#check List Nat  -- Type

Leanの全ての式(項)が型を持っていることを考えれば、「Type そのものはどのような型を持っているのか」と問うのは自然なことである。

#check Type      -- Type 1

では Type 1 の型を予想し、それから実際に Type 1 の型をチェックしてみよう。

#check Type 1
#check Type 2
#check Type 3
#check Type 4

予想は当たっただろうか。この実験において、我々は「Leanは型の無限階層の上に成り立っている」というLeanの型付けシステムの最も精緻な側面の一つに突き当たっている。

Type 0Nat のような「小さい」あるいは「普通の」型たちからなる宇宙だと思ってほしい。Type 1Type 0 を項にもつより大きい宇宙であり、Type 2Type 1 を項にもつより大きい宇宙である。この列に限りはない。つまり、任意の自然数 n に対して、型 Type n が存在する。Type とは Type 0 の略称である:

#check Type
#check Type 0

次の表は、今議論されていることを理解するのに役立つだろう。右方向に進むと「宇宙」がより大きいものへと変化し、下方向に進むと「度」と呼ばれるものが変化する。

sortProp (Sort 0)Type (Sort 1)Type 1 (Sort 2)Type 2 (Sort 3)...
type(型)TrueBoolNat -> TypeType -> Type 1...
term(項)trivialtruefun n => Fin nfun (_ : Type) => Type...

いくつかの演算子は型の宇宙に対してpolymorphic(多相)である必要がある。例えば、List α は、α がどの型宇宙にいようと意味をなすべきである。多相な関数 List の型は次のように表記される:

#check List    -- List.{u} (α : Type u) : Type u

ここで、u は宇宙レベルを表す変数である。コマンド #check List の出力は、α が型 Type n を持つなら、List αType n を持つことを表している。関数 Prod も多相である:

#check Prod    -- Prod.{u, v} (α : Type u) (β : Type v) : Type (max u v)

universe コマンドを使うと、明示的に宇宙変数を宣言することができる。宇宙変数を使うと多相な項を定義することができる:

universe u
def F (α : Type u) : Type u := Prod α α       -- `Type u` に属する型 `α` を受け取ると、`α` と `α` の直積型を返す関数
#check F                                      -- Type u → Type u

次のように {} を用いて宇宙パラメータを指定することもできる。そうすれば universe コマンドの使用を回避できる。

def F.{u} (α : Type u) : Type u := Prod α α

#check F    -- Type u → Type u

Function Abstraction and Evaluation (関数抽象と評価)

Leanでは、キーワード fun (あるいは λ) を使うと、式から関数を作ることができる。

-- 括弧は省略可能
-- fun (<入力引数の名前> : <入力引数の型名>) => <関数の出力を定義する式>
-- λ (<入力引数の名前> : <入力引数の型名>) => <関数の出力を定義する式>
#check fun (x : Nat) => x + 5   -- Nat → Nat
#check λ (x : Nat) => x + 5     -- `λ` と `fun` は同じ意味
#check fun x => x + 5           -- `x` は `Nat` 型だと推論される
#check λ x => x + 5             -- `x` は `Nat` 型だと推論される

要求されたパラメータを通すことにより、ラムダ関数を評価することができる。

#eval (λ x : Nat => x + 5) 10    -- 15

もし入力引数 x : α があり、さらに式 t : β が作れるなら、式 fun (x : α) => t (λ (x : α) => t) は型 α → β を持つ。このように、「入力引数」と「出力を定義する式」を結びつけて新しい関数を作るプロセスはlambda abstraction(ラムダ抽象)として知られている。fun (x : α) => t は型 α から型 β への、任意の値 x を値 t に写す関数だと考えてほしい。

他のいくつかの例を挙げる:

#check fun x : Nat => fun y : Bool => if not y then x + 1 else x + 2
#check fun (x : Nat) (y : Bool) => if not y then x + 1 else x + 2
#check fun x y => if not y then x + 1 else x + 2   -- Nat → Bool → Nat

Leanはこの3つの例を全く同じ式であると解釈する。最後の例では、入力の型が省略されているにも関わらず、#check コマンドは期待した型を返してくれている。これはLeanが式 if not y then x + 1 else x + 2 から入力の型を推論したからである。

関数の操作の数学的に一般的な例は、ラムダ抽象を用いて記述することができる:

def f (n : Nat) : String := toString n
def g (s : String) : Bool := s.length > 0

#check fun x : Nat => x        -- Nat → Nat
#check fun x : Nat => true     -- Nat → Bool
#check fun x : Nat => g (f x)  -- Nat → Bool
#check fun x => g (f x)        -- Nat → Bool

これらの式の意味を考えてみよう。式 fun x : Nat => xNat 上の恒等関数を表す。式 fun x : Nat => true は常に true を返す定数関数を表す。式 fun x : Nat => g (f x)fg の合成を表す。一般的に、入力引数の型注釈を省略して、Leanに型を推論してもらうことができる。例えば、fun x : Nat => g (f x) の代わりに fun x => g (f x) と書くことができる。

入力引数として関数を与えることもできる:

#check fun (g : String → Bool) (f : Nat → String) (x : Nat) => g (f x)
-- (String → Bool) → (Nat → String) → Nat → Bool

何なら型を入力引数として与えることもできる:

#check fun (α β γ : Type) (g : β → γ) (f : α → β) (x : α) => g (f x)

例えば、最後の式は3つの型 αβγ と2つの関数 g : β → γf : α → β を受け取り、gf の合成を返す。(この関数の型が意味をなすようにするには「依存積」の概念が必要になるが、それは後ほど説明される。)

ラムダ抽象の一般的な形式は fun x : α => t である。ここで、変数 xbounded variable(束縛変数)と呼ばれる: この x は実際単なるプレースホルダーであり、その「スコープ」は式 t の中に限定され、t を超えて広く及ぶことはない。例えば、定数 b が既に宣言されていたとする。この後に項 fun (b : β) (x : α) => b を作ったとしても、その中の b は既存の定数 b には影響しない。ラムダ抽象内の束縛変数名はどのように変えてもいい。実際、項 fun (b : β) (x : α) => b と項 fun (u : β) (z : α) => u は全く同じ関数である。

束縛変数の名前を変えることで互いに同じだと分かる式たちのことをalpha equivalent(α-同値)と呼ぶ。そして、α-同値なものは「同じ」だとみなされる。Leanはα-同値を認識している。

t : α → β を 項 s : α に適用すると、項 t s : β が得られることに注意してほしい。型を入力として受け取るようにし、分かりやすさのため束縛変数をリネームすると、以前の例は次のように書き換えられる:

#check (fun x : Nat => x) 1     -- Nat
#check (fun x : Nat => true) 1  -- Bool

def f (n : Nat) : String := toString n
def g (s : String) : Bool := s.length > 0

#check
  (fun (α β γ : Type) (u : β → γ) (v : α → β) (x : α) => u (v x)) Nat String Bool g f 0
  -- Bool

期待通り、式 (fun x : Nat => x) 1Nat 型を持つ。もっと詳しいことが言える: 1 に式 (fun x : Nat => x) を適用したら、値 1 が出力されるべきである。そして、実際にそうである:

def f (n : Nat) : String := toString n
def g (s : String) : Bool := s.length > 0

#eval (fun x : Nat => x) 1     -- 1
#eval (fun x : Nat => true) 1  -- true
#eval (fun (α β γ : Type) (u : β → γ) (v : α → β) (x : α) => u (v x)) Nat String Bool g f 0  -- true

これらの項がどのように評価されているのかは後ほど学ぶ。今のところ、次の依存型理論の重要な特徴に注目してほしい: 全ての項が計算上の動作を持ち、またnormalization(正規化)の概念をサポートしている。原則として、同じ値に簡約される2つの項はdefinitionally equalであると呼ばれる。Leanの型チェッカーはdefinitionally equalである2つの項を「同じ」とみなす。Leanは2つの項の間の相等関係を認識しサポートするために最善を尽くす。

Leanは完全なプログラミング言語である。バイナリ実行ファイルを生成するコンパイラと、対話型のインタプリタがある。コマンド #eval で式を評価することができ、#eval は関数をテストする方法として好まれている。

Definitions (定義)

def キーワードは名前付きの新しい項を宣言する重要な方法を提供することを思い出してほしい。

-- def <関数の名前> (<入力引数の名前> : <入力引数の型>) : <出力の型> := <出力を定義する式>

def double (x : Nat) : Nat :=
  x + x

他のプログラミング言語における関数の働きを知っていると、この定義の仕方がより馴染み深くなるかもしれない。doubleNat 型の入力パラメータ x を取り、呼び出すと x + x を出力する関数である。したがって、出力の型は Nat 型である。この関数は次のように使うことができる:

def double (x : Nat) : Nat :=
 x + x
#eval double 3    -- 6

def を名前付きのラムダ抽象だと考えることもできる。次の例は直前の例と同じ結果を得る:

def double : Nat → Nat :=
  fun x => x + x

#eval double 3    -- 6

型を推論するのに十分な情報があるなら、型宣言を省略することができる。型推論はLeanの重要な部分である:

def double :=
  fun (x : Nat) => x + x
#eval double 3    -- 6

改めて、定義に用いる一般的な文法は次のように書ける。

def foo : α := bar

ここで α は式 bar により定義される出力の型である。Leanは通常、型 α が何であるかを推論できるため、α を省略しても問題ないことが多い。しかし、α を明示的に書くことはしばしば良いことである。型の明示は書き手の意図を明確にする。そして、Leanは bar が持つ型と α が一致するかをチェックしてくれる。もし一致しなければ、Leanはエラーを返す。

右辺 bar はラムダ式に限らずどんな式でもよい。したがって、def は次のように単に値に名前を付けるために使ってもよい:

def pi := 3.141592654

def は複数の入力パラメータを受け取ることができる。2つの自然数を足し合わせる関数を作ってみよう:

def add (x y : Nat) :=
  x + y

#eval add 3 2               -- 5

パラメータリストは2つ以上に分けて書くこともできる:

def double (x : Nat) : Nat :=
 x + x
def add (x : Nat) (y : Nat) :=
  x + y

#eval add (double 3) (7 + 9)  -- 22

最後の行では、add への第1引数を作るために double 関数を呼び出したことに注意してほしい。

def の中では他の面白い式を使うこともできる:

def greater (x y : Nat) :=
  if x > y then x
  else y

#eval greater 7 6             -- 7
#eval greater 99 100          -- 100
#eval greater 5 5             -- 5

greater 関数がどんな働きをするかはおそらく推測できるだろう。

入力として他の関数を受け取る関数を定義することもできる。次の定義 doTwice は第1引数として与えられた関数 f を2回呼び出し、f に第2引数 x を入力して得られた出力を再び f に入力し、そこから得られた出力を返す:

def double :=
  fun (x : Nat) => x + x

def square :=
  fun (x : Nat) => x * x

def doTwice (f : Nat → Nat) (x : Nat) : Nat :=
  f (f x)

#eval doTwice double 2        -- 8
#eval doTwice square 3        -- 81

少し抽象的な例を見てみよう。次のように型を入力引数として指定することができる:

def compose (α β γ : Type) (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

compose は入力引数として任意の2つの関数 gf を受け取る。ただし、gf はともにただ1つの入力を受け取る関数でなければならない。さらに g : β → γf : α → β は、f の出力の型と g の入力の型が一致していなければならないという制約を意味する。gf が以上の制約を満たすなら、compose は意味をなす。そうでなければ2つの関数は合成不可能である。

compose は型 α を持つ第3引数 x をとる。xf に入力され、f からの出力は g に入力される。g は型 γ の項を返す。したがって、compose 関数の返り値の型も γ である。

compose は任意の型 α β γ について機能するという意味で非常に普遍的である。これは任意の2つの関数 g f がともにただ1つの入力を受け取り、f の出力の型と g の入力の型が一致しているならば、composegf を合成できることを意味する。以下に例を挙げる:

def double :=
  fun (x : Nat) => x + x

def square :=
  fun (x : Nat) => x * x

def compose (α β γ : Type) (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

#eval compose Nat Nat Nat double square 3        -- 18
#eval compose Nat Nat Nat square double 3        -- 36

Local Definitions (ローカルな定義)

キーワード let を使うと「ローカルな」定義を導入することができる。式 let a := t1; t2t2 の中に現れる全ての at1 で置き換えたものとdefinitionally equalである。

#check let y := 2 + 2; y * y   -- Nat
#eval  let y := 2 + 2; y * y   -- 16

def double_square (x : Nat) : Nat :=
  let y := x + x; y * y

#eval double_square 2   -- 16

y * y の中に現れる全ての yx + x で置き換えることにより、double_square x は項 (x + x) * (x + x) とdefinitionally equalであることが分かる。let 文を繋げることで、複数のローカルな定義を組み合わせることもできる:

#check let y := 2 + 2; let z := y + y; z * z   -- Nat
#eval  let y := 2 + 2; let z := y + y; z * z   -- 64

改行を挟めば ; を省略することができる。

def double_square (x : Nat) : Nat :=
  let y := x + x
  y * y

let a := t1; t2 の意味は (fun a => t2) t1 の意味と非常に似ているが、この2つは同じではないことに注意してほしい。let a := t1; t2 において、t2 の中に現れる at1 の省略形だと考えるべきである。一方 (fun a => t2) t1 では、a は変数である。したがって、fun a => t2a の値に依存せずに意味をなさなければならない。let は強力な省略手法であり、let a := t1; t2 とは表現できても (fun a => t2) t1 とは表現できない式が存在する。練習問題として、次の例において、なぜ foo は型チェックをパスするが bar は型チェックをパスしないのかを理解してみよう。

def foo := let a := Nat; fun x : a => x + 2
/-
  def bar := (fun a => fun x : a => x + 2) Nat
-/

Variables and Sections (変数とセクション)

次の3つの関数の定義を考えてみよう:

def compose (α β γ : Type) (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

def doTwice (α : Type) (h : α → α) (x : α) : α :=
  h (h x)

def doThrice (α : Type) (h : α → α) (x : α) : α :=
  h (h (h x))
  
#print compose
#print doTwice
#print doThrice

Leanでは、variable コマンドを使うとこのような定義をよりコンパクトにすることができる。variable (<変数の名前> : <変数の型名>) と書くと具体的な関数・定数定義と独立して変数名に型を付けることができる:

variable (α β γ : Type)

def compose (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

def doTwice (h : α → α) (x : α) : α :=
  h (h x)

def doThrice (h : α → α) (x : α) : α :=
  h (h (h x))
  
#print compose
#print doTwice
#print doThrice

variable を使うと、Type に限らない任意の型を変数に与えることができる:

variable (α β γ : Type)
variable (g : β → γ) (f : α → β) (h : α → α)
variable (x : α)

def compose := g (f x)
def doTwice := h (h x)
def doThrice := h (h (h x))

#print compose
#print doTwice
#print doThrice

#print <定義の名前> コマンドを使うと、指定した定義に関する情報が表示される。#print コマンドを使うと、上の3つの定義グループが同じ3つの関数を定義していることを確認することができる。

variable コマンドは、このコマンドで宣言された変数を参照する定義に、該当変数を束縛変数(入力引数)として挿入するようLeanに指示する。Leanは賢いので、各定義の中でどの変数が明示的あるいは暗黙のうちに使われているかを特定することができる。例えば、Leanは doTwice の中で変数 hx のみならず、α が使われていることを特定することができる。したがって、αβγgfhx は指定した型を持つ固定された項であると考えて定義を書けば、後はLeanが自動的に定義を抽象化してくれるのである。

一度 variable を使って変数が宣言されれば、その変数は宣言されたところから現在のファイルの最後まで有効である。しかしながら、変数のスコープを制限した方が使いやすいときもある。変数のスコープを制限するために、Leanはキーワード section を提供する:

section useful
  variable (α β γ : Type)
  variable (g : β → γ) (f : α → β) (h : α → α)
  variable (x : α)
  
  #check α              -- セクション内で変数 `α` は参照可能。
  
  def compose := g (f x)
  def doTwice := h (h x)
  def doThrice := h (h (h x))
end useful

#check compose          -- セクション内で定義された関数はセクション外でも参照可能。 
-- #check α             -- エラー。セクション外で変数 `α` は参照不可能。

例で示した通り、variable を用いてセクション内で宣言された変数は、セクション外ではもはや参照不可能である。

セクション内の行をインデントする必要はない。また、セクションに名前を付ける必要もない。つまり、無名の sectionend を使うことができる。ただし、セクションに名前を付けた場合は、同じ名前を使ってセクションを閉じる必要がある。また、セクションは入れ子にすることができ、入れ子になったセクションを使うと段階的に新しい変数を宣言することができる。

Namespaces (名前空間)

キーワード namespace を使うと、入れ子にできる階層的な名前空間を作ることができ、その中に定義を入れて定義をグループ化することができる:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a
  def ffa : Nat := f (f a)

  #check a
  #check f
  #check fa
  #check ffa
  #check Foo.fa
end Foo

-- #check a  -- error
-- #check f  -- error
#check Foo.a
#check Foo.f
#check Foo.fa
#check Foo.ffa

open Foo

#check a
#check f
#check fa
#check Foo.fa

名前空間 Foo 内で宣言した全ての識別子(定義の名前)は、宣言時の名前の頭に "Foo." を付けたフルネームを持つ。名前空間 Foo の中では、"Foo." を省略した短い名前で識別子を参照することができる。しかし、名前空間 Foo の外では、"Foo." を省略しないフルネームを使って識別子を参照しなければならない。セクションとは違い、名前空間には名前を付ける必要がある。Leanにおいて、ただ1つの無名の名前空間はルートレベルに存在する。これ以外に無名の名前空間の存在は許されない。

open <名前空間名> コマンドを使うと、"<名前空間名>." を省略した短い名前を使えるようになる。モジュール(外部ファイル)をインポートしたときは、短い名前を使うためにそのモジュールが含む名前空間を開きたくなるかもしれない。一方で、開きたい名前空間A内のある識別子 A.bar と他の名前空間B内の使用したい識別子 B.bar が衝突するときは、名前空間を開かず、そのような定義をフルネームで保護されたままにしておきたいと思うかもしれない。このように、名前空間は作業環境において名前を管理する方法を提供する。

例えば、Leanはリストに関する定義と定理を名前空間 List の中でグループ化している。

#check List.nil
#check List.cons
#check List.map

open List を使うと、これらの定義に短い名前でアクセスできるようになる:

open List

#check nil
#check cons
#check map

セクションのように、名前空間は入れ子にすることができる:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a

  namespace Bar
    def ffa : Nat := f (f a)

    #check fa
    #check ffa
  end Bar

  #check fa
  #check Bar.ffa
end Foo

#check Foo.fa
#check Foo.Bar.ffa

open Foo

#check fa
#check Bar.ffa

namespace <名前空間名>を用いることで、名前空間は閉じた後再び開くことができる。インポート元の名前空間をインポート先で開くことさえできる。

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a
end Foo

#check Foo.a
#check Foo.f

namespace Foo
  def ffa : Nat := f (f a)
end Foo

#check Foo.ffa

閉じられた名前空間は、例え他のファイルの中であっても、再び開くことができる:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a
end Foo

#check Foo.a
#check Foo.f

namespace Foo
  def ffa : Nat := f (f a)
end Foo

セクションと同様、入れ子になった名前空間は開かれた順に閉じられなければならない。名前空間とセクションは異なる役割を持つ: 名前空間はデータを整理し、セクションは定義に挿入される変数の宣言を整理する。セクションは set_optionopen のようなコマンドのスコープを区切るのにも便利である。

しかしながら、多くの側面で namespace ... end ブロックは section ... end ブロックと同様に振る舞う。特に、名前空間内で variable コマンドを使った場合、そのスコープは名前空間内に限定される。同様に、名前空間内で open コマンドを使った場合、open コマンドの効果はその名前空間が閉じられたときに切れる。

What makes dependent type theory dependent? (何が依存型理論を依存たらしめているのか?)

「型はパラメータ(引数)に依存することができる」、これが簡潔な説明である。既に良い例を見てきた: 型 List α は引数 α に依存し、この依存性こそが List NatList Bool を区別する。別の例として、型 Vector α n について考えてみよう。Vector α n は型 α の項からなる長さ n のベクトル(動的配列)の型である。この型は2つのパラメータ、ベクトルの要素の型 α : Type とベクトルの長さ n : Nat に依存する。

今、リストの先頭に新しい要素を挿入する関数 cons を作りたいとしよう。cons はどんな型を持つだろうか。このような関数は多相であってほしい: cons 関数は NatBool、ひいては任意の型 α に対して同様に動作してほしい。これは、cons は最初の引数として型 α をとるべきであることを意味する。そうすれば、任意の型 α に対して、cons α は型 α の項からなるリスト List α のための挿入関数となる。さらに、挿入する要素 a : αa が挿入されるリスト as : List α が引数として必要だろう。これらの引数があれば、consaas の先頭に挿入した新しいリストを作り、出力することができる。したがって cons α a as : List α という形の定義が適切だと考えられる。

cons α が型 α → List α → List α を持つべきなのは明らかである。それでは、型を与える前の cons はどんな型を持つべきだろうか。cons の持つ型として最初に思いつくのは Type → α → List α → List α だろう。しかしこれは意味をなさない: αType 型の引数を参照すべきだが、Type → α → List α → List α 内の α は何も参照しない。言い換えれば、αList α の意味は型 Type の引数によって決まるが、Type → α → List α → List α は型 Type の引数に関する情報を持たない。つまり、αList α は最初の引数 α : Type に依存しているのである。実際に cons の型を #check で確認してみよう。

def cons (α : Type) (a : α) (as : List α) : List α :=
  List.cons a as

#check cons Nat        -- Nat → List Nat → List Nat
#check cons Bool       -- Bool → List Bool → List Bool
#check cons            -- (α : Type) → α → List α → List α

これはdependent function type(依存関数型)あるいはdependent arrow type(依存矢印型)の一例である。α : Typeβ : α → Type があるとき、βα 上の型の族だと思ってほしい。つまり、項 β : α → Type を任意の a : αβ a : Type を割り当てる関数だと考えるのである。このとき、型 (a : α) → β a は任意の a : α に型 β a の項 f a を割り当てる関数 f の型を表す。言い換えると、関数 f によって返される値 f a の型 β a は入力 a に依存して変わる。(a : α) → β a という記法は、入力に依存して出力の型が変わるような関数の型を記述することができるのである。

(a : α) → β という依存関数型の記法は、任意の式 β に対して、たとえ β が引数 a : α に依存して変わるときでも、意味をなすのである。これは α → β という単純関数型の記法は β が引数 a : α に依存して変わるときには意味をなさないことと対照的である。つまり、依存関数型は単純関数型より表現の幅が広い。βa に依存しないときは、型 (a : α) → β と 型 α → β に違いはない。実際、依存型理論において、そしてLeanにおいて、α → β という記法は、βa : α に依存しないときの (a : α) → β の略記に過ぎない。

リストの例に戻る。#check コマンドを使うと、以下の List に関する関数の型をチェックすることができる。 記号 @、それから括弧 () と波括弧 {} の違いについてはすぐ後で説明する。

#check @List.cons    -- {α : Type u_1} → α → List α → List α
#check @List.nil     -- {α : Type u_1} → List α
#check @List.length  -- {α : Type u_1} → List α → Nat
#check @List.append  -- {α : Type u_1} → List α → List α → List α

βα に依存できるようにすることで依存関数型 (a : α) → β a が関数型 α → β を一般化するのと同様に、依存積型 (a : α) × β a は直積型 α × β を一般化する。依存積型は βa に依存することを可能にする。依存積型はsigma(Σ)型とも呼ばれ、(a : α) × β aΣ a : α, β a とも書かれる。Leanにおいて、⟨a, b⟩ あるいは Sigma.mk a b と書くと依存ペアを作ることができる。\langle または \< と打つと入力でき、\rangle または \> と打つと入力できる。

universe u v

def f (α : Type u) (β : α → Type v) (a : α) (b : β a) : (a : α) × β a :=
  ⟨a, b⟩

def g (α : Type u) (β : α → Type v) (a : α) (b : β a) : Σ a : α, β a :=
  Sigma.mk a b

#print f
#print g

def h1 (x : Nat) : Nat :=
  (f Type (fun α => α) Nat x).2

#eval h1 5 -- 5

def h2 (x : Nat) : Nat :=
  (g Type (fun α => α) Nat x).2

#eval h2 5 -- 5

def i : Type :=                 -- iは ``Nat`` 型のこと
  (f Type (fun α => α) Nat 5).1

def test : i := 5 + 5

#eval test -- 10

上記の fg は同じ関数である。

Implicit Arguments (暗黙の引数)

次のようなリストの実装 Lst があるとする:

universe u
def Lst (α : Type u) : Type u := List α
def Lst.cons (α : Type u) (a : α) (as : Lst α) : Lst α := List.cons a as
def Lst.nil (α : Type u) : Lst α := List.nil
def Lst.append (α : Type u) (as bs : Lst α) : Lst α := List.append as bs
#check Lst          -- Lst.{u} (α : Type u) : Type u
#check Lst.cons     -- Lst.cons.{u} (α : Type u) (a : α) (as : Lst α) : Lst α
#check Lst.nil      -- Lst.nil.{u} (α : Type u) : Lst α
#check Lst.append   -- Lst.append.{u} (α : Type u) (as bs : Lst α) : Lst α

このとき、次のように Nat の項からなるリストを作ることができる:

universe u
def Lst (α : Type u) : Type u := List α
def Lst.cons (α : Type u) (a : α) (as : Lst α) : Lst α := List.cons a as
def Lst.nil (α : Type u) : Lst α := List.nil
def Lst.append (α : Type u) (as bs : Lst α) : Lst α := List.append as bs
#check Lst          -- Type u_1 → Type u_1
#check Lst.cons     -- (α : Type u_1) → α → Lst α → Lst α
#check Lst.nil      -- (α : Type u_1) → Lst α
#check Lst.append   -- (α : Type u_1) → Lst α → Lst α → Lst α
#check Lst.cons Nat 0 (Lst.nil Nat)      -- Lst Nat
#eval Lst.cons Nat 0 (Lst.nil Nat)       -- [0]

def as : Lst Nat := Lst.nil Nat
def bs : Lst Nat := Lst.cons Nat 5 (Lst.nil Nat)

#check Lst.append Nat as bs              -- Lst Nat
#eval Lst.append Nat as bs               -- [5]

Lst とそのコンストラクタ Lst.consLst.nilLst.append は型について多相であるため、これらを使うときは毎回型 Nat を引数として与えなければならない。しかし、この情報は冗長である。Lst.cons Nat 5 (Lst.nil Nat) について考えてみよう。第二引数が 5 : Nat であることから、α が Nat であることは容易に推論できる。同様に、Lst.cons Nat 5 (Lst.nil Nat)Lst.nil の引数が Nat であることも、Lst.cons の引数の情報 (as : Lst α)αNat であることを照らし合わせれば分かる。

言わば、たとえ Lst.cons Nat 5 (Lst.nil Nat) が虫に食われて Lst.cons _ 5 (Lst.nil _) となっていたとしても、各 _ に入る型が何であるか推論できるのである。

これは依存型理論の中心的な特徴である: 項は多くの情報を持つ。そしていくつかの失われた情報は文脈から推論できる。Leanでは、アンダースコア _ を使うことで、ここの情報を自動で埋めてほしいとシステムに指示することができる。これは「implicit argument(暗黙の引数)」と呼ばれている。次に例を挙げる:

universe u
def Lst (α : Type u) : Type u := List α
def Lst.cons (α : Type u) (a : α) (as : Lst α) : Lst α := List.cons a as
def Lst.nil (α : Type u) : Lst α := List.nil
def Lst.append (α : Type u) (as bs : Lst α) : Lst α := List.append as bs
#check Lst          -- Type u_1 → Type u_1
#check Lst.cons     -- (α : Type u_1) → α → Lst α → Lst α
#check Lst.nil      -- (α : Type u_1) → Lst α
#check Lst.append   -- (α : Type u_1) → Lst α → Lst α → Lst α
#check Lst.cons _ 0 (Lst.nil _)      -- Lst Nat

def as : Lst Nat := Lst.nil _
def bs : Lst Nat := Lst.cons _ 5 (Lst.nil _)

#check Lst.append _ as bs            -- Lst Nat

見事に推論に成功している。

しかし、アンダースコアをいちいち入力するのはやはり面倒である。関数が一般的に文脈から推論できる引数をとる場合、「この引数は(デフォルトでは)暗黙のうちに推論してほしい引数である」とLeanに指示することができる。次のように波括弧 {} で引数をくくると、その引数を暗黙の引数とすることができる:

universe u
def Lst (α : Type u) : Type u := List α

def Lst.cons {α : Type u} (a : α) (as : Lst α) : Lst α := List.cons a as
def Lst.nil {α : Type u} : Lst α := List.nil
def Lst.append {α : Type u} (as bs : Lst α) : Lst α := List.append as bs

#check Lst.cons 0 Lst.nil      -- Lst Nat

def as : Lst Nat := Lst.nil
def bs : Lst Nat := Lst.cons 5 Lst.nil

#check Lst.append as bs        -- Lst Nat

変わったのは、関数の定義において α : Type u を波括弧で囲んだことだけである。他の例も挙げる:

universe u
def ident {α : Type u} (x : α) := x

#check ident         -- ?m → ?m
#check ident 1       -- Nat
#check ident "hello" -- String
#check @ident        -- {α : Type u_1} → α → α

この例では、関数 ident の第1引数が暗黙の引数になっている。こうすることで、型の指定が隠され、見かけ上 ident が任意の型の引数を取るように見える。実際、ident と同じ機能を持つ関数 id は標準ライブラリ内でこのように定義されている。今回は、名前の衝突を避けるため、伝統的でない名前 ident を用いた。

variable コマンドを使ったときも、変数を暗黙の引数として指定することができる。

universe u

section
  variable {α : Type u}
  variable (x : α)
  def ident := x
end

#check ident           -- {α : Type u_1} → α → α
#check ident 4         -- Nat
#check ident "hello"   -- String

この ident の定義は一つ上の定義と同じ効果を持つ。

Leanには暗黙の引数を推論するための非常に複雑なメカニズムがあり、それを使うと関数型や述語、さらには証明を推論できることを見ていく。このような「穴」または「プレースホルダー」を埋めるプロセスは、elaborationとしてよく知られている。暗黙の引数の存在により、ある式の意味を正確に確定させるための情報が不十分である状況が起こりうる。idList.nil のような表現は、文脈によって異なる意味を持つことがあるため、polymorphic(多相的)であると言われる。

e の型 T は、(e : T) と書くことで指定できる。これはLeanのelaboratorに、暗黙の引数の解決を試みるとき、e の型として T を使うように指示する。以下の4行目と5行目では、idList.nil の型を思い通りに指定するために、このメカニズムが使われている:

#check List.nil               -- List ?m
#check id                     -- ?m → ?m

#check (List.nil : List Nat)  -- List Nat
#check (id : Nat → Nat)       -- Nat → Nat

Leanでは数字はオーバーロードされる。しかし数字の型が推論できない場合、Leanはデフォルトでその数字の型は自然数であると仮定する。次の例の1行目にはその機能の実例が示されている。3行目では数字の型が指定されているため、1行目と2行目とは異なり 2 の型が Int であると推論されている。

#check 2            -- Nat
#check (2 : Nat)    -- Nat
#check (2 : Int)    -- Int

しかし、ある関数の引数を暗黙の引数として宣言しておきながら、その引数を明示的に与えたいという状況に陥ることがある。foo がそのような関数である場合、@foo という表記は、すべての引数を明示的にした同じ関数を表す。

#check @id        -- {α : Sort u_1} → α → α
#check @id Nat    -- Nat → Nat
#check @id Bool   -- Bool → Bool

#check @id Nat 1     -- Nat
#check @id Bool true -- Bool

最初の #check コマンドは、何のプレースホルダーも与えていない状態での恒等関数 id そのものの型を表していることに注意してほしい。さらに、#check コマンドの表示は、第1引数が暗黙の引数であること波括弧を用いて示している。

Propositions and Proofs (命題と証明)

第2章で、Leanにおいて項と関数を定義する方法を見てきた。この章では、依存型理論の言語を用いて数学的な主張と証明を書く方法を学ぶ。

Propositions as Types (型としての命題)

依存型理論の言語で定義された項に関する命題を証明するための戦略の一つは、定義に用いる言語の上に命題に用いる言語と証明に用いる言語を重ねることである。しかし、複数の言語を使う必要はない。 依存型理論は、命題と証明を同じ一般的な枠組みで表現するのに十分な柔軟さと表現力を兼ね備えている。

例えば、命題を表す新しい型 Prop を導入することができる。さらに、他の命題から新しい命題を構築するコンストラクタを導入することができる。

def Implies (p q : Prop) : Prop := p → q
#check And     -- Prop → Prop → Prop
#check Or      -- Prop → Prop → Prop
#check Not     -- Prop → Prop
#check Implies -- Prop → Prop → Prop

variable (p q r : Prop)
#check And p q                      -- Prop
#check Or (And p q) r               -- Prop
#check Implies (And p q) (And q p)  -- Prop

それから、各要素 p : Prop に対して、p の証明の型 Proof p を導入できる。 「公理」とは、Proof p のような命題の証明の型を持った定数である。

def Implies (p q : Prop) : Prop := p → q
structure Proof (p : Prop) : Type where
  proof : p
#check Proof   -- Proof (p : Prop) : Type

axiom and_comm (p q : Prop) : Proof (Implies (And p q) (And q p))

variable (p q : Prop)
#check and_comm p q     -- and_comm p q : Proof (Implies (p ∧ q) (q ∧ p))
#check and_comm q p     -- and_comm q p : Proof (Implies (q ∧ p) (p ∧ q))

公理だけでなく、既存の証明から新しい証明を作るためのルールも必要である。例えば、命題論理の証明系の多くには、"modus ponens(モーダス・ポネンス)"という推論規則がある。

modus ponens : Implies p q の証明と p の証明があれば、そこから q の証明が得られる。

Leanではモーダス・ポネンスを次のように表現できる。

def Implies (p q : Prop) : Prop := p → q
structure Proof (p : Prop) : Type where
  proof : p
axiom modus_ponens : (p q : Prop) → Proof (Implies p q) → Proof p → Proof q

一般的に、命題論理のための自然演繹のシステムは次の推論規則も採用している:

含意導入 : p を仮定すると q の証明が得られるとする。このとき、Implies p q の証明が得られる。

含意導入はLean上で次のように表現できる。

def Implies (p q : Prop) : Prop := p → q
structure Proof (p : Prop) : Type where
  proof : p
axiom implies_intro : (p q : Prop) → (Proof p → Proof q) → Proof (Implies p q)

以上の手法は、命題と証明の合理的な構築方法を提供する。この手法において、ある式 t が命題 p : Prop の正しい証明であることを確定させるには、tProof p という型を持つことをチェックすればよい。

いくつかの簡略化が可能である。まず、命題 p : Prop があるとき、p 自体を型として解釈することができる。さらに、型 pp の証明の型と解釈する。つまり、型 p と型 Proof p を同一視する。すると、「tp の証明である」という主張をシンプルに t : p と書くことができる。 この簡略化により、毎回 Proof と書く手間が省ける。

さらにこの手法を発展させる。命題 pq の間の含意 Implies p q は、p の任意の要素にq の要素を一つ割り当てる関数 p → q と同一視できる。結果として、Implies という結合子の導入は不要である。含意の型 Implies p q の代わりに、依存型理論の関数の型 p → q を使えばよいのである。

これがCalculus of Constructionsのアプローチであり、Leanはこのアプローチを採用している。自然演繹の証明系における含意の関する規則が、関数のラムダ抽象と適用に関する規則と正確に対応しているという事実は、Curry-Howard isomorphism(カリー=ハワード同型)の一例であり、proposition-as-types(型としての命題)パラダイムとして知られている。実は、型 Prop は前章で説明した型階層の最下層である型 Sort 0 の糖衣構文である(PropSort 0 は全く同じ意味である)。さらに言えば、型 Type u は型 Sort (u+1) の糖衣構文に過ぎない。Prop はいくつか特別な特徴を持っているが、他の型宇宙と同様に、アローコンストラクタの下で閉じている。つまり、p q : Prop ならば p → q : Prop である(α β : Type ならば α → β : Type であるのと同様である)。

「型としての命題」について考えるには、少なくとも2通りの方法がある。論理学や数学について構成主義的な立場をとる人にとっては、「型としての命題」は「命題とはどういうものか」を忠実に表現している。命題 pp の証明を構成するデータの型を表している。p の証明とは、単に正しく型付けされた項 t : p である。

このような主義に傾倒していない人にとって、「型としての命題」はむしろ単純なコーディング・トリックだと考えることができる。各命題 p に対して、 p が偽なら p に空の型を関連付ける。p が真なら p にただ一つの項 * を持つ型を関連付ける。後者のとき、p (に関連付けられた型)を inhabited(有項) と呼び、型 p が持つ項 *inhabitant(住人) と呼ぶ。このとき、たまたま、関数の適用と抽象化の規則が、Prop のどの要素が有項かを追跡するのに便利だったのである。つまり、項 t : p を構築することが、p が真であることを保証してくれるのである。 このとき、p の住人 t とは「p が真であるという事実」のことだと考えることができる。そうすると、p → q の証明とは「p が真であるという事実」を受け取って「q が真であるという事実」を返す関数のことだと考えることができる。

実際、Leanのカーネルは (fun x => t) st[s/x](項 t の中の全ての xs で置き換えた項) をdefinitionally equalとみなすのと同様に、任意の p : Prop に対して任意の2つの項 t1 t2 : p をdefinitionally equalとみなす。t1 t2 : p をdefinitionally equalとみなすことはproof irrelevance(証明無関係)と呼ばれ、前段落の解釈と矛盾しない。つまり、証明 t : p は依存型理論の言語の中で普通の項として扱うことができるが、tp が真であるという事実以上の情報は持っていないということである。

以上で提案した「型としての命題」パラダイムについて考えるための2つの方法は、根本的なところで異なっている。構成主義的な観点からすると、証明は抽象的な数学的対象であり、依存型理論において適切な項で「表現」される。対照的に、前述のコーディング・トリックの観点からすると、項 t : p そのものは何も面白いものを示さない。むしろ、項を書き下し、その項がきちんと型付けされていることを確認することで、問題の命題が真であることを保証するのである。つまり、項「そのもの」が証明なのである。

以下の説明では、ある項がある命題の証明を「構築する」「生成する」「返す」と表現したり、単にある項がある命題の証明「である」と表現したり、この両方の表現を使うことにする。これは、計算機科学者が、あるプログラムがある関数を「計算する」と言いながら、時にはそのプログラムがその関数「である」と言うことで、構文論と意味論の区別を曖昧にすることがあるのと似たようなものである。

いずれにせよ、本当に重要なのは次のことである: 依存型理論の言語で数学的な命題 p を形式的に表現するには、項 p : Prop を構築する必要がある。命題 p を「証明」するには、項 t : p を構築する必要がある。証明支援系Leanの仕事は、このような項 t を構築する手助けをし、そして t が適切な形をとっていて正しい型を持つことを検証することである。

Working with Propositions as Types (「型としての命題」を実践する)

「型としての命題」パラダイムにおいては、 と命題だけを含む定理はラムダ抽象と関数適用を使って証明することができる。Leanでは、theorem コマンドを使うと新しい定理を導入できる。

variable {p : Prop}
variable {q : Prop}

theorem t1 : p → q → p := fun hp : p => fun hq : q => hp

この証明を、型 α → β → α (αβ は型 Type の項) の項 fun x : α => fun y : β => x と比較してほしい。fun x : α => fun y : β => x は引数 x : α と引数 y : β をとり、x を返す。 p → q → p の証明は同じ形をとる。唯一の違いは pqType ではなく Prop の項であることだけである。 直観的には、我々の p → q → p の証明は命題 p : Prop と命題 q : Prop が正しいことを前提とし、最初の前提から p が正しいことを(自明に)結論づける。

theorem コマンドは def コマンドと全く同じである。つまり、命題と型の対応の下で、定理 p → q → p を証明することは、型 p → q → p の要素を定義することと全く同じである。実際、Leanのカーネルの型チェッカーにとって、theorem コマンドと def コマンドの間に違いはない。

しかしながら、定義と定理の間にはいくつかの実用的な違いがある。通常、定理の「定義」を展開する必要はない。証明無関係の原則により、ある定理の任意の2つの証明はdefinitionally equalである。一度定理の証明が完成したら、通常はその定理の証明が存在することだけが分かればよく、その証明が何であるかは重要ではない。この事実をふまえ、Leanは証明にirreducibleとタグ付けする。ファイルを処理するとき、このタグはパーサー(より正確にはelaborator)に対して、「このタグが付いたものを展開する必要はない」というヒントとして機能する。実際、Leanは一般的に証明の処理とチェックを平行して行うことができる。これは、ある証明の正しさを検証する際に、他の証明の詳細を知る必要がないからできることである。

定義と同様に、#print コマンドは定理の証明を表示する。

variable {p : Prop}
variable {q : Prop}
theorem t1 : p → q → p := fun hp : p => fun hq : q => hp

#print t1    -- ∀ {p q : Prop}, p → q → p := fun {p q} hp hq => hp

ラムダ抽象 hp : phq : qp → q → p の証明における一時的な前提と見なせることに注意してほしい。また、Leanでは、最後の項 hp の型を、show 文で明示的に指定することができる。

variable {p : Prop}
variable {q : Prop}
theorem t1 : p → q → p :=
  fun hp : p =>
  fun hq : q =>
  show p from hp      -- show <型> from <項>

このような情報を追加することで、証明の分かりやすさを向上させ、証明を書く際の誤りを発見しやすくすることができる。show コマンドは型に注釈をつける以上のことはしない。内部的には、これまで見てきた t1 の表現は全て同じ項を生成している。

通常の定義と同様に、theorem コマンドにおいても、ラムダ抽象された変数をコロンの左側に持ってくることができる。

variable {p : Prop}
variable {q : Prop}
theorem t1 (hp : p) (hq : q) : p := hp

#print t1    -- p → q → p

定理 t1 は関数適用と同様に他の項に適用することができる。

variable {p : Prop}
variable {q : Prop}
theorem t1 (hp : p) (hq : q) : p := hp

axiom hp : p

theorem t2 : q → p := t1 hp

ここで、axiom 宣言は与えられた型の項の存在を無条件に認めるため、axiom コマンドの使い方によっては論理的整合性を損なう可能性がある。例えば、axiom コマンドにより空の型 False が項を持つことを認めることさえ可能である。

axiom unsound : False
-- `False`(偽)からは任意の命題を示すことができる
theorem ex : 1 = 0 :=    -- 本来は偽の命題
  False.elim unsound

「公理」hp : p を宣言することは、hp の存在を無条件に認め、hp により p が真であると宣言することと等価である。p が真だと主張する公理 hp : p に定理 t1 : p → q → p を適用すると、定理 t1 hp : q → p が得られる。

定理 t1 は次のように書けることを思い出そう。

theorem t1 {p q : Prop} (hp : p) (hq : q) : p := hp

#print t1    -- ∀ {p q : Prop}, p → q → p := fun {p q} hp hq => hp

t1 の型は ∀ {p q : Prop}, p → q → p だと表示される。これは、「任意の命題のペア p q について、p → q → p が成立する」と読める。 この結果を用いると、t1 の全ての引数をコロンの右側に持っていくことができる。

theorem t1 : ∀ {p q : Prop}, p → q → p :=
  fun {p q : Prop} (hp : p) (hq : q) => hp

pqvariable コマンドを使って宣言されているなら、Leanは自動的に pq を全称化する。

variable {p q : Prop}

theorem t1 : p → q → p := fun (hp : p) (hq : q) => hp

#print t1    -- ∀ {p q : Prop}, p → q → p := fun {p q} hp hq => hp

「型としての命題」対応に従って、p は正しいという前提 hp を別の変数として宣言することができる。

variable {p q : Prop}
variable (hp : p)

theorem t1 : q → p := fun (hq : q) => hp

#print t1    -- ∀ {p q : Prop}, p → q → p := fun {p q} hp hq => hp

Leanはこの証明が hp を使っていることを検出し、自動的に hp : p を前提に追加する。どの例でも #print t1∀ p q : Prop, p → q → p を表示する。この型は ∀ (p q : Prop) (hp : p) (hq : q), p とも書けることに注意してほしい。

t1 を全称化すれば、t1 を様々な命題のペアに適用させることで、一般的な定理 t1 の様々な例を得ることができる。

theorem t1 (p q : Prop) (hp : p) (hq : q) : p := hp

variable (p q r s : Prop)

#check t1 p q                -- p → q → p
#check t1 r s                -- r → s → r
#check t1 (r → s) (s → r)    -- (r → s) → (s → r) → r → s

variable (h : r → s)
#check t1 (r → s) (s → r) h  -- (s → r) → r → s

再び、「型としての命題」対応を利用すると、r → s 型の変数 h を「r → s は真である」という前提とみなすことができる。

別の例として、前章で説明した合成関数を、今度は型の代わりに命題を使って考えてみよう。

variable (p q r s : Prop)

theorem t2 (h₁ : q → r) (h₂ : p → q) : p → r :=
  fun h₃ : p =>
  show r from h₁ (h₂ h₃)

命題論理の定理として見ると、t2 は何を表現しているだろうか?

この例で使ったように、前提として使える証明項の名前にはUnicode数字添字を使うのが便利である。これらは\0\1\2、...と打つと入力できる。

Propositional Logic (命題論理)

Leanでは標準的な論理的結合子と記法の全てが定義されている。命題論理の結合子は次のように表す:

AsciiUnicodeエディターでの入力方法定義
TrueTrue
FalseFalse
Not¬\not, \negNot
/\\andAnd
\/\orOr
->\to, \r, \imp
<->\iff, \lrIff

これらは Prop 型の項(命題)を取り、Prop 型の新しい項(命題)を返す。

variable (p q : Prop)

#check p → q → p ∧ q
#check ¬p → p ↔ False
#check p ∨ q → q ∨ p

演算の順序は次の通り: 単項否定 ¬ が一番最初に結合し、次に 、最後に が結合する。例えば、a ∧ b → c ∨ d ∧ e と書かれていたら、それは (a ∧ b) → (c ∨ (d ∧ e)) のことである。 引数の型が Prop であっても他の型であっても、 は右から順に結合していくことを忘れないでほしい。つまり、p q r : Prop とすると、p → q → r という式は p → (q → r) と同じである。これは、「カリー化」された p ∧ q → r である。

前節ではラムダ抽象を の「導入則」とみなすことができることを説明した。ラムダ抽象が含意命題を「導入」あるいは構築する方法だとすると、関数適用は「含意の除去則」だとみなせる。つまり、関数適用は証明の中で含意を「除去する」あるいは使う方法である。他の命題論理の結合子はLeanのライブラリのファイル Init/Prelude.lean (ライブラリの階層構造についてはImporting Files (ファイルのインポート)を参照のこと)で定義されている。それぞれの結合子には正規化された導入則と除去則が存在する。

Conjunction (連言)

And.intro h1 h2 は証明 h1 : p と証明 h2 : q を使って p ∧ q の証明を構築する。And.intro は一般的に「連言の導入則」と表現される。次の例では、And.intro を使って p → q → p ∧ q の証明を作る。

variable (p q : Prop)

example (hp : p) (hq : q) : p ∧ q := And.intro hp hq

#check fun (hp : p) (hq : q) => And.intro hp hq

example コマンドは、定理に名前を付けたり、永続する文脈に定理を保存することなく、定理を記述するのに使う。基本的には、example コマンドは与えられた項が与えられた型を持っているかどうかをチェックするだけである。実例を示すのに便利で、よく使うコマンドである。

And.left h は証明 h : p ∧ q から p の証明を作る。同様に、And.right h は証明 h : p ∧ q から q の証明を作る。これらは一般的に「左連言除去則」と「右連言除去則」として知られている。

variable (p q : Prop)

example (h : p ∧ q) : p := And.left h
example (h : p ∧ q) : q := And.right h

ここまでの知識を使って、次のように p ∧ q → q ∧ p を証明することができる。

variable (p q : Prop)

example (h : p ∧ q) : q ∧ p :=
  And.intro (And.right h) (And.left h)

連言導入と連言除去は直積ペアの構築と射影の操作に似ていることに注意してほしい。p : Propq : Prophp : phq : q のとき、And.intro hp hq は型 p ∧ q : Prop を持つ。一方、p : Typeq : Typehp : phq : q のとき、Prod hp hq は型 p × q : Type を持つ。 この類似性は「カリー=ハワード同型対応」の別の例である。この類似性によると、今作った証明は直積ペアの要素を入れ替える関数に似ていることになる。しかし、含意と関数型コンストラクタとは対照的に、Leanでは × は別々に扱われている。

9章 Structures and Records (構造体とレコード)で、Leanにはstructures(構造体)と呼ばれる型があることを学ぶ。構造体 S は適切な引数の列から S の要素を構築する単一で正規のconstructor(コンストラクタ)によって定義される。任意の p q : Prop に対して、p ∧ q は構造体の一例である。構造体 p ∧ q の要素を構築する正規の方法は、適切な引数 hp : phq : qAnd.intro を適用することである。 Leanでは、関連する型が帰納型であり、文脈から型推論できる場合、anonymous constructor(匿名コンストラクタ)表記 ⟨arg1, arg2, ...⟩ を使うことができる。特に、And.intro hp hq の代わりに ⟨hp, hq⟩ と書くことがよくある。

variable (p q : Prop)
variable (hp : p) (hq : q)

#check (⟨hp, hq⟩ : p ∧ q)

角括弧 ⟨ ⟩\< \> と打つことで入力できる。

他にもLeanには便利な構文機能がある。項 e が(パラメータをとる可能性のある)帰納型 Foo を持つとき、e.barFoo.bar e の略記である。この記法は名前空間を開くことなく関数にアクセスする便利な方法を提供する。例えば、次の2つの項は全く同じである:

variable (xs : List Nat)

#check List.length xs
#check xs.length

結果として、h : p ∧ q があるとき、And.left h の代わりに h.left と書け、And.right h の代わりに h.right と書ける。従って、上記の例は次のように簡潔に書ける:

variable (p q : Prop)

example (h : p ∧ q) : q ∧ p :=
  ⟨h.right, h.left⟩

簡潔さと難解さは紙一重であり、このように情報を省略することは時として証明を読みにくくする。しかし、上のような簡単な例で、h の型と構築したい型がはっきりしている場合、この記法は簡潔で効果的である。

And. のような構築を繰り返すことは普通である。Leanはネストされた角括弧を外すことができる。このとき、各引数は右から結合する。したがって、次の2つの証明は等価である:

variable (p q : Prop)

example (h : p ∧ q) : q ∧ p ∧ q :=
  ⟨h.right, ⟨h.left, h.right⟩⟩

example (h : p ∧ q) : q ∧ p ∧ q :=
  ⟨h.right, h.left, h.right⟩

これも便利である。

Disjunction (選言)

Or.intro_left q hp は 証明 hp : p から p ∨ q の証明を作る。同様に、Or.intro_right p hq は証明 hq : q から p ∨ q の証明を作る。これらは「左選言導入則」と「右選言導入則」に相当する。

variable (p q : Prop)
example (hp : p) : p ∨ q := Or.intro_left q hp
example (hq : q) : p ∨ q := Or.intro_right p hq

「選言除去則」は少し複雑である。p から r が導かれることと、 q から r が導かれることの両方を示せば、p ∨ q から r を証明できるという考えを使う。つまり、これは場合分けによる証明である。式 Or.elim hpq hpr hqr の中で、Or.elim は3つの引数 hpq : p ∨ qhpr : p → rhqr : q → r を取り、r の証明を作る。次の例の中で、p ∨ q → q ∨ p を証明するのに Or.elim を使う。

variable (p q r : Prop)

example (h : p ∨ q) : q ∨ p :=
  Or.elim h
    (fun hp : p =>
      show q ∨ p from Or.intro_right q hp)
    (fun hq : q =>
      show q ∨ p from Or.intro_left p hq)

ほとんどの場合、Or.intro_right の第1引数と Or.intro_left の第1引数はLeanによって自動的に推論される。Leanは Or.intro_right _ の略記として Or.inr を、Or.intro_left _ の略記として Or.inl を提供する。したがって、上記の証明はより簡潔に書ける:

variable (p q r : Prop)

example (h : p ∨ q) : q ∨ p :=
  Or.elim h (fun hp => Or.inr hp) (fun hq => Or.inl hq)

この簡潔な式の中に、Leanが hphq の型を推論するのに十分な情報が含まれていることに注意してほしい。しかし、型注釈を用いた長い記述を用いることは、証明を読みやすくし、エラーを発見してデバッグするのに役立つ。

Or は2つのコンストラクタを持つ、つまり単一で正規のコンストラクタを持たないため、Or の構築のために匿名コンストラクタを使うことはできない。しかし、Or.elim h の代わりに h.elim と書くことはできる:

variable (p q r : Prop)

example (h : p ∨ q) : q ∨ p :=
  h.elim (fun hp => Or.inr hp) (fun hq => Or.inl hq)

繰り返しになるが、このような略記が読みやすさを向上させるか低下させるか、書き手が判断する必要がある。

Negation and Falsity (否定と恒偽)

否定 ¬pp → False と定義される。したがって、¬p の証明は p から矛盾を導くことで得られる。同様に、式 hnp hphp : phnp : ¬p から False の証明を作る。 次の例ではこれらの規則の両方を使って (p → q) → ¬q → ¬p の証明を作る(記号 ¬\not あるいは \neg と打つことで入力できる)。

variable (p q : Prop)

example (hpq : p → q) (hnq : ¬q) : ¬p :=
  fun hp : p =>
  show False from hnq (hpq hp)

結合子 False は単一の除去則 False.elim を持つ。False.elim は矛盾からは任意の命題が導かれるという事実を表現している。この規則はex falso (ex falso sequitur quodlibetの略記)あるいはprinciple of explosion(爆発律)と呼ばれる。

variable (p q : Prop)

example (hp : p) (hnp : ¬p) : q := False.elim (hnp hp)

恒偽から導かれる任意の命題 q は暗黙の引数であり、自動的に型推論される。矛盾する前提から任意の命題を導くパターンは非常によく見られ、absurd で表現される。

variable (p q : Prop)

example (hp : p) (hnp : ¬p) : q := absurd hp hnp

次は ¬p → q → (q → p) → r の証明である:

variable (p q r : Prop)

example (hnp : ¬p) (hq : q) (hqp : q → p) : r :=
  absurd (hqp hq) hnp

ちなみに、False が除去則しか持たないように、True は導入則である True.intro : True しか持たない。つまり、True は単に真であり、True.intro という正規の証明を持っている。

Logical Equivalence (論理的同値)

Iff.intro h1 h2h1 : p → qh2 : q → p から p ↔ q の証明を作る。 式 Iff.mp hh : p ↔ q から p → q の証明を作る。同様に、Iff.mpr hh : p ↔ q から q → p の証明を作る。以下は p ∧ q ↔ q ∧ p の証明である。

variable (p q : Prop)

theorem and_swap : p ∧ q ↔ q ∧ p :=
  Iff.intro
    (fun h : p ∧ q =>
     show q ∧ p from And.intro (And.right h) (And.left h))
    (fun h : q ∧ p =>
     show p ∧ q from And.intro (And.right h) (And.left h))

#check and_swap p q    -- p ∧ q ↔ q ∧ p

variable (h : p ∧ q)
example : q ∧ p := Iff.mp (and_swap p q) h

匿名コンストラクタ記法を使って、p → q の証明と q → p の証明から p ↔ q の証明を構築することができる。また、mpmpr について . に関する記法が使える。これらを使うと、上記の例は次のように簡潔に書くことができる:

variable (p q : Prop)

theorem and_swap : p ∧ q ↔ q ∧ p :=
  ⟨ fun h => ⟨h.right, h.left⟩, fun h => ⟨h.right, h.left⟩ ⟩

example (h : p ∧ q) : q ∧ p := (and_swap p q).mp h

Introducing Auxiliary Subgoals (補助的なサブゴールの導入)

そろそろ長い証明を書く際に役に立つ機能 have の紹介をする頃合いだろう。have は証明の中で補助的なサブゴールを導入する。次は前節から抜粋した短い例である。

variable (p q : Prop)

example (h : p ∧ q) : q ∧ p :=
  have hp : p := h.left
  have hq : q := h.right
  show q ∧ p from And.intro hq hp

内部的には、式 have h : p := s; t は項 (fun (h : p) => t) s を作る。つまり、sp の証明であり、t は 前提 h : p の下で欲しい結論の証明であり、st はラムダ抽象と関数適用によって組み合わせられる。have は、長い証明を構築する際に、最終的なゴールに至るための踏み台として使えるため、非常に便利である。

Leanは、ゴールからbackward reasoning(後ろ向き推論)する構造化された方法もサポートしている。これは通常の数学における「Aを示すにはBを示せば十分である」という証明を模した手法である。次の例は、前の証明の最後の2行を単に並べ替えたものである。

variable (p q : Prop)

example (h : p ∧ q) : q ∧ p :=
  have hp : p := h.left
  suffices hq : q from And.intro hq hp
  show q from And.right h

suffices hq : q を使った後は2つのゴールを示す必要がある。まず、q ∧ p を示すには q を示せば十分であることを実際に示す必要がある。そのためには追加された前提 hq : q を使って元のゴール q ∧ p を証明すればよい。 最後に、q を示す必要がある。

Classical Logic (古典論理)

これまで見てきた導入則と除去則は全て構成的論理(直観主義論理)のものである。これは「型としての命題」対応に基づいた論理的結合子の計算論的理解を反映したものである。通常の古典論理では、以上の導入則と除去則に加え、排中律 p ∨ ¬p を認める。この原則を使うには、名前空間 Classical を開く必要がある。

open Classical

variable (p : Prop)
#check em p      -- p ∨ ¬p

直感的には、構成的論理の「Or」は非常に強い主張であり、p ∨ q を主張することは、どちらが正しいかを知っていることに等しい。リーマン予想を RH と表すと、古典論理を採用している数学者は、RH¬RH のどちらが正しいのか分からないうちに RH ∨ ¬RH を主張することを厭わない。構成的論理を採用すると、このような主張の仕方はできない。

排中律の帰結として、二重否定除去則が得られる。

open Classical

theorem dne {p : Prop} (h : ¬¬p) : p :=
  Or.elim (em p)
    (fun hp : p => hp)
    (fun hnp : ¬p => absurd hnp h)

¬p を仮定すると False が導かれるとき、二重否定除去を使うと命題 p を証明することができる。なぜなら、仮定 ¬p から False を導いたことは、¬¬p を証明したことと同義だからである。つまり、二重否定消去を使えば、構成的論理では一般には不可能な、矛盾による証明を行うことができる。練習として、逆を、つまり dne から em が証明できることを示してみよう。

古典論理の公理はまた、em により正当化される追加の証明パターンを使えるようにする。例えば、場合分けによる証明を行うことができる:

open Classical
variable (p : Prop)

example (h : ¬¬p) : p :=
  byCases
    (fun h1 : p => h1)
    (fun h1 : ¬p => absurd h1 h)

hpq : p → qhnpq : ¬p → q のとき、byCases hpq hnpqq の証明を作る。

あるいは、矛盾により証明を行うこともできる:

open Classical
variable (p : Prop)

example (h : ¬¬p) : p :=
  byContradiction
    (fun h1 : ¬p =>
     show False from h h1)

hnpf : ¬p → False のとき、byContradiction hnpfp の証明を作る。

もし構成的論理の考え方に慣れていないなら、古典論理的な推論がどこで使われているのか感覚を掴むのに時間がかかるかもしれない。次の例は、構成的論理では、 pq が両立しないと分かってもどちらが偽であるかは必ずしも分からないので、古典論理が必要である:

open Classical
variable (p q : Prop)
example (h : ¬(p ∧ q)) : ¬p ∨ ¬q :=
  Or.elim (em p)
    (fun hp : p =>
      Or.inr
        (show ¬q from
          fun hq : q =>
          h ⟨hp, hq⟩))
    (fun hp : ¬p =>
      Or.inl hp)

構成的論理には、排中律や二重否定除去のような原則が許される状況が「ある」ことを後に学ぶ。そのような状況では、Leanは排中律に頼ることなく古典論理的な推論の使用をサポートする。

古典論理的な推論を行うためにLeanで採用されている全ての公理の一覧は12章 Axioms and Computation (公理と計算)で論じられている。

Examples of Propositional Validities (命題論理における恒真式の例)

Leanの標準ライブラリは命題論理における恒真式の証明をいくつも含んでいる。その全ては読者自身の証明を書く際に自由に用いてよい。命題論理における恒真式のいくつかを以下に示す。

可換性:

  1. p ∧ q ↔ q ∧ p
  2. p ∨ q ↔ q ∨ p

結合性:

  1. (p ∧ q) ∧ r ↔ p ∧ (q ∧ r)
  2. (p ∨ q) ∨ r ↔ p ∨ (q ∨ r)

分配性:

  1. p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r)
  2. p ∨ (q ∧ r) ↔ (p ∨ q) ∧ (p ∨ r)

他の性質:

  1. (p → (q → r)) ↔ (p ∧ q → r)
  2. ((p ∨ q) → r) ↔ (p → r) ∧ (q → r)
  3. ¬(p ∨ q) ↔ ¬p ∧ ¬q
  4. ¬p ∨ ¬q → ¬(p ∧ q)
  5. ¬(p ∧ ¬p)
  6. p ∧ ¬q → ¬(p → q)
  7. ¬p → (p → q)
  8. (¬p ∨ q) → (p → q)
  9. p ∨ False ↔ p
  10. p ∧ False ↔ False
  11. ¬(p ↔ ¬p)
  12. (p → q) → (¬q → ¬p)

これらは古典論理的な推論を必要とする:

  1. (p → r ∨ s) → ((p → r) ∨ (p → s))
  2. ¬(p ∧ q) → ¬p ∨ ¬q
  3. ¬(p → q) → p ∧ ¬q
  4. (p → q) → (¬p ∨ q)
  5. (¬q → ¬p) → (p → q)
  6. p ∨ ¬p
  7. (((p → q) → p) → p)

sorry は魔法のようにあらゆる証明を生成したり任意の型の項を提供したりする。もちろん、sorry は証明方法としては不健全である。例えば、sorry を使って False を証明することができる。Leanは、sorry に依存する定理を使ったり、インポートしたりすると、深刻な警告を発する。しかし、長い証明を段階的に構築する際は便利である。サブ証明を sorry で埋めながら、証明をトップダウンで書いてみよう。sorry だけで構築された項をLeanが受容することを確認してほしい。そうでない場合は、修正する必要があるエラーが存在する。確認と修正が済んだら、実際の証明で sorry を一つ残らず書き換えよう。

もう一つ、便利な技がある。sorry を使う代わりに、アンダースコア _ をプレースホルダーとして使うことができる。アンダースコアは引数が暗黙であることをLeanに伝えることを思い出してほしい。そしてアンダースコアはLeanによって自動的に埋められる。もしLeanがアンダースコアを埋めることに失敗したら、エラーメッセージ "don't know how to synthesize placeholder" が返され、続いて項の予想される型とその文脈で使用可能な全ての項と前提が返される。言い換えると、解決できなかったプレースホルダー1つ1つに対して、Leanはその時点で埋める必要のあるサブゴールを報告する。最終的に、プレースホルダーを段階的に埋めていくことで、証明を構築することができる。

参考として、上記のリストから抜粋した恒真式の証明の例を2つ紹介する。

open Classical

-- 分配性
example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) :=
  Iff.intro
    (fun h : p ∧ (q ∨ r) =>
      have hp : p := h.left
      Or.elim (h.right)
        (fun hq : q =>
          show (p ∧ q) ∨ (p ∧ r) from Or.inl ⟨hp, hq⟩)
        (fun hr : r =>
          show (p ∧ q) ∨ (p ∧ r) from Or.inr ⟨hp, hr⟩))
    (fun h : (p ∧ q) ∨ (p ∧ r) =>
      Or.elim h
        (fun hpq : p ∧ q =>
          have hp : p := hpq.left
          have hq : q := hpq.right
          show p ∧ (q ∨ r) from ⟨hp, Or.inl hq⟩)
        (fun hpr : p ∧ r =>
          have hp : p := hpr.left
          have hr : r := hpr.right
          show p ∧ (q ∨ r) from ⟨hp, Or.inr hr⟩))

-- 古典論理を必要とする例
example (p q : Prop) : ¬(p ∧ ¬q) → (p → q) :=
  fun h : ¬(p ∧ ¬q) =>
  fun hp : p =>
  show q from
    Or.elim (em q)
      (fun hq : q => hq)
      (fun hnq : ¬q => absurd (And.intro hp hnq) h)

Exercises (練習問題)

"sorry" プレースホルダーを実際の証明で置き換えて、以下の恒真式を証明せよ。

variable (p q r : Prop)

-- ∧ と ∨ の可換性
example : p ∧ q ↔ q ∧ p := sorry
example : p ∨ q ↔ q ∨ p := sorry

-- ∧ と ∨ の結合性
example : (p ∧ q) ∧ r ↔ p ∧ (q ∧ r) := sorry
example : (p ∨ q) ∨ r ↔ p ∨ (q ∨ r) := sorry

-- 分配性
example : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := sorry
example : p ∨ (q ∧ r) ↔ (p ∨ q) ∧ (p ∨ r) := sorry

-- 他の性質
example : (p → (q → r)) ↔ (p ∧ q → r) := sorry
example : ((p ∨ q) → r) ↔ (p → r) ∧ (q → r) := sorry
example : ¬(p ∨ q) ↔ ¬p ∧ ¬q := sorry
example : ¬p ∨ ¬q → ¬(p ∧ q) := sorry
example : ¬(p ∧ ¬p) := sorry
example : p ∧ ¬q → ¬(p → q) := sorry
example : ¬p → (p → q) := sorry
example : (¬p ∨ q) → (p → q) := sorry
example : p ∨ False ↔ p := sorry
example : p ∧ False ↔ False := sorry
example : (p → q) → (¬q → ¬p) := sorry

"sorry" プレースホルダーを実際の証明で置き換えて、以下の恒真式を証明せよ。これらは古典論理を必要とする。

open Classical

variable (p q r : Prop)

example : (p → q ∨ r) → ((p → q) ∨ (p → r)) := sorry
example : ¬(p ∧ q) → ¬p ∨ ¬q := sorry
example : ¬(p → q) → p ∧ ¬q := sorry
example : (p → q) → (¬p ∨ q) := sorry
example : (¬q → ¬p) → (p → q) := sorry
example : p ∨ ¬p := sorry
example : (((p → q) → p) → p) := sorry

最後に、古典論理を使わずに ¬(p ↔ ¬p) を証明せよ。

Quantifiers and Equality (量化子と等号)

第3章では、命題論理の結合子を含む定理の証明を構築する方法を紹介した。この章では、命題論理の結合子に加え、全称量化子、存在量化子、等号関係を用いた定理とその証明を構築する方法を紹介する。

The Universal Quantifier (全称量化子)

任意の型 α に対して、α 上の一変数述語 p は、型 α → Prop の項として表現できることに注目してほしい。この場合、x : α が与えられると、p xx について p が成り立つという主張を表す。同様に、項 r : α → α → Propα 上の二項関係を表す。x y : α が与えられると、r x yxy の間に二項関係 r が成立するという主張を表す。

全称量化子 を用いた主張 ∀ x : α, p x は、「全ての x : α に対して、p x が成立する」という主張を表す。命題結合子と同様に、自然演繹の体系においては、全称量化子は導入則と除去則によって統制される。非形式的には、全称量化子の導入則は次のように表される:

x : α が任意に選べる文脈で p x の証明が与えられたとき、∀ x : α, p x の証明を得ることができる。

全称量化子の除去則は次のように表される:

∀ x : α, p x の証明があるとき、任意の項 t : α に対して、p t の証明を得ることができる。

含意の場合と同様に、「型としての命題」の考え方が有効である。依存関数型の導入則と除去則を思い出してほしい:

x : α が任意に選べる文脈で型 β x の項 t が作れるとき、項 (fun x : α => t) : (x : α) → β x が作れる。

依存関数型の除去則は次のように表される:

s : (x : α) → β x が与えられたとき、型 α の任意の項 t : α に対して、項 s t : β t を得ることができる。

p xProp 型を持つとき、型 (x : α) → β x を型 ∀ x : α, p x とみなすことで、依存関数型の導入則と除去則を全称量化子の導入則と除去則とみなすことができる。これらの規則に従って、全称量化子を含む証明を構築することができる。

「型としての命題」の考え方に従って、Calculus of Constructionsでは、従属関数型と全称量化子を同一視する。つまり、任意の項 p に対して、∀ x : α, p(x : α) → p の代替表現に過ぎず、p が命題のときは、前者の表現の方が後者より自然である、と考えるのである。

通常の関数の場合、α → ββx : α に依存しない場合の (x : α) → β だと解釈できることを思い出してほしい。同様に、命題間の含意 p → q は命題 qx : p に依存しない場合の ∀ x : p, q だと解釈することができる。

以下は、全称量化子に関する「型としての命題」対応がどのように実践されるかの例である。

example (α : Type) (p q : α → Prop) : (∀ x : α, p x ∧ q x) → ∀ y : α, p y :=
  fun h : ∀ x : α, p x ∧ q x =>
  fun y : α =>
  show p y from (h y).left

表記上の慣習として、Leanは全称量化子に可能な限り広いスコープを与えるので、上の例では x に対する量化子のスコープを限定するために括弧が必要である。そして、∀ y : α, p y を証明する正規の方法は、y を任意に取り、p y を証明することである。これが全称量化子の導入則の使用例である。次に、型 ∀ x : α, p x ∧ q x を持つ項 h が与えられると、項 h y は型 p y ∧ q y を持つ。これが全称量化子の除去則の使用例である。連言命題 h y の左の命題を取ると、所望の結論 p y が得られる。

束縛変数の名前を変えることで同じにできる2つの式は、等価であるとみなされる(α-同値)ことを思い出してほしい。例えば、上記の例について、結論の前件と後件の両方で同じ束縛変数名 x を用いて、証明の中では別の束縛変数名 z を使うこともできる。

example (α : Type) (p q : α → Prop) : (∀ x : α, p x ∧ q x) → ∀ x : α, p x :=
  fun h : ∀ x : α, p x ∧ q x =>
  fun z : α =>
  show p z from And.left (h z)

もう一つの例として、関係 r が推移的であることはどのように表現されるかを提示しよう:

variable (α : Type) (r : α → α → Prop)
variable (trans_r : ∀ x y z, r x y → r y z → r x z)

variable (a b c : α)
variable (hab : r a b) (hbc : r b c)

#check trans_r                -- ∀ (x y z : α), r x y → r y z → r x z
/- (r : α → α → Prop) と 「r x y → r y z → r x z」により x,y,z の型が推論されている -/
#check trans_r a b c          -- r a b → r b c → r a c
#check trans_r a b c hab      -- r b c → r a c
#check trans_r a b c hab hbc  -- r a c

この例で何が起こっているのかを考えてみよう。trans_r を値 a b c に適用すると、これは r a b → r b c → r a c の証明になる。これを「前提」hab : r a b に適用すると、含意命題 r b c → r a c の証明が得られる。最後に、これを前提 hbc に適用すると、結論 r a c の証明が得られる。

habhbc があれば最初の3つの引数が a b c であることは容易に推論できる。このような状況において、引数 a b c を毎回与えるのは面倒かもしれない。そのため、これらを暗黙の引数にするのが一般的である:

variable (α : Type) (r : α → α → Prop)
variable (trans_r : ∀ {x y z}, r x y → r y z → r x z)

variable (a b c : α)
variable (hab : r a b) (hbc : r b c)

#check trans_r          -- r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3
#check trans_r hab      -- r b ?m.42 → r a ?m.42
#check trans_r hab hbc  -- r a c

x y z を暗黙の引数にする利点は、r a c の証明を trans_r hab hbc と簡単に書けることである。欠点は、項 trans_r と項 trans_r hab の型を推論するのに必要な情報をLeanに与えることができないことである。最初の #check コマンドの出力は r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3 であり、暗黙の引数が特定できなかったことを示している。

次は r が同値関係であるという前提を使って初歩的な推論を行う例である:

variable (α : Type) (r : α → α → Prop)

variable (refl_r : ∀ x, r x x)
variable (symm_r : ∀ {x y}, r x y → r y x)
variable (trans_r : ∀ {x y z}, r x y → r y z → r x z)

example (a b c d : α) (hab : r a b) (hcb : r c b) (hcd : r c d) : r a d :=
  trans_r (trans_r hab (symm_r hcb)) hcd

全称量化子の使い方に慣れるために、この章の最後にある練習問題をいくつかやってみるとよい。

依存関数型には型付け規則があるが、全称量化子には特殊な型付け規則がある。これが Prop と他の型の違いである。α : Sort iβ : Sort j があり、項 βx : α に依存するかもしれないとする。このとき、(x : α) → β は型 Sort (imax i j) の項である。ここで、imax i jj が0でないなら ij の最大値で、j が0なら0である。

imax i j の定義は次のように解釈すればよい。もし j0 でないなら、(x : α) → β は型 Sort (max i j) の項である。言い換えれば、α から β への依存関数型は、インデックスが ij の最大値である宇宙に「住んで」いる。他方で、βSort 0、つまり Prop の項であるとしよう。この場合、α がどの階層的型宇宙に住んでいるかに関わらず、(x : α) → βSort 0 (Prop) の項となる。言い換えれば、βα に依存する命題であれば、 ∀ x : α, β も命題であるということである。これは、Prop は単なるデータの型ではなく命題の型であるという解釈を反映している。そして以上のことは Propimpredicative(非可述的)にしている。

predicative(可述的)という用語は、20世紀初頭の数学基礎論の発展に由来する。当時、ポアンカレやラッセルといった論理学者が、集合論におけるパラドックスを、性質Aを持つ集合を量化することで性質Aを定義するときに生じる「悪循環」のせいにしたのである。任意の型 α に対して、α 上の全ての(一変数)述語からなる型 α → Prop (α の「べき型」)を作れることに注目してほしい。Prop のimpredicativity(非可述性)とは、α → Prop を量化した命題を作れることを意味する。特に、α 上の述語を全称量化することで、α 上の述語を定義することができ(∀ X : α → Prop, β と書くことで「全ての α 上の述語に対して β が成立する」という α 上の述語を定義することができ)、これはまさにかつて問題視された類の循環である。

Equality (等号)

ここで、Leanのライブラリで定義されている最も基本的な関係の一つである「等号関係」に注目しよう。7章 Inductive Types (帰納型)では、Leanのlogical framework(論理フレームワーク)の根本から「どのように」等号が定義されるかを説明する。その前に、ここでは等号の使い方を説明する。

もちろん、等号の基本的な性質の一つは、「等号は同値関係である」という性質である:

#check Eq.refl    -- Eq.refl.{u_1} (a : α) : a = a
#check Eq.symm    -- Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a
#check Eq.trans   -- Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c

Leanに暗黙の引数(ここではメタ変数として表示されている)を挿入しないように指示することで、出力を読みやすくすることができる。

universe u

#check @Eq.refl.{u}   -- @Eq.refl : ∀ {α : Sort u} (a : α), a = a
#check @Eq.symm.{u}   -- @Eq.symm : ∀ {α : Sort u} {a b : α}, a = b → b = a
#check @Eq.trans.{u}  -- @Eq.trans : ∀ {α : Sort u} {a b c : α}, a = b → b = c → a = c

.{u} という記法は、宇宙パラメータとして u を使うことをLeanに指示する。

したがって、例えば、前節の例を等号関係に特化させることができる:

variable (α : Type) (a b c d : α)
variable (hab : a = b) (hcb : c = b) (hcd : c = d)

example : a = d :=
  Eq.trans (Eq.trans hab (Eq.symm hcb)) hcd

射影表記(Foo.bar ee.bar という略記)も使うことができる:

variable (α : Type) (a b c d : α)
variable (hab : a = b) (hcb : c = b) (hcd : c = d)
example : a = d := (hab.trans hcb.symm).trans hcd

反射律 Eq.refl は見た目よりも強力である。Calculus of Constructionsにおいて、任意の型は計算可能な解釈を持ち、論理フレームワークは同一の簡約結果を持つ項たちを同じものとして扱うことを思い出してほしい。その結果、いくつかの非自明な恒等式を反射律によって証明することができる:

variable (α β : Type)

example (f : α → β) (a : α) : (fun x => f x) a = f a := Eq.refl _
example (a : α) (b : β) : (a, b).1 = a := Eq.refl _
example : 2 + 3 = 5 := Eq.refl _

論理フレームワークのこの機能は非常に重要であるため、Leanのライブラリでは Eq.refl _ により rfl という記法を定義している:

variable (α β : Type)
example (f : α → β) (a : α) : (fun x => f x) a = f a := rfl
example (a : α) (b : β) : (a, b).1 = a := rfl
example : 2 + 3 = 5 := rfl

しかし、等号は同値関係以上のものである。等号は、左辺の式を右辺の式に置き換えても、あるいは右辺の式を左辺の式に置き換えても真理値が変わらないという意味で、全ての命題が等号によって主張される同値性を尊重するという重要な性質を持っている。つまり、h1 : a = bh2 : p a があれば、代入 Eq.subst h1 h2 を使って p b の証明を作ることができる。

example (α : Type) (a b : α) (p : α → Prop)
        (h1 : a = b) (h2 : p a) : p b :=
  Eq.subst h1 h2

example (α : Type) (a b : α) (p : α → Prop)  -- h2の型の中に登場するh1の左辺をh1の右辺で書き換える
    (h1 : a = b) (h2 : p a) : p b :=
  h1 ▸ h2

example (α : Type) (a b : α) (p : α → Prop)  -- h2の型の中に登場するh1の右辺をh1の左辺で書き換える
    (h1 : a = b) (h2 : p b) : p a :=
  h1 ▸ h2

2番目、3番目の例の中の三角形は、Eq.substEq.symm の上に構築されたマクロで、\t と打つことで入力できる。h1 ▸ h2 は「h1 を使って h2 を書き換える」と解釈できる。

Eq.subst 規則は、より明示的な置換を行う以下の補助規則を定義するために使われる。これらは関数適用項、つまり s t の形の項を扱うためのものである。具体的には、congrArgs を固定して t を置換するのに使われ、congrFunt を固定して s を置換するのに使われ、congrst の両方を一度に置換するのに使われる。

variable (α : Type)
variable (a b : α)
variable (f g : α → Nat)
variable (h₁ : a = b)
variable (h₂ : f = g)

example : f a = f b := congrArg f h₁
example : f a = g a := congrFun h₂ a
example : f a = g b := congr h₂ h₁

Leanのライブラリには次のような一般的な恒等式が多数収載されている:

variable (a b c : Nat)

example : a + 0 = a := Nat.add_zero a
example : 0 + a = a := Nat.zero_add a
example : a * 1 = a := Nat.mul_one a
example : 1 * a = a := Nat.one_mul a
example : a + b = b + a := Nat.add_comm a b
example : a + b + c = a + (b + c) := Nat.add_assoc a b c
example : a * b = b * a := Nat.mul_comm a b
example : a * b * c = a * (b * c) := Nat.mul_assoc a b c
example : a * (b + c) = a * b + a * c := Nat.mul_add a b c
example : a * (b + c) = a * b + a * c := Nat.left_distrib a b c
example : (a + b) * c = a * c + b * c := Nat.add_mul a b c
example : (a + b) * c = a * c + b * c := Nat.right_distrib a b c

Nat.mul_addNat.add_mul はそれぞれ Nat.left_distribNat.right_distrib の別名である。上記の性質は、自然数(Nat 型)に関するものである。

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y :=
  have h1 : (x + y) * (x + y) = (x + y) * x + (x + y) * y :=
    Nat.mul_add (x + y) x y
  have h2 : (x + y) * (x + y) = x * x + y * x + (x * y + y * y) :=
    (Nat.add_mul x y x) ▸ (Nat.add_mul x y y) ▸ h1
  h2.trans (Nat.add_assoc (x * x + y * x) (x * y) (y * y)).symm

Eq.subst の2番目の暗黙の引数は、置換が行われる文脈を提供するもので、α → Prop 型を持っていることに注意してほしい。

#check Eq.subst  -- {α : Sort u} {motive : α → Prop} {a b : α} (h₁ : a = b) (h₂ : motive a) : motive b

したがって、この述語を推論するには、higher-order unification(高階ユニフィケーション)の解(高階単一子)が必要である。一般論として、高階単一子が存在するかを決定する問題は決定不能であり、Leanはせいぜいこの問題に対して不完全で近似的な解を提供することしかできない。そのため、Eq.subst は必ずしも思い通りに動くとは限らない。マクロ h ▸ e はこの暗黙の引数を計算する際により効果的なヒューリスティクスを使う。そのため、Eq.subst の適用が失敗するような状況でも、h ▸ e が成功することがしばしばある。

等式の推論は非常に一般的で重要であるため、Leanはそれをより効率的に実行するためのメカニズムを数多く提供している。次の節では、より自然で簡潔な方法で計算的証明を書くための構文を提供する。しかし、より重要なのは、等式推論がrewriter(項書き換え器)、simplifier(単純化器)、その他の自動化によって成り立っていることである。項書き換え器と単純化器については次の節で簡単に説明し、次の章でさらに詳しく説明する。

Calculational Proofs (計算的証明)

計算的証明は、等号の推移律などの基本原則によって構成される中間結果の連鎖にすぎない。Leanにおいて、計算的証明はキーワード calc から始まる以下の構文を持つ:

calc
  <expr>_0  'op_1'  <expr>_1  ':='  <proof>_1
  '_'       'op_2'  <expr>_2  ':='  <proof>_2
  ...
  '_'       'op_n'  <expr>_n  ':='  <proof>_n

calc 以降の一連の行は全て同じインデントを持つ必要があることに注意。そうでなければコンパイルエラーになる。各 <proof>_i<expr>_{i-1} op_i <expr>_i の証明である必要がある。

最初の行に calc <expr>_0 と書いた後、その次の行から _ を使う事もできる。これは関係命題と証明の組からなる行を揃えるのに便利である。

calc <expr>_0 
    '_' 'op_1' <expr>_1 ':=' <proof>_1
    '_' 'op_2' <expr>_2 ':=' <proof>_2
    ...
    '_' 'op_n' <expr>_n ':=' <proof>_n

次は計算的証明の一例である:

variable (a b c d e : Nat)
variable (h1 : a = b)
variable (h2 : b = c + 1)
variable (h3 : c = d)
variable (h4 : e = 1 + d)

theorem T : a = e :=
  calc
    a = b      := h1
    _ = c + 1  := h2
    _ = d + 1  := congrArg Nat.succ h3
    _ = 1 + d  := Nat.add_comm d 1
    _ = e      := Eq.symm h4

この証明の書き方は、次章で詳しく説明する simp タクティクや rewrite タクティクと併用すると最も効果的である。例えば、rewrite の略語 rw を使うと、上記の証明は次のように書ける:

variable (a b c d e : Nat)
variable (h1 : a = b)
variable (h2 : b = c + 1)
variable (h3 : c = d)
variable (h4 : e = 1 + d)
theorem T : a = e :=
  calc
    a = b      := by rw [h1]
    _ = c + 1  := by rw [h2]
    _ = d + 1  := by rw [h3]
    _ = 1 + d  := by rw [Nat.add_comm]
    _ = e      := by rw [h4]

基本的に、rw タクティクは [] でくくられた等式(前提、定理名、複合的な項のいずれでもよい)を用いてゴールを「書き換える」。その結果、ゴールが恒等式 t = t になったら、rw タクティクは自動で反射律を使ってゴールを証明する。

段階的な書き換えを一度に実行することもできる。上の証明は次のように短縮できる:

variable (a b c d e : Nat)
variable (h1 : a = b)
variable (h2 : b = c + 1)
variable (h3 : c = d)
variable (h4 : e = 1 + d)
theorem T : a = e :=
  calc
    a = d + 1  := by rw [h1, h2, h3]
    _ = 1 + d  := by rw [Nat.add_comm]
    _ = e      := by rw [h4]

ここまで短くしてもよい:

variable (a b c d e : Nat)
variable (h1 : a = b)
variable (h2 : b = c + 1)
variable (h3 : c = d)
variable (h4 : e = 1 + d)
theorem T : a = e :=
  by rw [h1, h2, h3, Nat.add_comm, h4]

simp タクティクは、ゴールの項の中に与えられた恒等式が適用できる場所がある限り、与えられた恒等式を任意の順番で繰り返し適用し、ゴールを書き換える。また、システム内で宣言された既存のルールも活用し、書き換えのループを避けるため可換性を賢く適用する。上記の証明は simp を使って次のように証明することもできる:

variable (a b c d e : Nat)
variable (h1 : a = b)
variable (h2 : b = c + 1)
variable (h3 : c = d)
variable (h4 : e = 1 + d)
theorem T : a = e :=
  by simp [h1, h2, h3, Nat.add_comm, h4]

次の章では rwsimp の派生について説明する。

calc コマンドは、何らかの形で推移律を持つあらゆる関係に対して使うことができる。計算的証明の中で異なる関係を組み合わせることもできる。

example (a b c d : Nat) (h1 : a = b) (h2 : b ≤ c) (h3 : c + 1 < d) : a < d :=
  calc
    a = b     := h1
    _ < b + 1 := Nat.lt_succ_self b
    _ ≤ c + 1 := Nat.succ_le_succ h2
    _ < d     := h3

Trans 型クラスの新しいインスタンスを追加することで、calc に新しい推移律の定理を「教える」ことができる。型クラスについては後で紹介するが、ここではとりあえず新しい Trans インスタンスを使って calc 記法を拡張する方法を示す小さな例を以下に挙げる。

def divides (x y : Nat) : Prop :=
  ∃ k, k*x = y

def divides_trans (h₁ : divides x y) (h₂ : divides y z) : divides x z :=
  let ⟨k₁, d₁⟩ := h₁
  let ⟨k₂, d₂⟩ := h₂
  ⟨k₁ * k₂, by rw [Nat.mul_comm k₁ k₂, Nat.mul_assoc, d₁, d₂]⟩

def divides_mul (x : Nat) (k : Nat) : divides x (k*x) :=
  ⟨k, rfl⟩

instance : Trans divides divides divides where
  trans := divides_trans

example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) :=
  calc
    divides x y     := h₁
    _ = z           := h₂
    divides _ (2*z) := divides_mul ..

infix:50 " ∣ " => divides

example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) :=
  calc
    x ∣ y   := h₁
    _ = z   := h₂
    _ ∣ 2*z := divides_mul ..

上記の例から、ユーザーが定義した関係がinfix表記を持たなくても、その関係について calc が使えることがわかる。最後に、上記の例の縦棒 はunicodeのものである。match ... with 式で使われるASCIIの | のオーバーロードを避けるためにunicodeの記号を用いた。

calc を用いると、前節の証明をより自然にわかりやすく書くことができる。

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y :=
  calc
    (x + y) * (x + y) = (x + y) * x + (x + y) * y  := by rw [Nat.mul_add]
    _ = x * x + y * x + (x + y) * y                := by rw [Nat.add_mul]
    _ = x * x + y * x + (x * y + y * y)            := by rw [Nat.add_mul]
    _ = x * x + y * x + x * y + y * y              := by rw [←Nat.add_assoc]

ここでは、calc の他の記法を検討する価値がある。最初の式がこれだけ広いスペースをとる場合、最初の関係式に _ を使うと、全ての関係式が自然に整列される:

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y :=
  calc (x + y) * (x + y)
    _ = (x + y) * x + (x + y) * y       := by rw [Nat.mul_add]
    _ = x * x + y * x + (x + y) * y     := by rw [Nat.add_mul]
    _ = x * x + y * x + (x * y + y * y) := by rw [Nat.add_mul]
    _ = x * x + y * x + x * y + y * y   := by rw [←Nat.add_assoc]

ここで、Nat.add_assoc の前の左矢印は、書き換えの際に与えられた恒等式を逆向きに使うように rw に指示する(左矢印は \l と打つと入力できる。これと等価なascii文字列 <- を使ってもいい)。簡潔さを求めるなら、次のように単独の rwsimp を使うだけで証明を完結させることもできる。

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y :=
  by rw [Nat.mul_add, Nat.add_mul, Nat.add_mul, ←Nat.add_assoc]

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y :=
  by simp [Nat.mul_add, Nat.add_mul, Nat.add_assoc]

The Existential Quantifier (存在量化子)

最後に、存在量化子について考えよう。存在量化子は exists x : α, p x または ∃ x : α, p x と書くことができる。どちらの記法も、Leanのライブラリで定義されている Exists (fun x : α => p x) という長ったらしい表現の便利な省略形である。

もうお分かりのように、Leanのライブラリは存在量化子の導入則と除去則を含んでいる。導入則は簡単である: ∃ x : α, p x を証明するには、適切な項 t : αp t の証明を与えればよい。次はその例である:

example : ∃ x : Nat, x > 0 :=
  have h : 1 > 0 := Nat.zero_lt_succ 0
  Exists.intro 1 h

example (x : Nat) (h : x > 0) : ∃ y, y < x :=
  Exists.intro 0 h

example (x y z : Nat) (hxy : x < y) (hyz : y < z) : ∃ w, x < w ∧ w < z :=
  Exists.intro y (And.intro hxy hyz)

#check @Exists.intro  -- @Exists.intro : ∀ {α : Sort u_1} {p : α → Prop} (w : α), p w → Exists p

型が文脈から明らかな場合、Exists.intro t h の代わりに、匿名コンストラクタ表記 ⟨t, h⟩ を使うことができる。

example : ∃ x : Nat, x > 0 :=
  have h : 1 > 0 := Nat.zero_lt_succ 0
  ⟨1, h⟩

example (x : Nat) (h : x > 0) : ∃ y, y < x :=
  ⟨0, h⟩

example (x y z : Nat) (hxy : x < y) (hyz : y < z) : ∃ w, x < w ∧ w < z :=
  ⟨y, hxy, hyz⟩  -- ⟨y, hxy, hyz⟩ は ⟨y, ⟨hxy, hyz⟩ ⟩ と同じ

Exists.intro には暗黙の引数があることに注意してほしい: Leanは結論 ∃ x, p x から述語 p : α → Prop が何であるかを推論しなければならない。これは簡単なことではない。例えば、hg : g 0 0 = 0 とし、Exists.intro 0 hg と書くとする。このとき、述語 p は 定理 ∃ x, g x x = x∃ x, g x x = 0∃ x, g x 0 = x などに対応する様々な値を取りうる。Leanは文脈からどれが適切かを推論する。次の例では、pp.explicit オプションを true に設定し、#print コマンドに暗黙の引数を表示するように問い合わせている。

variable (g : Nat → Nat → Nat)
variable (hg : g 0 0 = 0)

theorem gex1 : ∃ x, g x x = x := ⟨0, hg⟩
theorem gex2 : ∃ x, g x 0 = x := ⟨0, hg⟩
theorem gex3 : ∃ x, g 0 0 = x := ⟨0, hg⟩
theorem gex4 : ∃ x, g x x = 0 := ⟨0, hg⟩

set_option pp.explicit true  -- 暗黙の引数を表示する
#print gex1
#print gex2
#print gex3
#print gex4

Exists.intro は主張本体の証人(存在量化を受けた主張を満たす項)を隠すため、情報を隠す操作であると解釈することができる。存在量化子の除去則 Exists.elim はその逆の操作を行う。Exists.elim は任意の値 w : α に対して p w ならば q が成立することを示すことで、∃ x : α, p x から命題 q を証明することを可能にする。大雑把に言えば、∃ x : α, p x が成立するなら p x を満たす x が存在することがわかるので、その x に名前、例えば w を与えることができる。もし qw に言及していなければ、qp w から導かれることを示すことは、qp w を満たす x の存在から導かれることを示すことに等しい。次はその例である:

variable (α : Type) (p q : α → Prop)

example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
  Exists.elim h
    (fun w =>
     fun hw : p w ∧ q w =>
     show ∃ x, q x ∧ p x from ⟨w, hw.right, hw.left⟩)

#check @Exists.elim  -- ∀ {α : Sort u_1} {p : α → Prop} {b : Prop}, (∃ x, p x) → (∀ (a : α), p a → b) → b

ここで、匿名コンストラクタ表記 ⟨w, hw.right, hw.left⟩ が入れ子になったコンストラクタの適用を省略していることに注意。⟨w, hw.right, hw.left⟩⟨w, ⟨hw.right, hw.left⟩⟩ と書いたのと同じである。

存在量化子除去則と選言除去則を比較することは有用であろう: 主張 ∃ x : α, p x は、 a が型 α の全ての項をわたるときの、命題 p a 全てを選言で繋げたものと考えることができる。

存在命題は、2章の従属型の節で説明したシグマ型(依存積型)に非常に似ていることに注目しよう。a : αh : p a が与えられたとき、項 Exists.intro a h は型 (∃ x : α, p x) : Prop を持つ一方で、Sigma.mk a h は型 (Σ x : α, p x) : Type を持つ。この Σ の類似性はカリー=ハワード同型のもう一つの例である。

section exist_prop
variable (a : α) (p : α → Prop) (h : p a)

#check Exists.intro a h  -- Exists p
end exist_prop

section sigma_type
variable (a : α) (p : α → Type) (h : p a)

#check Sigma.mk a h      -- Sigma p
end sigma_type

Leanは、match 式を用いた、存在量化子を除去するためのより便利な方法を提供する:

variable (α : Type) (p q : α → Prop)

example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
  match h with
  | ⟨w, hw⟩ => ⟨w, hw.right, hw.left⟩

match 式はLeanの関数定義システムの一部であり、複雑な関数を定義する便利で表現力豊かな方法を提供する。再びカリー=ハワード同型により、この関数定義方法 match を証明の記述にも応用させることができる。match 文は存在量化された主張を whw に「分解」する。これらは命題の証明記述内で使うことができる。より明確にするために、マッチで分解されてできた要素に型の注釈を付けることができる:

variable (α : Type) (p q : α → Prop)
example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
  match h with
  | ⟨(w : α), (hw : p w ∧ q w)⟩ => ⟨w, hw.right, hw.left⟩

match文を使って、存在量化子と連言を同時に分解することもできる:

variable (α : Type) (p q : α → Prop)
example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
  match h with
  | ⟨(w : α), (hpw : p w), (hqw : q w)⟩ => ⟨w, hqw, hpw⟩

Leanは let キーワードにもパターンマッチングを提供する:

variable (α : Type) (p q : α → Prop)
example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
  let ⟨w, hpw, hqw⟩ := h
  ⟨w, hqw, hpw⟩

これは、基本的に上記の match 構文の代替表記に過ぎない。Leanでは、fun キーワードの中で暗黙の match を使うこともできる:

variable (α : Type) (p q : α → Prop)
example : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x :=
  fun ⟨w, hpw, hqw⟩ => ⟨w, hqw, hpw⟩

8章 Induction and Recursion (帰納と再帰) では、これらの派生構文は全てより一般的なパターンマッチング構文の特殊例であることを説明する。

次の例では、is_even a∃ b, a = 2 * b と定義し、2つの偶数の和は偶数であることを示す。

def is_even (a : Nat) : Prop := ∃ b : Nat, a = 2 * b

theorem even_plus_even {a b : Nat} (h1 : is_even a) (h2 : is_even b) : is_even (a + b) :=
  Exists.elim h1 (fun w1 (hw1 : a = 2 * w1) =>
  Exists.elim h2 (fun w2 (hw2 : b = 2 * w2) =>
    Exists.intro (w1 + w2)
      (calc a + b
        _ = 2 * w1 + 2 * w2 := by rw [hw1, hw2]
        _ = 2 * (w1 + w2)   := by rw [Nat.mul_add])))

theorem even_plus_even2 : ∀ a b : Nat, is_even a → is_even b → is_even (a + b) :=
  fun a : Nat =>
  fun b : Nat => 
  fun ⟨(w1 : Nat), (hw1 : a = 2 * w1)⟩ =>
  fun ⟨(w2 : Nat), (hw2 : b = 2 * w2)⟩ =>
    have hw3 : a + b = 2 * (w1 + w2) :=
      calc a + b
        _ = 2 * w1 + 2 * w2 := by rw [hw1, hw2]
        _ = 2 * (w1 + w2)   := by rw [Nat.mul_add]
    ⟨(w1 + w2 : Nat), (hw3 : a + b = 2 * (w1 + w2))⟩

マッチ文、匿名コンストラクタ、rewrite タクティク……、この章で説明した様々な小道具を使ってこの証明を簡潔に書くと次のようになる:

def is_even (a : Nat) := ∃ b, a = 2 * b
theorem even_plus_even (h1 : is_even a) (h2 : is_even b) : is_even (a + b) :=
  match h1, h2 with
  | ⟨w1, hw1⟩, ⟨w2, hw2⟩ => ⟨w1 + w2, by rw [hw1, hw2, Nat.mul_add]⟩

構成的(構成的論理の)「または」が古典的「または」よりも強いように、構成的「存在する」も古典的「存在する」より強い。次の例に挙げるような含意命題を証明するためには、古典論理的な推論を必要とする。なぜなら、構成的論理では、「全ての x¬ p を満たす」の否定が真であることと、「p を満たす x が存在する」が真であることは同じではないからである。

open Classical
universe u
variable (α : Sort u) (p : α → Prop)

example (h : ¬ ∀ x, ¬ p x) : ∃ x, p x :=
  byContradiction
    (fun h1 : ¬ ∃ x, p x =>
      have h2 : ∀ x, ¬ p x :=
        fun x =>
        fun h3 : p x =>
        have h4 : ∃ x, p x := ⟨x, h3⟩
        show False from h1 h4
      show False from h h2)

以下に、練習問題として存在量化子を含む一般的な恒等式を挙げる。ここでは、できる限り多くの命題を証明することを勧める。また、どの命題が非構成的で、古典論理的な推論を必要とするかは、読者の判断に委ねる。

open Classical

variable (α : Type) (p q : α → Prop)
variable (r : Prop)

example : (∃ x : α, r) → r := sorry
example (a : α) : r → (∃ x : α, r) := sorry
example : (∃ x, p x ∧ r) ↔ (∃ x, p x) ∧ r := sorry
example : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) := sorry

example : (∀ x, p x) ↔ ¬ (∃ x, ¬ p x) := sorry
example : (∃ x, p x) ↔ ¬ (∀ x, ¬ p x) := sorry
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := sorry
example : (¬ ∀ x, p x) ↔ (∃ x, ¬ p x) := sorry

example : (∀ x, p x → r) ↔ (∃ x, p x) → r := sorry
example (a : α) : (∃ x, p x → r) ↔ (∀ x, p x) → r := sorry
example (a : α) : (∃ x, r → p x) ↔ (r → ∃ x, p x) := sorry

2番目の例と最後の2つの例は、型 α には少なくとも1つの要素 a が存在するという前提を必要とすることに注意してほしい。

以下は2つの難しい問題への解答である:

open Classical

variable (α : Type) (p q : α → Prop)
variable (a : α)
variable (r : Prop)

example : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) :=
  Iff.intro
    (fun ⟨a, (h1 : p a ∨ q a)⟩ =>
      Or.elim h1
        (fun hpa : p a => Or.inl ⟨a, hpa⟩)
        (fun hqa : q a => Or.inr ⟨a, hqa⟩))
    (fun h : (∃ x, p x) ∨ (∃ x, q x) =>
      Or.elim h
        (fun ⟨a, hpa⟩ => ⟨a, (Or.inl hpa)⟩)
        (fun ⟨a, hqa⟩ => ⟨a, (Or.inr hqa)⟩))

example : (∃ x, p x → r) ↔ (∀ x, p x) → r :=
  Iff.intro
    (fun ⟨b, (hb : p b → r)⟩ =>
     fun h2 : ∀ x, p x =>
     show r from hb (h2 b))
    (fun h1 : (∀ x, p x) → r =>
     show ∃ x, p x → r from
       byCases
         (fun hap : ∀ x, p x => ⟨a, fun h' : p a => h1 hap⟩)
         (fun hnap : ¬ ∀ x, p x =>
          byContradiction
            (fun hnex : ¬ ∃ x, p x → r =>
              have hap : ∀ x, p x :=
                fun x =>
                byContradiction
                  (fun hnp : ¬ p x =>
                    have hex : ∃ x, p x → r := ⟨x, (fun hp : p x => absurd hp hnp)⟩
                    show False from hnex hex)
              show False from hnap hap)))

More on the Proof Language (証明言語の詳細)

funhaveshow などのキーワードにより、非形式的な数学的証明の構造を反映した形式的証明項を書くことができることを見てきた。この節では、証明言語の他の便利な機能について説明する。

まず、ラベルを付けることなく補助ゴールを導入するために、無名の「have」式を使うことができる。this キーワードを用いると、無名の「have」式を使って導入された最後の項を参照することができる:

variable (f : Nat → Nat)
variable (h : ∀ x : Nat, f x ≤ f (x + 1))

example : f 0 ≤ f 3 :=
  have : f 0 ≤ f 1 := h 0
  have : f 0 ≤ f 2 := Nat.le_trans this (h 1)
  show f 0 ≤ f 3 from Nat.le_trans this (h 2)

証明の中ではいくつもの事実を使い捨てることが多いので、ラベルの付いた項が増えすぎてごちゃごちゃするのを防ぐには無名の「have」式が有効である。

ゴール(今ここで使いたい項の型)が推論できる場合は、by assumption と書くことでLeanに証明を埋めるよう頼むこともできる:

variable (f : Nat → Nat)
variable (h : ∀ x : Nat, f x ≤ f (x + 1))
example : f 0 ≤ f 3 :=
  have : f 0 ≤ f 1 := h 0
  have : f 0 ≤ f 2 := Nat.le_trans (by assumption) (h 1)
  show f 0 ≤ f 3 from Nat.le_trans (by assumption) (h 2)

by assumption はLeanに assumption タクティクを使うように指示し、assumption タクティクはローカルなコンテキストで適切な前提命題(の証明項)を見つけることでゴールを証明する。assumption タクティクについては次の章で詳しく説明する。

‹p› と書くことで、Leanに証明を埋めるよう頼むこともできる。ここで、‹p› はLeanにコンテキストから証明を見つけてもらいたい命題である。この角ばった括弧はそれぞれ \f<\f> と打つと入力できる。"f" は "フランス語" を意味する。なぜならこのunicode記号はフランス語における引用符としても使われるからである。この表記はLeanにおいて次のように定義されている:

notation "‹" p "›" => show p by assumption

このアプローチは、推論してほしい前提の型が明示的に与えられるため、by assumption を用いるよりもロバストである。また、証明も読みやすくなる。以下は、より詳細な例である:

variable (f : Nat → Nat)
variable (h : ∀ x : Nat, f x ≤ f (x + 1))

example : f 0 ≥ f 1 → f 1 ≥ f 2 → f 0 = f 2 :=
  fun _ : f 0 ≥ f 1 =>
  fun _ : f 1 ≥ f 2 =>
  have : f 0 ≥ f 2 := Nat.le_trans ‹f 1 ≥ f 2› ‹f 0 ≥ f 1›
  have : f 0 ≤ f 2 := Nat.le_trans (h 0) (h 1)
  show f 0 = f 2 from Nat.le_antisymm this ‹f 0 ≥ f 2›

フランス語の引用符は、匿名で導入されたものだけでなくコンテキスト中の「あらゆるもの」を参照できることを覚えておこう。フランス語の引用符の適用範囲は命題だけにとどまらないが、これをデータに対して使うのはやや奇妙である:

example (n : Nat) : Nat := ‹Nat›

以降の章で、Leanのマクロシステムを使って証明言語を拡張する方法を紹介する。

Exercises (練習問題)

  1. 以下の命題を証明せよ:
variable (α : Type) (p q : α → Prop)

example : (∀ x, p x ∧ q x) ↔ (∀ x, p x) ∧ (∀ x, q x) := sorry
example : (∀ x, p x → q x) → (∀ x, p x) → (∀ x, q x) := sorry
example : (∀ x, p x) ∨ (∀ x, q x) → ∀ x, p x ∨ q x := sorry

最後の例について、逆の命題が導出できないのはなぜかを理解してみよう。

  1. 式の一部が全称量化された変数に依存しない場合、それを全称量化子の外側に持ってくることはしばしば可能である。以下の命題を証明してみよう(このうち2つ目の命題の1方向は古典論理を必要とする):
variable (α : Type) (p q : α → Prop)
variable (r : Prop)

example : α → ((∀ x : α, r) ↔ r) := sorry
example : (∀ x, p x ∨ r) ↔ (∀ x, p x) ∨ r := sorry
example : (∀ x, r → p x) ↔ (r → ∀ x, p x) := sorry
  1. 「理髪師のパラドックス」について考えてみよう。これは、ある町において、「自分で髭を剃らない男性全員の髭を剃り、自分で髭を剃る男性の髭は一切剃らない男性の理髪師がいる」という主張である。この主張が矛盾することを示せ:
variable (men : Type) (barber : men)
variable (shaves : men → men → Prop)

example (h : ∀ x : men, shaves barber x ↔ ¬ shaves x x) : False := sorry
  1. パラメータを持たない Prop 型の項は(それが真か偽かを問わない)単なる主張である。まず以下の primeFermat_prime の定義を埋め、それらを使って他の定義を構築せよ。例えば、任意の自然数 n に対して、n より大きな素数が存在すると主張することで、素数は無限に存在すると言うことができる。弱いゴールドバッハ予想は、5より大きい任意の奇数は3つの素数の和で表されることを主張している。必要であれば、フェルマー素数や他の記述の定義を調べてみよう。
def even (n : Nat) : Prop := sorry

def prime (n : Nat) : Prop := sorry

def infinitely_many_primes : Prop := sorry

def Fermat_prime (n : Nat) : Prop := sorry

def infinitely_many_Fermat_primes : Prop := sorry

def goldbach_conjecture : Prop := sorry

def Goldbach's_weak_conjecture : Prop := sorry

def Fermat's_last_theorem : Prop := sorry
  1. 存在量化子の節で列挙した恒真式をできるだけ多く証明せよ。

Tactics (タクティク)

この章では、tactics(タクティク)を使って証明を構築する方法について説明する。証明項とは数学的証明の表現であり、タクティクは数学的証明を構築する手順を記述するコマンド(指示)である。定理 A ↔ B を証明する際に、非形式的に、「まず A → B を証明する。最初に定義を展開し、次に既存の補題を適用し、それから式を単純化する」という導入から数学の証明を始めることがあるかもしれない。これらの言明が証明を見つける方法を読者に伝える指示であるのと同様に、タクティクは証明項を構築する方法をLeanに伝える指示である。タクティクは、証明を分解し、一歩ずつゴールに向かうという段階的な証明の書き方を自然にサポートする。

タクティクの連続からなる証明を「タクティクスタイル」の証明と呼び、これまで見てきた証明項の構築の仕方を「項スタイル」の証明と呼ぶ。それぞれのスタイルには長所と短所がある。例えば、タクティクスタイルの証明は、各タクティクの結果を予測・推測することを読者に要求するため、項スタイルの証明より読みにくいという短所がある。しかし、短くて書きやすいという長所もある。さらに、タクティクはLeanの自動証明を利用するための入り口になる。なぜなら、Leanに自動証明を指示するコマンド自体がタクティクだからである。

用語に関する注意

この節は翻訳に際して追加した節である。

この章では、含意命題 p → q あるいはより一般的に依存関数型 (x : α) → β の中に登場する型 p や型 (x : α) を「前件(antecedent)」、型 q や型 β を「後件(consequent)」と呼び、一方で「ゴール(Goal)」 A ⊢ B の中に登場する項の集まり A を「コンテキスト(Context)」または「前提(Premise)」、ゴールにおいて項構築の目標となる型 B を「ターゲット(Target)」または「結論(Conclusion)」と呼び明確に区別する。特に、ゴールが閉じられていない(open)/未達成(not accomplished)/未証明(not proved)/未解決(not solved)のときは「コンテキスト(Context)」「ターゲット(Target)」という用語を使い、ゴールが閉じられている(closed)/達成済み(accomplished)/証明済み(proved)/解決済(solved)のときは「前提(Premise)」「結論(Conclusion)」という用語を使う。

コンテキスト(あるいは前提)に含まれる各項を「仮説(Hypothesis)」と呼ぶ。

ここでいう「ターゲット」を「ゴール」と呼ぶこともあるが、混乱を避けるため本訳ではこのような用語の使い方はしない。

intro タクティクや revert タクティクの働きを理解する際に、この用語の区別が重要となる。

参考記事

Entering Tactic Mode (タクティクモードへの入り方)

定理を述べたり、have文を使うと、ゴール、すなわち期待された型を持つ項を構築するという目標が生成される。例えば、仮説 p q : Prophp : phq : q を持つコンテキストでは、次のような記述は p ∧ q ∧ p という型の項を構築するというゴールを作成する:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p :=
  sorry

このゴールは次のように記述できる:

    p : Prop, q : Prop, hp : p, hq : q ⊢ p ∧ q ∧ p

実際、上記の例で "sorry" をアンダースコアに置き換えると、Leanはまさにこのゴールが未解決であることを報告する。

通常は、明示的に証明項を記述することでこのようなゴールを達成する。しかし、Leanでは、項が記述されることが期待される任意の場所に、項の記述の代わりに by <tactics> ブロックを挿入することができる。ここで、<tactics> はセミコロンまたは改行で区切られたコマンドの列である。by <tactics> ブロックを使って上記の定理を証明することができる:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p :=
  by apply And.intro
     exact hp
     apply And.intro
     exact hq
     exact hp

つまり、by キーワードを書くことでタクティクモードに入れるのである。しばしば by キーワードは前の行に書き、上記の例はこのように書かれる:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro
  exact hp
  apply And.intro
  exact hq
  exact hp

apply タクティクは、0個以上の引数をとる関数を表現する項 t を現在のゴールに適用する。apply タクティクは現在のゴールのターゲット( の後に書かれる型)と関数 t の出力の型を同一視し、引数(関数 t の入力の型を持つ項)を構築するという新しいゴールを作る。ただし、後続の引数の型が先行の引数に依存しない場合に限る。上記の例では、コマンド apply And.intro は2つのサブゴールを生成する:

    case left
    p q : Prop
    hp : p
    hq : q
    ⊢ p

    case right
    p q : Prop
    hp : p
    hq : q
    ⊢ q ∧ p

最初のゴールはコマンド exact hp で達成される。exact コマンドは apply コマンドの一種で、「与えられた項がターゲットと同じ型を持つことを確認し、確認できたらゴールを閉じよ」とLeanに指示する。exact コマンドをタクティク証明で使うのは良いことである。なぜなら、exact コマンドの失敗は何かが間違っていることを示すからである。また exactapply よりもロバストである。なぜなら、elaboratorは、与えられた項を処理する際に、今期待されている型(ゴールのターゲット)が何であるかを考慮に入れるからである。しかしながら、上記の例では apply も同様に機能する。

#print コマンドを使って最終的に得られた証明項を確認することができる:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
 apply And.intro
 exact hp
 apply And.intro
 exact hq
 exact hp
#print test
/-
theorem test : ∀ (p q : Prop), p → q → p ∧ q ∧ p :=
fun p q hp hq => { left := hp, right := { left := hq, right := hp } }
-/

タクティク証明は段階的に書くことができる。VS Codeでは、Ctrl-Shift-Enter を押すことでメッセージウィンドウを開くことができる。カーソルがタクティクブロック内にあるときはいつでも、このウィンドウは現在のゴールを表示する。Emacsでは、タクティクブロック内の任意の行末で C-c C-g を押すことで現在のゴールを見ることができる。また、カーソルを最後のタクティクの1文字目に置くことで、不完全な証明内の残りのゴールを見ることができる。証明が不完全な場合、キーワード by に赤い波線が引かれ、エラーメッセージには残りのゴールが表示される。

タクティクコマンドは単一の項名だけでなく、複合式を受け取ることもできる。以下は、前述の証明の短縮版である:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro hp
  exact And.intro hq hp

当然のことながら、この証明記述は全く同じ証明項を生成する。

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
 apply And.intro hp
 exact And.intro hq hp
#print test

複数のタクティク適用をセミコロンで連結して1行で書くことができる。

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro hp; exact And.intro hq hp

複数のサブゴールを生成する可能性のあるタクティクは、自動的に各サブゴールにタグを付けることが多い。例えば、タクティク apply And.intro は最初のサブゴールにタグ left を、2つ目のサブゴールにタグ right を付ける。この場合において、タグ名は And.intro の宣言の中で使われた引数の名前から推測される。case <tag> => <tactics> という表記を使うことで、タクティクを構造化することができる。つまり、<tactics> をどのタグ付けされたサブゴールに適用するかを明示することができる。以下は、この章の最初のタクティク証明の構造化されたバージョンである:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro
  case left => exact hp
  case right =>
    apply And.intro
    case left => exact hq
    case right => exact hp

case 記法を使うと、サブゴール rightleft よりも先に解くことができる:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro
  case right =>
    apply And.intro
    case left => exact hq
    case right => exact hp
  case left => exact hp

case ブロック内で、Leanが他のゴールを隠していることに注意してほしい。言わば、Leanは選択されたゴールに「集中」しているのである。さらに、case ブロックの終了時に選択されたゴールが完全には解かれていない場合、Leanはエラーフラグを建てる。

サブゴールが単純である場合、タグを使ってサブゴールを選択する価値はないかもしれないが、その場合でも証明を構造化したい場合は case が有用である。また、Leanは証明を構造化するための「箇条書き」記法 . <tactics> (あるいは · <tactics>) を提供する。. <tactics> 記法を使うと、Leanは一番上のゴールに「集中」する。

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := by
  apply And.intro
  . exact hp
  . apply And.intro
    . exact hq
    . exact hp

Basic Tactics (基本的なタクティク)

applyexact に加えて、もう一つの便利なタクティクが intro である。intro タクティクはゴールのターゲットの前件(ゴールのターゲットの の前にある命題)をゴールのコンテキスト(ゴールの の前)に移動させる。以降、この intro タクティクの機能を「ターゲットの前件をコンテキストに導入する」または単に「導入する」と表現する。以下は、3章で証明した命題論理の恒真式を今一度タクティクを使って証明した例である。

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro h
    apply Or.elim (And.right h)
    . intro hq
      apply Or.inl
      apply And.intro
      . exact And.left h
      . exact hq
    . intro hr
      apply Or.inr
      apply And.intro
      . exact And.left h
      . exact hr
  . intro h
    apply Or.elim h
    . intro hpq
      apply And.intro
      . exact And.left hpq
      . apply Or.inl
        exact And.right hpq
    . intro hpr
      apply And.intro
      . exact And.left hpr
      . apply Or.inr
        exact And.right hpr

intro タクティクはより一般的に任意の型の項をコンテキストに導入できる:

example (α : Type) : α → α := by
  intro a
  exact a

example (α : Type) : ∀ x : α, x = x := by
  intro x
  exact Eq.refl x

intro タクティクは複数の項を一度に導入できる:

example : ∀ a b c : Nat, a = b → a = c → c = b := by
  intro a b c h₁ h₂
  exact Eq.trans (Eq.symm h₂) h₁

apply タクティクが対話的に関数適用を構築するためのコマンドであるように、intro タクティクは対話的に関数抽象(つまり fun x => e の形の項)を構築するためのコマンドである。ラムダ抽象記法と同様に、intro タクティクでは暗黙の match を使うことができる。

example (α : Type) (p q : α → Prop) : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x := by
  intro ⟨w, hpw, hqw⟩
  exact ⟨w, hqw, hpw⟩

また、intro タクティクは match 式のようにコンテキストに導入した項を場合分けすることもできる(詳しくは8章 Induction and Recursion (帰納と再帰)を参照のこと)。

example (α : Type) (p q : α → Prop) : (∃ x, p x ∨ q x) → ∃ x, q x ∨ p x := by
  intro
  | ⟨w, Or.inl h⟩ => exact ⟨w, Or.inr h⟩
  | ⟨w, Or.inr h⟩ => exact ⟨w, Or.inl h⟩

example (α : Type) (p q : α → Prop) : (∃ x, p x ∨ q x) → ∃ x, q x ∨ p x := by
  intro h
  let ⟨w, hpq⟩ := h
  exact Or.elim hpq (fun hp : p w => ⟨w, Or.inr hp⟩) (fun hq : q w => ⟨w, Or.inl hq⟩)

intros タクティクは引数を与えずに使うことができる。その場合、intros タクティクはできる限り多くの項を一度に導入し、導入した各項に自動で名前を付ける。その例はすぐ後に紹介する。

assumption タクティクは現在のゴールの仮説たちに目を通し、それらの中にゴールのターゲットと合致するものがあればそれを適用する。

example (x y z w : Nat) (h₁ : x = y) (h₂ : y = z) (h₃ : z = w) : x = w := by
  apply Eq.trans h₁
  apply Eq.trans h₂
  assumption   -- applied h₃

assumption タクティクは、必要に応じてゴールのターゲット内のメタ変数( ?b など、どんな項が代入されるかが未決定の変数)を解決する(メタ変数に具体的な項を代入する):

example (x y z w : Nat) (h₁ : x = y) (h₂ : y = z) (h₃ : z = w) : x = w := by
  apply Eq.trans
  assumption      -- solves x = ?b with h₁
  apply Eq.trans
  assumption      -- solves y = ?h₂.b with h₂
  assumption      -- solves z = w with h₃

次の例では、intros タクティクを用いて3つの変数と2つの命題の証明を自動的にコンテキストに導入している:

example : ∀ a b c : Nat, a = b → a = c → c = b := by
  intros
  apply Eq.trans
  apply Eq.symm
  assumption
  assumption

デフォルトでは、Leanが自動生成した名前( a✝ など)にはアクセスできないことに注意してほしい。この仕様はタクティク証明の成否が自動生成された名前に依存しないようにするためにあり、この仕様があるおかげで証明はよりロバストになる。ただし、キーワード unhygienicby の後に書くことでこの制限を無効にすることができる(証明のロバスト性は低下する)。

example : ∀ a b c : Nat, a = b → a = c → c = b := by unhygienic
  intros
  apply Eq.trans
  apply Eq.symm
  exact a_2
  exact a_1

また、rename_i タクティクを用いて、現在のゴール内の最も直近のアクセス不能な名前を変更することができる。次の例では、タクティク rename_i h1 _ h2 がゴール内の最後の3つの仮説のうち2つの名前を変更している。

example : ∀ a b c d : Nat, a = b → a = d → a = c → c = b := by
  intros
  rename_i h1 _ h2
  apply Eq.trans
  apply Eq.symm
  exact h2
  exact h1

rfl タクティクは exact rfl の、つまり exact Eq.refl _ の糖衣構文である。

example (y : Nat) : (fun x : Nat => 0) y = 0 := by
  rfl

example (y : Nat) : (fun x : Nat => 0) y = 0 := by
  exact Eq.refl _

タクティクの前に repeat キーワードを書くと、そのタクティクは何度か繰り返し適用される。

example : ∀ a b c : Nat, a = b → a = c → c = b := by
  intros
  apply Eq.trans
  apply Eq.symm
  repeat assumption

revert タクティクは時々有用である。これは intro タクティクの逆の機能を持つ。つまり、指定した項をゴールのコンテキストからゴールのターゲットの前件に移動させる。以降、revert タクティクの機能を「コンテキストの一部をターゲットに戻す」または単に「戻す」と表現する。

example (x : Nat) : x = x := by
  revert x
  -- goal is ⊢ ∀ (x : Nat), x = x
  intro y
  -- goal is y : Nat ⊢ y = y
  rfl

(この章の最初に書いたように用語の区別をしていれば明らかなことだが、)コンテキスト(の一部)をターゲットに移すと含意命題が得られる:

example (x y : Nat) (h : x = y) : y = x := by
  revert h
  -- goal is x y : Nat ⊢ x = y → y = x
  intro h₁
  -- goal is x y : Nat, h₁ : x = y ⊢ y = x
  apply Eq.symm
  assumption

しかし、revert はさらに賢く、指定した項だけでなく、指定した項に依存する型を持つ要素も全てゴールのターゲットに移動させる。例えば、上の例で x を戻すと、h も一緒に戻される:

example (x y : Nat) (h : x = y) : y = x := by
  revert x
  -- goal is y : Nat ⊢ ∀ (x : Nat), x = y → y = x
  intros
  apply Eq.symm
  assumption

また、コンテキスト内の複数の仮説を一度に戻すこともできる:

example (x y : Nat) (h : x = y) : y = x := by
  revert x y
  -- goal is ⊢ ∀ (x y : Nat), x = y → y = x
  intros
  apply Eq.symm
  assumption

revert で戻せるのは現在のゴールのコンテキスト内の項(仮説)だけである。しかし、タクティク generalize e = x を使えば、ゴールのターゲットに登場する任意の式 e を新しい変数 x に置き換えることができる。また、タクティク generalize e = x at h₁ を使えば、ゴールの仮説 h₁ に登場する任意の式 e を新しい変数 x に置き換えることができる。

example : 3 = 3 := by
  generalize 3 = x
  -- goal is x : Nat ⊢ x = x
  revert x
  -- goal is ⊢ ∀ (x : Nat), x = x
  intro y
  -- goal is y : Nat ⊢ y = y
  rfl

上記の例において、generalize 3 = x3 に任意の変数 x を割り当てることでゴールのターゲットを一般化している。全ての一般化がゴールの証明可能性を保存するわけではないことに注意してほしい。次の例では、generalizerfl を使うだけで証明できるゴールを決して証明できないゴールに置き換えている:

example : 2 + 3 = 5 := by
  generalize 3 = x
  -- goal is x : Nat ⊢ 2 + x = 5
  admit

admit タクティクは exact sorry の糖衣構文である。これは現在のゴールを閉じ、sorry が使われたという警告を出す。一般化以前のゴールの証明可能性を保存するために、generalize タクティクを使う際に 3x に置き換えられたという事実を記録することができる。そのためには、置き換えの事実を保存するためのラベルを generalize に与えるだけでよい:

example : 2 + 3 = 5 := by
  generalize h : 3 = x
  -- goal is x : Nat, h : 3 = x ⊢ 2 + x = 5
  rw [← h]

ここでは、rewrite タクティク(略称は rw)が h を用いて x3 で再び置き換えている。rewrite タクティクについては後述する。

More Tactics (他のタクティク)

命題やデータを構築したり分解したりするには、他のいくつかのタクティクが有用である。例えば、p ∨ q の形のターゲットに対して apply タクティクを使う場合は、タクティク apply Or.inlapply Or.inr を使うだろう。逆に、cases タクティクは選言命題型(など)の仮説を分解する。

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h with
  | inl hp => apply Or.inr; exact hp
  | inr hq => apply Or.inl; exact hq

cases タクティクの構文は match 式の構文と似ていることに注意。新しいサブゴールは好きな順番で解くことができる。

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h with
  | inr hq => apply Or.inl; exact hq
  | inl hp => apply Or.inr; exact hp

with と後続のタクティクを書かずに(構造化されていない) cases を使うこともできる。この場合、複数のサブゴールが生成される。

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h
  apply Or.inr
  assumption
  apply Or.inl
  assumption

(構造化されていない) cases は、同じタクティクを使って複数のサブゴールを閉じられる場合に特に便利である。

example (p : Prop) : p ∨ p → p := by
  intro h
  cases h
  repeat assumption

tac1 <;> tac2 という結合子を使えば、タクティク tac1 により生成された各サブゴールに tac2 を適用することもできる。

example (p : Prop) : p ∨ p → p := by
  intro h
  cases h <;> assumption

構造化されていない cases タクティクと case 記法や . 記法を組み合わせることができる。

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h
  . apply Or.inr
    assumption
  . apply Or.inl
    assumption

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h
  case inr h =>
    apply Or.inl
    assumption
  case inl h =>
    apply Or.inr
    assumption

example (p q : Prop) : p ∨ q → q ∨ p := by
  intro h
  cases h
  case inr h =>
    apply Or.inl
    assumption
  . apply Or.inr
    assumption

cases タクティクは連言命題を分解することもできる。

example (p q : Prop) : p ∧ q → q ∧ p := by
  intro h
  cases h with
  | intro hp hq => constructor; exact hq; exact hp

example (p q : Prop) : p ∧ q → q ∧ p := by
  intro h
  cases h with
  | intro hp hq => apply And.intro; exact hq; exact hp

この例では、cases タクティクにより h : p ∧ q が一対の項 hp : phq : q に置き換えられた。constructor タクティクは、連言のための唯一のコンストラクタ And.intro をターゲットに適用する。これらのタクティクにより、前節の例は以下のように書き換えられる:

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro h
    cases h with
    | intro hp hqr =>
      cases hqr
      . apply Or.inl; constructor <;> assumption
      . apply Or.inr; constructor <;> assumption
  . intro h
    cases h with
    | inl hpq =>
      cases hpq with
      | intro hp hq => constructor; exact hp; apply Or.inl; exact hq
    | inr hpr =>
      cases hpr with
      | intro hp hr => constructor; exact hp; apply Or.inr; exact hr

これらのタクティクを非常に一般的に用いることができることは、7章 Inductive Types (帰納型)で説明する。端的に説明すると、cases タクティクは帰納的に定義された型の任意の項を分解することができる。constructor タクティクは常に、帰納的に定義された型の適用可能な最初のコンストラクタを適用する。例えば、casesconstructor は存在量化子に対して使うことができる:

example (p q : Nat → Prop) : (∃ x, p x) → ∃ x, p x ∨ q x := by
  intro h
  cases h with
  | intro x px => constructor; apply Or.inl; exact px

example (p q : Nat → Prop) : (∃ x, p x) → ∃ x, p x ∨ q x := by
  intro h
  cases h with
  | intro x px => apply Exists.intro; apply Or.inl; exact px

ここでは、constructor タクティクは Exists.intro の最初の引数である x の値を暗黙のままにしている。これは一旦メタ変数で表され、そのメタ変数の値は後で決定される。前の例では、メタ変数の適切な値はタクティク exact px が使われた時点で決定される。なぜなら、px は型 p x を持つからである。存在量化子に対する証人を明示的に指定したい場合は、代わりに exists タクティクを使うことができる:

example (p q : Nat → Prop) : (∃ x, p x) → ∃ x, p x ∨ q x := by
  intro h
  cases h with
  | intro x px => exists x; apply Or.inl; exact px

これは他の例である:

example (p q : Nat → Prop) : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x := by
  intro h
  cases h with
  | intro x hpq =>
    cases hpq with
    | intro hp hq =>
      exists x

cases タクティクやconstructor タクティクは命題だけでなくデータにも使える。次の例では、直積型の項の成分を入れ替える関数と直和型の項の成分を入れ替える関数を定義するためにこれらのタクティクが使われている:

def swap_pair : α × β → β × α := by
  intro p
  cases p
  constructor <;> assumption

def swap_sum : Sum α β → Sum β α := by
  intro p
  cases p
  . apply Sum.inr; assumption
  . apply Sum.inl; assumption

theorem swap_and : a ∧ b → b ∧ a := by
  intro p
  cases p
  constructor <;> assumption

theorem swap_or : a ∨ b → b ∨ a := by
  intro p
  cases p
  . apply Or.inr; assumption
  . apply Or.inl; assumption

上2つの関数定義の記述と、下2つの定理の証明が、わずかな差を除いて同じであることに注意してほしい。

cases タクティクは自然数を「場合分け」することもできる:

open Nat
example (P : Nat → Prop) (h₀ : P 0) (h₁ : ∀ n, P (succ n)) (m : Nat) : P m := by
  cases m with
  | zero    => exact h₀
  | succ m' => exact h₁ m'

cases タクティクとその仲間である induction タクティクについては、Tactics for Inductive Types (帰納型のためのタクティク)節で詳しく説明する。

contradiction タクティクは現在のゴールのコンテキストの中から矛盾を探す:

example (p q : Prop) : p ∧ ¬ p → q := by
  intro h
  cases h
  contradiction

match はタクティクブロック内でも使うことができる。

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro h
    match h with
    | ⟨_, Or.inl _⟩ => apply Or.inl; constructor <;> assumption
    | ⟨_, Or.inr _⟩ => apply Or.inr; constructor <;> assumption
  . intro h
    match h with
    | Or.inl ⟨hp, hq⟩ => constructor; exact hp; apply Or.inl; exact hq
    | Or.inr ⟨hp, hr⟩ => constructor; exact hp; apply Or.inr; exact hr

intro hmatch h ... と「組み合わせる」と、上記の例は次のように書ける:

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro
    | ⟨hp, Or.inl hq⟩ => apply Or.inl; constructor <;> assumption
    | ⟨hp, Or.inr hr⟩ => apply Or.inr; constructor <;> assumption
  . intro
    | Or.inl ⟨hp, hq⟩ => constructor; assumption; apply Or.inl; assumption
    | Or.inr ⟨hp, hr⟩ => constructor; assumption; apply Or.inr; assumption

Structuring Tactic Proofs (タクティク証明の構造化)

タクティクはしばしば証明を構築する効率的な方法を提供するが、長いタクティクの列は証明の構造を不明瞭にすることがある。このセクションでは、タクティクスタイルの証明をより読みやすくよりロバストにするために、タクティクスタイルの証明を構造化する方法を説明する。

Leanの証明記述構文の優れている点のひとつは、項スタイルの証明とタクティクスタイルの証明をミックスさせて、その間を自由に行き来できることだ。例えば、apply タクティクや exact タクティクは haveshow などを使って書かれた任意の型の項を受け取ることができる。逆に、Leanで任意の項を書くときは、by キーワードを挿入することで、いつでもタクティクモードを呼び出すことができる。以下はその例である:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := by
  intro h
  exact
    have hp : p := h.left
    have hqr : q ∨ r := h.right
    show (p ∧ q) ∨ (p ∧ r) by
      cases hqr with
      | inl hq => exact Or.inl ⟨hp, hq⟩
      | inr hr => exact Or.inr ⟨hp, hr⟩

次はより自然な例である:

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro h
    cases h.right with
    | inl hq => exact Or.inl ⟨h.left, hq⟩
    | inr hr => exact Or.inr ⟨h.left, hr⟩
  . intro h
    cases h with
    | inl hpq => exact ⟨hpq.left, Or.inl hpq.right⟩
    | inr hpr => exact ⟨hpr.left, Or.inr hpr.right⟩

実際、show タクティクというものがあり、これは項スタイルの証明の show 式に似ている。これは、タクティクモードの中で、解こうとしているゴールのターゲットの型を宣言するだけのタクティクである。

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  . intro h
    cases h.right with
    | inl hq =>
      show (p ∧ q) ∨ (p ∧ r)
      exact Or.inl ⟨h.left, hq⟩
    | inr hr =>
      show (p ∧ q) ∨ (p ∧ r)
      exact Or.inr ⟨h.left, hr⟩
  . intro h
    cases h with
    | inl hpq =>
      show p ∧ (q ∨ r)
      exact ⟨hpq.left, Or.inl hpq.right⟩
    | inr hpr =>
      show p ∧ (q ∨ r)
      exact ⟨hpr.left, Or.inr hpr.right⟩

実は、show タクティクは、ゴールのターゲットをdefinitionally equalな他の表現に書き換えるために使うことができる:

example (n : Nat) : n + 1 = Nat.succ n := by
  -- goal is n: Nat ⊢ n + 1 = Nat.succ n
  show Nat.succ n = Nat.succ n
  -- goal is n: Nat ⊢ Nat.succ n = Nat.succ n
  rfl

example (n : Nat) : n + 1 = Nat.succ n := by
  -- goal is n: Nat ⊢ n + 1 = Nat.succ n
  rfl

項スタイルの証明のときと同様に、have タクティクは、宣言した型の項を作るというサブゴールを導入する:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := by
  intro ⟨hp, hqr⟩
  show (p ∧ q) ∨ (p ∧ r)
  cases hqr with
  | inl hq =>
    have hpq : p ∧ q := And.intro hp hq
    apply Or.inl
    exact hpq
  | inr hr =>
    have hpr : p ∧ r := And.intro hp hr
    apply Or.inr
    exact hpr

項スタイルの証明のときと同様に、have タクティクでは項に付けるラベルを省略することもできる。その場合、デフォルトのラベル this が使われる:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := by
  intro ⟨hp, hqr⟩
  show (p ∧ q) ∨ (p ∧ r)
  cases hqr with
  | inl hq =>
    have : p ∧ q := And.intro hp hq
    apply Or.inl
    exact this
  | inr hr =>
    have : p ∧ r := And.intro hp hr
    apply Or.inr
    exact this

have タクティクでは型も省略することができる。したがって、have hp := h.lefthave hqr := h.right と書くことができる。これらの省略記法を用いると、have タクティクにおいて型とラベルの両方を省略することさえできる。その場合、新しい項にはデフォルトのラベル this が使われる。

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := by
  intro ⟨hp, hqr⟩
  cases hqr with
  | inl hq =>
    have := And.intro hp hq
    apply Or.inl; exact this
  | inr hr =>
    have := And.intro hp hr
    apply Or.inr; exact this

Leanには let タクティクもある。これは have タクティクに似ているが、have タクティクが補助的な事実を導入するのに対して、let タクティクは局所的な定義を導入する。これは項スタイルの証明における let に類似したタクティクである。

example : ∃ x, x + 2 = 8 := by
  let a : Nat := 3 * 2
  exists a

have と同様に、let a := 3 * 2 と書くことで、型を暗黙のままにすることができる。lethave の違いは、let はコンテキストの中でローカルな定義を導入することである。ローカルに宣言された定義はその証明の中で展開することができる。

先ほど、. を使って入れ子のタクティクブロックを作成した。入れ子のブロックの中では、Leanは最初のゴールに注目し、そのブロックの最後でそのゴールが完全に解決されていなければエラーを生成する。これは、タクティクによって導入された複数のサブゴールを一つ一つ証明するのに便利である。. 記法はインデントに敏感である。なぜなら、インデントを見て各タクティクブロックが終了したかどうかを検知するからである。あるいは、波括弧とセミコロンを使ってタクティクブロックを表すこともできる。

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := by
  apply Iff.intro
  { intro h;
    cases h.right;
    { show (p ∧ q) ∨ (p ∧ r);
      exact Or.inl ⟨h.left, ‹q›⟩ }
    { show (p ∧ q) ∨ (p ∧ r);
      exact Or.inr ⟨h.left, ‹r›⟩ } }
  { intro h;
    cases h;
    { show p ∧ (q ∨ r);
      rename_i hpq;
      exact ⟨hpq.left, Or.inl hpq.right⟩ }
    { show p ∧ (q ∨ r);
      rename_i hpr;
      exact ⟨hpr.left, Or.inr hpr.right⟩ } }

証明を構造化するためにインデントを使うと便利である: タクティクが2つ以上のサブゴールを残すたびに、残りのサブゴールをブロックで囲んでインデントして分離するとよい。もし定理 foo の適用が1つのゴールから4つのサブゴールを生成するなら、証明の見た目は次のようになるだろう:

  apply foo
  . <proof of first goal>
  . <proof of second goal>
  . <proof of third goal>
  . <proof of final goal>

あるいは

  apply foo
  case <tag of first goal>  => <proof of first goal>
  case <tag of second goal> => <proof of second goal>
  case <tag of third goal>  => <proof of third goal>
  case <tag of final goal>  => <proof of final goal>

あるいは

  apply foo
  { <proof of first goal>  }
  { <proof of second goal> }
  { <proof of third goal>  }
  { <proof of final goal>  }

Tactic Combinators (タクティク結合子)

Tactic combinators(タクティク結合子) は既存のタクティクから新しいタクティクを作る。逐次結合子 ;by ブロックの中で既に暗黙のうちに使われている:

example (p q : Prop) (hp : p) : p ∨ q :=
  by apply Or.inl; assumption

ここで、apply Or.inl; assumption はまず単一のタクティク apply Or.inl を使ってから assumption を使うのと機能的に同等である。

t₁ <;> t₂ において、結合子 <;> は逐次結合子の「パラレル」版である: まず t₁ が現在のゴールに適用される。それから t₂ がサブゴール「全て」に適用される。

example (p q : Prop) (hp : p) (hq : q) : p ∧ q :=
  by constructor <;> assumption

この方法は、t₁ の適用の結果得られるサブゴールが一様の形式を持つ場合、あるいは少なくとも、全てのゴールを一様な方法で進めることができる場合に特に有効である。

first | t₁ | t₂ | ... | tₙ はどれか一つが成功するまで各 tᵢ を適用する。その全てが成功しなかったら失敗する:

example (p q : Prop) (hp : p) : p ∨ q := by
  first | apply Or.inl; assumption | apply Or.inr; assumption

example (p q : Prop) (hq : q) : p ∨ q := by
  first | apply Or.inl; assumption | apply Or.inr; assumption

最初の例では左のタクティクが成功し、2番目の例では右のタクティクが成功している。次の3つの例では、いずれも同じ複合タクティクにより証明が成功している。

example (p q r : Prop) (hp : p) : p ∨ q ∨ r :=
  by repeat (first | apply Or.inl; assumption | apply Or.inr | assumption)

example (p q r : Prop) (hq : q) : p ∨ q ∨ r :=
  by repeat (first | apply Or.inl; assumption | apply Or.inr | assumption)

example (p q r : Prop) (hr : r) : p ∨ q ∨ r :=
  by repeat (first | apply Or.inl; assumption | apply Or.inr | assumption)

/- repeat と first を使わなかった場合 -/

example (p q r : Prop) (hp : p) : p ∨ q ∨ r :=
  by apply Or.inl; assumption

example (p q r : Prop) (hq : q) : p ∨ q ∨ r :=
  by apply Or.inr
     apply Or.inl; assumption

example (p q r : Prop) (hr : r) : p ∨ q ∨ r :=
  by apply Or.inr
     apply Or.inr
     assumption

このタクティクはまず選言命題の左側をターゲットにし、それを assumption で解こうとする。それが失敗したら、選言命題の右側に注目する。もしそれも失敗したら、assumption タクティクを呼び出す。

もう気付いているだろうが、タクティクは失敗することがある。実際、first 結合子がバックトラックして次のタクティクを試すのは、一番最初のタクティクが「失敗」したときである。try 結合子は、つまらないかもしれない方法によって、常に成功するタクティクを構築する: try tt を実行し、たとえ t が失敗しても成功したと報告する。try tfirst | t | skip と等価である。ここで、skip は何もせず、そして何もしないことで成功するタクティクである。次の例では、2番目の constructor は連言の右側 q ∧ r については成功するが、連言の左側 p については失敗する (連言と選言は右結合的であることを覚えておこう)。try タクティクは逐次結合されたタクティクが成功することを保証する。

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := by
  constructor <;> (try constructor) <;> assumption

注意: try t は決して失敗しないため、repeat (try t) は無限にループする。

証明では、複数のゴールが未解決であることがよくある。並列逐次結合子 <;> は1つのタクティクを複数のゴールに適用する1つの方法だが、他にも方法はある。例えば、all_goals t は全ての未解決のゴールに t を適用する:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := by
  constructor
  all_goals (try constructor)
  all_goals assumption

この例では、any_goals タクティクはよりロバストな解を提供する。タクティク any_goals tall_goals t に似ているが、any_goals tt が少なくとも1つのゴールで成功すれば成功する。

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := by
  constructor
  any_goals constructor
  any_goals assumption

次の例において、最初のタクティクは連言命題を繰り返し分解する:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) :
      p ∧ ((p ∧ q) ∧ r) ∧ (q ∧ r ∧ p) := by
  repeat (any_goals constructor)
  all_goals assumption

実際、上記の例の全てのタクティクを一行に詰め込むことができる:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) :
      p ∧ ((p ∧ q) ∧ r) ∧ (q ∧ r ∧ p) := by
  repeat (any_goals (first | constructor | assumption))

タクティク focus t は、他のゴールを一時的にスコープから隠し、t を現在のゴール(一番上のゴール)だけに作用させる。したがって、通常は t が現在のゴールだけに作用する場合、focus (all_goals t)all_goals の機能を打ち消して t と同じ作用を持つ。

Rewriting (書き換え)

Calculational Proofs (計算的証明)の節で、rewrite タクティク(省略版: rw)と simp タクティクを簡単に紹介した。本節と次節では、これらについてさらに詳しく説明する。

rewrite タクティクはターゲットとコンテキストに置換を適用するための基本的なメカニズムであり、等式を扱う便利で効率的な方法を提供する。このタクティクの最も基本的な構文は rewrite [t] である。ここで、t はある等式が成立することを主張する型である。例えば、仮説 h : x = yt として採用することができる。tadd_comm : ∀ x y, x + y = y + x のような全称命題でもよい。その場合、rewrite タクティクは xy に対して適切な項を見つけようとする。あるいは、それが具体的な等式あるいは等式に関する全称命題であれば、t は関数適用などを含む複合的な項であってもよい。次の例では、仮説を用いてターゲットを書き換えるために基本的な構文 rewrite [t] を使う。

example (f : Nat → Nat) (k : Nat) (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := by
  rw [h₂] -- replace k with 0
  rw [h₁] -- replace f 0 with 0

上記の例では、最初の rw はターゲット f k = 0 内の k0 に置き換え、2番目の rw はターゲット f 0 = 0 内の f 00 に置き換えている。このタクティクは t = t という形の任意のゴールを(rfl を使うまでもなく)自動的に閉じる。次は複合的な項を使った書き換えの例である:

example (x y : Nat) (p : Nat → Prop) (q : Prop) (h : q → x = y)
        (h' : p y) (hq : q) : p x := by
  rw [h hq]; assumption

ここで h hqx = y の証明を構築している。

rw [t_1, ..., t_n] という記法を使って、複数回の書き換えを1つにまとめることができる。これは rw [t_1]; ...; rw [t_n] の略記である。この記法を用いると、先ほどの例は次のように書ける:

example (f : Nat → Nat) (k : Nat) (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := by
  rw [h₂, h₁]

デフォルトでは、rw は等式を順方向に用いる。つまり、書き換え対象の中で t の左辺とマッチした(全ての)部分項を t の右辺で置き換える。記法 ←t を使うと、等式 t を逆向きに使うように指示することができる。つまり、書き換え対象の中で t の右辺とマッチした部分項を t の左辺で置き換えることができる。

example (f : Nat → Nat) (a b : Nat) (h₁ : a = b) (h₂ : f a = 0) : f b = 0 := by
  rw [←h₁, h₂]

この例では、項 ←h₁ba に置き換えるよう書き換え器に指示する。エディターでは、\l と打つと を入力することができる。また、これと等価なアスキー文字列 <- を使うこともできる。

用いる等式が全称命題の場合、等式の左辺が書き換え対象内の複数の部分項とマッチすることがある。例えば、書き換え対象が a + b + c = a + c + b であるとき、rw [Nat.add_comm]a + b とも a + c とも a + b + c とも a + c + b ともマッチしうる。その場合、rw タクティクは書き換え対象を走査したときに最初にマッチした部分項を書き換える。それが希望するものではない場合は、追加の引数を与えることでマッチさせたい部分項を指定することができる。

example (a b c : Nat) : a + b + c = a + c + b := by
  rw [Nat.add_assoc, Nat.add_comm b, ← Nat.add_assoc]

example (a b c : Nat) : a + b + c = a + c + b := by
  rw [Nat.add_assoc, Nat.add_assoc, Nat.add_comm b]

example (a b c : Nat) : a + b + c = a + c + b := by
  rw [Nat.add_assoc, Nat.add_assoc, Nat.add_comm _ b]

上記の最初の例では、最初のステップで a + b + ca + (b + c) に書き換えている。次のステップでは、項 b + c に可換性を適用している。ここで、引数 b を指定しなければ、a + (b + c)(b + c) + a に書き換えられていただろう。最後に、結合性を逆向きに使うことで a + (c + b)a + c + b に書き換えている。次の2つの例では、まず結合性を2回使って両辺の括弧を右に寄せ、それから bc を入れ替えている。最後の例では、Nat.add_comm の第2引数を指定することで、左辺ではなく右辺の書き換えを指示していることに注意してほしい。

デフォルトでは、rw タクティクはゴールのターゲットだけに影響する。rw [t] at h という記法は、仮説 ht による書き換えを適用する。

example (f : Nat → Nat) (a : Nat) (h : a + 0 = 0) : f a = f 0 := by
  rw [Nat.add_zero] at h
  rw [h]

最初のステップでは、rw [Nat.add_zero] at h が仮説 h の型を a + 0 = 0 から a = 0 に書き換えている。それからターゲットを f 0 = f 0 に書き換えるために h : a = 0 が用いられている。

rewrite タクティクは命題だけにとどまらず変数の型を書き換えることもできる。次の例では、rw [h] at t が仮説 t : Tuple α n の型を t : Tuple α 0 に書き換えている。

def Tuple (α : Type) (n : Nat) :=
  { as : List α // as.length = n }

example (n : Nat) (h : n = 0) (t : Tuple α n) : Tuple α 0 := by
  rw [h] at t
  exact t

Using the Simplifier (単純化器の使用)

rewrite タクティクがゴールを操作するために特化したツールとして設計されているのに対し、simplifier(単純化器) はより強力な自動化を提供する。Leanのライブラリには、[simp] 属性が付けられた恒等式が多数収載されており、simp タクティクはそれらを使って単純化対象内の部分項を繰り返し書き換える。

example (x y z : Nat) : (x + 0) * (0 + y * 1 + z * 0) = x * y := by
  simp

example (x y z : Nat) (p : Nat → Prop) (h : p (x * y))
        : p ((x + 0) * (0 + y * 1 + z * 0)) := by
  simp; assumption

最初の例では、ターゲットの等式の左辺は、0と1を含む普通の恒等式を使って単純化され、ターゲットは x * y = x * y となる。この時点で、simp は反射律を用いてゴールを閉じる。2番目の例では、simp がゴールを p (x * y) に簡約し、その時点で h がゴールを閉じる。次はリストに関する例である:

open List

example (xs : List Nat)
        : reverse (xs ++ [1, 2, 3]) = [3, 2, 1] ++ reverse xs := by
  simp

example (xs ys : List α)
        : length (reverse (xs ++ ys)) = length xs + length ys := by
  simp [Nat.add_comm]

ここで、simp [t][simp] 属性が付けられた恒等式に加えて t を用いて単純化対象を書き換える。

rw と同様に、キーワード at を使うとコンテキスト内の仮説を単純化することができる:

example (x y z : Nat) (p : Nat → Prop)
        (h : p ((x + 0) * (0 + y * 1 + z * 0))) : p (x * y) := by
  simp at h; assumption

さらに、「ワイルドカード」* を使うと、コンテキスト内の全ての仮説とターゲットを単純化することができる:

attribute [local simp] Nat.mul_comm Nat.mul_assoc Nat.mul_left_comm
attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm

example (w x y z : Nat) (p : Nat → Prop)
        (h : p (x * y + z * w * x)) : p (x * w * z + y * x) := by
  simp at *; assumption

example (x y z : Nat) (p : Nat → Prop)
        (h₁ : p (1 * x + y)) (h₂ : p (x * z * 1))
        : p (y + 0 + x) ∧ p (z * x) := by
  simp at * <;> constructor <;> assumption

自然数の乗法のような可換性と結合性を満たす演算の場合、単純化器は可換性と結合性に加えてleft commutativity(左可換性)を用いて式を書き換える。乗法の場合、左可換性は次のように表される: x * (y * z) = y * (x * z)local 修飾子は、現在のファイル(あるいはセクションや名前空間)内でこれらの規則を使用するように単純化器に指示する。可換性と左可換性は、どちらか片方を繰り返し適用するとループが生じるという点で問題があるように思えるかもしれない。しかし、単純化器は引数を並べ替える恒等式を検出し、ordered rewriting(順序付き書き換え)として知られるテクニックを使用する。これは、システムが項の内部的な順序を保持し、項の順序が小さくなる場合にのみ恒等式を適用することを意味する。上記の可換性、結合性、左可換性の恒等式は全て、部分項中の括弧を右に寄せるという効果を持つ。そのため、項を(多少恣意的ではあるが)正規化された順序で並べることができる。したがって、結合性と可換性の下で等価な式は、同じ正規形に書き換えられる。

attribute [local simp] Nat.mul_comm Nat.mul_assoc Nat.mul_left_comm
attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm
example (w x y z : Nat)
        : x * y + z * w * x = x * w * z + y * x := by
  simp

example (w x y z : Nat) (p : Nat → Prop)
        (h : p (x * y + z * w * x)) : p (x * w * z + y * x) := by
  simp        -- ⊢ p (x * y + w * (x * z))
  simp at h   -- h: p (x * y + w * (x * z))
  assumption

rewrite と同様に、simp [t_1, ..., t_n] と書くことで、単純化の際に使用する恒等式のリストに t_1 ... t_n を追加することができる。追加するものは一般的な補題でも仮説でも定義の展開でも複合的な項でもよい。simp タクティクも rewrite と同様に ←t 構文を認識する。

def f (m n : Nat) : Nat :=
  m + n + m

example {m n : Nat} (h : n = 1) (h' : 0 = m) : (f m n) = n := by
  simp [h, ←h', f]

仮説を用いてゴールを単純化するのがよくある使い方である:

example (f : Nat → Nat) (k : Nat) (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := by
  simp [h₁, h₂]

単純化の際に全ての仮説を使いたい場合は、ワイルドカード記号 * を使う:

example (f : Nat → Nat) (k : Nat) (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := by
  simp [*]

次は他の例である:

example (u w x y z : Nat) (h₁ : x = y + z) (h₂ : w = u + x)
        : w = z + y + u := by
  simp [*, Nat.add_assoc, Nat.add_comm, Nat.add_left_comm]

単純化器は命題の書き換えも行う。例えば、仮説 hp : p を用いて、p ∧ qq に、p v qTrue に書き換え、最終的に自明な命題に書き換えられたゴールを閉じる。命題の書き換えを繰り返すことで、非自明な命題推論を行うことができる。

example (p q : Prop) (hp : p) : p ∧ q ↔ q := by
  simp [*]

example (p q : Prop) (hp : p) : p ∨ q := by
  simp [*]

example (p q r : Prop) (hp : p) (hq : q) : p ∧ (q ∨ r) := by
  simp [*]

次の例では、単純化器はコンテキスト内の全ての仮説を単純化し、それらをターゲットに適用しゴールを閉じている。

example (u w x x' y y' z : Nat) (p : Nat → Prop)
        (h₁ : x + 0 = x') (h₂ : y + 0 = y')
        : x + y + 0 = x' + y' := by
  simp at *
  simp [*]

単純化器の特に便利なところは、ライブラリが発展するにつれてその機能が成長していくところだ。例えば、リスト xs を受け取ると、xs に反転 xs.reverse を追加して xs を対称化するリスト演算 mk_symm を定義したとしよう:

def mk_symm (xs : List α) :=
  xs ++ xs.reverse

このとき、任意のリスト xs に対して、List.reverse (mk_symm xs)mk_symm xs と等しいことが、定義を展開することで容易に証明できる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

reverse_mk_symm が証明された今、新しい定理を証明するために reverse_mk_symm を使った単純化を用いることができる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
theorem reverse_mk_symm (xs : List α)
       : (mk_symm xs).reverse = mk_symm xs := by
 simp [mk_symm]
example (xs ys : List Nat)
        : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := by
  simp [reverse_mk_symm]

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp [reverse_mk_symm] at h; assumption

simp [reverse_mk_symm] を使うのは一般的に決して悪いことではないが、ユーザーが明示的にこれを呼び出す必要がない方がなおよいだろう。定理を定義する際に、「これは単純化の際に使う定理だ」とマークすることで明示的な呼び出しの省略を実現できる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
@[simp] theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

example (xs ys : List Nat)
        : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := by
  simp

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp at h; assumption

記法 @[simp]reverse_mk_symm[simp] 属性を持つことを宣言する。この宣言はより明示的に記述することができる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

attribute [simp] reverse_mk_symm

example (xs ys : List Nat)
        : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := by
  simp

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp at h; assumption

定理が宣言された後なら、いつでもその定理に属性を付与することができる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

example (xs ys : List Nat)
        : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := by
  simp [reverse_mk_symm]

attribute [simp] reverse_mk_symm

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp at h; assumption

しかし、一度属性が付与されると、それを永続的に削除する方法はない。そして、属性は、その属性が割り当てられているファイルをインポートする全てのファイルに適用される。Attributes (属性)の節で詳しく説明するが、local 修飾子を使えば、属性の適用範囲を現在のファイルやセクションに限定することができる:

def mk_symm (xs : List α) :=
 xs ++ xs.reverse
theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

section
attribute [local simp] reverse_mk_symm

example (xs ys : List Nat)
        : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := by
  simp

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp at h; assumption
end

local を使った場合、そのセクションの外では、単純化器はデフォルトで reverse_mk_symm を使わなくなる。

これまで説明してきた様々な simp のオプション (ルールの明示的なリストを与える、at を使って適用対象を指定するなど) は組み合わせることができるが、オプションを記述する順序は厳格であることに注意してほしい。エディタの中では、simp キーワードにカーソルを合わせて、simp に関連するドキュメントを読むことで、オプションの正しい順序を確認することができる。

さらに2つの便利な修飾子がある。デフォルトでは、simp は属性 [simp] でマークされた全ての定理を利用する。simp only と書くと、デフォルトで使われる定理は全て除外され、より明確に作られた定理のリストを使うことができる。以下の例では、マイナス記号 -onlyreverse_mk_symm の適用をブロックするために使われている。

def mk_symm (xs : List α) :=
  xs ++ xs.reverse
@[simp] theorem reverse_mk_symm (xs : List α)
        : (mk_symm xs).reverse = mk_symm xs := by
  simp [mk_symm]

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p (mk_symm ys ++ xs.reverse) := by
  simp at h; assumption

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p ((mk_symm ys).reverse ++ xs.reverse) := by
  simp [-reverse_mk_symm] at h; assumption

example (xs ys : List Nat) (p : List Nat → Prop)
        (h : p (xs ++ mk_symm ys).reverse)
        : p ((mk_symm ys).reverse ++ xs.reverse) := by
  simp only [List.reverse_append] at h; assumption

simp タクティクには多くの設定オプションがある。例えば、次のように文脈的な単純化(ターゲットの前件を用いた単純化)を有効にすることができる。

example : if x = 0 then y + x = y else x ≠ 0 := by
  simp (config := { contextual := true })

contextual := true のとき、 simpy + x = y を単純化する際は x = 0 という事実を使い、x ≠ 0 を単純化する際には x ≠ 0 を用いる。次は他の例である:

example : ∀ (x : Nat) (h : x = 0), y + x = y := by
  simp (config := { contextual := true })

もうひとつの便利な設定オプションは、算術的な単純化を可能にする arith := true である。これは非常に便利なので、simp_arithsimp (config := { arith := true }) の省略形になっている。

example : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  simp_arith

Split Tactic (Splitタクティク)

split タクティクは、入れ子の if-then-else 式や match 式を場合分けするのに便利である。n 個の場合分けを持つ match 式に対して、split タクティクは最大 n 個のサブゴールを生成する。次に例を示す:

def f (x y z : Nat) : Nat :=
  match x, y, z with
  | 5, _, _ => y
  | _, 5, _ => y
  | _, _, 5 => y
  | _, _, _ => 1

example (x y z : Nat) : x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 := by
  intros
  simp [f]
  split
  . contradiction
  . contradiction
  . contradiction
  . rfl

上記の例のタクティク証明は次のように短縮することができる。

def f (x y z : Nat) : Nat :=
 match x, y, z with
 | 5, _, _ => y
 | _, 5, _ => y
 | _, _, 5 => y
 | _, _, _ => 1
example (x y z : Nat) : x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 := by
  intros; simp [f]; split <;> first | contradiction | rfl

タクティク split <;> first | contradiction | rfl は、まず split タクティクを適用し、次に生成された各サブゴールに対して contradiction を試し、それが失敗したら rfl を試す。simp のように、split をコンテキスト内の特定の仮説に適用することもできる。

def g (xs ys : List Nat) : Nat :=
  match xs, ys with
  | [a, b], _ => a+b+1
  | _, [b, c] => b+1
  | _, _      => 1

example (xs ys : List Nat) (h : g xs ys = 0) : False := by
  simp [g] at h; split at h <;> simp_arith at h

Extensible Tactics (拡張可能なタクティク)

次の例では、コマンド syntax を使って triv という記法を定義する。次に、macro_rules コマンドを使って、triv が使われたときの処理を指定する(triv のマクロ展開を指定する)。triv に対して複数のマクロ展開を指定することができ、タクティク解釈器はどれかが成功するまで全てのマクロ展開を試す。

-- 新しい記法を定義する
syntax "triv" : tactic

macro_rules
  | `(tactic| triv) => `(tactic| assumption)

example (h : p) : p := by
  triv

-- 現時点では、`triv` を使って次の定理を証明することはできない
-- example (x : α) : x = x := by
--  triv

-- `triv` を拡張しよう。タクティク解釈器はどれかが成功するまで
-- `triv` のための全てのマクロ展開を試す
macro_rules
  | `(tactic| triv) => `(tactic| rfl)

example (x : α) : x = x := by
  triv

example (x : α) (h : p) : x = x ∧ p := by
  apply And.intro <;> triv

-- (再帰的な)マクロ展開を追加する
macro_rules | `(tactic| triv) => `(tactic| apply And.intro <;> triv)

example (x : α) (h : p) : x = x ∧ p := by
  triv

Exercises (練習問題)

  1. 3章 Propositions and Proofs (命題と証明)4章 Quantifiers and Equality (量化子と等号) に戻り、タクティク証明を用いて出来るだけ多くの練習問題を解き直せ。rwsimp も適切に使うこと。

  2. タクティク結合子を使って、次の定理の証明を1行で書け:

example (p q r : Prop) (hp : p)
        : (p ∨ q ∨ r) ∧ (q ∨ p ∨ r) ∧ (q ∨ r ∨ p) := by
  admit

Interacting with Lean (Leanとの対話)

数学的オブジェクトを定義するための言語としての、そして証明を構築するための言語としての依存型理論の基本は理解していただけただろう。読者がまだ手にしていないのは、新しいデータ型を定義する方法である。このギャップを埋めるため、次の章ではinductive data type(帰納データ型)の概念を紹介する。その前に、この章では型理論そのものの説明は一旦お休みして、Leanのコードを書く際の実用的な機能について学ぶ。

ここに掲載されている情報の全てが、すぐ役に立つとは限らない。この章は軽く読んでLeanの特徴を掴み、後で必要に応じて戻ってくることをお勧めする。

Importing Files (ファイルのインポート)

Leanのフロントエンドの目的は、ユーザーの入力を解釈し、形式的な式を構築し、そしてそれらが正しい構文規則に従っており、正しく片付けされることをチェックすることである。Leanは様々なエディタでの使用をサポートしており、ユーザーはエディタ上で継続的なチェックとフィードバックを受けることができる。詳しくはLeanのdocumentation pagesを参照してほしい。

Leanの標準ライブラリの定義と定理は複数のファイルに散在している。ユーザーは追加のライブラリを使用したり、複数のファイルからなる独自のプロジェクトを開発することができる。Leanが起動すると、ライブラリの Init フォルダの内容が自動的にインポートされる。Init フォルダには基本的な定義や構文が多数含まれている。その結果、ここで紹介する例のほとんどは追加設定なしでそのまま動作する。

しかし、追加ファイルを使いたい場合は、ファイルの先頭に import 文を書いて手動でインポートする必要がある。コマンド

import Bar.Baz.Blah

Bar/Baz/Blah.olean ファイルを読み込む(.olean はコンパイル済のLeanファイルの拡張子である)。このコマンドにおいて、Bar.Baz.Blah はLeanのsearch path(検索パス)からの相対パスとして解釈される。検索パスがどのように決定されるかについては、documentation pagesを参照してほしい。デフォルトでは、標準ライブラリディレクトリと、場合によってはユーザーのローカルプロジェクトのルートが検索パスに含まれる。(Lean4では、カレントディレクトリからの相対パスでインポートファイルを指定することはできない。)

インポートは「推移的」である。言い換えれば、Foo をインポートして、FooBar をインポートする場合、Bar の内容にもアクセスできるようになる。したがって、Bar を明示的にインポートする必要はない。

More on Sections (セクションの詳細)

Leanは理論の構造化を手助けするために、様々なセクション分けの仕組みを提供している。Variables and Sections (変数とセクション)の節で、section コマンドを使うことで、理論の要素をグループ化できるだけでなく、必要に応じて定理や定義の引数として挿入される変数のスコープを区切ることができることを説明した。variable コマンドのポイントは、次の例のように、定理で使う変数を宣言できることであることを思い出してほしい:

section
variable (x y : Nat)

def double := x + x

#check double y
#check double (2 * x)

attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm

theorem t1 : double (x + y) = double x + double y := by
  simp [double]

#check t1 y
#check t1 (2 * x)

theorem t2 : double (x * y) = double x * y := by
  simp [double, Nat.add_mul]

end

variable (x y : Nat) が書かれていれば、double の定義において x を引数として宣言する必要はない。Leanは double の定義の中で x が使われていることを検出し、(x : Nat) を定義の引数に自動的に挿入する。同様に、Leanは t1t2 の中に xy が現れることを検出し、 (x : Nat)(y : Nat) を自動的に挿入する。double の定義の中に y は現れていないため、doubley を引数として持たないことに注意してほしい。variable コマンドで宣言された変数は、それらが実際に使用される定義宣言にのみ引数として挿入される。

More on Namespaces (名前空間の詳細)

Leanでは、識別子(定義や定理や定数の名前)は Foo.Bar.baz のようなhierarchical names(階層名)で与えられる。Leanが階層名を扱うためのメカニズムを提供していることは、Namespaces (名前空間)の節で説明した。コマンド namespace foo は、end fooに行き当たるまで、各定義と定理の名前の前に foo を付加する。コマンド open foo は、接頭辞 foo で始まる各定義と定理に、元の「フルネーム」に加えて接頭辞 foo を除いた一時的な「別名」を与える。

namespace Foo
def bar : Nat := 1
end Foo

open Foo

#check bar
#check Foo.bar

次のような定義

def Foo.bar : Nat := 1

はマクロとして扱われ、次のように展開される。

namespace Foo
def bar : Nat := 1
end Foo

定理や定義の名前は一意でなければならないが、短い「別名」は一意でないことがある。名前空間を開いたとき、識別子の指示対象が曖昧になる可能性がある。Leanは型情報を使って文脈上の意味を曖昧でなくしようとするが、フルネームを記すことで常に曖昧さをなくすことができる。全ての識別子にフルネームを与えるため、空の接頭辞を明示的に記述するための文字列 _root_ が存在する。

def String.add (a b : String) : String :=
  a ++ b

def Bool.add (a b : Bool) : Bool :=
  a != b

def add (α β : Type) : Type := Sum α β

open Bool
open String
-- #check add -- これは曖昧である
#check String.add           -- String → String → String
#check Bool.add             -- Bool → Bool → Bool
#check _root_.add           -- Type → Type → Type

#check add "hello" "world"  -- String
#check add true false       -- Bool
#check add Nat Nat          -- Type

protected キーワードを使って定義を宣言することで、短い別名が作られることを防ぐことができる:

protected def Foo.bar : Nat := 1

open Foo

-- #check bar -- error
#check Foo.bar

一般的な名前のオーバーロードを防ぐため、protected キーワードは Nat.recNat.recOn のような名前にも用いられる。

open コマンドにはバリエーションがある。コマンド open Nat (succ zero gcd) は列挙された識別子 succ zero gcd に対してのみ短い別名を生成する:

open Nat (succ zero gcd)
#check zero     -- Nat
#eval gcd 15 6  -- 3

コマンド open Nat hiding succ gcd は、 Nat 名前空間内の列挙された識別子 succ gcd除く全てに対して短い別名を生成する:

open Nat hiding succ gcd
#check zero     -- Nat
-- #eval gcd 15 6  -- error
#eval Nat.gcd 15 6  -- 3

コマンド open Nat renaming mul → times, add → plus は、Nat.multimes に、Nat.addplus にリネームした上で短い別名を生成する:

open Nat renaming mul → times, add → plus
#eval plus (times 2 2) 3  -- 7
-- #eval mul 1 2          -- error
#eval Nat.mul 1 2         -- 2

ある名前空間から別の名前空間へ、あるいはルートレベルへ別名を export することは時に便利である。現在の名前空間 Foo の中で、コマンド export Nat (succ add sub) は、Nat.succNat.addNat.sub に対して別名 Foo.succFoo.addFoo.sub を生成する。したがって、名前空間が開かれているときは、いつでもこれらの別名を使うことができる。名前空間の外で export コマンドが使われたときは、短い別名がルートレベルにエクスポートされる。

namespace Foo
export And (intro left right)

#check And.intro  -- And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
#check Foo.intro  -- And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
#check intro      -- And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
#check left       -- And.left {a b : Prop} (self : a ∧ b) : a
end Foo

-- #check intro   -- error

export And (intro left right)
#check intro      -- And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
-- #check _root_.intro  -- error

export コマンドが上手く機能しない場合は、protected キーワードや属性によって保護されている可能性を考えよう。

Attributes (属性)

Leanの主な機能はユーザーの入力を形式的な式に翻訳することであり、その形式的な式はカーネルによって正しさがチェックされ、後で使用するために環境に保存される。しかし、いくつかのコマンドは、環境内のオブジェクトに属性を割り当てたり、記法を定義したり、10章 Type Classes (型クラス)で説明される型クラスのインスタンスを宣言したりと、環境に別の影響を与える。そのようなコマンドのほとんどはグローバルな効果を持つ、つまり現在のファイル内だけでなく、現在のファイルをインポートした任意のファイル内でその効果が持続する。しかしながら、このようなコマンドは local 修飾子をサポートしていることが多い。local 修飾子を使えば、コマンドの効果を現在のセクションや名前空間が閉じられるまで、あるいは現在のファイルの終わりまでに限定することができる。

Using the Simplifier (単純化器の使用)の節では、定理に [simp] 属性を付与することで、単純化器がそれらの定理を使用できるようになることを説明した。次の例では、リストの「接頭辞関係」を定義し、この関係が反射的であることを証明し、その定理に [simp] 属性を割り当てている。

def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
  ∃ t, l₁ ++ t = l₂

@[simp] theorem List.isPrefix_self (as : List α) : isPrefix as as :=
  ⟨[], by simp⟩

example : isPrefix [1, 2, 3] [1, 2, 3] := by
  simp

それから、単純化器は isPrefix [1, 2, 3] [1, 2, 3]True に書き換えることでこれを証明している。

定義がなされた後ならいつでも、その定義に属性を割り当てることができる:

def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
 ∃ t, l₁ ++ t = l₂
theorem List.isPrefix_self (as : List α) : isPrefix as as :=
  ⟨[], by simp⟩

attribute [simp] List.isPrefix_self

local 修飾子を付けなかった場合、属性は、属性の宣言が行われたファイルをインポートするどのファイルにおいても有効である。local 修飾子を付加すると、属性のスコープは制限される:

def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
 ∃ t, l₁ ++ t = l₂
section

theorem List.isPrefix_self (as : List α) : isPrefix as as :=
  ⟨[], by simp⟩

attribute [local simp] List.isPrefix_self

example : isPrefix [1, 2, 3] [1, 2, 3] := by
  simp

end

-- Error:
-- example : isPrefix [1, 2, 3] [1, 2, 3] := by
--  simp

他の例として、instance コマンドを使うと isPrefix 関係に という表記を割り当てることができる。10章 Type Classes (型クラス)で説明するが、instance コマンドは関連する定義に [instance] 属性を割り当てることで機能する。

def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
  ∃ t, l₁ ++ t = l₂

instance : LE (List α) where
  le := isPrefix

theorem List.isPrefix_self (as : List α) : as ≤ as :=
  ⟨[], by simp⟩

instance を用いた表記の割り当てもローカルにすることができる:

def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
  ∃ t, l₁ ++ t = l₂
def instLe : LE (List α) :=
  { le := isPrefix }

section
attribute [local instance] instLe

example (as : List α) : as ≤ as :=
  ⟨[], by simp⟩

end

-- Error:
-- example (as : List α) : as ≤ as :=
--  ⟨[], by simp⟩

後述のNotation (記法)の節では、Leanの記法を定義するメカニズムについて説明し、このメカニズムが local 修飾子もサポートしていることを確認する。しかし、以下のSetting Options (オプションの設定)の節でLeanのオプション設定のメカニズムを説明するが、これは今までのパターンに従っていない: オプションはローカルに設定することしかできない。つまり、オプション設定のスコープは常に現在のセクションかファイルに限定される。

More on Implicit Arguments (暗黙の引数の詳細)

Implicit Arguments (暗黙の引数)の節で、Leanにおいて項 t の型を {x : α} → β x と表すとき、波括弧は xt の暗黙の引数であることを表す、と説明した。これは、t が記述されたときは常にその後にプレースホルダー(穴)が挿入されることを、つまり t@t _ に置換されることを意味する。それを望まない場合は、 t の代わりに @t と書く必要がある。

暗黙の引数は貪欲に、可能な限り挿入されることに注意してほしい。y だけを暗黙の引数にして関数 f (x : Nat) {y : Nat} (z : Nat) を定義したとする。このとき、2番目以降の引数を書かずに f 7 と書くと、これは構文解析器(パーサ)により f 7 _ とパースされる。Leanは弱い暗黙の引数(より控えめに挿入される暗黙の引数)を指定する記法 {{y : Nat}} を提供している。これは当該引数の後ろに明示的な引数がある場合にのみ、その明示的な引数の前にプレースホルダーを追加することを指定する。この記法は ⦃y : Nat⦄ と書くこともでき、このunicode括弧はそれぞれ \{{\}} と打つと入力できる。f (x : Nat) ⦃y : Nat⦄ (z : Nat) と書いた場合、f 7 はそのままパースされ、f 7 3f 7 _ 3 とパースされる。

この違いを説明するために、反射的ユークリッド関係が対称的かつ推移的であることを示す次の例を考えてみよう。

def reflexive {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ (a : α), r a a

def symmetric {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {a b : α}, r a b → r b a

def transitive {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {a b c : α}, r a b → r b c → r a c

def euclidean {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {a b c : α}, r a b → r a c → r b c

theorem th1 {α : Type u} {r : α → α → Prop}
            (reflr : reflexive r) (euclr : euclidean r)
            : symmetric r :=
  fun {a b : α} =>
  fun (h : r a b) =>
  show r b a from euclr h (reflr a)

theorem th2 {α : Type u} {r : α → α → Prop}
            (symmr : symmetric r) (euclr : euclidean r)
            : transitive r :=
  fun {a b c : α} =>
  fun (rab : r a b) (rbc : r b c) =>
  show r a c from euclr (symmr rab) rbc

theorem th3 {α : Type u} {r : α → α → Prop}
            (reflr : reflexive r) (euclr : euclidean r)
            : transitive r :=
 th2 (th1 reflr @euclr) @euclr

variable (r : α → α → Prop)
variable (euclr : euclidean r)

#check @euclr  -- euclidean r
#check euclr   -- r ?m1 ?m2 → r ?m1 ?m3 → r ?m2 ?m3

この例は3つの小さなステップからなっている: th1 は反射的かつユークリッド的な関係が対称的であることを示す。th2 は対称的かつユークリッド的な関係が推移的であることを示す。そして th3 は2つの定理を組み合わせ、反射的かつユークリッド的な関係が推移的であることを示している。しかし、th3 の証明において、euclr の暗黙の引数を手動で無効にしなければならない。そうしなければ、証明中の euclr に必要以上に多くの暗黙の引数が挿入されてしまうからである。つまり、証明内の @euclr は命題 ∀ {a b c : α}, r a b → r a c → r b c の証明項である一方で、証明内の euclr@euclr を暗黙のメタ引数 ?m1 ?m2 ?m3 : α に適用してできた、命題 r ?m1 ?m2 → r ?m1 ?m3 → r ?m2 ?m3 の証明項なのである。弱い暗黙の引数を使えばこの問題は解決する:

def reflexive {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ (a : α), r a a

def symmetric {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {{a b : α}}, r a b → r b a

def transitive {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {{a b c : α}}, r a b → r b c → r a c

def euclidean {α : Type u} (r : α → α → Prop) : Prop :=
  ∀ {{a b c : α}}, r a b → r a c → r b c

theorem th1 {α : Type u} {r : α → α → Prop}
            (reflr : reflexive r) (euclr : euclidean r)
            : symmetric r :=
  fun {a b : α} =>
  fun (h : r a b) =>
  show r b a from euclr h (reflr a)

theorem th2 {α : Type u} {r : α → α → Prop}
            (symmr : symmetric r) (euclr : euclidean r)
            : transitive r :=
  fun {a b c : α} =>
  fun (rab : r a b) (rbc : r b c) =>
  show r a c from euclr (symmr rab) rbc

theorem th3 {α : Type u} {r : α → α → Prop}
            (reflr : reflexive r) (euclr : euclidean r)
            : transitive r :=
 th2 (th1 reflr euclr) euclr

variable (r : α → α → Prop)
variable (euclr : euclidean r)

#check @euclr  -- euclidean r
#check euclr   -- euclidean r

角括弧 [] で表される3種類目の暗黙の引数がある。10章 Type Classes (型クラス)で説明するように、これらは型クラスのために用いられる。

Notation (記法)

Leanの識別子には、ギリシャ文字を含む任意の英数字(依存型理論で特別な意味を持つ∀、Σ、λは除く)を使うことができる。また、\_ と打った後に希望の添字を打つことで、添字を入力することもできる。

Leanのパーサは拡張可能である、つまり、ユーザーは新しい記法を定義することができる。

ユーザーは、Leanの構文を、基本的なmixfix記法からユーザーカスタムのelaboratorまで、あらゆるレベルで拡張したりカスタマイズできる。実際、全てのビルトイン構文は、ユーザー向けに提供されているメカニズムやAPIと同じものを使ってパースされ、処理される。この節では、様々な拡張方法について記述し、説明する。

新しい記法を導入することは、プログラミング言語では比較的まれなことであり、コードを不明瞭にする可能性があるため時には嫌われることさえあるが、形式化においては、各分野で確立された慣習や記法をコードで簡潔に表現するための非常に貴重なツールである。基本的な記法にとどまらず、よくある定型的なコードを抽出して(うまく機能する)マクロに落とし込んだり、カスタムされたドメイン固有言語(DSL)全体を組み込んで部分問題を効率的かつ読みやすくテキストにエンコードするLeanの機能は、プログラマーと証明エンジニアの双方にとって大きなベネフィットとなる。

Notations and Precedence (記法と優先順位)

最も基本的な構文拡張コマンドを使うと、新しい(あるいはオーバーロードされた)前置演算子、中置演算子、後置演算子を導入することができる。

infixl:65   " + " => HAdd.hAdd  -- 左結合的な中置演算子
infix:50    " = " => Eq         -- 非結合的な中置演算子
infixr:80   " ^ " => HPow.hPow  -- 右結合的な中置演算子
prefix:100  "-"   => Neg.neg    -- 前置演算子
set_option quotPrecheck false
postfix:max "⁻¹"  => Inv.inv    -- 後置演算子

演算子の種類(その「固定位置」)を表す最初のコマンド名とコロン : の後に、演算子のparsing precedence(パース優先順位)を与える。次に新規または既存の記号を二重引用符で囲み(空白はコードの見た目を整えるために使われる)、矢印 => の後にこの演算子の変換先の関数を指定する。

優先順位とは演算子と引数の結びつきの「強さ」を表す自然数で、演算の順序を表す。上記がマクロ展開されてできるコマンドを見ることで、これらをより正確に理解することができる:

notation:65 lhs:65 " + " rhs:66 => HAdd.hAdd lhs rhs
notation:50 lhs:51 " = " rhs:51 => Eq lhs rhs
notation:80 lhs:81 " ^ " rhs:80 => HPow.hPow lhs rhs
notation:100 "-" arg:100 => Neg.neg arg
set_option quotPrecheck false
notation:1024 arg:1024 "⁻¹" => Inv.inv arg  -- ``max`` は優先度1024の略記

最初のコードブロックの全てのコマンドは、実際にはより一般的な notation コマンドに変換するコマンドマクロであることがわかった。このようなマクロの書き方は後の節で学ぶ。notation コマンドは、単一の記号の代わりに、記号と「優先順位を持つ名前付き項プレースホルダー」を組み合わせたシーケンスを受け付ける。このシーケンスは => の右辺で参照され、右辺にあるプレースホルダーはシーケンス内の位置に基づいてパースされた各項によって置換される。優先順位 p を持つプレースホルダーは、その場所で p 以上の優先順位を持つ表記のみを受け付ける。したがって、文字列 a + b + ca + (b + c) とパースすることはできない。なぜなら、infixl コマンドの演算子の右辺は、演算子自体よりも優先順位が1大きいからである。対照的に、infixr コマンドの演算子の右辺は、演算子と同じ優先順位を持つ。そのため、a ^ b ^ ca ^ (b ^ c) とパースされる。優先順位が結合性を明確に決定しない場合、notation コマンドを直接使って次のような中置演算子を導入すると、Leanはこの演算子をデフォルトで右結合的にパースすることに注意してほしい:

set_option quotPrecheck false
notation:65 lhs:65 " ~ " rhs:65 => wobble lhs rhs

より正確には、曖昧な文法が存在する場合、Leanのパーサはローカルなlongest parse rule(最長構文解析ルール)に従う: a ~ b ~ c の中の a ~ の右辺をパースするとき、パーサは(優先順位が許す限り)最も長くパースを続ける。つまり b をパースした後に停止せず、~ c もパースする。したがって、項 a ~ b ~ ca ~ (b ~ c) と等価である。(Leanのパーサは、あくまで最も左側に位置する演算子から順にパースを試みることに注意してほしい。)

上記で言及したように、notation コマンドを使うと、記号とプレースホルダーを自由にミックスした任意のmixfix構文を定義することができる。

set_option quotPrecheck false
notation:max "(" e ")" => e
notation:10 Γ " ⊢ " e " : " τ => Typing Γ e τ

優先順位が明記されていないプレースホルダーの優先順位はデフォルトで 0 であり、つまりその位置にある任意の項を受け入れる。2つの記法が重なる場合、再び最長構文解析ルールを適用する。

notation:65 a " + " b:66 " + " c:66 => a + b - c
#eval 1 + 2 + 3  -- 0

新しい記法は2項記法よりも優先される。なぜなら2項記法はパースが連鎖せず、1 + 2 をパースしたところで構文解析をやめてしまうからである。最長のパースを受け入れる記法が複数ある場合、どちらが選択されるかはelaborationまで遅延され、ただ1つのオーバーロード記法が正しい型を持つ場合のみ成功し、それ以外の場合は曖昧さを解決できず失敗する。

Coercions (型強制)

Leanでは、自然数の型 Nat と整数の型 Int は異なる。しかし、自然数を整数の中に埋め込む Int.ofNat という関数があり、これを使うと必要なときに自然数を整数に変換することができる。Leanはこの種の型変換の必要性を検出して型変換を実行するメカニズムを持っている。このような自動的な型変換をcoercions(強制)と呼ぶ。

variable (m n : Nat)
variable (i j : Int)

#check i + m      -- i + Int.ofNat m : Int
#check i + m + j  -- i + Int.ofNat m + j : Int
#check i + m + n  -- i + Int.ofNat m + Int.ofNat n : Int

Displaying Information (情報の表示)

Leanに現在の状態や、現在のコンテキストで利用可能なオブジェクトや定理に関する情報を問い合わせる方法はいくつもある。最も一般的なコマンド #check#eval は既に紹介した。#check@ 記号と一緒に使われることが多く、こうすると定理や定義の引数を暗黙の引数を含めて全て明示することができる。さらに、#print コマンドを使うと、任意の識別子に関する情報を得ることができる。識別子が定義や定理を表す場合、#print コマンドはその識別子の型と定義式を表示する。識別子が定数や公理である場合、#print コマンドは「この識別子は定数(または公理)である」という事実を示し、その型を表示する。

-- examples with equality
#check Eq
#check @Eq
#check Eq.symm
#check @Eq.symm

#print Eq.symm

-- examples with And
#check And
#check And.intro
#check @And.intro

-- a user-defined function
def foo {α : Type u} (x : α) : α := x

#check foo
#check @foo
#print foo

-- axiom example
#print propext

Setting Options (オプションの設定)

Leanは、Leanの動作を制御するためにユーザーが設定することができる内部変数をいくつも保持している。そのための構文は次の通り:

set_option <name> <value>

非常に便利なオプション群の1つは、Leanのpretty-printer(プリティプリンタ)が項を表示する方法を制御する。以下のオプションはtrueかfalseを入力として受け取る:

pp.explicit  : 暗黙の引数を表示する
pp.universes : 隠れた宇宙パラメータを表示する
pp.notation  : 定義された記法を用いて出力を表示する

例として、次のように設定すると、かなり長い出力が得られる:

#check 2 + 2 = 4
#reduce (fun x => x + 2) = (fun x => x + 3)
#check (fun x => x + 1) 1

set_option pp.explicit true
set_option pp.universes true
set_option pp.notation false

#check 2 + 2 = 4
#reduce (fun x => x + 2) = (fun x => x + 3)
#check (fun x => x + 1) 1

コマンド set_option pp.all true はこれらの設定を一度に実行し、set_option pp.all false はこれらのオプションを元の値に戻す。証明をデバッグするときや、不可解なエラーメッセージを理解しようとするときに、付加的な情報を表示させることはしばしば非常に役に立つ。しかし、情報が多すぎると圧倒されるかもしれない。普通にLeanを使う場合はデフォルトの情報表示で一般的には十分である。

Using the Library (ライブラリの使用)

Leanを効率的に使おうと思ったら、必然的にライブラリにある定義や定理を利用する必要が出てくるだろう。ファイルの先頭に import コマンドを書くと他のファイルから既にコンパイルされた結果をインポートできることと、インポートは推移的であることを思い出してほしい。現在のファイルが Foo をインポートし、FooBar をインポートしているなら、現在のファイルで Foo のみならず Bar の定義や定理も利用できる。しかし、名前空間を開くという行為(これによりより短い名前が提供される)はインポート先に引き継がれない。各ファイルで、使用したい名前空間を開く必要がある。

一般に、ライブラリとその内容に詳しくなることは重要である。そうすれば、どのような定理、定義、記法、リソースが利用できるかを知ることができる。Leanのエディタモードも必要なものを見つけるのに役に立つが、ライブラリの内容を直接勉強することはしばしば避けられない。Leanの標準ライブラリはGitHubにあり、オンラインで見ることができる:

GitHubのブラウザインターフェースを使えば、これらのディレクトリやファイルの内容を見ることができる。自分のコンピュータにLeanをインストールした場合は、lean フォルダの中でライブラリ(例えば、.../.elan/toolchains/leanprover--lean4---nightly/src/lean/Lean)を見つけて、ファイルマネージャで探索することができる。各ファイルの先頭にあるコメントヘッダは、ファイルに関する追加情報を提供する。

Leanのライブラリ開発者は一般的な命名ガイドラインに従っている。そのため、ユーザーが必要な定理の名前を推測することや、Leanモードをサポートしているエディタでタブ補完を使って定理を見つけることがより簡単になっている。これらについては次の節で説明する。識別子は通常 camelCase で、型は CamelCase で命名される。定理には、異なる構成要素を _ で区切った説明的な名前を宛てている(変数名は省略される)。多くの場合、定理の名前はシンプルに結論だけを表す:

#check Nat.succ_ne_zero
#check Nat.zero_add
#check Nat.mul_one
#check Nat.le_of_succ_le_succ

Leanにおいて、識別子たちは階層的な名前空間の中で整理できることを覚えてほしい。例えば、名前空間 Nat の中の le_of_succ_le_succ という定理は Nat.le_of_succ_le_succ というフルネームを持っているが、コマンド open Nat を使うことで、(protected とマークされていない名前については)より短い名前が利用可能になる。Leanにおいて、構造体と帰納データ型を定義すると、定義した型に関連する操作が生成され、それらは定義中の型の名前と同じ名前の名前空間に格納されることは、7章 Inductive Types (帰納型)9章 Structures and Records (構造体とレコード)で説明する。例えば、直積型には以下の操作が付属している:

#check @Prod.mk
#check @Prod.fst
#check @Prod.snd
#check @Prod.rec

Prod.mk はペアを構成するために使われる。一方で、Prod.fstProd.snd はそれぞれ直積の1つ目の要素と2つ目の要素を射影する。Prod.rec は直積の2つの構成要素に対する関数から、直積に対する関数を定義するメカニズムを提供する。Prod.rec のような名前は保護されており、たとえ Prod 名前空間が開いているときでもフルネームを使用しなければならない。

型としての命題対応において、論理的結合子は帰納型の例である。したがって、論理的結合子に対してドット記法を使うことが多い:

#check @And.intro
#check @And.casesOn
#check @And.left
#check @And.right
#check @Or.inl
#check @Or.inr
#check @Or.elim
#check @Exists.intro
#check @Exists.elim
#check @Eq.refl
#check @Eq.subst

Auto Bound Implicit Arguments (自動束縛暗黙引数)

以前の節で、暗黙の引数が関数をより便利に使えるようにすることを示した。しかし、compose のような関数はまだ定義が冗長である。宇宙多相的な compose の定義は2章で定義したものよりもさらに冗長であることに注意してほしい。

universe u v w
def compose {α : Type u} {β : Type v} {γ : Type w}
            (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

compose を定義するときに宇宙パラメータ .{u, v, w} を与えることで、universe コマンドを省略することができる。

def compose.{u, v, w}
            {α : Type u} {β : Type v} {γ : Type w}
            (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

Lean 4は、auto bound implicit arguments(自動束縛暗黙引数)という新機能をサポートしている。この機能により、compose のような関数をより便利に書くことができる。Leanが定義宣言の前提を処理するとき、まだ束縛されていない識別子が1文字の小文字かギリシャ文字であれば、それらが暗黙引数として自動的に追加される。この機能を使うと、 compose を次のように書くことができる。

def compose (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

#check @compose
-- {β : Sort u_1} → {γ : Sort u_2} → {α : Sort u_3} → (β → γ) → (α → β) → α → γ

Type の代わりに Sort を用いることで、Leanはより一般的な型を推論したことに注意してほしい。

私たち(原文筆者たち)はこの機能が大好きで、Leanを実装する際に広く使用しているが、一部のユーザーはこの機能を不快に感じるかもしれないことも認識している。そのため、コマンド set_option autoImplicit false を使ってこの機能を無効にすることができる。

set_option autoImplicit false
/- 次の定義は `unknown identifier` エラーを生成する -/
-- def compose (g : β → γ) (f : α → β) (x : α) : γ :=
--   g (f x)

Implicit Lambdas (暗黙ラムダ式)

Lean 3の標準ライブラリでは、@_ を多用した怖ろしいイディオム(コードパターン)の実例をよく見かける。このイディオムは、期待される型が暗黙引数を持つ関数型で、さらに暗黙引数を取る定数(実例では reader_t.pure)がある場合によく使われる。Lean 4では、elaboratorにより暗黙引数を消費するためのラムダ式が自動的に導入される。私たちはこの機能を探究し、その影響を分析しているところだが、これまでの経験はとてもポジティブなものである。以下は、上記リンクの実例に対してLean 4の暗黙ラムダ式を使った例である。

variable (ρ : Type) (m : Type → Type) [Monad m]
instance : Monad (ReaderT ρ m) where
  pure := ReaderT.pure
  bind := ReaderT.bind

@ を使うか、{} または [] 束縛記法でラムダ式を書くことで、暗黙ラムダ式機能を無効にすることができる。以下はその例である:

namespace ex2
def id1 : {α : Type} → α → α :=
  fun x => x

def listId : List ({α : Type} → α → α) :=
  (fun x => x) :: []

-- 次の例において、``fun`` の前に ``@`` があるときは
-- 暗黙ラムダ式導入機能は無効になっている
def id2 : {α : Type} → α → α :=
  @fun α (x : α) => id1 x

def id3 : {α : Type} → α → α :=
  @fun α x => id1 x

def id4 : {α : Type} → α → α :=
  fun x => id1 x

-- 次の例では、束縛記法 ``{...}`` を使っているため、
-- 暗黙ラムダ式導入機能は無効になっている
def id5 : {α : Type} → α → α :=
  fun {α} x => id1 x
end ex2

Sugar for Simple Functions (単純な関数のための糖衣構文)

Lean 3では、括弧を使うことで、中置演算子から簡単な関数を作ることができる。例えば、(+1)fun x, x + 1 の糖衣構文である。Lean 4では、· をプレースホルダーとして使うことでこの表記を一般化する。以下はその例である:

namespace ex3
#check (· + 1)
-- fun a => a + 1
#check (2 - ·)
-- fun a => 2 - a
#eval [1, 2, 3, 4, 5].foldl (·*·) 1
-- 120

def f (x y z : Nat) :=
  x + y + z

#check (f · 1 ·)
-- fun a b => f a 1 b

#eval [(1, 2), (3, 4), (5, 6)].map (·.1)
-- [1, 3, 5]
end ex3

Lean 3と同様、この糖衣構文は括弧を使うことでアクティベートされ、ラムダ抽象は括弧で囲まれた · を引数として集めることで作られる。引数の収集は、入れ子になった括弧によって中断される。次の例では、2つの異なるラムダ式が生成されている:

#check (Prod.mk · (· + 1))
-- fun a => (a, fun b => b + 1)

Named Arguments (名前付き引数)

名前付き引数を使うと、引数リスト内の位置と引数をマッチさせるだけでなく、関数定義時に指定された引数名と引数をマッチさせることができる。引数の順番は覚えていないが引数の名前は知っている場合、その名前を使えばどんな順番でもその引数を与えることができる。Leanが暗黙引数を推論できなかった場合、名前付き引数を使ってその暗黙引数の値を指定することもできる。名前付き引数は、各引数が何を表しているのかを明確にすることで、コードの読みやすさも向上させる。

def sum (xs : List Nat) :=
  xs.foldl (init := 0) (·+·)

#eval sum [1, 2, 3, 4]
-- 10

example {a b : Nat} {p : Nat → Nat → Nat → Prop} (h₁ : p a b b) (h₂ : b = a)
    : p a a b :=
  Eq.subst (motive := fun x => p a x b) h₂ h₁

以下に、名前付き引数とデフォルト引数の相互作用を例示する。

def f (x : Nat) (y : Nat := 1) (w : Nat := 2) (z : Nat) :=
  x + y + w - z
-- ``(y : Nat := 1)`` は ``y`` のデフォルトの値が ``1`` であることを表す

example (x z : Nat) : f (z := z) x = x + 1 + 2 - z := rfl

example (x z : Nat) : f x (z := z) = x + 1 + 2 - z := rfl

example (x y : Nat) : f x y = fun z => x + y + 2 - z := rfl

example : f = (fun x z => x + 1 + 2 - z) := rfl

example (x : Nat) : f x = fun z => x + 1 + 2 - z := rfl

example (y : Nat) : f (y := 5) = fun x z => x + 5 + 2 - z := rfl

def g {α} [Add α] (a : α) (b? : Option α := none) (c : α) : α :=
  match b? with
  | none   => a + c
  | some b => a + b + c

variable {α} [Add α]

example : g = fun (a c : α) => a + c := rfl

example (x : α) : g (c := x) = fun (a : α) => a + x := rfl

example (x : α) : g (b? := some x) = fun (a c : α) => a + x + c := rfl

example (x : α) : g x = fun (c : α) => x + c := rfl

example (x y : α) : g x y = fun (c : α) => x + y + c := rfl

.. を使えば、足りない明示的引数全てに _ を指定することができる。この機能と名前付き引数を組み合わせると、パターンを書くのに便利である。以下はその例である:

inductive Term where
  | var    (name : String)
  | num    (val : Nat)
  | app    (fn : Term) (arg : Term)
  | lambda (name : String) (type : Term) (body : Term)

def getBinderName : Term → Option String
  | Term.lambda (name := n) .. => some n
  | _ => none

def getBinderType : Term → Option Term
  | Term.lambda (type := t) .. => some t
  | _ => none

省略記号は、明示的な引数が自動的に推論され、かつ _ の連続を避けたい場合にも便利である。

example (f : Nat → Nat) (a b c : Nat) : f (a + b + c) = f (a + (b + c)) :=
  congrArg f (Nat.add_assoc ..)

Inductive Types (帰納型)

Leanの形式的基礎は Prop, Type 0, Type 1, Type 2, ... という基本的な型を含み、(x : α) → β といった依存関数型の形成を可能にすることが分かった。例の中では、BoolNatInt などの追加的な型や、List、直積型 × などの型コンストラクタも使用した。実際、Leanのライブラリにおいて、宇宙以外の全ての具体的な型と、依存関数型以外の全ての型コンストラクタは、inductive types(帰納型)と呼ばれる型コンストラクタの集まりである(逆に、宇宙と依存関数型はfoundational types(基礎型)として知られる)。型宇宙、依存関数型、そして帰納型のみで巨大で頑丈な数学の体系を構築できることは驚くべきことである。それ以外の全てはこの3種類の型から派生する。

直感的には、帰納型は指定されたコンストラクタのリストから構築される。Leanにおいて、帰納型を指定する構文は次の通りである:

inductive Foo where
  | constructor₁ : ... → Foo
  | constructor₂ : ... → Foo
  ...
  | constructorₙ : ... → Foo

直感的には、各コンストラクタは、以前に構築された項から Foo の新しい項を構築する方法を指定する。Foo 型は各コンストラクタによって構築された項全てからなり、それ以外は何も含まない。帰納的宣言の最初の文字 | は省略可能である。| の代わりにコンマ , を使ってコンストラクタを区切ることもできる。

以下では、コンストラクタの引数に Foo 型の項を含めることができるが、その際に Foo の任意の要素がボトムアップで構築されることを保証する、ある「正の」制約が適用されることを説明する。大雑把に言えば、各コンストラクタの引数は Foo 型と以前に定義された型からなる任意の依存関数型を持つことができる。その依存関数型の中で、Foo は、もし現れるとすれば、「ターゲット」としてのみ現れる。

以下に帰納型の例をいくつか提供する。また、上記のスキームを少し一般化して、相互に定義された帰納型や、いわゆるinductive families(帰納族)についても考える。

論理的結合子と同様、全ての帰納型には導入則と除去則がある。導入則はその型の項の構築方法を示す。除去則は、その型の項を別の項の構築のために「使う」方法を示す。論理的結合子と帰納型の類似性は驚くにはあたらない。なぜなら、後述するように、論理的結合子たちも帰納的な型構築の例だからである。帰納型の導入則とは、すでに見た通りで型の定義で指定されるコンストラクタにすぎない。除去則はその型における再帰の原理を規定するものである。帰納法の原理は再帰の原理の特別な場合である。

次の章では、Leanの関数定義パッケージについて説明する。このパッケージは帰納型上の関数を定義し、帰納的証明を実行するためのさらに便利な方法を提供する。しかし、帰納型の概念は非常に基礎的で重要なため、低レベルで実践的な理解から始めることが重要だと考えている。ここでは、帰納型の基本的な例から始め、より精巧で複雑な例へとステップアップしていく。

Enumerated Types (列挙型)

最も単純な帰納型は、列挙型、すなわち有限個の項を列挙したリストを持つ型である。

inductive Weekday where
  | sunday : Weekday
  | monday : Weekday
  | tuesday : Weekday
  | wednesday : Weekday
  | thursday : Weekday
  | friday : Weekday
  | saturday : Weekday

inductive コマンドは新しい型 Weekday を作成する。全てのコンストラクタは Weekday 名前空間の中に格納される。

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
#check Weekday.sunday
#check Weekday.monday

open Weekday

#check sunday
#check monday

帰納型 Weekday を宣言する際は、: Weekday を省略できる。

inductive Weekday where
  | sunday
  | monday
  | tuesday
  | wednesday
  | thursday
  | friday
  | saturday

sundaymonday、...、saturdayWeekday の互いに異なる項であり、それ以外に互いに異なる項はないと考えてほしい。除去則 Weekday.rec は 型 Weekday とそのコンストラクタを用いて定義されている。除去則はrecursor(再帰子)とも呼ばれ、この型をinductive(帰納的)にしている: 除去則は、各コンストラクタに対応する値を割り当てることで、Weekday 上の関数を定義することを可能にしている。直感的には、帰納型の項は各コンストラクタによって網羅的に生成され、帰納型はコンストラクタが構築する項以外の項を持たないということである。

Weekday から自然数への関数を定義するには、match 文を使うことができる:

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
open Weekday

def numberOfDay (d : Weekday) : Nat :=
  match d with
  | sunday    => 1
  | monday    => 2
  | tuesday   => 3
  | wednesday => 4
  | thursday  => 5
  | friday    => 6
  | saturday  => 7

#eval numberOfDay Weekday.sunday  -- 1
#eval numberOfDay Weekday.monday  -- 2
#eval numberOfDay tuesday         -- 3

match 式は帰納型を宣言したときに生成された再帰子 Weekday.rec を使ってコンパイルされることに注意してほしい。

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
open Weekday

def numberOfDay (d : Weekday) : Nat :=
  match d with
  | sunday    => 1
  | monday    => 2
  | tuesday   => 3
  | wednesday => 4
  | thursday  => 5
  | friday    => 6
  | saturday  => 7

set_option pp.all true    -- 詳細な情報を表示させるオプション
#print numberOfDay
-- 定義中に``numberOfDay.match_1``というものが現れている
#print numberOfDay.match_1
-- 定義中に``Weekday.casesOn``というものが現れている
#print Weekday.casesOn
-- 定義中に``Weekday.rec``というものが現れている
#check @Weekday.rec
/-
@Weekday.rec.{u}
 : {motive : Weekday → Sort u} →
    motive Weekday.sunday →
    motive Weekday.monday →
    motive Weekday.tuesday →
    motive Weekday.wednesday →
    motive Weekday.thursday →
    motive Weekday.friday →
    motive Weekday.saturday →
    (t : Weekday) → motive t
-/

帰納データ型を宣言するとき、deriving Repr と書くと、Weekday の項をテキストに変換する関数を生成するようLeanに指示することができる。#eval コマンドはこの関数を使って Weekday の項を表示する。

inductive Weekday where
  | sunday
  | monday
  | tuesday
  | wednesday
  | thursday
  | friday
  | saturday
  deriving Repr

open Weekday

#eval tuesday   -- Weekday.tuesday (``deriving Repr`` を外すとエラーになる)

ある帰納型に関連する定義や定理を、型と同じ名前の名前空間にまとめると便利なことが多い。例えば、numberOfDay 関数を Weekday 名前空間に置くとよい。そうすると、名前空間 Weekday を開いたときに、コンストラクタにも定義・定理・関数にも短い名前を用いてアクセスすることができる。

Weekday から Weekday への関数を定義することができる:

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
 deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
  match d with
  | sunday    => monday
  | monday    => tuesday
  | tuesday   => wednesday
  | wednesday => thursday
  | thursday  => friday
  | friday    => saturday
  | saturday  => sunday

def previous (d : Weekday) : Weekday :=
  match d with
  | sunday    => saturday
  | monday    => sunday
  | tuesday   => monday
  | wednesday => tuesday
  | thursday  => wednesday
  | friday    => thursday
  | saturday  => friday

#eval next (next tuesday)      -- Weekday.thursday
#eval next (previous tuesday)  -- Weekday.tuesday

example : next (previous tuesday) = tuesday :=
  rfl

end Weekday

どうすればより一般的な定理「任意のWeekday d に対して、next (previous d) = d」が証明できるだろうか。match を使うと各コンストラクタに対して主張の証明を与えることができる:

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
 deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
 match d with
 | sunday    => monday
 | monday    => tuesday
 | tuesday   => wednesday
 | wednesday => thursday
 | thursday  => friday
 | friday    => saturday
 | saturday  => sunday
def previous (d : Weekday) : Weekday :=
 match d with
 | sunday    => saturday
 | monday    => sunday
 | tuesday   => monday
 | wednesday => tuesday
 | thursday  => wednesday
 | friday    => thursday
 | saturday  => friday
theorem next_previous (d : Weekday) : next (previous d) = d :=
  match d with
  | sunday    => rfl
  | monday    => rfl
  | tuesday   => rfl
  | wednesday => rfl
  | thursday  => rfl
  | friday    => rfl
  | saturday  => rfl

タクティク証明を使えば、定理 next_previous の証明はより簡潔になる:

inductive Weekday where
 | sunday : Weekday
 | monday : Weekday
 | tuesday : Weekday
 | wednesday : Weekday
 | thursday : Weekday
 | friday : Weekday
 | saturday : Weekday
 deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
 match d with
 | sunday    => monday
 | monday    => tuesday
 | tuesday   => wednesday
 | wednesday => thursday
 | thursday  => friday
 | friday    => saturday
 | saturday  => sunday
def previous (d : Weekday) : Weekday :=
 match d with
 | sunday    => saturday
 | monday    => sunday
 | tuesday   => monday
 | wednesday => tuesday
 | thursday  => wednesday
 | friday    => thursday
 | saturday  => friday
def next_previous (d : Weekday) : next (previous d) = d := by
  cases d <;> rfl

以下の節 Tactics for Inductive Types (帰納型のためのタクティク) では、帰納型を利用するのに特化したタクティクを紹介する。

「型としての命題対応」の下では、定理証明と関数定義の両方に match を使えることに注目してほしい。言い換えれば、「型としての命題対応」の下では、場合分けによる証明は場合分けによる定義の一種なのである。そこには「定義」されるものが「データ型の項」なのか「命題型の証明」なのかの違いしかない。

Leanのライブラリにおいて、Bool 型は列挙型の一例である。

namespace Hidden
inductive Bool where
  | false : Bool
  | true  : Bool
end Hidden

(この例において、標準ライブラリ内の Bool と今定義する Bool の衝突を防ぐために、今定義する Bool を名前空間 Hidden の中に置いた。この処置は必須である。なぜなら、Bool はLeanの標準ライブラリ内の最初のファイル Init/Prelude.lean の一部であり、Leanが起動したときに自動的にインポートされるからである。)

練習として、「Bool 型の導入則と除去則は何か」を考えるといいだろう。発展問題として、名前空間 Hidden の中で、自分の手で Bool 型と Bool 型上の演算子(ブーリアン演算子) andornot を定義し、それらに関する基本的な恒等式を証明してみることをお勧めする。and のような2項演算子は match を使って定義できることに注目してほしい:

namespace Hidden
def and (a b : Bool) : Bool :=
  match a with
  | true  => b
  | false => false
end Hidden

同様に、ほとんどの恒等式は適切に match を使ってから rfl を使うことで証明できる。

Constructors with Arguments (引数を持つコンストラクタ)

列挙型は、そのコンストラクタが全く引数を取らないという点で帰納型の非常に特殊な例である。一般的に、コンストラクタは入力データ(引数)を使って項を構築することができる。ライブラリ内の直積型と直和型の定義について考えてみよう:

namespace Hidden
inductive Prod (α : Type u) (β : Type v)
  | mk : α → β → Prod α β

inductive Sum (α : Type u) (β : Type v) where
  | inl : α → Sum α β
  | inr : β → Sum α β
end Hidden

この例の中で何が起きているか考えてみよう。直積型は唯一のコンストラクタ Prod.mk を持ち、これは2つの引数を取る。Prod α β 型の項を引数にとる関数を定義するには、入力が Prod.mk a b の形であると仮定し、ab を用いて出力を指定する必要がある。この関数定義法は Prod α β 型の項のための2つの射影を定義する際にも使える。標準ライブラリにおいて、記法 α × βProd α β を表し、(a, b)Prod.mk a b を表すことを覚えてほしい。

namespace Hidden
inductive Prod (α : Type u) (β : Type v)
  | mk : α → β → Prod α β
def fst {α : Type u} {β : Type v} (p : Prod α β) : α :=
  match p with
  | Prod.mk a b => a

def snd {α : Type u} {β : Type v} (p : Prod α β) : β :=
  match p with
  | Prod.mk a b => b
end Hidden

関数 fst はペア p を受け取る。matchp をペア Prod.mk a b として解釈する。2章 Dependent Type Theory (依存型理論)において、これらの定義に出来るだけ強い普遍性を与えるには、型 αβ がどの型宇宙に属していてもよいように定義を書く必要がある、と学んだことを思い出してほしい。

以下は match の代わりに再帰子 Prod.casesOn を使った例である。

def prod_example (p : Bool × Nat) : Nat :=
  Prod.casesOn (motive := fun _ => Nat) p (fun b n => cond b (2 * n) (2 * n + 1))

#eval prod_example (true, 3)
#eval prod_example (false, 3)

引数 motive は構築したい項の型を指定するために使われる。そして、構築したい項の型は入力データ(この場合はペア)に依存して変わりうるので、motive は関数である。cond 関数はブール値を用いた条件分岐を実現する: cond b t1 t2b が真なら t1 を返し、そうでなければ t2 を返す。上記の例で、関数 prod_example はブール値 b と自然数 n を取り、b が真なら 2 * n を返し、b が偽なら 2 * n + 1 を返す。

対照的に、直和型は2つのコンストラクタ inlinr ("insert left"と"insert right"の略) を持ち、それぞれは1つの(明示的な)引数を取る。Sum α β 型の項を引数にとる関数を定義するには、2つの場合に対応しなければならない: 入力が inl a の形なら a を使って出力する値を指定する必要がある。入力が inr b の形なら b を使って出力する値を指定する必要がある。

def sum_example (s : Sum Nat Nat) : Nat :=
  Sum.casesOn (motive := fun _ => Nat) s
    (fun n => 2 * n)
    (fun n => 2 * n + 1)

#eval sum_example (Sum.inl 3)
#eval sum_example (Sum.inr 3)

def sum_example2 (s : Sum Nat Nat) : Nat :=
  match s with
  | Sum.inl a => 2 * a
  | Sum.inr b => 2 * b + 1

#eval sum_example2 (Sum.inl 3)
#eval sum_example2 (Sum.inr 3)

この例は1つ前の例に似ている。しかし、この例では sum_example への入力は暗黙的に inl ninr n のいずれかの形をとる。1つ目の場合なら関数は 2 * n を返し、2つ目の場合なら関数は 2 * n + 1 を返す。

直積型は引数 α β : Type を持つことに注意してほしい。これらは Prod の引数であると同時に、Prod のコンストラクタの引数の型でもある。引数 α β がコンストラクタへの後続の引数や戻り値の型から推論できる場合、Leanは引数 α β が何であるかを特定し、それらを暗黙の引数にする。

Defining the Natural Numbers (自然数を定義する)の節では、帰納型のコンストラクタがその帰納型自体の項を引数として取るときに何が起こるかを説明する。その節で考える例の特徴は、各コンストラクタが当該型の項として既に特定された項のみを引数として取ることである。

複数のコンストラクタを持つ型は「選言的」であることに注意してほしい: Sum α β の項は inl a または inr b の形をしている。複数の引数を取るコンストラクタは「連言的」な情報を提供する: Prod α β の項 Prod.mk a b からは a b を抽出することができる。任意の帰納型は、任意個のコンストラクタを持つことができ、各コンストラクタは任意個の引数を取ることができるので、選言的かつ連言的な特徴を持つ可能性がある。

関数定義と同様に、Leanの帰納型定義構文内では、コンストラクタ名とコロンの間に名前付き引数を与えることができる:

namespace Hidden
inductive Prod (α : Type u) (β : Type v) where
  | mk (fst : α) (snd : β) : Prod α β

inductive Sum (α : Type u) (β : Type v) where
  | inl (a : α) : Sum α β
  | inr (b : β) : Sum α β
end Hidden

これらの定義は上で与えた定義と本質的に同じである。

Prod のように、ただ1つのコンストラクタを持つ型は純粋に連言的である: そのコンストラクタは単に引数のリストを1つのデータにまとめる。このようなデータは後続の要素の型が最初の要素の型に依存することができるタプルと本質的に同じである。このような型はrecord(レコード)あるいはstructure(構造体)と呼ばれる。Leanでは、キーワード structure を使うと、レコードや構造体の定義とその射影の定義を同時に行うことができる。

namespace Hidden
structure Prod (α : Type u) (β : Type v) where
  mk :: (fst : α) (snd : β)
end Hidden

この例では、帰納型 Prod、そのコンストラクタ mk、通常のエリミネータ(recrecOn)と射影 fstsnd (上で定義したものと同じ)を同時に定義している。

コンストラクタに名前を付けなかった場合、デフォルトのコンストラクタの名前として mk が使われる。次の例は、RGB値の3つ組として色を保存するレコードを定義する:

structure Color where
  (red : Nat) (green : Nat) (blue : Nat)
  deriving Repr

def yellow := Color.mk 255 255 0

#print Color.red  -- ``structure`` キーワードにより生成された射影関数
#eval Color.red yellow

yellow の定義は3つの値を持ったレコードを形成する。そして射影 Color.red は赤の要素を返す。

各フィールドの間に改行を挟めば、括弧の使用を避けることができる。

structure Color where
  red : Nat
  green : Nat
  blue : Nat
  deriving Repr

structure コマンドは代数的構造を定義するときに特に有用である。Leanは代数的構造を扱うための実質的なインフラを提供している。例えば、以下は半群の定義である:

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc : ∀ a b c, mul (mul a b) c = mul a (mul b c)

9章 Structures and Recordsではさらに例を見ていく。

依存直積型 Sigma は既に紹介した:

namespace Hidden
inductive Sigma {α : Type u} (β : α → Type v) where
  | mk : (a : α) → β a → Sigma β
end Hidden

次はライブラリ内にあるさらに2つの帰納型の例である:

namespace Hidden
inductive Option (α : Type u) where
  | none : Option α
  | some : α → Option α

inductive Inhabited (α : Type u) where
  | mk : α → Inhabited α
end Hidden

依存型理論の意味論では、全ての関数は全域関数である。関数型 α → β または依存関数型 (a : α) → β の各項は、全ての入力に対して一意の値を持つと想定される。したがって、依存型理論の意味論に部分関数の概念は組み込まれていない。しかし、Option 型を使うと(全域関数を使って)部分関数を表現することができる。Option β の項は、nonesome b(ここで、b : β) の形をとる。したがって、α → Option β 型の項 f は、α から β への部分関数であると考えることができる: 任意の a : α に対して、f anonesome b を返す。nonef a が「未定義」であることを表す。

Inhabited α の項は、単に α の項が存在することの証人となる。後ほど、Leanにおいて Inhabitedtype class(型クラス)の一例であることを説明する: Leanに適切な基底型がinhabited(有項)であると指示することができる。Leanはその指示に基づいて、他の構築された型が有項であることを自動的に推論することができる。

練習として、α から β への部分関数と β から γ への部分関数の合成の概念を定義し、それが期待通りの振る舞いをすることを示すことを勧める。また、BoolNat が有項であること、2つの有項な型の直積型が有項であること、有項な型への関数の型は有項であることを示すことも勧める。

Inductively Defined Propositions (帰納的に定義された命題)

帰納的に定義された型は、一番下の型である Prop を含め、どの階層の型宇宙にも住むことができる。実際、論理的結合子は次のように定義されている。

namespace Hidden
inductive False : Prop

inductive True : Prop where
  | intro : True

inductive And (a b : Prop) : Prop where
  | intro : a → b → And a b

inductive Or (a b : Prop) : Prop where
  | inl : a → Or a b
  | inr : b → Or a b
end Hidden

これらの定義が、すでに見た論理的結合子の導入則と除去則をどのように生み出すのかを考える必要がある。帰納型のエリミネータがその型を除去して何の型を作れるかを規定するルールがある。つまり、どのような型を再帰子の対象とすることができるかを規定するルールがある。大雑把に言えば、Prop に属する帰納型には、その型を除去することで Prop に属する他の型しか作ることができないというルールがある。これは、「もし p : Propなら、hp : p は何のデータも持たない」という理解と矛盾しない。しかしながら、Inductive Families (帰納族)の節で説明するように、このルールには小さな例外がある。

存在量化子でさえ帰納的に定義されている:

namespace Hidden
inductive Exists {α : Sort u} (p : α → Prop) : Prop where
  | intro (w : α) (h : p w) : Exists p
end Hidden

記法 ∃ x : α, pExists (fun x : α => p) の糖衣構文であることを思い出してほしい。

FalseTrueAndOr の定義は、EmptyUnitProdSum の定義と完全に類似している。違いは、最初のグループは Prop の項を生成し、2番目のグループはある宇宙パラメータ u に対して Type u の項を生成することである。同様に、∃ x : α, pΣ x : α, p は類似しているが、前者は Prop の項で、後者は Type u の項である。

もうひとつの帰納型 {x : α // p} に触れる良い機会だろう。これは、∃ x : α, PΣ x : α, P のハイブリッドのようなものである。

namespace Hidden
inductive Subtype {α : Type u} (p : α → Prop) where
  | mk : (x : α) → p x → Subtype p
end Hidden

実際には、Leanでは Subtypestructure コマンドを使って定義されている:

namespace Hidden
structure Subtype {α : Sort u} (p : α → Prop) where
  val : α
  property : p val
end Hidden

記法 {x : α // p x}Subtype (fun x : α => p x) の糖衣構文である。これは集合論における部分集合の内包的表記をモデルにしている: {x : α // p x} は性質 p を持つ α の要素全体からなる集合を表す。

Defining the Natural Numbers (自然数を定義する)

これまで見てきた帰納型は「フラット」である: コンストラクタはデータ(引数)を包んである型の項を作り、対応する再帰子はその項を展開してそれに作用する。コンストラクタがそのコンストラクタたちによってまさに今帰納的に定義されている型の項を受け取ると、物事はもっと面白くなる。典型的な例が自然数の型 Nat である:

namespace Hidden
inductive Nat where
  | zero : Nat
  | succ : Nat → Nat
end Hidden

Nat のコンストラクタは2つある。まずは zero : Nat から始める。コンストラクタ zero は引数を取らないので、Nat は最初から項 zero を持っている。対照的に、コンストラクタ succ は既に構築された Nat の項に対してのみ適用できる。これを zero に適用すると、succ zero : Nat が得られる。もう一度適用すると succ (succ zero) : Nat が得られる。直感的には、Nat はこのようなコンストラクタを持つ「最小の」型であるといえる。このようなコンストラクタを持つことは、Nat の項は、 zero から始めて succ を繰り返し適用することで網羅的に(そして自由に)生成されることを意味する。

以前と同様に、Nat のための再帰子は、Nat から任意の型への依存関数 f、つまりある motive : Nat → Sort u について (n : Nat) → motive n の項 f を定義するように設計されている。再帰子は、入力が zero の場合と、入力がある n : Nat について succ n の形をとる場合の2種類の場合に対応しなければならない。最初のケースでは、先ほどと同じように、適切な型を持つ出力値を指定するだけでよい。2番目のケースはそうはいかない。しかし、2番目のケースでは、再帰子は n における f の値がすでに計算されていると想定することができる。その結果、再帰子の次の引数は nf n を用いて f (succ n) の値を指定する。再帰子 Nat.rec の型をチェックしてみると、

namespace Hidden
inductive Nat where
 | zero : Nat
 | succ : Nat → Nat
#check @Nat.rec
end Hidden

次のような表示が得られる:

  {motive : Nat → Sort u}
  → motive Nat.zero
  → ((n : Nat) → motive n → motive (Nat.succ n))
  → (t : Nat) → motive t

暗黙の引数 motive は、定義される関数のコドメインである。型理論では、motive は除去/再帰のmotive(動機)であると言うのが一般的である。それは motive t が入力 t に対して構築したい項の型を表しているからである。motive の次の2つの引数は、上述した2つの場合、つまり入力が zero の場合と succ n の形をとる場合の計算方法をそれぞれ指定する。この2つの引数はminor premises(小前提)としても知られている。最後に、t : Nat は定義したい関数への入力である。入力 tmajor premise(大前提)としても知られている。

Nat.recOnNat.rec とほとんど同じだが、大前提が小前提の前に現れている。

@Nat.recOn :
  {motive : Nat → Sort u}
  → (t : Nat)
  → motive Nat.zero
  → ((n : Nat) → motive n → motive (Nat.succ n))
  → motive t

例えば、自然数に対する加法関数 add m n を定義することを考えてみよう。m を固定すると、n に関する再帰によって加法を定義することができる。base caseでは、add m zerom と定義する。successor stepでは、add m n の値が既に決定されていると仮定して、add m (succ n)succ (add m n) と定義する。

namespace Hidden
inductive Nat where
  | zero : Nat
  | succ : Nat → Nat
  deriving Repr

def add (m n : Nat) : Nat :=
  match n with
  | Nat.zero   => m
  | Nat.succ n => Nat.succ (add m n)

open Nat

#eval add (succ (succ zero)) (succ zero)
end Hidden

このような関数定義は名前空間 Nat に入れておくと便利である。それから、その名前空間の中でお馴染みの記法 + を定義することができる。ここまですると、加法に関する2つの等式が成立することが定義により示せる:

namespace Hidden
inductive Nat where
 | zero : Nat
 | succ : Nat → Nat
 deriving Repr
namespace Nat

def add (m n : Nat) : Nat :=
  match n with
  | Nat.zero   => m
  | Nat.succ n => Nat.succ (add m n)

instance : Add Nat where
  add := add

theorem add_zero (m : Nat) : m + zero = m := rfl
theorem add_succ (m n : Nat) : m + succ n = succ (m + n) := rfl

end Nat
end Hidden

instance コマンドがどのように機能するかは、10章 Type Classes (型クラス)で説明する。以下の例では、Leanの標準ライブラリで定義された自然数を使う。

上記では rfl を使うだけで定理が証明できた。しかし、zero + m = m のような定理は帰納法による証明を必要とする。上述の通り、帰納法の原理は再帰の原理の特殊な場合にすぎない。帰納法において、motive n のコドメインは一般的な型 Sort u ではなく Prop である。これは帰納法による証明のお馴染みのパターンを表現している: ∀ n, motive n を証明するために、まず motive 0 を証明する。次に任意の n : Nat について ih : motive n を仮定し、その上で motive (succ n) を証明する。

namespace Hidden
open Nat

theorem zero_add (n : Nat) : 0 + n = n :=
  Nat.recOn (motive := fun x => 0 + x = x)
   n
   (show 0 + 0 = 0 from rfl)
   (fun (n : Nat) (ih : 0 + n = n) =>
    show 0 + succ n = succ n from
    calc 0 + succ n
      _ = succ (0 + n) := rfl
      _ = succ n       := by rw [ih])
end Hidden

繰り返しになるが、証明内で Nat.recOn を使うことは、証明内で帰納法の原理を使うことを意味することに注意してほしい。このような証明において、rewrite タクティクと simp タクティクは非常に効果的である。今回の場合、simp タクティクを使うと次のように証明を短くすることができる:

namespace Hidden
open Nat

theorem zero_add (n : Nat) : 0 + n = n :=
  Nat.recOn (motive := fun x => 0 + x = x) n
    rfl
    (fun n ih => by simp [add_succ, ih])
end Hidden

もう一つの例として、加法の結合性 ∀ m n k, m + n + k = m + (n + k) を証明しよう。ここで、+ という記法は左結合的なので、m + n + k とは (m + n) + k のことである。一番難しいのは、どの変数に関して帰納法を行うかの見当をつけることだ。加法は2番目の引数に関する再帰によって定義されるため、k を選ぶのはよい判断である。一度変数を選択すれば、証明はほとんど自ずと導かれる:

namespace Hidden
open Nat
theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) :=
  Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k
    (show m + n + 0 = m + (n + 0) from rfl)
    (fun k (ih : m + n + k = m + (n + k)) =>
      show m + n + succ k = m + (n + succ k) from
      calc m + n + succ k
        _ = succ (m + n + k)   := rfl
        _ = succ (m + (n + k)) := by rw [ih]
        _ = m + succ (n + k)   := rfl
        _ = m + (n + succ k)   := rfl)
end Hidden

再び、simp タクティクを使って証明を短くすることができる:

open Nat
theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) :=
  Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k
    rfl
    (fun k ih => by simp [Nat.add_succ, ih])

次は加法の可換性を証明してみよう。2番目の引数に関して帰納法を行うことを選択すると、次のように証明を始めることができる:

open Nat
theorem add_comm (m n : Nat) : m + n = n + m :=
  Nat.recOn (motive := fun x => m + x = x + m) n
   (show m + 0 = 0 + m by rw [Nat.zero_add, Nat.add_zero])
   (fun (n : Nat) (ih : m + n = n + m) =>
    show m + succ n = succ n + m from
    calc m + succ n
      _ = succ (m + n) := rfl
      _ = succ (n + m) := by rw [ih]
      _ = succ n + m   := sorry)

ここで、もう1つの補題 succ n + m = succ (n + m) が必要であるとわかる。これは m に関する帰納法で証明できる:

open Nat

theorem succ_add (n m : Nat) : succ n + m = succ (n + m) :=
  Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m
    (show succ n + 0 = succ (n + 0) from rfl)
    (fun (m : Nat) (ih : succ n + m = succ (n + m)) =>
     show succ n + succ m = succ (n + succ m) from
     calc succ n + succ m
       _ = succ (succ n + m)   := rfl
       _ = succ (succ (n + m)) := by rw [ih]
       _ = succ (n + succ m)   := rfl)

それから、上記の証明の sorrysucc_add に置き換えて、証明を完結させることができる。再び、これらの証明は短くできる:

namespace Hidden
open Nat
theorem succ_add (n m : Nat) : succ n + m = succ (n + m) :=
  Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m
    rfl
    (fun m ih => by simp only [add_succ, ih])

theorem add_comm (m n : Nat) : m + n = n + m :=
  Nat.recOn (motive := fun x => m + x = x + m) n
    (by simp)
    (fun m ih => by simp [add_succ, succ_add, ih])
end Hidden

Other Recursive Data Types (他の再帰データ型)

帰納的に定義された型の例をもう少し考えてみよう。任意の型 α に対して、いくつかの α の項からなるリストの型 List α がライブラリで定義されている。

namespace Hidden
inductive List (α : Type u) where
  | nil  : List α
  | cons : α → List α → List α

namespace List

def append (as bs : List α) : List α :=
  match as with
  | nil       => bs
  | cons a as => cons a (append as bs)

theorem nil_append (as : List α) : append nil as = as :=
  rfl

theorem cons_append (a : α) (as bs : List α)
                    : append (cons a as) bs = cons a (append as bs) :=
  rfl

end List
end Hidden

α 型の項を要素に持つリストは、空リスト nilcons h t の形をとるかのいずれかである。ここで、cons h t は項 h : α の後にリスト t : List α が続くリストである。最初の要素 h は一般にリストの"head"と呼ばれ、残りの要素 t は"tail"と呼ばれる。

練習問題として、以下の定理を証明せよ:

namespace Hidden
inductive List (α : Type u) where
| nil  : List α
| cons : α → List α → List α
namespace List
def append (as bs : List α) : List α :=
 match as with
 | nil       => bs
 | cons a as => cons a (append as bs)
theorem nil_append (as : List α) : append nil as = as :=
 rfl
theorem cons_append (a : α) (as bs : List α)
                    : append (cons a as) bs = cons a (append as bs) :=
 rfl
theorem append_nil (as : List α) : append as nil = as :=
  sorry

theorem append_assoc (as bs cs : List α)
        : append (append as bs) cs = append as (append bs cs) :=
  sorry
end List
end Hidden

また、リストの長さを返す関数 length : {α : Type u} → List α → Nat を定義せよ。さらにそれが期待通りに振る舞うことを証明せよ。例えば、length (append as bs) = length as + length bs を証明せよ。

別の例として、binary trees((全)二分木)の型を定義することができる:

/- 1. ただ1つの頂点からなる有向グラフは全二分木である。
   2. 次数0の頂点vと2つの全二分木A,Bを用意し、
   ラベル付き有向辺(左,v,Aの根)と(右,v,Bの根)を追加し、vを根としたものは全二分木である。
   3. 以上の手続きで全二分木だとわかるものだけが全二分木である。 -/
inductive BinaryTree where
  | leaf : BinaryTree
  | root (left : BinaryTree) (right : BinaryTree) : BinaryTree

countably branching trees((全)可算無限分木)の型を定義することさえできる:

/- 1. ただ1つの頂点からなる有向グラフは全可算無限分木である。
   2. 次数0の頂点vと2つの全可算無限分木T_0,T_1,...を用意し、
   ラベル付き有向辺(0,v,T_0の根),(1,v,T_1の根),...を追加し、vを根としたものは全可算無限分木である。
   3. 以上の手続きで全可算無限分木だとわかるものだけが全可算無限分木である。 -/
inductive CBTree where
  | leaf : CBTree
  | sup : (Nat → CBTree) → CBTree

namespace CBTree

def succ (t : CBTree) : CBTree :=
  sup (fun _ => t)

def toCBTree : Nat → CBTree
  | 0 => leaf
  | n+1 => succ (toCBTree n)

def omega : CBTree :=
  sup toCBTree

end CBTree

Tactics for Inductive Types (帰納型のためのタクティク)

Leanにおける帰納型の基礎的な重要性を鑑みれば、帰納型を効率的に扱うためにデザインされたタクティクが数多くあることは驚くことではない。ここではそのようなタクティクのいくつかを紹介する。

cases タクティクは帰納的に定義された型の項に対して機能し、その名前が示す通りの働きをする: cases タクティクは存在する各コンストラクタに従って項を分解する。最も基本的な形では、cases タクティクは現在のコンテキスト内の項 x に適用される。そして、x を各コンストラクタで置換して、ゴールを場合分けする。

example (p : Nat → Prop) (hz : p 0) (hs : ∀ n, p (Nat.succ n)) : ∀ n, p n := by
  intro n
  cases n
  . exact hz  -- goal is p 0
  . apply hs  -- goal is a : Nat ⊢ p (succ a)

さらに便利な機能がある。まず、with キーワードを使うと、cases タクティク使用時の各場合分けにおいて変数に名前を付けることができる。次の例では、succ の引数に m という名前を付け、2番目の場合で nsucc m で置換するように指示している。さらに重要なのは、cases タクティクが、現在のコンテキスト内にある、場合分け対象の変数に依存する型を持つ項を検出することである。cases タクティクはこれらの項をゴールのターゲットに戻し、ゴールを場合分けし、再び項をゴールのコンテキストに導入する。次の例では、仮説 h : n ≠ 0 が最初の分岐では h : 0 ≠ 0 になり、2番目の分岐では h : succ m ≠ 0 になることに注目してほしい。

open Nat

example (n : Nat) (h : n ≠ 0) : succ (pred n) = n := by
  cases n with
  | zero =>
    -- goal: h : 0 ≠ 0 ⊢ succ (pred 0) = 0
    apply absurd rfl h
  | succ m =>
    -- second goal: h : succ m ≠ 0 ⊢ succ (pred (succ m)) = succ m
    rfl

cases は命題を証明するだけでなく、データを生成するためにも使用できることに注目してほしい。

def f (n : Nat) : Nat := by
  cases n; exact 3; exact 7

example : f 0 = 3 := rfl
example : f 5 = 7 := rfl

繰り返しになるが、cases x はコンテキスト内にある x に依存する型を持つ項をターゲットに戻し、ゴールを場合分けし、再び項をコンテキストに導入する。

def Tuple (α : Type) (n : Nat) :=
  { as : List α // as.length = n }

def f {n : Nat} (t : Tuple α n) : Nat := by
  cases n; exact 3; exact 7

def myTuple : Tuple Nat 3 :=
  ⟨[0, 1, 2], rfl⟩

example : f myTuple = 7 :=
  rfl

次は引数をとる複数のコンストラクタを持つ帰納型における例である。

inductive Foo where
  | bar1 : Nat → Nat → Foo
  | bar2 : Nat → Nat → Nat → Foo

def silly (x : Foo) : Nat := by
  cases x with
  | bar1 a b => exact b
  | bar2 c d e => exact e

#eval silly (Foo.bar1 1 2)    -- 2
#eval silly (Foo.bar2 3 4 5)  -- 5

各コンストラクタによって生成された各ゴールは、帰納型の定義におけるコンストラクタの宣言順に解く必要はない。

inductive Foo where
  | bar1 : Nat → Nat → Foo
  | bar2 : Nat → Nat → Nat → Foo

def silly (x : Foo) : Nat := by
  cases x with
  | bar2 c d e => exact e
  | bar1 a b => exact b

with の構文は構造化された証明を書くのに便利である。また、Leanは cases を補完する case タクティクを提供する。これを使うと、各ケースで変数名を割り当てることに集中することができる。

inductive Foo where
  | bar1 : Nat → Nat → Foo
  | bar2 : Nat → Nat → Nat → Foo
def silly (x : Foo) : Nat := by
  cases x
  case bar1 a b => exact b
  case bar2 c d e => exact e

case タクティクは賢く、各コンストラクタを適切なゴールにマッチさせることができる。例えば、上記のゴールを逆順で埋めることができる:

inductive Foo where
  | bar1 : Nat → Nat → Foo
  | bar2 : Nat → Nat → Nat → Foo
def silly (x : Foo) : Nat := by
  cases x
  case bar2 c d e => exact e
  case bar1 a b => exact b

さらに、任意の式 e に対して cases e を使うこともできる。もしその式がゴールのターゲットに現れるなら、cases タクティクはその式を一般化し、その結果生じる全称量化された変数を導入し、その上でゴールを場合分けする。

open Nat

example (p : Nat → Prop) (hz : p 0) (hs : ∀ n, p (succ n)) (m k : Nat)
        : p (m + 3 * k) := by
  cases m + 3 * k
  . exact hz   -- goal is p 0
  . apply hs   -- goal is a : Nat ⊢ p (succ a)

これは、「(m + 3 * k は自然数なので、)m + 3 * k が0か、ある数の後者かで場合分けする」と言っているのだと考えてほしい。この結果は機能的には次の例と等価である:

open Nat

example (p : Nat → Prop) (hz : p 0) (hs : ∀ n, p (succ n)) (m k : Nat)
        : p (m + 3 * k) := by
  generalize m + 3 * k = n
  cases n
  . exact hz   -- goal is p 0
  . apply hs   -- goal is a : Nat ⊢ p (succ a)

m + 3 * kgeneralize によって消去されることに注意してほしい; 重要なのは、それが 0succ a のどちらの形式を持つかだけである。 この使い方の場合、cases は等式(この場合は m + 3 * k = n)中の項について言及している仮説をゴールのターゲットに戻さない。そのような仮説がコンテキストの中にあり、それについても一般化したい場合は、明示的にそれを revert で戻す必要がある。

場合分けの対象となる式がゴールのターゲット内にない場合、cases タクティクは have を使い、分解後の式を型として持つ項をコンテキストに導入する。以下はその例である:

example (p : Prop) (m n : Nat)
        (h₁ : m < n → p) (h₂ : m ≥ n → p) : p := by
  cases Nat.lt_or_ge m n
  case inl hlt => exact h₁ hlt
  case inr hge => exact h₂ hge

Nat.lt_or_ge m n とは m < n ∨ m ≥ n の証明項であり、上の証明はこの2つの場合に分かれると考えるのが自然である。最初の分岐では仮説 hlt : m < n を持つ。2番目の分岐では仮説 hge : m ≥ n を持つ。上の証明は、機能的には次と等価である:

example (p : Prop) (m n : Nat)
        (h₁ : m < n → p) (h₂ : m ≥ n → p) : p := by
  have h : m < n ∨ m ≥ n := Nat.lt_or_ge m n
  cases h
  case inl hlt => exact h₁ hlt
  case inr hge => exact h₂ hge

have タクティクにより仮説 h : m < n ∨ m ≥ n が得られるので、その仮説に対して cases を適用する。

次の例では、自然数における等式の決定可能性を利用して、m = nm ≠ n の場合に分けて証明する。

#check Nat.sub_self

theorem t1 (m n : Nat) : m - n = 0 ∨ m ≠ n := by
  cases Decidable.em (m = n) with
  | inl heq => rw [heq]; apply Or.inl; exact Nat.sub_self n
  | inr hne => apply Or.inr; exact hne

/- ``Decidable.em`` は排中律 ``Classical.em`` を必要としない -/
#print axioms t1  -- 't1' does not depend on any axioms

open Classical を使えば、任意の命題に対して排中律 em が使えることを思い出してほしい。しかし、型クラス推論(10章 Type Classes (型クラス)を参照)を使えば、Leanは排中律に似て非なる決定手続きを見つけることができる。つまり、計算可能関数において場合分け p ∨ ¬p を使うことができるのである。

cases タクティクが場合分けによる証明を行うのに使えるように、induction タクティクは帰納法による証明を行うのに使える。induction タクティクの構文は cases タクティクの構文と似ているが、前者は引数が現在のコンテキストの項でなければならない点が異なる。以下はその例である:

namespace Hidden
theorem zero_add (n : Nat) : 0 + n = n := by
  induction n with
  | zero => rfl
  | succ n ih => rw [Nat.add_succ, ih]
end Hidden

cases と同様に、induction タクティクでは with キーワードの代わりに case タクティクを使うことができる。

namespace Hidden
theorem zero_add (n : Nat) : 0 + n = n := by
  induction n
  case zero => rfl
  case succ n ih => rw [Nat.add_succ, ih]
end Hidden

以下に例を追加する:

namespace Hidden
theorem add_zero (n : Nat) : n + 0 = n := Nat.add_zero n
open Nat

theorem zero_add (n : Nat) : 0 + n = n := by
  induction n <;> simp [*, add_zero, add_succ]

theorem succ_add (m n : Nat) : succ m + n = succ (m + n) := by
  induction n <;> simp [*, add_zero, add_succ]

theorem add_comm (m n : Nat) : m + n = n + m := by
  induction n <;> simp [*, add_zero, add_succ, succ_add, zero_add]

theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) := by
  induction k <;> simp [*, add_zero, add_succ]
end Hidden

induction タクティクは複数の引数(大前提)をとるユーザー定義の帰納法原理もサポートしている。

/-
theorem Nat.mod.inductionOn
      {motive : Nat → Nat → Sort u}
      (x y  : Nat)
      (ind  : ∀ x y, 0 < y ∧ y ≤ x → motive (x - y) y → motive x y)
      (base : ∀ x y, ¬(0 < y ∧ y ≤ x) → motive x y)
      : motive x y :=
-/

example (x : Nat) {y : Nat} (h : y > 0) : x % y < y := by
  induction x, y using Nat.mod.inductionOn with
  | ind x y h₁ ih =>
    rw [Nat.mod_eq_sub_mod h₁.2]
    exact ih h
  | base x y h₁ =>
    have : ¬ 0 < y ∨ ¬ y ≤ x := Iff.mp (Decidable.not_and_iff_or_not ..) h₁
    match this with
    | Or.inl h₁ => exact absurd h h₁
    | Or.inr h₁ =>
      have hgt : y > x := Nat.gt_of_not_le h₁
      rw [← Nat.mod_eq_of_lt hgt] at hgt
      assumption

タクティク証明の中で match 記法を使うこともできる:

example : p ∨ q → q ∨ p := by
  intro h
  match h with
  | Or.inl _  => apply Or.inr; assumption
  | Or.inr h2 => apply Or.inl; exact h2

便利なことに、パターンマッチングは introfunext のようなタクティクに統合されている。

example : s ∧ q ∧ r → p ∧ r → q ∧ p := by
  intro ⟨_, ⟨hq, _⟩⟩ ⟨hp, _⟩
  exact ⟨hq, hp⟩

example :
    (fun (x : Nat × Nat) (y : Nat × Nat) => x.1 + y.2)
    =
    (fun (x : Nat × Nat) (z : Nat × Nat) => z.2 + x.1) := by
  funext (a, b) (c, d)
  show a + d = d + a
  rw [Nat.add_comm]

最後に、injection タクティクを紹介してセクションを閉じる。このタクティクは帰納型を扱いやすくするためにデザインされている。Leanの設計上、帰納型の項は自由に生成される。つまり、各コンストラクタは単射であり、各コンストラクタの値域は互いに交わりを持たない。injection タクティクはこの事実を利用するようにデザインされている:

open Nat

example (m n k : Nat) (h : succ (succ m) = succ (succ n))
        : n + k = m + k := by
  injection h with h'
  injection h' with h''
  rw [h'']

証明の1行目は仮説 h' : succ m = succ n をコンテキストに追加し、証明の2行目は仮説 h'' : m = n をコンテキストに追加する。

また、injection タクティクは「異なるコンストラクタ(あるいは異なる入力を受けたコンストラクタ)の出力が互いに等しい」としたときに生じる矛盾を検出し、その矛盾を使ってゴールを閉じる。

open Nat

example (m n : Nat) (h : succ m = 0) : n = n + 7 := by
  injection h

example (m n : Nat) (h : succ m = 0) : n = n + 7 := by
  contradiction

example (h : 7 = 4) : False := by
  contradiction

2番目の例は、contradiction タクティクもこの形の矛盾を検出することを示している。

Inductive Families (帰納族)

Leanが受け入れる帰納的定義のほとんど全てを説明し終えた。ここまでの説明で、Leanでは任意個の再帰的コンストラクタを持つ帰納型を導入できることが分かった。実際、今までの知識を応用して、これから説明する方法を使うと、ただ1つの帰納的定義を使って帰納型の添字付きfamily(族)を導入することもできる。

帰納族とは、次のような形の1つの帰納的定義によって同時に定義される、添字を持つ型の族である:

inductive foo : ... → Sort u where
  | constructor₁ : ... → foo ...
  | constructor₂ : ... → foo ...
  ...
  | constructorₙ : ... → foo ...

ある Sort u の項を構築する通常の帰納的定義に対して、より一般的な帰納的定義は関数 ... → Sort u を構築する。ここで、...indices(添字)とも呼ばれる引数の型の列を表す。そして、各コンストラクタは族のいくつかの要素を構築する。その一例が Vector α n の定義である。これは α の項を要素に持つ長さ n のベクトルの型である:

namespace Hidden
inductive Vector (α : Type u) : Nat → Type u where
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
end Hidden

cons コンストラクタは αVector α n の項を取り、Vector α (n+1) の項を返す。これにより、族の1つの要素(型)の項を利用して別の要素(型)の項を構築することができる。

より風変わりな例は、Leanにおける等式型の定義である:

namespace Hidden
inductive Eq {α : Sort u} (a : α) : α → Prop where
  | refl : Eq a a
end Hidden

固定された各 α : Sort u と各 a : α に対して、この定義は x : α を添字とする型の族 Eq a x を構築する。しかし、注目すべきは、族 Eq a x には Eq a a の項を構築するただ1つのコンストラクタ refl しかないことである。直感的には、Eq a x の証明を構築するには、xa である場合に反射律を使うしかないと言い換えることができる。Eq a a は型の族 Eq a x の中で唯一の有項型であることに注意してほしい。

Leanにより生成された等号の除去則は次の通り:

universe u v

#check (@Eq.rec : {α : Sort u} → {a : α} → {motive : (x : α) → a = x → Sort v}
                  → motive a rfl → {b : α} → (h : a = b) → motive b h)

コンストラクタ refl とエリミネータ Eq.rec だけから等式の基本的な公理の全てが導かれるのは驚くべき事実である。ただし、節 Axiomatic Details (公理の詳細) で説明するように、等式の定義は非典型的である。

再帰子 Eq.rec は代入の定義にも使われる:

namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop} (h₁ : Eq a b) (h₂ : p a) : p b :=
  Eq.rec (motive := fun x _ => p x) h₂ h₁
end Hidden

match を使って subst を定義(証明)することもできる。

namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop} (h₁ : Eq a b) (h₂ : p a) : p b :=
  match h₁ with
  | rfl => h₂
end Hidden

実際、Leanは Eq.rec に基づいた定義を用いて match 式をコンパイルする。

namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop} (h₁ : Eq a b) (h₂ : p a) : p b :=
  match h₁ with
  | rfl => h₂

set_option pp.all true
#print subst
  -- ... subst.match_1 ...
#print subst.match_1
  -- ... Eq.casesOn ...
#print Eq.casesOn
  -- ... Eq.rec ...
end Hidden

h₁ : a = b に対して再帰子 Eq.rec または match を使うと、ab が同じだと仮定することができる。その下で、p bp a は同じである。

Eq が対称的かつ推移的であることを証明するのは難しくない。以下の例では、等号の対称性 symm を示す。推移性 transcongruence(合同性) congr の証明は練習問題として残す。

namespace Hidden
theorem symm {α : Type u} {a b : α} (h : Eq a b) : Eq b a :=
  match h with
  | rfl => rfl

theorem trans {α : Type u} {a b c : α} (h₁ : Eq a b) (h₂ : Eq b c) : Eq a c :=
  sorry

theorem congr {α β : Type u} {a b : α} (f : α → β) (h : Eq a b) : Eq (f a) (f b) :=
  sorry
end Hidden

型理論の研究においては、帰納的定義の更なる一般化が存在する。例えば、induction-recursioninduction-induction の原理がある。これらはLeanではサポートされていない。

Axiomatic Details (公理の詳細)

これまで、例を通して帰納型とその構文について説明してきた。この節では、公理的な基礎に興味のある人のために、追加の情報を提供する。

帰納型のコンストラクタは、parameters(パラメータ, 帰納的な構築の間、固定されたままの引数)とindices(添字, 同時進行で構築される型の族をパラメータ化する引数)を取りうることを見てきた。各コンストラクタは、型を持つ必要がある。ここで、コンストラクタの引数の型は既に定義された型、パラメータと添字の型、現在定義中の帰納族の要素だけから構成される必要がある。コンストラクタの引数の型に現在定義中の帰納族の要素が現れる場合、それはstrictly positivelyに出現しなければならないという要件がある。これは単純に、任意のコンストラクタの任意の引数の型は、定義中の帰納型が結果としてのみ出現する依存関数型でなければならないことを意味する。ここで、添字は定数と前の引数を使って与えられる。

帰納型はある u が存在して Sort u の項なので、当該帰納型を項としてみたとき、その項が持つ型宇宙としてどの宇宙レベル u がふさわしいかを問うのは合理的である。帰納族 C の定義内の各コンストラクタ c は以下の形式をとる。

  c : (a : α) → (b : β[a]) → C a p[a,b]

ここで、a はデータ型パラメータの列、b はコンストラクタへの引数の列、p[a, b] は添字である。この添字 p[a, b] によって、そのコンストラクタが帰納族のどの要素の項を構築するかが決まる。(この説明はやや不正確である。実際には、コンストラクタへの引数は、依存関係が意味をなす限りどのような順番で現れてもよい。)C の宇宙レベルは、その帰納型が Prop(つまり、Sort 0)に属するように指定されているかどうかによって、2種類の制約を持つ。

まず、帰納型が Prop に属するように指定されていない場合を考えよう。このとき、宇宙レベル u は次を満たすように制約される:

上記の各コンストラクタ c と、列 β[a] 内の各 βk[a] に対して、もし βk[a] : Sort v なら、uv である。

言い換えれば、当該帰納型の宇宙レベル u は、各コンストラクタの各引数の型の宇宙レベル以上であることが要求される。

帰納型が Prop に属するように指定されている場合、コンストラクタの引数の型の宇宙レベルには制約がない。しかし、コンストラクタの引数の型の宇宙レベルは除去則に影響を与える。一般的に、Prop に属する帰納型の場合、除去則のmotive(動機)の型は Prop に属することが要求される。

この最後のルールには例外がある: コンストラクタが1つしかなく、コンストラクタの各引数が Prop の項あるいは添字である場合、除去則によって帰納的に定義された Prop を除去して任意の Sort の項を作ることが許される。この場合、直感的には、エリミネータは引数の型が有項であるという事実なしに引数の情報を利用することはない、と言うことができる。この特別なケースはsingleton elimination(シングルトン除去)として知られている。

帰納的に定義された等式型のエリミネータ Eq.rec の実用例で、シングルトン除去が活躍するのをすでに見てきた。p ap bProp に限らない任意の型を持つ場合でも、項 h : Eq a b を使って項 t' : p ap b にキャストすることができる。なぜなら、このキャストは新しいデータを生成せず、すでに持っているデータを再解釈するだけだからだ。シングルトン除去はheterogeneous equality(異型等式)やwell-founded recursion(整礎再帰)でも使われるが、これについては次章のWell-Founded Recursion and Induction (整礎再帰と整礎帰納法)の節で説明する。

Mutual and Nested Inductive Types (相互帰納型と入れ子帰納型)

ここで、帰納型を一般化して得られるしばしば便利な2つの型の概念Mutual Inductive Types(相互帰納型)とNested Inductive Types(入れ子帰納型)について考えよう。Leanは、これらを上で説明したようなよりプリミティブな種類の帰納型に「コンパイル」することでこの2つの概念をサポートする。言い換えると、Leanはより一般的な定義を構文解析し、それに基づいて補助的な帰納型を定義し、その補助的な型を使って本当に必要なものを定義する。これらの型を効果的に利用するためには、次の章で説明するLeanの等式コンパイラが必要である。それでも、これらの型の宣言は通常の帰納的定義の簡単な変化形であるため、ここで説明することに意味がある。

まず、Leanはmutually defined(相互に定義された)帰納型をサポートする。これは、同時に定義される、それぞれが定義中の他の型を参照する帰納型たちである。

mutual
  inductive Even : Nat → Prop where
    | even_zero : Even 0
    | even_succ : (n : Nat) → Odd n → Even (n + 1)

  inductive Odd : Nat → Prop where
    | odd_succ : (n : Nat) → Even n → Odd (n + 1)
end

open Even Odd

example : Even 2 := even_succ 1 (odd_succ 0 even_zero)

この例では、2つの帰納型が同時に定義されている: 自然数 n は、0 であるか奇数 Odd より1大きいときは偶数 Even であり、偶数 Even より1大きいときは奇数 Odd である。以下の練習問題では、その詳細を記述せよ。

相互帰納型は、α の項でラベル付けされた頂点を持つ有限木を定義するのにも使える:

mutual
    inductive Tree (α : Type u) where
      | node : α → TreeList α → Tree α

    inductive TreeList (α : Type u) where
      | nil  : TreeList α
      | cons : Tree α → TreeList α → TreeList α
end

この定義により、α の項 a と、空であってもよい部分木のリストを与えることで、a を根とする Tree α の項を構築することができる。部分木のリストは型 TreeList α の項として表現され、これは空リスト nil か、Tree α の項と TreeList α の項の cons のいずれかであると定義される。

しかしながら、この TreeTreeList の定義は扱いにくい。特にLeanのライブラリにはリストを扱うための関数や定理が数多く含まれているため、部分木のリストが型 List (Tree α) の項として与えられたら、これらの定義はもっと扱いやすくなるだろう。型 TreeList αList (Tree α) と「同型」であることを示すことはできる。しかし、この同型に沿って片方で得られた結果を他方へ翻訳するのは面倒である。

実際、Leanでは本当に必要としている帰納型を定義することができる:

inductive Tree (α : Type u) where
  | mk : α → List (Tree α) → Tree α

これはnested inductive type(入れ子帰納型)として知られている。この Tree の定義は前節で示した帰納型の厳密な仕様から外れている。なぜなら、mk の引数の型の中で、Tree はstrictly positivelyに現れず、List 型コンストラクタの中に入れ子になっているからである。Leanはカーネルの中で TreeList αList (Tree α) の間に同型を構築し、その同型の観点からこの入れ子帰納型 Tree のコンストラクタを定義する。

Exercises (練習問題)

  1. 自然数に対する他の演算、例えば乗法、前者関数(pred 0 = 0 とする)、切り捨て減法(mn以上のとき n - m = 0)、べき乗などを定義してみよ。次に、既に証明した定理を基に、それらの基本的な性質をいくつか証明してみよ。

    それらの多くはLeanのコアライブラリで既に定義されている。名前衝突を避けるため、Hidden のような名前の名前空間の中で作業することを勧める。

  2. リストに関する length 関数や reverse 関数のような操作をいくつか定義せよ。それらについて、次のような性質をいくつか証明せよ。

    a. length (s ++ t) = length s + length t

    b. length (reverse t) = length t

    c. reverse (reverse t) = t

  3. 以下のコンストラクタから構築される項からなる帰納データ型を定義せよ:

    • const n, 自然数 n を表す定数
    • var n, n 番目の変数
    • plus s t, st の和を表す
    • times s t, st の積を表す

    今定義した型の項を評価する関数を再帰的に定義せよ。ただし、変数には値を割り当てることができるとする。

  4. 同様に、命題論理式の型と、その型に関する関数を定義せよ: 例えば、評価関数、式の複雑さを測る関数、与えられた変数に別の式を代入する関数などを定義せよ。

Induction and Recursion (帰納と再帰)

前章では、帰納的定義がLeanに新しい型を導入する強力な手段となることを説明した。さらに言えば、コンストラクタと再帰子(エリミネータ)は、帰納型から他の型への関数を定義する唯一の手段である。型としての命題対応により、この事実は帰納法が証明の基本的な方法であることを意味する。

Leanは再帰関数の定義、パターンマッチングの実行、帰納的証明の記述に対して自然な方法を提供する。関数を定義するには、その関数が満たすべき等式を指定する。定理を証明するには、起こりうる全てのケースをどのように扱うかを指定する。裏では、これらの記述はequation compiler(等式コンパイラ)と呼ばれるものを用いて、プリミティブな再帰子へと「コンパイル」される。等式コンパイラはtrusted code base(システムの信頼性保証において最も基礎的で重要なコード)の一部ではない。等式コンパイラの出力はカーネルによって独立にチェックされる項で構成される。

用語に関する注意

この節は翻訳に際して追加した節である。

この章では、次のような定義

open Nat
def foo : Nat → Nat → Nat
  | zero  , zero   => 0
  | zero  , succ y => 1
  | succ x, zero   => 2
  | succ x, succ y => 3

があるとき、zerosucc y などを「パターン(pattern)」、| zero , zero などを「ケース(case)」、定義として与えられたケース全てをまとめたものを「ケースリスト(list of cases)」、不足なくケースが与えられたケースリストを用いてパターンマッチングすることを「場合分け(by cases)」あるいは「場合分けする(split on cases)」と呼ぶ。

Pattern Matching (パターンマッチング)

schematic patternsの解釈は、コンパイルの最初のステップである。帰納型のコンストラクタと casesOn 再帰子を使って、関数を定義したり、場合分けによる定理の証明が行えることを見てきた。しかし、複雑な定義は、入れ子になった casesOn 適用をいくつも使うことがあり、そのような記述は読みにくく理解しにくいかもしれない。パターンマッチングはより便利で、関数型プログラミング言語ユーザーに馴染みのあるアプローチを提供する。

帰納的に定義された自然数の型について考える。全ての自然数は zerosucc x のどちらかの形をとるため、それぞれのケースにおいて出力の値を指定することで、自然数から任意の型への関数を定義することができる:

open Nat

/- 等式コンパイラによるパターンマッチングを使った関数定義の構文

   この構文を使って帰納型から任意の型への関数を定義する場合、
   `:=` は不要である(書くとコンパイルエラーになる)ことに注意。
   定義したい項が型Tを持つとき、この構文は型Tの(1つ以上の)前件とパターンマッチングする -/

def sub1 : Nat → Nat
  | zero   => zero
  | succ x => x

def isZero : Nat → Bool
  | zero   => true
  | succ x => false

以上の関数を定義するために使われる等式の集まり(例えば、sub1 zero = zero は上記の関数定義の一部だとみなせる)はdefinitionallyに成立する:

open Nat
def sub1 : Nat → Nat
  | zero   => zero
  | succ x => x
def isZero : Nat → Bool
  | zero   => true
  | succ x => false
example : sub1 0 = 0 := rfl
example (x : Nat) : sub1 (succ x) = x := rfl

example : isZero 0 = true := rfl
example (x : Nat) : isZero (succ x) = false := rfl

example : sub1 7 = 6 := rfl
example (x : Nat) : isZero (x + 3) = false := rfl

zerosucc の代わりに、より馴染みのある表記を使うことができる:

def sub1 : Nat → Nat
  | 0   => 0
  | x+1 => x

def isZero : Nat → Bool
  | 0   => true
  | x+1 => false

加法とゼロ表記には [match_pattern] 属性が割り当てられているため、これらの表記をパターンマッチングで使うことができる。Leanは、コンストラクタ zerosucc が出現するまで、加法やゼロ表記を含む式を単純に正規化する。

パターンマッチングは直積型や Option 型など、任意の帰納型に対して機能する:

def swap : α × β → β × α
  | (a, b) => (b, a)

def foo : Nat × Nat → Nat
  | (m, n) => m + n

def bar : Option Nat → Nat
  | some n => n + 1
  | none   => 0

パターンマッチングは関数定義だけでなく、場合分けによる証明にも使うことができる:

namespace Hidden
def not : Bool → Bool
  | true  => false
  | false => true

theorem not_not : ∀ (b : Bool), not (not b) = b
  | true  => rfl  -- proof that not (not true) = true
  | false => rfl  -- proof that not (not false) = false

theorem not_not' : (b : Bool) → not (not b) = b
  | true  => rfl
  | false => rfl
end Hidden

パターンマッチングは帰納的に定義された命題を分解するためにも使うことができる:

example (p q : Prop) : p ∧ q → q ∧ p
  | And.intro h₁ h₂ => And.intro h₂ h₁

example (p q : Prop) : p ∨ q → q ∨ p
  | Or.inl hp => Or.inr hp
  | Or.inr hq => Or.inl hq

パターンマッチングは論理的結合子を含む仮説を分解するコンパクトな方法も提供する。

以上の全ての例で、パターンマッチングは「フラットな」場合分けを実行するために使われている。さらに興味深いことに、パターンは次のように入れ子になったコンストラクタを含むこともある。

def sub2 : Nat → Nat
  | 0   => 0
  | 1   => 0
  | x+2 => x

この例において、等式コンパイラはまず入力が zerosucc x の形であるかで最初の場合分けを行う。入力が zero のときは 0 を返す。入力が succ x の形のときは、その xzerosucc x の形であるかで2回目の場合分けを行う。等式コンパイラは提示されたケースリストから場合分けの方法を決定し、適切な場合分けに失敗したときはエラーを生じる。ここでも、次のように算術の記法を使うことができる。いずれにせよ、関数を定義する等式はdefinitionallyに成立する。

def sub2 : Nat → Nat
  | 0   => 0
  | 1   => 0
  | x+2 => x
example : sub2 0 = 0 := rfl
example : sub2 1 = 0 := rfl
example : sub2 (x+2) = x := rfl

example : sub2 5 = 3 := rfl

#print sub2 と書けば、この関数が再帰子を含むどんな式にコンパイルされたかが分かる。(Leanは sub2 が内部の補助関数 sub2.match_1 を使って定義されていることを伝えるかもしれないが、#print コマンドを使って sub2.match_1 の定義を表示させることもできる。)Leanはこれらの補助関数を使って match 式をコンパイルする。実際には、上記の定義 sub2 は次のように展開される。

def sub2 : Nat → Nat :=
  fun x =>
    match x with
    | 0   => 0
    | 1   => 0
    | x+2 => x

入れ子になったパターンマッチングの例をさらに挙げる:

example (p q : α → Prop)
        : (∃ x, p x ∨ q x) → (∃ x, p x) ∨ (∃ x, q x)
  | Exists.intro x (Or.inl px) => Or.inl (Exists.intro x px)
  | Exists.intro x (Or.inr qx) => Or.inr (Exists.intro x qx)

def foo : Nat × Nat → Nat
  | (0, n)     => 0
  | (m+1, 0)   => 1
  | (m+1, n+1) => 2

等式コンパイラは複数の引数を連続して処理することができる。例えば、一つ上の例 foo は2つの引数を持つ関数として定義する方が自然だろう:

def foo : Nat → Nat → Nat
  | 0,   n   => 0
  | m+1, 0   => 1
  | m+1, n+1 => 2

別の例を挙げる:

-- `a :: as` は `cons a as` の糖衣構文

def bar : List Nat → List Nat → Nat
  | [],      []      => 0
  | a :: as, []      => a
  | [],      b :: bs => b
  | a :: as, b :: bs => a + b

複数の引数を持つケースは、パターン毎にカンマで区切られることに注意してほしい。

以下の各例では、2番目以降の引数がケースに含まれているが、最初の引数での場合分けのみが行われる。

namespace Hidden
def and : Bool → Bool → Bool
  | true,  a => a
  | false, _ => false

def or : Bool → Bool → Bool
  | true,  _ => true
  | false, a => a

def cond : Bool → α → α → α
  | true,  x, y => x
  | false, x, y => y
end Hidden

また、ある引数の値が出力を定義するのに必要ない場合は、その引数のパターンにアンダースコアを使うことができることに注意してほしい。このアンダースコアはwildcard pattern(ワイルドカードパターン)あるいはanonymous variable(匿名変数)として知られている。等式コンパイラ以外での使い方とは違い、ここでアンダースコアは暗黙の引数を表すものではない。ワイルドカードにアンダースコアを使うのは関数型プログラミング言語では一般的なので、Leanもその表記を採用した。節Wildcards and Overlapping Patterns (ワイルドカードとケースの重複)ではワイルドカードの概念を拡張し、節Inaccessible Patterns (アクセス不能パターン)ではパターン内で暗黙の引数を使用する方法を説明する。

7章 Inductive Types (帰納型)で説明したように、帰納データ型はパラメータに依存しうる。次の例では、パターンマッチングを用いて tail 関数を定義している。引数 α : Type u は帰納データ型のパラメータであり、パターンマッチングに参加しないことを示すために(関数名・引数リストと型名を区切る)コロンの前に置かれる。Leanは : の後に帰納型のパラメータが来ることも許可するが、パラメータをパターンマッチさせることはできない。

def tail1 {α : Type u} : List α → List α
  | []      => []
  | a :: as => as

def tail2 : {α : Type u} → List α → List α
  | α, []      => []
  | α, a :: as => as

この2つの例では、パラメータ α の出現位置が異なるにもかかわらず、どちらの例でも場合分けに参加しないという意味で同じように扱われている。

Leanは、依存型の引数が場合分けにおいて「このケースが生じることはない」という追加の制約を与えるような、より複雑な形のパターンマッチングも扱うことができる。このようなdependent pattern matching(依存パターンマッチング)の例については、Dependent Pattern Matching (依存パターンマッチング)の節で説明する。

Wildcards and Overlapping Patterns (ワイルドカードとケースの重複)

前節の例の1つについて考える:

def foo : Nat → Nat → Nat
  | 0,   n   => 0
  | m+1, 0   => 1
  | m+1, n+1 => 2

この例は次のように表現することもできる:

def foo : Nat → Nat → Nat
  | 0, n => 0
  | m, 0 => 1
  | m, n => 2

2つ目の表現では、ケースが重複している。例えば、引数のペア 0 0 は3つのケース全てにマッチする。しかし、Leanは上のケースから順にパターンマッチングを試し、一番最初にマッチしたケースを使うことで曖昧さを解消するので、この2つの例は結果的に同じ関数を定義している。実際、2つ目の表現について以下の等式がdefinitionallyに成立する:

def foo : Nat → Nat → Nat
  | 0, n => 0
  | m, 0 => 1
  | m, n => 2
example : foo 0     0     = 0 := rfl
example : foo 0     (n+1) = 0 := rfl
example : foo (m+1) 0     = 1 := rfl
example : foo (m+1) (n+1) = 2 := rfl

mn の値は出力の定義に必要ないので、代わりにワイルドカードパターンを使ってもよい。

def foo : Nat → Nat → Nat
  | 0, _ => 0
  | _, 0 => 1
  | _, _ => 2

直前の定義と同様に、この foo の定義が4つの恒等式をdefinitionallyに満たすことは容易に確認できる。

関数型プログラミング言語の中には、incomplete pattern matching(不完全なパターンマッチング)をサポートするものがある。これらの言語では、インタプリタはケースリスト内のどのケースともマッチしない入力に対して例外を生成するか、任意の値を返す。Leanでは、Inhabited 型クラスを使うと、「どのケースともマッチしない入力に対して任意の値を返す」アプローチをシミュレートできる。大雑把に言うと、Inhabited αα の要素が存在することの証人である。10章 Type Classes (型クラス)では、Leanに適切な基底型がinhabited(有項)であることを指示でき、Leanはその指示に基づいて、他の構築された型が有項であることを自動的に推論できることを説明する。これに基づいて、標準ライブラリは任意の有項型のデフォルト項 default を提供する。

Option α を使って不完全なパターンマッチングをシミュレートすることもできる。このアプローチでは、入力が適切に提供されたケースとマッチしたときは some a を返し、不完全なケースとマッチしたときは none を返す。次の例は、両方のアプローチを示している。

def f1 : Nat → Nat → Nat
  | 0, _  => 1
  | _, 0  => 2
  | _, _  => default  -- the "incomplete" case

example : f1 0     0     = 1       := rfl
example : f1 0     (a+1) = 1       := rfl
example : f1 (a+1) 0     = 2       := rfl
example : f1 (a+1) (b+1) = default := rfl

def f2 : Nat → Nat → Option Nat
  | 0, _  => some 1
  | _, 0  => some 2
  | _, _  => none     -- the "incomplete" case

example : f2 0     0     = some 1 := rfl
example : f2 0     (a+1) = some 1 := rfl
example : f2 (a+1) 0     = some 2 := rfl
example : f2 (a+1) (b+1) = none   := rfl

等式コンパイラは賢い。以下の定義のどれかのケースを省くと、エラーメッセージでどんなケースがカバーされていないかを知らせてくれる。

def bar : Nat → List Nat → Bool → Nat
  | 0,   _,      false => 0
  | 0,   b :: _, _     => b
  | 0,   [],     true  => 7
  | a+1, [],     false => a
  | a+1, [],     true  => a + 1
  | a+1, b :: _, _     => a + b

また、等式コンパイラは適切な状況では casesOn の代わりに "if ... then ... else" 構文を用いる。

def foo : Char → Nat
  | 'A' => 1
  | 'B' => 2
  | _   => 3

#print foo.match_1

Structural Recursion and Induction (構造的再帰と構造的帰納法)

再帰的定義もサポートしていることが、等式コンパイラを強力なものにしている。次の3つの節では、ここに挙げる3つの概念それぞれについて説明する:

  • structurally recursive definitions(構造的再帰的定義)
  • well-founded recursive definitions(整礎再帰的定義)
  • mutually recursive definitions(相互再帰的定義)

一般的に、等式コンパイラは次の形式の入力を処理する:

def foo (a : α) : (b : β) → γ
  | [patterns₁] => t₁
  ...
  | [patternsₙ] => tₙ

ここで、(a : α) はパラメータの列、(b : β) はパターンマッチングが行われる引数の列、γ は任意の型であり、γab に依存することができる。[patterns₁] から [patternsₙ] は同じ数のパターンを含むべきであり、1つのパターンが β の各要素と対応する。これまで見てきたように、パターンは変数、他のパターンにコンストラクタを適用したもの、またはそのような形式に正規化される式のいずれかである(ここで、コンストラクタでないものは [match_pattern] 属性でマークされる)。コンストラクタの出現はケースの分割を促す。ここで、コンストラクタへの引数は与えられた変数で表される。節Dependent Pattern Matching (依存パターンマッチング)では、パターンマッチングでは何の役割も果たさないが、式の型チェックを行うために必要となる明示的な項をパターンに含める必要がある場合があることを説明する。この明示的な項は今述べた理由によりinaccessible patterns(アクセス不能パターン)と呼ばれる。しかし、節Dependent Pattern Matching (依存パターンマッチング)より前では、アクセス不能パターンを使う必要はない。

前節で見たように、出力を定義する項 t₁, ..., tₙ は、任意のパラメータ a だけでなく、対応するパターン内で導入された変数を利用することができる。再帰と帰納が可能なのは、出力を定義する項が foo への再帰的呼び出しを含むことすら可能だからである。この節では、structural recursion(構造的再帰)を扱う。構造的再帰では、=> の右辺に現れる foo への引数は => の左辺のパターンの部分項である。これは、部分項はマッチした引数よりstructurally small(構造的に小さい)であるため、帰納型の項としてマッチした引数より先に構築されるという考え方である。前章の構造的再帰の例を、今度は等式コンパイラを使って定義しよう:

open Nat
def add : Nat → Nat → Nat
  | m, zero   => m
  | m, succ n => succ (add m n)

theorem add_zero (m : Nat)   : add m zero = m := rfl
theorem add_succ (m n : Nat) : add m (succ n) = succ (add m n) := rfl

theorem zero_add : ∀ n, add zero n = n
  | zero   => rfl
  | succ n => congrArg succ (zero_add n)

def mul : Nat → Nat → Nat
  | n, zero   => zero
  | n, succ m => add (mul n m) n

theorem mul_zero (m : Nat)   : mul m zero = zero := rfl
theorem mul_succ (m n : Nat) : mul m (succ n) = add (mul m n) m := rfl

この zero_add の証明は、Leanにおいて帰納法による証明が再帰の一つの形であることを明らかにしている。

上の例は、addmul を定義する式がdefinitionallyに成立することを示している。等式コンパイラは構造的帰納法による証明内でも、可能な限り関数を定義する式がdefinitionallyに成立するようにする。例えば、zero_add の証明において、引数が zero のケースは rfl を使うだけで示せる。しかしながら、他の状況では、引数の部分項に関する当該定理(例えば zero_add n)はpropositionallyにしか成立しない。つまり、これは明示的に適用されなければならない等式定理である。等式コンパイラは zero_add n のような定理を内部的に作成するが、これらの定理はユーザーが直接使うものではなく、simp タクティクが必要に応じて使うように設定されている。したがって、次の zero_add の証明も機能する:

open Nat
def add : Nat → Nat → Nat
  | m, zero   => m
  | m, succ n => succ (add m n)
theorem zero_add : ∀ n, add zero n = n
  | zero   => by simp [add]
  | succ n => by simp [add, zero_add]

パターンマッチングによる定義と同様に、構造的再帰や構造的帰納法のパラメータがコロンの前に現れることがある。このようなパラメータは、定義が処理される前にローカルコンテキストに追加される。例えば、加法の定義は次のように書くこともできる:

open Nat
def add (m : Nat) : Nat → Nat
  | zero   => m
  | succ n => succ (add m n)

この例を match を使って書くこともできる。


/- `match` を使う場合はもちろん `:=` が必要になる -/

open Nat
def add (m n : Nat) : Nat :=
  match n with
  | zero   => m
  | succ n => succ (add m n)

構造的再帰のもっと面白い例はフィボナッチ関数 fib である。

def fib : Nat → Nat
  | 0   => 1
  | 1   => 1
  | n+2 => fib (n+1) + fib n

example : fib 0 = 1 := rfl
example : fib 1 = 1 := rfl
example : fib (n + 2) = fib (n + 1) + fib n := rfl

example : fib 7 = 21 := rfl

ここで、n + 2( succ (succ n) とdefinitionally equal)における fib 関数の値は、n + 1( succ n とdefinitionally equal)における値と n における値で定義される。しかし、これはフィボナッチ関数を計算する方法としてはきわめて非効率的で、実行時間は指数 n の指数関数となる。もっと良い方法がある:

def fibFast (n : Nat) : Nat :=
  (loop n).2
where
  loop : Nat → Nat × Nat
    | 0   => (0, 1)
    | n+1 => let p := loop n; (p.2, p.1 + p.2)

#eval fibFast 100

where の代わりに let rec を用いた定義は次の通り:

def fibFast (n : Nat) : Nat :=
  let rec loop : Nat → Nat × Nat
    | 0   => (0, 1)
    | n+1 => let p := loop n; (p.2, p.1 + p.2)
  (loop n).2

どちらの例でも、Leanは補助関数 fibFast.loop を生成する。

構造的再帰を処理するために、等式コンパイラは各帰納型の定義時に自動生成される定数 belowbrecOn を用いて、course-of-values recursion(累積再帰)を使用する。Nat.belowNat.brecOn の型を見れば、それらがどのように機能するかを知ることができる:

variable (C : Nat → Type u)

#check (@Nat.below C : Nat → Type u)

#reduce @Nat.below C (3 : Nat)

#check (@Nat.brecOn C : (n : Nat) → ((n : Nat) → @Nat.below C n → C n) → C n)

@Nat.below C (3 : nat) は、C 0C 1C 2 の項を格納するデータ構造である。累積再帰は Nat.brecOn によって実装される。Nat.brecOn は型 (n : Nat) → C n を持つ依存関数の入力 m における値を、(@Nat.below C m の要素として表される)その関数の以前の全ての値を使って定義することを可能にする。

累積再帰の利用は、等式コンパイラがLeanのカーネルに対して特定の関数が停止することを正当に主張するために使うテクニックの一つである。他の関数型プログラミング言語のコンパイラと同様、再帰関数をコンパイルするコードジェネレータに影響を与えることはない。#eval fib n の実行時間は指数 n の指数関数となることを思い出してほしい。一方で、#reduce fib nbrecOn による構築に基づいた定義を使用するため効率的である。

def fib : Nat → Nat
  | 0   => 1
  | 1   => 1
  | n+2 => fib (n+1) + fib n

-- #eval fib 50 -- slow
#reduce fib 50  -- fast

#print fib

再帰的定義のもう一つの良い例がリストの append 関数である。

def append : List α → List α → List α
  | [],    bs => bs
  | a::as, bs => a :: append as bs

example : append [1, 2, 3] [4, 5] = [1, 2, 3, 4, 5] := rfl

もう一つ例を挙げる: listAdd x y は2つのリストのどちらかの要素がなくなるまで、最初のリストの先頭の要素 a と2番目のリストの先頭の要素 b を削除して a + b をリスト z に追加する操作を繰り返し、最後に z を返す。

def listAdd [Add α] : List α → List α → List α
  | [],      _       => []
  | _,       []      => []
  | a :: as, b :: bs => (a + b) :: listAdd as bs

#eval listAdd [1, 2, 3] [4, 5, 6, 6, 9, 10]
-- [5, 7, 9]

以下の練習問題で、これに似た例に関して実験してみることをお勧めする。

Local Recursive Declarations (ローカルな再帰的定義)

let rec キーワードを使うと、ローカルな再帰的定義を宣言することができる。

def replicate (n : Nat) (a : α) : List α :=
  let rec loop : Nat → List α → List α
    | 0,   as => as
    | n+1, as => loop n (a::as)
  loop n []

#check @replicate.loop
-- {α : Type} → α → Nat → List α → List α

Leanは各 let rec に対して補助定義を作成する。上の例では、replicate の定義内にある let rec loop のために replicate.loop の定義を作成している。Leanは、let rec 宣言内で使われたローカル変数を定義中の関数のパラメータに追加することで、宣言を「閉じる」ことに注意してほしい。例えば、ローカル変数 alet rec loop 内で使われている。

let rec をタクティクモード内で使うこともできる。また、帰納法による証明を作るために let rec を使うこともできる。

def replicate (n : Nat) (a : α) : List α :=
 let rec loop : Nat → List α → List α
   | 0,   as => as
   | n+1, as => loop n (a::as)
 loop n []
theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := by
  let rec aux (n : Nat) (as : List α)
              : (replicate.loop a n as).length = n + as.length := by
    match n with
    | 0   => simp [replicate.loop]
    | n+1 => simp [replicate.loop, aux n, Nat.add_succ, Nat.succ_add]
  exact aux n []

定義を記述した後に where キーワードを使って補助再帰的定義を導入することもできる。Leanはこれらを let rec に変換する。

def replicate (n : Nat) (a : α) : List α :=
  loop n []
where
  loop : Nat → List α → List α
    | 0,   as => as
    | n+1, as => loop n (a::as)

theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := by
  exact aux n []
where
  aux (n : Nat) (as : List α)
      : (replicate.loop a n as).length = n + as.length := by
    match n with
    | 0   => simp [replicate.loop]
    | n+1 => simp [replicate.loop, aux n, Nat.add_succ, Nat.succ_add]

Well-Founded Recursion and Induction (整礎再帰と整礎帰納法)

構造的再帰が使えない場合は、整礎再帰を使えば再帰的定義中の関数が停止することを証明できる。整礎再帰に必要なのは、整礎関係と、各再帰適用(f t の形をとる)が特定の項 t をこの関係において「減少させる」ことの証明である。依存型理論は整礎再帰を表現し、その正当性を証明するのに十分強力である。これらの仕組みを理解するために必要な論理的背景の説明から始めよう。

Leanの標準ライブラリは2つの述語 Acc r aWellFounded r を定義している。ここで、r は型 α 上の二項関係であり、a は型 α の要素である。

variable (α : Sort u)
variable (r : α → α → Prop)

#check (Acc r : α → Prop)
#check (WellFounded r : Prop)

1つ目 Acc は帰納的に定義された述語である。定義(唯一のコンストラクタ Acc.intro)によると、Acc r x∀ y, r y x → Acc r y と同値である。Acc r xr の下で x がアクセス可能であることを意味する。r y x が一種の順序関係 y ≺ x を表すと考えるなら、Acc r xx の全ての前者がアクセス可能であることと同値である。特に、x が前者を持たない場合、x はアクセス可能である。任意の型 α の任意のアクセス可能な項 x に対して、x の全ての前者に先に値を割り当てることで、x にも値を割り当てることができるはずである。

noncomputable def f {α : Sort u}
      (r : α → α → Prop)
      (h : WellFounded r)
      (C : α → Sort v)
      (F : (x : α) → ((y : α) → r y x → C y) → C x)
      : (x : α) → C x := WellFounded.fix h F

f の引数が長い列をなしているが、前半はすでに見たとおりである: α は型、r は二項関係、そして h は「r が整礎である」という仮説である。変数 C は再帰的定義の動機を表す: 各項 x : α について、C x の項を構築したい。関数 FC x の項を構築するための帰納的レシピを提供する: Fx の各前者 y について C y の項が与えられたとき、要素 C x を構築する方法を教えてくれる。

WellFounded.fix は帰納法においても同様に機能する。つまり、もし が整礎で、∀ x, C x を証明したいなら、任意の x に対して「∀ y ≺ x, C y ならば C x」を示せば十分である。

上の例では、コードジェネレータが現在 WellFounded.fix をサポートしていないため、noncomputable という修飾子を使用している。関数 WellFounded.fix はLeanが関数の停止を正当に主張するために使う(累積再帰とは別の)ツールである。

Leanは、自然数に関する通常の順序 < が整礎であることを知っている。また、Leanは既存の順序から新しい整礎順序を構築する方法もいくつか知っている。例えば、辞書式順序を使う方法がある。

以下は、標準ライブラリ内にある自然数の除法の定義である。

open Nat

theorem div_lemma {x y : Nat} : 0 < y ∧ y ≤ x → x - y < x :=
  fun h => sub_lt (Nat.lt_of_lt_of_le h.left h.right) h.left

def div.F (x : Nat) (f : (x₁ : Nat) → x₁ < x → Nat → Nat) (y : Nat) : Nat :=
  if h : 0 < y ∧ y ≤ x then
    (f (x - y) (div_lemma h) y) + 1
  else
    zero

noncomputable def div := WellFounded.fix (measure id).wf div.F

#reduce div 8 2 -- 4

この定義はやや分かりにくい。ここで再帰は x に対して行われ、div.F x f : Nat → Nat はその固定された x に対する「y で割る」関数を返す。再帰のレシピである div.F の第2引数 f は、x より小さい全ての値 x₁ に対して「y で割る」関数を返す(と想定される)関数であることを理解する必要がある。

elaboratorはこのような定義をより便利に作成できるようにデザインされている。次のような定義の書き方が認められる:

def div (x y : Nat) : Nat :=
  if h : 0 < y ∧ y ≤ x then
    have : x - y < x := Nat.sub_lt (Nat.lt_of_lt_of_le h.1 h.2) h.1
    (div (x - y) y) + 1
  else
    0

再帰的な定義に遭遇すると、Leanはまず構造的再帰を試み、それが失敗したときだけ整礎再帰を試みる。Leanは decreasing_tactic というタクティクを使って、再帰適用後の項が元の項より「小さい」ことを示す。上の例の補助命題 x - y < x はこのタクティクのためのヒントとみなされる。

div を定義する等式はdefinitionallyに成立しない。しかし、unfold タクティクを使えば div を展開することはできる。convを使うと、展開したい div 適用後の項を選択できる。

def div (x y : Nat) : Nat :=
 if h : 0 < y ∧ y ≤ x then
   have : x - y < x := Nat.sub_lt (Nat.lt_of_lt_of_le h.1 h.2) h.1
   div (x - y) y + 1
 else
   0
example (x y : Nat) : div x y = if 0 < y ∧ y ≤ x then div (x - y) y + 1 else 0 := by
  conv => lhs; unfold div -- 等式の左辺の `div` を展開する

example (x y : Nat) (h : 0 < y ∧ y ≤ x) : div x y = div (x - y) y + 1 := by
  conv => lhs; unfold div
  simp [h]

次の例も同様の構文を使って整礎再帰を行う: natToBin は任意の自然数を0と1のリストとして表される2進表記に変換する。まず、再帰適用後の項が元の項より減少する証拠を示さなければならないが、これは sorry で行う。sorry はインタプリタが関数を正常に評価することを妨げるものではない。(訳者注: #eval を使って関数を評価するだけなら証拠は不要ということだと思われる。しかし証拠がない場合、任意の引数 n に対して #eval natToBin n が停止する保証はないと思われる。)

def natToBin : Nat → List Nat
  | 0     => [0]
  | 1     => [1]
  | n + 2 =>
    have : (n + 2) / 2 < n + 2 := sorry
    natToBin ((n + 2) / 2) ++ [n % 2]

#eval natToBin 1234567

最後の例として、Leanではアッカーマン関数が本来の定義をそのまま書くだけで定義できることを見る。なぜなら、この整礎再帰は自然数の辞書式順序の整礎性によって正当化されるからである。termination_by キーワードはLeanに辞書式順序を使うように指示している。実際には、このキーワードは関数の2つの引数を Nat × Nat 型の項にマッピングしている。そして、Leanは型クラス解決を使って WellFoundedRelation (Nat × Nat) 型の要素を合成する。

def ack : Nat → Nat → Nat
  | 0,   y   => y+1
  | x+1, 0   => ack x 1
  | x+1, y+1 => ack x (ack (x+1) y)
termination_by x y => (x, y)

#eval ack 3 5
-- アッカーマン関数は入力値の増加に伴い出力値が急速に増加する関数であり、
-- 例えば `#eval ack 4 1` などはバッファオーバーフロー等のエラーを引き起こす可能性が高いため、
-- 実行しないことをお勧めする。

インスタンス WellFoundedRelation (α × β) が辞書式順序を使うため、上の定義の中では辞書式順序が使われていることに注意してほしい。また、Leanは標準ライブラリ内で次のインスタンス instWellFoundedRelation も定義している。

instance (priority := low) [SizeOf α] : WellFoundedRelation α :=
  sizeOfWFRel

次の例では、as.size - i が再帰適用によって減少することを示すことで、go の停止性を証明する。

-- Array `as` を先頭から見て、
-- `as` の要素 `a` が `p a` を満たす限りArray `r` に `a` を追加し、`r` を返す関数
def takeWhile (p : α → Bool) (as : Array α) : Array α :=
  go 0 #[]
where
  go (i : Nat) (r : Array α) : Array α :=
    if h : i < as.size then
      let a := as.get ⟨i, h⟩
      if p a then
        go (i+1) (r.push a)
      else
        r
    else
      r
termination_by as.size - i

#eval takeWhile (fun n : Nat => if n % 2 = 1 then true else false) #[1, 3, 5, 6, 7]

この例では、補助関数 go は再帰的だが、takeWhile は再帰的でないことに注意してほしい。

デフォルトでは、Leanは decreasing_tactic タクティクを使って再帰適用後の項が元の項より小さいことを証明する。decreasing_by 修飾子を使うと、独自のタクティクを提供することができる。以下はその例である。

theorem div_lemma {x y : Nat} : 0 < y ∧ y ≤ x → x - y < x :=
  fun ⟨ypos, ylex⟩ => Nat.sub_lt (Nat.lt_of_lt_of_le ypos ylex) ypos

def div (x y : Nat) : Nat :=
  if h : 0 < y ∧ y ≤ x then
    div (x - y) y + 1
  else
    0
decreasing_by apply div_lemma; assumption

decreasing_bytermination_by を置き換えるものではなく、互いに補完し合うものである。termination_by は整礎関係を指定するのに使われ、decreasing_by は再帰適用後の項が元の項より小さいことを示すために独自のタクティクを提供するために使われる。次の例ではその両方を使う。

def ack : Nat → Nat → Nat
  | 0,   y   => y+1
  | x+1, 0   => ack x 1
  | x+1, y+1 => ack x (ack (x+1) y)
termination_by x y => (x, y)
decreasing_by
  all_goals simp_wf -- unfolds well-founded recursion auxiliary definitions
  · apply Prod.Lex.left; simp_arith
  · apply Prod.Lex.right; simp_arith
  · apply Prod.Lex.left; simp_arith

decreasing_by sorry を使えば、Leanに関数が停止することを「信じ」させることができる。

def natToBin : Nat → List Nat
  | 0     => [0]
  | 1     => [1]
  | n + 2 => natToBin ((n + 2) / 2) ++ [n % 2]
decreasing_by sorry

#eval natToBin 1234567

sorry を使うことは新しい公理を導入することと同じであり、避けるべきであることを思い出してほしい。次の例では、sorry を使って False を証明した。コマンド #print axioms は、unsoundsorry を実装するために使われている不健全な公理 sorryAx に依存していることを示す。

def unsound (x : Nat) : False :=
  unsound (x + 1)
decreasing_by sorry

#check unsound 0
-- `unsound 0` is a proof of `False`

#print axioms unsound
-- 'unsound' depends on axioms: [sorryAx]

要約:

  • termination_by がない場合、Leanはある引数を選択し、型クラス解決を使って選択した引数の型上の整礎関係を合成することで、(可能であれば)整礎関係が導出される。

  • termination_by が指定されている場合、termination_by は関数の引数を α 型にマッピングし、再び型クラス解決が使われる。β × γ に関するデフォルトインスタンスは βγ 上の整礎関係に基づいた辞書式順序であることを思い出してほしい。

  • Nat に関するデフォルトの整礎関係インスタンスは < である。

  • デフォルトでは、選択された整礎関係について、再帰適用後の項が元の項より小さいことを示すために decreasing_tactic タクティクが使われる。decreasing_tactic が失敗した場合に表示されるエラーメッセージは残りのゴール ... |- G を含む。decreasing_tacticassumption タクティクを使うことに注意。つまり、have 式を使ってコンテキストに仮説を追加することがターゲット G の証明の役に立つことがある。decreasing_by を使って独自のタクティクを提供することもできる。

Mutual Recursion (相互再帰)

Leanはmutual recursive definitions(相互再帰的定義)もサポートしている。その構文は相互帰納型と似ている。以下に例を挙げる:

mutual
  def even : Nat → Bool
    | 0   => true
    | n+1 => odd n

  def odd : Nat → Bool
    | 0   => false
    | n+1 => even n
end

example : even (a + 1) = odd a := by
  simp [even]

example : odd (a + 1) = even a := by
  simp [odd]

theorem even_eq_not_odd : ∀ a, even a = not (odd a) := by
  intro a; induction a
  . simp [even, odd]
  . simp [even, odd, *]

evenodd を用いて定義され、oddeven を用いて定義されているため、この定義は相互定義になっている。内部では、この定義は単一の再帰的定義としてコンパイルされる。内部で定義された関数は、直和型の項を引数として取る。直和型の項の片側は even への入力、もう片側は odd への入力と解釈される。そして、入力に適した出力を返す。この関数を定義するために、Leanは適切な整礎関係を使用するが、その内部はユーザーから隠されている。このような定義を利用する正規の方法は、前節でやったように simp または unfold を使うことである。

また、相互再帰的定義は相互帰納型および入れ子帰納型を扱う自然な方法を提供する。前章で示した、相互帰納述語型としての EvenOdd の定義を思い出してほしい。

mutual
  inductive Even : Nat → Prop where
    | even_zero : Even 0
    | even_succ : ∀ n, Odd n → Even (n + 1)

  inductive Odd : Nat → Prop where
    | odd_succ : ∀ n, Even n → Odd (n + 1)
end

コンストラクタ even_zeroeven_succodd_succ はある自然数が偶数か奇数かを示す積極的な手段を提供する。0が奇数でないこと、そして後の2つのコンストラクタの意味が逆であることを知るには、この相互帰納型がこれらのコンストラクタによって生成されたという事実を使う必要がある。いつものように、コンストラクタは定義された型の名前を持つ名前空間に保管されており、コマンド open Even Odd を使えば、コンストラクタに便利にアクセスできるようになる。

mutual
 inductive Even : Nat → Prop where
   | even_zero : Even 0
   | even_succ : ∀ n, Odd n → Even (n + 1)
 inductive Odd : Nat → Prop where
   | odd_succ : ∀ n, Even n → Odd (n + 1)
end
open Even Odd

theorem not_odd_zero : ¬ Odd 0 :=
  fun h => nomatch h

theorem even_of_odd_succ : ∀ n, Odd (n + 1) → Even n
  | _, odd_succ n h => h

theorem odd_of_even_succ : ∀ n, Even (n + 1) → Odd n
  | _, even_succ n h => h

別の例を挙げる。入れ子帰納型を使って、型 Term の項を帰納的に定義することを考える。ここで、Term の項は定数(文字列によって与えられる名前を持つ)か、定数のリストに定数を適用した結果のどちらかになるとする。

inductive Term where
  | const : String → Term
  | app   : String → List Term → Term

そして、相互再帰を使って、各項の中に登場する定数の数を数える関数と、項のリストの中に登場する要素の数を数える関数を定義することができる。

inductive Term where
 | const : String → Term
 | app   : String → List Term → Term
namespace Term

mutual
  def numConsts : Term → Nat
    | const _ => 1
    | app _ cs => numConstsLst cs

  def numConstsLst : List Term → Nat
    | [] => 0
    | c :: cs => numConsts c + numConstsLst cs
end

def sample := app "f" [app "g" [const "x"], const "y"]
def sample2 := [app "g" [const "x", const "y"], const "y"]

#eval numConsts sample
#eval numConstsLst sample2

end Term

最後の例として、項 e 内の定数 ab に置き換える関数 replaceConst a b e を定義し、e が持つ定数の個数と replaceConst a b e が持つ定数の個数が同じであることを証明する。この証明は、相互帰納法を使っていることに注意してほしい。

inductive Term where
 | const : String → Term
 | app   : String → List Term → Term
namespace Term
mutual
 def numConsts : Term → Nat
   | const _ => 1
   | app _ cs => numConstsLst cs
  def numConstsLst : List Term → Nat
   | [] => 0
   | c :: cs => numConsts c + numConstsLst cs
end
mutual
  def replaceConst (a b : String) : Term → Term
    | const c => if a == c then const b else const c
    | app f cs => app f (replaceConstLst a b cs)

  def replaceConstLst (a b : String) : List Term → List Term
    | [] => []
    | c :: cs => replaceConst a b c :: replaceConstLst a b cs
end

mutual
  theorem numConsts_replaceConst (a b : String) (e : Term)
            : numConsts (replaceConst a b e) = numConsts e := by
    match e with
    | const c => simp [replaceConst]; split <;> simp [numConsts]
    | app f cs => simp [replaceConst, numConsts, numConsts_replaceConstLst a b cs]

  theorem numConsts_replaceConstLst (a b : String) (es : List Term)
            : numConstsLst (replaceConstLst a b es) = numConstsLst es := by
    match es with
    | [] => simp [replaceConstLst, numConstsLst]
    | c :: cs =>
      simp [replaceConstLst, numConstsLst, numConsts_replaceConst a b c,
            numConsts_replaceConstLst a b cs]
end

Dependent Pattern Matching (依存パターンマッチング)

Pattern Matching (パターンマッチング)の節で説明した全ての例は、casesOnrecOn を使って簡単に書くことができる。しかし、Vector α n のような添字付き帰納族では、ケース分割が添字の値に制約を課すため、簡単に書けないことがよくある。もし等式コンパイラが無かったら、再帰子だけを使って mapzipunzip などの非常に単純な関数を定義するために、多くの定型的なコードが必要になっただろう。その難しさを理解するために、ベクトル v : Vector α (succ n) を受け取り、最初の要素を削除したベクトルを返す関数 tail を定義するためには何が必要かを考えてみよう。まずは、casesOn 関数を使うことが考えられる:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)

namespace Vector

#check @Vector.casesOn
/-
  {α : Type u}
  → {motive : (a : Nat) → Vector α a → Sort v} →
  → {a : Nat} → (t : Vector α a)
  → motive 0 nil
  → ((a : α) → {n : Nat} → (a_1 : Vector α n) → motive (n + 1) (cons a a_1))
  → motive a t
-/

end Vector

しかし、入力が nil の場合は何を返せばいいだろうか。何かおかしなことが起こっている: v の型が Vector α (succ n) なら、vnil であるはずがない。しかし、それを casesOn に伝える方法は明らかではない。

1つの解決策は、補助関数を定義することである:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def tailAux (v : Vector α m) : m = n + 1 → Vector α n :=
  Vector.casesOn (motive := fun x _ => x = n + 1 → Vector α n) v
    (fun h : 0 = n + 1 => Nat.noConfusion h)
    (fun (a : α) (m : Nat) (as : Vector α m) =>
     fun (h : m + 1 = n + 1) =>
       Nat.noConfusion h (fun h1 : m = n => h1 ▸ as))

def tail (v : Vector α (n+1)) : Vector α n :=
  tailAux v rfl
end Vector

補助関数 tailAux において、vnil の場合、m の値は 0 に決定し、noConfusion0 = succ n は成立しえないという事実を使う。そうでなければ、va :: w の形をとり、w を長さ m のベクトルから長さ n のベクトルへキャストした後、単純に w を返すことができる(h1 ▸ as)。

tail を定義する上で難しいのは、添字間の関係を維持することである。tailAux 内の仮説 e : m = n + 1 は、n と小前提(Vector.casesOn の4番目と5番目の引数)に関連した添字(0m + 1)の関係を noConfusion 等に伝えるために使われる。さらに、zero = n + 1 のケースは到達不能である。このようなケースを破棄する正規の方法は noConfusion を使うことである。

しかし、実際は tail 関数は再帰を使って簡単に定義できる。そして、等式コンパイラが全ての定型コードを自動的に生成してくれる。似た例をいくつか紹介しよう:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def head : {n : Nat} → Vector α (n+1) → α
  | n, cons a as => a

def tail : {n : Nat} → Vector α (n+1) → Vector α n
  | n, cons a as => as

theorem eta : ∀ {n : Nat} (v : Vector α (n+1)), cons (head v) (tail v) = v
  | n, cons a as => rfl

def map (f : α → β → γ) : {n : Nat} → Vector α n → Vector β n → Vector γ n
  | 0,   nil,       nil       => nil
  | n+1, cons a as, cons b bs => cons (f a b) (map f as bs)

def zip : {n : Nat} → Vector α n → Vector β n → Vector (α × β) n
  | 0,   nil,       nil       => nil
  | n+1, cons a as, cons b bs => cons (a, b) (zip as bs)
end Vector

再帰的定義において、head nil のような「到達不能」なケースはケースリストから除外できることに注意してほしい。自動生成される添字付き帰納族の定義は単純とは言いがたいものである。次の例について、#print コマンドの出力を見てほしい:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def map (f : α → β → γ) : {n : Nat} → Vector α n → Vector β n → Vector γ n
  | 0,   nil,       nil       => nil
  | n+1, cons a as, cons b bs => cons (f a b) (map f as bs)

#print map
#print map.match_1
end Vector

map 関数を手で定義するのは tail 関数よりもさらに面倒である。自信のある人は recOncasesOnnoConfusion を使って map 関数を手作りしてみてほしい。

Inaccessible Patterns (アクセス不能パターン)

依存パターンマッチングにおいて、項の型を適切に特殊化するために、定義には必要のない引数を含まなければならない場合がある。Leanでは、このような補助項を、パターンマッチングにおいてinaccessible(アクセス不能)なものとしてマークすることができる。例えば、左辺に出現する項が変数単体でも変数にコンストラクタを適用したものでもない場合、これらの注釈は不可欠である。なぜなら、それらの項はパターンマッチングにおいて不適切なターゲットだからである。このようなアクセス不能パターンは、ケースの左辺のdon't careな構成要素とみなすことができる。.(t) と書くことで、補助項へのアクセスが不能であることを宣言することができる。アクセス不能パターンの形が推論できる場合は、_ と書いてもよい。

次の例では、「f のimage(像)の中にある」という性質を定義する帰納型を宣言する。型 ImageOf f b の項は、bf の像の中にあることの証拠だと見なすことができる。コンストラクタ imf はそのような証拠を構築するために使われる。それから、f の像の中にある項 b を受け取り、証拠 imf a に基づいて、f によって b にマップされた要素の1つ a を返す「逆関数」を持つ関数 f を定義することができる。型付けのルールに従うと、最初の引数を f a と書かなければならないが、この項は変数単体でも変数にコンストラクタを適用したものでもないため、パターンマッチングを用いた定義において何の役割も果たさない。以下の逆関数 inverse を定義するためには、f a にアクセス不能であるとマークしなければならない

inductive ImageOf {α β : Type u} (f : α → β) : β → Type u where
  | imf : (a : α) → ImageOf f (f a)

open ImageOf

/-
def bad_inverse {f : α → β} : (b : β) → ImageOf f b → α
  | b, imf a => a  -- `imf a` has type `ImageOf f (f a)` but is expected to have type `ImageOf f b`

def bad_inverse' {f : α → β} : (b : β) → ImageOf f b → α
  | f a, imf a => a  -- invalid pattern
-/

def inverse {f : α → β} : (b : β) → ImageOf f b → α
  | .(f a), imf a => a

def inverse' {f : α → β} : (b : β) → ImageOf f b → α
  | _, imf a => a

上記の例では、アクセス不能注釈は f がパターンマッチング対象の変数ではないことを明確にしている。

アクセス不能パターンは、依存パターンマッチングを利用する定義を明確にし、制御するために使用することができる。ある型 α に関連する加法演算があると仮定して、α の項を要素に持つ2つのベクトルを足し合わせる関数 Vector.add を定義することを考えてみよう:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)

namespace Vector

def add [Add α] : {n : Nat} → Vector α n → Vector α n → Vector α n
  | 0,   nil,       nil       => nil
  | n+1, cons a as, cons b bs => cons (a + b) (add as bs)

end Vector

引数 {n : Nat} はコロンの後に現れるが、これはパターンマッチングを用いた定義内で n を固定し続けることができないからである。この定義を実装したとき、等式コンパイラは最初の引数が 0n+1 の形をとるかのケース判別から始める。続いて、次の2つの引数についてネストされたケース判別がなされる。それぞれのケースについて、等式コンパイラは最初の引数 n のパターンと整合性のないケースを除外する(例えば、n+1, nil, nil というパターンたちを持つケースを除外する)。

しかし、実際には最初の引数 n についてケース判別をする必要はない。VectorcasesOn エリミネータは2番目の引数でケース判別をするときに、引数 n の値を自動的に抽象化して 0n + 1 に置き換える。アクセス不能パターンを使うことで、等式コンパイラに n でのケース判別を避けるように促すことができる。

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def add [Add α] : {n : Nat} → Vector α n → Vector α n → Vector α n
  | .(_), nil,       nil       => nil
  | .(_), cons a as, cons b bs => cons (a + b) (add as bs)
end Vector

この位置をアクセス不能パターンとしてマークすることは、等式コンパイラに次の2つのことを伝える。第一に、最初の引数の形式は他の引数によってもたらされる制約から推論されるべきである。第二に、最初の引数はパターンマッチングに参加すべきではない

アクセス不能パターン .(_) は簡便のため _ と書くことができる。

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def add [Add α] : {n : Nat} → Vector α n → Vector α n → Vector α n
  | _, nil,       nil       => nil
  | _, cons a as, cons b bs => cons (a + b) (add as bs)
end Vector

上述したように、引数 {n : Nat} は定義内で固定し続けることができないため、パターンマッチングの一部のならざるを得ない。Leanの以前のバージョンでは、ユーザーはこのような余分な判別子を含めなければならないことが面倒だとしばしば感じていた。そこで、Lean 4は新機能discriminant refinement(判別子の絞り込み)を実装した。この機能は余分な判別子を自動でパターンマッチングに含める。

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def add [Add α] {n : Nat} : Vector α n → Vector α n → Vector α n
  | nil,       nil       => nil
  | cons a as, cons b bs => cons (a + b) (add as bs)
end Vector

auto bound implicits(自動束縛暗黙引数)機能と組み合わせると、add の定義をより簡潔に書くことができる:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def add [Add α] : Vector α n → Vector α n → Vector α n
  | nil,       nil       => nil
  | cons a as, cons b bs => cons (a + b) (add as bs)
end Vector

これらの新機能を使うことで、前節で定義した他のベクトル関数を次のようによりコンパクトに書くことができる:

inductive Vector (α : Type u) : Nat → Type u
  | nil  : Vector α 0
  | cons : α → {n : Nat} → Vector α n → Vector α (n+1)
namespace Vector
def head : Vector α (n+1) → α
  | cons a as => a

def tail : Vector α (n+1) → Vector α n
  | cons a as => as

theorem eta : (v : Vector α (n+1)) → cons (head v) (tail v) = v
  | cons a as => rfl

def map (f : α → β → γ) : Vector α n → Vector β n → Vector γ n
  | nil,       nil       => nil
  | cons a as, cons b bs => cons (f a b) (map f as bs)

def zip : Vector α n → Vector β n → Vector (α × β) n
  | nil,       nil       => nil
  | cons a as, cons b bs => cons (a, b) (zip as bs)
end Vector

Match Expressions (マッチ式)

Leanは、多くの関数型言語で見られるmatch-with式のコンパイラも提供している。

def isNotZero (m : Nat) : Bool :=
  match m with
  | 0   => false
  | n+1 => true

match は普通のパターンマッチングによる定義とあまり変わらないように見える。しかし、match のポイントは、定義内のどこでも使えることと、任意の引数に対して使えることである。

def isNotZero (m : Nat) : Bool :=
  match m with
  | 0   => false
  | n+1 => true

def filter (p : α → Bool) : List α → List α
  | []      => []
  | a :: as =>
    match p a with
    | true => a :: filter p as
    | false => filter p as

example : filter isNotZero [1, 0, 0, 3, 0] = [1, 3] := rfl

別の例を挙げる:

def foo (n : Nat) (b c : Bool) :=
  5 + match n - 5, b && c with
      | 0,   true  => 0
      | m+1, true  => m + 7
      | 0,   false => 5
      | m+1, false => m + 3

#eval foo 7 true false

example : foo 7 true false = 9 := rfl

Leanは、システムの全ての部分で、パターンマッチングを実装するために内部で match 構文を使用する。したがって、以下の4つの定義は全て同じ効果を持つ。

def bar₁ : Nat × Nat → Nat
  | (m, n) => m + n

def bar₂ (p : Nat × Nat) : Nat :=
  match p with
  | (m, n) => m + n

def bar₃ : Nat × Nat → Nat :=
  fun (m, n) => m + n

def bar₄ (p : Nat × Nat) : Nat :=
  let (m, n) := p; m + n

命題を分解する際には、上記の変形版が役に立つ:

variable (p q : Nat → Prop)

example : (∃ x, p x) → (∃ y, q y) → ∃ x y, p x ∧ q y
  | ⟨x, px⟩, ⟨y, qy⟩ => ⟨x, y, px, qy⟩

example (h₀ : ∃ x, p x) (h₁ : ∃ y, q y)
        : ∃ x y, p x ∧ q y :=
  match h₀, h₁ with
  | ⟨x, px⟩, ⟨y, qy⟩ => ⟨x, y, px, qy⟩

example : (∃ x, p x) → (∃ y, q y) → ∃ x y, p x ∧ q y :=
  fun ⟨x, px⟩ ⟨y, qy⟩ => ⟨x, y, px, qy⟩

example (h₀ : ∃ x, p x) (h₁ : ∃ y, q y)
        : ∃ x y, p x ∧ q y :=
  let ⟨x, px⟩ := h₀
  let ⟨y, qy⟩ := h₁
  ⟨x, y, px, qy⟩

Exercises (練習問題)

  1. 名前の衝突を避けるために名前空間 Hidden を開き、等式コンパイラを使って自然数上の加法、乗法、べき乗を定義せよ。次に、等式コンパイラを使って、それらの基本的な性質を証明せよ。

  2. 同様に、等式コンパイラを使ってリストに対する基本的な操作(reverse 関数など)を定義し、リストに関する定理を帰納法で証明せよ。例えば、任意のリストに対して、reverse (reverse xs) = xs となることを示せ。

  3. 累積再帰を実行する自然数上の関数を自分で定義せよ。同様に、WellFounded.fix を自分で定義する方法を見つけられるか試してみよ。

  4. Dependent Pattern Matching (依存パターンマッチング)の例に従って、2つのベクトル va vb を受け取り、va の末尾に vb を追加したベクトルを返す関数を定義せよ。これは厄介で、補助関数を定義しなければならないだろう。

  5. 次のような算術式の型を考える。ここで、var n は変数 vₙを、const n は値が n である定数を表す。

inductive Expr where
  | const : Nat → Expr
  | var : Nat → Expr
  | plus : Expr → Expr → Expr
  | times : Expr → Expr → Expr
  deriving Repr

open Expr

def sampleExpr : Expr :=
  plus (times (var 0) (const 7)) (times (const 2) (var 1))

ここで、sampleExpr(v₀ * 7) + (2 * v₁) を表す。

var nv n に評価した上で、このような式(Expr の項)を評価する関数を書け。

inductive Expr where
  | const : Nat → Expr
  | var : Nat → Expr
  | plus : Expr → Expr → Expr
  | times : Expr → Expr → Expr
  deriving Repr
open Expr
def sampleExpr : Expr :=
  plus (times (var 0) (const 7)) (times (const 2) (var 1))
def eval (v : Nat → Nat) : Expr → Nat
  | const n     => sorry
  | var n       => v n
  | plus e₁ e₂  => sorry
  | times e₁ e₂ => sorry

def sampleVal : Nat → Nat
  | 0 => 5
  | 1 => 6
  | _ => 0

-- 次のコマンドを実行せよ。`47` が出力されたら正解である。
-- #eval eval sampleVal sampleExpr

補助関数 simpConst を使って、5 + 7 のような部分項を 12 に単純化する「定数融合関数」fuse を実装せよ。plustimes を単純化するために、まず引数を再帰的に単純化せよ。次に simpConst を適用して結果の単純化を試みよ。

inductive Expr where
  | const : Nat → Expr
  | var : Nat → Expr
  | plus : Expr → Expr → Expr
  | times : Expr → Expr → Expr
  deriving Repr
open Expr
def eval (v : Nat → Nat) : Expr → Nat
  | const n     => sorry
  | var n       => v n
  | plus e₁ e₂  => sorry
  | times e₁ e₂ => sorry
def simpConst : Expr → Expr
  | plus (const n₁) (const n₂)  => const (n₁ + n₂)
  | times (const n₁) (const n₂) => const (n₁ * n₂)
  | e                           => e

def fuse : Expr → Expr := sorry

theorem simpConst_eq (v : Nat → Nat)
        : ∀ e : Expr, eval v (simpConst e) = eval v e :=
  sorry

theorem fuse_eq (v : Nat → Nat)
        : ∀ e : Expr, eval v (fuse e) = eval v e :=
  sorry

最後の2つの定理は、simpConstfuse が式の値を保存することを表す。

Structures and Records (構造体とレコード)

Leanの基礎システムは帰納型を含むことを見てきた。さらに、型宇宙、依存関数型、そして帰納型のみで巨大で頑丈な数学の体系を構築できるという驚くべき事実を説明した。それ以外の全てはこの3種類の型から派生するのである。Leanの標準ライブラリには帰納型の具体例(例えば NatProdList)が多数含まれており、論理的結合子でさえも帰納型を用いて定義されている。

コンストラクタを1つだけ持つ非再帰的帰納型はstructure(構造体)またはrecord(レコード)と呼ばれることを思い出してほしい。直積型は構造体であり、依存積型(シグマ型)も同様に構造体である。一般に、構造体 S が定義されるとき、S の各インスタンス(レコード or オブジェクト)を「分解」し、そのフィールドに格納されている値を取り出すことができるprojection(射影)関数も同時に定義することが多い。直積ペアの1番目の要素を返す関数 prod.fst と2番目の要素を返す関数 prod.snd はそのような射影関数の例である。

プログラムを書いたり数学を形式化するとき、多くのフィールドを含む構造を定義することは珍しくない。Leanでは、structure コマンドが構造体の定義をサポートするインフラを提供する。このコマンドを使って構造体を定義すると、Leanは各フィールドに対する射影関数を自動生成する。structure コマンドは、以前に定義した構造体に基づいて新しい構造体を定義することもできる。さらに、Leanは与えられた構造体のインスタンスを定義するための便利な記法も提供する。

Declaring Structures (構造体を定義する)

structure コマンドは、言わば帰納データ型を定義するための「フロントエンド」である。全ての structure 宣言は、構造体に与えられた名前と同じ名前の名前空間を導入する。structure コマンドの構文の一般的な形式は次の通りである:

    structure <name> <parameters> <parent-structures> where
      <constructor> :: <fields>

ほとんどの部分はオプションである。構造体定義の例を挙げる:

structure Point (α : Type u) where
  mk :: (x : α) (y : α)

Point 型の値はコンストラクタ Point.mk a b を使って生成され、点 p のフィールドには Point.x pPoint.y p を使ってアクセスする(以下で見るように、p.xp.y も同様に機能する)。structure コマンドは定義した構造体に関する有用な再帰子や定理も自動生成する。上の Point 型の宣言の際に自動生成されたもののいくつかを以下に挙げる。

structure Point (α : Type u) where
 mk :: (x : α) (y : α)
#check Point       -- a Type
#check @Point.rec  -- the eliminator
#check @Point.mk   -- the constructor
#check @Point.x    -- a projection
#check @Point.y    -- a projection

コンストラクタ名を指定しなかった場合、デフォルトでコンストラクタは mk と名付けられる。また、各フィールドの間に改行を入れると、フィールド名を括弧で囲むのを省略することができる。

structure Point (α : Type u) where
  x : α
  y : α

structure コマンドにより自動生成されたものを使った簡単な定理や式をいくつか紹介しよう。いつものように、open Point コマンドを使えば Point という接頭辞を省略した名前を使えるようになる。

structure Point (α : Type u) where
 x : α
 y : α
#eval Point.x (Point.mk 10 20)
#eval Point.y (Point.mk 10 20)

open Point

example (a b : α) : x (mk a b) = a :=
  rfl

example (a b : α) : y (mk a b) = b :=
  rfl

p : Point Nat が与えられたとき、ドット記法 p.xPoint.x p の略記である。これは構造体のフィールドにアクセスする便利な方法である。

structure Point (α : Type u) where
 x : α
 y : α
def p := Point.mk 10 20

#check p.x  -- Nat
#eval p.x   -- 10
#eval p.y   -- 20

ドット記法はレコードの射影関数にアクセスするときだけでなく、同じ名前空間内で定義された他の関数を適用するときにも便利である。節Conjunction (連言)の内容を思い出してほしい。pPoint 型を持ち、foo の最初の非暗黙引数が Point 型を持つなら、式 p.fooPoint.foo p と解釈される。したがって、次の例のように、式 p.add qPoint.add p q の略記となる。

structure Point (α : Type u) where
  x : α
  y : α
  deriving Repr

def Point.add (p q : Point Nat) :=
  mk (p.x + q.x) (p.y + q.y)

def p : Point Nat := Point.mk 1 2
def q : Point Nat := Point.mk 3 4

#eval p.add q  -- {x := 4, y := 6}

次の章では、型 α に関連する加法演算があるという仮定の下、add のような関数を定義して、それが Point Nat だけでなく Point α の項に対して汎用的に機能するようにする方法を学ぶ。

より一般的には、項 p : Point と式 p.foo x y z が与えられると、Leanは Point.foo の「Point 型の」最初の引数として p を挿入する。例えば、以下のスカラー倍の定義では、p.smul 3Point.smul 3 p と解釈される。

structure Point (α : Type u) where
 x : α
 y : α
 deriving Repr
def Point.smul (n : Nat) (p : Point Nat) :=
  Point.mk (n * p.x) (n * p.y)

def p : Point Nat := Point.mk 1 2

#eval p.smul 3  -- {x := 3, y := 6}

List.map 関数では同様のトリックがよく使われる。List.map 関数は2番目の非暗黙引数としてリストを取る:

#check @List.map

def xs : List Nat := [1, 2, 3]
def f : Nat → Nat := fun x => x * x

#eval xs.map f  -- [1, 4, 9]

ここで、xs.map fList.map f xs と解釈されている。

Objects (オブジェクト)

これまでコンストラクタを使って構造体の項を作成してきた。多くのフィールドを含む構造体の場合、コンストラクタを使って構造体の項を作成する方法は、フィールドが定義された順番を覚えておく必要があるため、しばしば不便である。そこで、Leanでは構造体の項を定義するための次のような代替記法を用意している。(訳者注: * は括弧内が1回以上の繰り返しからなることを表す。実際にこの構文を用いるときに括弧を書く必要はない。)

    { (<field-name> := <expr>)* : structure-type }
    or
    { (<field-name> := <expr>)* }

接尾辞 : structure-type は、期待される構造体の型が与えられた情報から推論できる場合はいつでも省略できる。例えば、Point 型のオブジェクト「点」を定義するためにこの記法を用いる。フィールドを指定する順番は任意であるため、以下の式は全て同じ点を定義する。

structure Point (α : Type u) where
  x : α
  y : α

#check { x := 10, y := 20 : Point Nat }    -- { (<field-name> := <expr>)* : structure-type }
#check { y := 20, x := 10 : Point _ }      -- フィールドを指定する順番は任意
#check ({ x := 10, y := 20 } : Point Nat)  -- { (<field-name> := <expr>)* } 構造体の型が明らか

example : Point Nat :=
  { y := 20, x := 10 }                     -- { (<field-name> := <expr>)* } 構造体の型が明らか

フィールドの値が指定されていない場合、Leanはその値を推論しようとする。指定されていないフィールドの値を推論できなかった場合、Leanは対応するプレースホルダーを埋められなかったことを示すエラーフラグを立てる。

structure MyStruct where
    {α : Type u}
    {β : Type v}
    a : α
    b : β

#check { a := 10, b := true : MyStruct }

Record update(レコード更新)は、古いレコード(オブジェクト)の1つまたは複数のフィールドの値を変更して新しいレコードを作成する、もう1つの一般的操作である。Leanでは、フィールドへの値の割り当ての前に s with という注釈を追加することで、新しいレコード内の値未割り当てのフィールドを古いレコード s から取得するように指示することができる。複数の古いレコードが提供された場合、新しいレコード内でまだ値が指定されていないフィールドを含むレコードを見つけるまで、Leanは提供されたレコードを順番に参照する。全てのオブジェクトを参照した後、新しいレコード内に値未指定のフィールドが存在した場合、Leanはエラーを発生させる。

structure Point (α : Type u) where
  x : α
  y : α
  deriving Repr

def p : Point Nat :=
  { x := 1, y := 2 }

#eval { p with y := 3 }  -- { x := 1, y := 3 }
#eval { p with x := 4 }  -- { x := 4, y := 2 }

structure Point3 (α : Type u) where
  x : α
  y : α
  z : α

def q : Point3 Nat :=
  { x := 5, y := 5, z := 5 }

def r : Point3 Nat :=
  { p, q with x := 6 }

example : r.x = 6 := rfl
example : r.y = 2 := rfl
example : r.z = 5 := rfl

Inheritance (継承)

新しいフィールドを追加することで、既存の構造体をextend(拡張)させることができる。この機能によって、一種のinheritance(継承)をシミュレートすることができる。

structure Point (α : Type u) where
  x : α
  y : α

inductive Color where
  | red | green | blue

structure ColorPoint (α : Type u) extends Point α where
  c : Color

次の例では、多重継承(複数の親構造体を一度に継承すること)を使って新しい構造体 RedGreenPoint を定義し、各親構造体のオブジェクトを使って RedGreenPoint 型のオブジェクトを定義する。

structure Point (α : Type u) where
  x : α
  y : α
  z : α

structure RGBValue where
  red : Nat
  green : Nat
  blue : Nat

structure RedGreenPoint (α : Type u) extends Point α, RGBValue where
  no_blue : blue = 0

def p : Point Nat :=
  { x := 10, y := 10, z := 20 }

def rgp : RedGreenPoint Nat :=
  { p with red := 200, green := 40, blue := 0, no_blue := rfl }

example : rgp.x   = 10 := rfl
example : rgp.red = 200 := rfl

Type Classes (型クラス)

型クラスは、関数型プログラミング言語においてアドホックな多相性を実現する原理的な方法として導入された。まず、次のことを見る: もし関数が型固有の加法の実装を引数として取り、残りの引数に対してその加法の実装を呼び出すだけであれば、加法のようなアドホックな多相性関数を実装するのは簡単である。例えば、Leanで加法の実装を保持する構造体を宣言したとしよう。

namespace Ex
structure Add (a : Type) where
  add : a → a → a

#check @Add.add
-- Add.add : {a : Type} → Add a → a → a → a
-- `Add.add` はstructure宣言によって自動生成される射影関数
end Ex

このLeanコードでは、add フィールドへの射影関数は Add.add : {a : Type} → Add a → a → a → a という型を持っている。ここで、型 a を囲んでいる波括弧は、a が暗黙の引数であることを示している。次のようにして double 関数を実装することができる:

namespace Ex
structure Add (a : Type) where
 add : a → a → a
def double (s : Add a) (x : a) : a :=
  s.add x x

#eval double { add := Nat.add } 10
-- 20

#eval double { add := Nat.mul } 10
-- 100

#eval double { add := Int.add } 10
-- 20
end Ex

double { add := Nat.add } n と書くことで自然数 n を2倍することができる。もちろん、このように手動で実装(を保持するレコード)を渡すのはユーザーにとって非常に面倒である。実際、そのような面倒さがあれば、アドホック多相性の潜在的な利点のほとんどを失うことになる。

型クラスの主な考え方は、まず Add a のような型の引数を暗黙にすることである。それから、ユーザーが定義したインスタンスを保管するデータベースを使用して、typeclass resolution(型クラス解決)として知られるプロセスを通じて、目的のインスタンス {s : Add a} を自動合成することである。Leanでは、上の例で structureclass に書き換えることで、Add.add の型は次のように変化する:

namespace Ex
class Add (a : Type) where
  add : a → a → a

#check @Add.add
-- Add.add : {a : Type} → [self : Add a] → a → a → a
end Ex

ここで、角括弧 []Add a 型の引数がinstance implicit(インスタンス暗黙引数)であること、つまり型クラス解決を使って合成されるべきであることを示している。class 宣言によって自動生成された add 射影関数は、Haskellの add :: Add a => a -> a -> a のLean版である。そして、ユーザー定義インスタンスは次のように登録できる:

namespace Ex
class Add (a : Type) where
 add : a → a → a
instance : Add Nat where
  add := Nat.add

instance : Add Int where
  add := Int.add

instance : Add Float where
  add := Float.add
end Ex

インスタンスの登録後、n : Natm : Nat に対して、Add.add n m という項は、Add Nat 型のインスタンス合成を目標とする型クラス解決を引き起こす。型クラス解決は上で instance 宣言を用いて登録した Add Nat のインスタンスを参照し、目標のインスタンスを合成する。インスタンス暗黙引数を使って、double を再実装することができる:

namespace Ex
class Add (a : Type) where
  add : a → a → a
instance : Add Nat where
 add := Nat.add
instance : Add Int where
 add := Int.add
instance : Add Float where
 add := Float.add
def double [Add a] (x : a) : a :=
  Add.add x x

#check @double
-- @double : {a : Type} → [inst : Add a] → a → a

#eval double 10
-- 20

#eval double (10 : Int)
-- 20

#eval double (7 : Float)
-- 14.000000

#eval double (239.0 + 2)
-- 482.000000

end Ex

一般的に、インスタンスは複雑な方法で他のインスタンスに依存することがある。例えば、「もし a が加法を持つなら、Array a も加法を持つ」と主張する(匿名)インスタンスを宣言することができる:

instance [Add a] : Add (Array a) where
  add x y := Array.zipWith x y (· + ·)

#eval Add.add #[1, 2] #[3, 4]
-- #[4, 6]

#eval #[1, 2] + #[3, 4]
-- #[4, 6]

Leanにおいて (· + ·)fun x y => x + y の略記であることに注意してほしい。

上記の例では、記法をオーバーロード(多重定義)するために型クラスを使う方法を実践した。別の応用例も見てみよう。まず、Leanにおいて、型は項を1つも持たないことがあることを思い出してほしい。しかし、型が有項なら、その型について様々なことができるようになる。Leanを使っていると、ある型の任意の項が必要になることがよくある。例えば、「コーナーケース」において任意の項を返す関数を定義したいと思うことがよくある。また、xsList a 型を持つとき、head xsa 型を持ってほしいと思うかもしれない。同様に、型が空でないという付加的な仮定の下では、多くの付加的な定理が成立する。例えば、a が型であるとき、exists x : a, x = x が成立するためには a が空でないことが必要である。標準ライブラリは、有項型のdefault要素を推論できるようにするために、Inhabited という型クラスを定義している。今述べた応用例を実践するために、まず適切なクラスを宣言することから始めよう:

namespace Ex
class Inhabited (a : Type u) where
  default : a

#check @Inhabited.default
-- Inhabited.default : {a : Type u} → [self : Inhabited a] → a
end Ex

Inhabited.default は明示的な引数を持たないことに注意してほしい。

Inhabited a クラスの項とは、ある項 x : a に対する Inhabited.mk x という形の式である。射影 Inhabited.default を使えば、Inhabited a の項から a の項 x を「抽出」することができる。次に、このクラスにいくつかのインスタンスを登録する:

namespace Ex
class Inhabited (a : Type _) where
 default : a
instance : Inhabited Bool where
  default := true

instance : Inhabited Nat where
  default := 0

instance : Inhabited Unit where
  default := ()

instance : Inhabited Prop where
  default := True

#eval (Inhabited.default : Nat)
-- 0

#eval (Inhabited.default : Bool)
-- true
end Ex

export コマンドを使うと、Inhabited.default に対して別名 default を生成することができる(正確には、export コマンドは名前空間 Foo 内で Bar.baz に対して別名 Foo.baz を生成する)。

namespace Ex
class Inhabited (a : Type _) where
 default : a
instance : Inhabited Bool where
 default := true
instance : Inhabited Nat where
 default := 0
instance : Inhabited Unit where
 default := ()
instance : Inhabited Prop where
 default := True
export Inhabited (default)

#eval (default : Nat)
-- 0

#eval (default : Bool)
-- true
end Ex

Chaining Instances (インスタンスの連鎖)

型クラス推論で出来ることがこれで終わりだとしたら、それほど印象的なものではないだろう。もしそうなら、型クラス推論はユーザー定義インスタンスを保存して、elaboratorがルックアップテーブル(配列や連想配列などのデータ構造)からそれらを見つけられるようにする仕組みに過ぎないからである。型クラス推論が強力なのは、インスタンスを「連鎖」させることができるからである。つまり、インスタンス宣言は、他の型クラスの暗黙のインスタンスに依存することができる。これにより、型クラス推論は、Prolog-likeな探索を用いて、必要に応じてバックトラッキングしながら、再帰的にインスタンスを連鎖させることができる。

例えば、次の定義は、2つの型 ab が有項なら、その直積型 a × b も有項であることを示している:

instance [Inhabited a] [Inhabited b] : Inhabited (a × b) where
  default := (default, default)

前節のインスタンス宣言に今の宣言を加えることで、例えば Nat × Bool のデフォルト項を推論できるようになる:

namespace Ex
class Inhabited (a : Type u) where
 default : a
instance : Inhabited Bool where
 default := true
instance : Inhabited Nat where
 default := 0
opaque default [Inhabited a] : a :=
 Inhabited.default
instance [Inhabited a] [Inhabited b] : Inhabited (a × b) where
  default := (default, default)

#eval (default : Nat × Bool)
-- (0, true)
end Ex

同様に、適切な定数関数の存在により、型 b が有項なら関数型 a → b も有項であることを示すことができる:

instance [Inhabited b] : Inhabited (a → b) where
  default := fun _ => default

練習として、List 型や Sum 型などの他の型のデフォルトインスタンスを定義してみよう。

Leanの標準ライブラリには inferInstance という定義がある。これは型 {α : Sort u} → [i : α] → α を持ち、期待される型がインスタンスを持つときに型クラス解決手続きを実行させるのに便利である。

#check (inferInstance : Inhabited Nat) -- Inhabited Nat

def foo : Inhabited (Nat × Nat) :=
  inferInstance

#eval foo.default  -- (0, 0)

theorem ex : foo.default = (default, default) :=
  rfl

#print コマンドを使うと、inferInstance がいかにシンプルかを見ることができる。

#print inferInstance

ToString (ToString 型クラス)

ToString 型クラスの多相メソッド toString は型 {α : Type u} → [ToString α] → α → String を持つ。ユーザー定義の型 Foo に対して型 ToString Foo のインスタンスを宣言すると、連鎖を利用して複雑な値を文字列に変換することができる。Leanでは、ほとんどのビルトイン型 α について ToString α のインスタンスが付属している。

structure Person where
  name : String
  age  : Nat

instance : ToString Person where
  toString p := p.name ++ "@" ++ toString p.age

#eval toString { name := "Leo", age := 542 : Person }
#eval toString ({ name := "Daniel", age := 18 : Person }, "hello")  -- `instToStringProd` と `instToStringPerson` の連鎖

Numerals (数字)

Leanでは数字は多相である。型クラス OfNat に関するインスタンスを持つ任意の型の項を表すために、数字(例えば 2)を使うことができる。

structure Rational where
  num : Int
  den : Nat
  inv : den ≠ 0

instance instOfNatRational (n : Nat) : OfNat Rational n where
  ofNat := { num := n, den := 1, inv := by decide }

instance : ToString Rational where
  toString r := s!"{r.num}/{r.den}"

#eval (2 : Rational) -- 2/1

#check (2 : Rational) -- Rational
#check (2 : Nat)      -- Nat

#check @OfNat.ofNat Nat 2 (instOfNatNat 2)  -- Nat
#check @OfNat.ofNat Rational 2 (instOfNatRational 2)  -- Rational

Leanのelaboratorは、項 (2 : Nat)(2 : Rational) をそれぞれ @OfNat.ofNat Nat 2 (instOfNatNat 2)@OfNat.ofNat Rational 2 (instOfNatRational 2) に変換する。変換後の項中に出現する数字 2 は「生の」自然数と呼ばれる。マクロ nat_lit 2 を使うと生の自然数 2 を入力することができる。

#check nat_lit 2  -- Nat

生の自然数は多相ではない

OfNat インスタンスは生の自然数を引数に取る。そのため、特定の数字に対して OfNat のインスタンスを定義することができる。OfNat 型クラスの2番目の引数は、上の例のように変数であることが多いが、生の自然数の場合もある。

class Monoid (α : Type u) where
  unit : α
  op   : α → α → α

instance [s : Monoid α] : OfNat α (nat_lit 1) where
  ofNat := s.unit

def getUnit [Monoid α] : α :=
  1

Output Parameters (出力パラメータ)

デフォルトでは、Leanは型 T のデフォルト項が既知であり、T が欠落部分を含まない場合にのみ、Inhabited T のインスタンスを合成しようとする。次のコマンドは、型が欠落部分(つまり _)を含むため、"failed to create type class instance for Inhabited (Nat × ?m.1499)" というエラーを生成する。

#check_failure (inferInstance : Inhabited (Nat × _))

Inhabited 型クラスのパラメータは、Inhabited 型クラスのコンストラクタの「入力」の型とみなすことができる。型クラスが複数のパラメータを持つ場合、そのうちのいくつかを出力パラメータ(型クラスのインスタンス合成時に、既に与えられた型から推論される型)としてマークすることができる。出力パラメータの推論のために使われる型は入力パラメータと呼ばれる。出力パラメータに欠落部分があっても、Leanは型クラスのインスタンス合成を開始する。以下の例では、出力パラメータを使ってheterogeneous(異種)多相乗法を定義している。

namespace Ex
class HMul (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hMul : α → β → γ

export HMul (hMul)

instance : HMul Nat Nat Nat where
  hMul := Nat.mul

instance : HMul Nat (Array Nat) (Array Nat) where
  hMul a bs := bs.map (fun b => hMul a b)

#eval hMul 4 3           -- 12
#eval hMul 4 #[2, 3, 4]  -- #[8, 12, 16]
end Ex

パラメータ αβ は入力パラメータ、γ は出力パラメータとみなされる。関数適用 hMul a b が与えられると、ab の型が分かっているなら型クラスインスタンス合成器が呼び出され、型クラスインスタンス合成器は hMul a b の出力の型情報を(入力パラメータ αβ から推論された)出力パラメータ γ から得る。上の例では、2つのインスタンスを定義した。最初のインスタンスは自然数上の同種乗法である。2つ目のインスタンスは配列のスカラー倍である。2つ目のインスタンスの定義では、インスタンスの連鎖が使われていることに注意してほしい。

namespace Ex
class HMul (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hMul : α → β → γ

export HMul (hMul)

instance : HMul Nat Nat Nat where
  hMul := Nat.mul

instance : HMul Int Int Int where
  hMul := Int.mul

instance [HMul α β γ] : HMul α (Array β) (Array γ) where
  hMul a bs := bs.map (fun b => hMul a b)

#eval hMul 4 3                    -- 12
#eval hMul 4 #[2, 3, 4]           -- #[8, 12, 16]
#eval hMul (-2) #[3, -1, 4]       -- #[-6, 2, -8]
#eval hMul 2 #[#[2, 3], #[0, 4]]  -- #[#[4, 6], #[0, 8]]
end Ex

この新しい配列スカラー倍インスタンスは、HMul α β γ のインスタンスがあれば、いつでも Array β 型の配列と α 型のスカラーに対して使用することができる。最後の #eval では、HMul Nat Nat Nat のインスタンスから HMul Nat (Array Nat) (Array Nat) のインスタンスが合成され、HMul Nat (Array Nat) (Array Nat) のインスタンスから HMul Nat (Array (Array Nat)) (Array (Array Nat)) のインスタンスが合成されていることに注意してほしい。

Default Instances (デフォルトインスタンス)

型クラス HMul では、パラメータ αβ は入力パラメータとして扱われる。したがって、型クラスインスタンス合成はこれら2つの型が特定されてから開始される。場合によっては、この制約は強すぎるかもしれない。

namespace Ex
class HMul (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hMul : α → β → γ

export HMul (hMul)

instance : HMul Int Int Int where
  hMul := Int.mul

def xs : List Int := [1, 2, 3]

-- Error "typeclass instance problem is stuck, it is often due to metavariables HMul ?m.89 ?m.90 ?m.91"
-- `y` の型を明示的に与えればエラーは消える
#check_failure fun y => xs.map (fun x => hMul x y)
end Ex

y の型が与えられていないため、HMul のインスタンスはLeanによって合成されない。しかし、このような状況では yx の型は同じはずだと考えるのが自然である。default instances(デフォルトインスタンス)を使えば、まさにそれを実現できる。

namespace Ex
class HMul (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hMul : α → β → γ

export HMul (hMul)

@[default_instance]
instance : HMul Int Int Int where
  hMul := Int.mul

def xs : List Int := [1, 2, 3]

#check fun y => xs.map (fun x => hMul x y)    -- Int → List Int
#eval (fun y => xs.map (fun x => hMul x y)) 3 -- [3, 6, 9]
end Ex

上記のインスタンスに default_instance 属性を付けることで、保留中の型クラスインスタンス合成問題においてこのインスタンスを使用するようLeanに指示することができる。実際のLeanの実装では、各算術演算子について同種型クラス(Add など)と異種型クラス(HAdd など)が定義されている。さらに言うと、a+ba*ba-ba/ba%b は異種版演算子を表す。OfNat Nat n のインスタンスは OfNat 型クラスのデフォルトインスタンス(優先度100)である。これが、期待される型が不明な場合に、数字 2Nat 型を持つ理由である。より高い優先度を持つデフォルトインスタンスを定義することで、ビルトインのデフォルトインスタンスをオーバーライドすることができる。

structure Rational where
  num : Int
  den : Nat
  inv : den ≠ 0

@[default_instance 200]
instance : OfNat Rational n where
  ofNat := { num := n, den := 1, inv := by decide }

instance : ToString Rational where
  toString r := s!"{r.num}/{r.den}"

#check 2 -- Rational

優先度は、異なるデフォルトインスタンス間の相互作用を制御するのにも便利である。例えば、xsList α 型を持つとする。elaboratorが式 xs.map (fun x => 2 * x) を解釈するとき、この式が多相性を持つために、同種乗法のデフォルトインスタンスが OfNat のデフォルトインスタンスより高い優先度を持つようにしたい。これは、Hmul α α α のインスタンスを実装し、HMul Nat α α のインスタンスは実装しなかった場合に特に重要である。ここで、Leanにおいて表記 a*b がどう定義されているかを種明かしする。

namespace Ex
class OfNat (α : Type u) (n : Nat) where
  ofNat : α

@[default_instance]
instance (n : Nat) : OfNat Nat n where
  ofNat := n

class HMul (α : Type u) (β : Type v) (γ : outParam (Type w)) where
  hMul : α → β → γ

class Mul (α : Type u) where
  mul : α → α → α

@[default_instance 10]
instance [Mul α] : HMul α α α where
  hMul a b := Mul.mul a b

infixl:70 " * " => HMul.hMul
end Ex

Mul 型クラスは、同種乗法は実装されているが異種乗法は実装されていない型を扱う際に便利である。

Local Instances (ローカルインスタンス)

Leanにおいて、型クラスのインスタンスは属性を用いて実装される。そのため、local 修飾子を使うことで、そのインスタンスが現在のセクションや名前空間が閉じられるまで、あるいは現在のファイルの終わりまでしか効果がないことを示すことができる。

structure Point where
  x : Nat
  y : Nat

section

local instance : Add Point where
  add a b := { x := a.x + b.x, y := a.y + b.y }

def double (p : Point) :=
  p + p

end -- もうインスタンス `Add Point` は使えない

-- def triple (p : Point) :=
--  p + p + p  -- Error: failed to synthesize instance

コマンド attribute [-instance] を使えば、現在のセクションや名前空間が閉じられるまで、あるいは現在のファイルの終わりまで、指定したインスタンスを一時的に無効化することもできる。

structure Point where
  x : Nat
  y : Nat

instance addPoint : Add Point where
  add a b := { x := a.x + b.x, y := a.y + b.y }

def double (p : Point) :=
  p + p

attribute [-instance] addPoint

-- def triple (p : Point) :=
--  p + p + p  -- Error: failed to synthesize instance

コマンド attribute [-instance] は問題の分析時にのみ使うことを勧める。

Scoped Instances (スコープ付きインスタンス)

scoped instance を用いてスコープ付きのインスタンスを宣言することもできる。スコープ付きインスタンスは、名前空間の中にいるとき、または名前空間を開いているときのみ使用可能である。

structure Point where
  x : Nat
  y : Nat

namespace Point

scoped instance : Add Point where
  add a b := { x := a.x + b.x, y := a.y + b.y }

def double (p : Point) :=
  p + p

end Point
-- インスタンス `Add Point` はもう使えない

-- #check fun (p : Point) => p + p + p  -- Error

namespace Point
-- インスタンス `Add Point` は再び利用可能になった
#check fun (p : Point) => p + p + p

end Point

open Point -- 名前空間を開き、インスタンス `Add Point` を利用可能にする
#check fun (p : Point) => p + p + p

コマンド open scoped <namespace> を使うと、名前空間 <namespace> 内の scoped 属性のついた定義が利用可能になるが、名前空間 <namespace> 内のそれ以外の定義に短い別名でアクセスすることはできない。

structure Point where
  x : Nat
  y : Nat

namespace Point

scoped instance : Add Point where
  add a b := { x := a.x + b.x, y := a.y + b.y }

def double (p : Point) :=
  p + p

end Point

open scoped Point -- インスタンス `Add Point` を利用可能にする
#check fun (p : Point) => p + p + p

-- #check fun (p : Point) => double p -- Error: unknown identifier 'double'

Decidable Propositions (決定可能命題)

標準ライブラリで定義されている、命題に関する型クラス Decidable について考えてみよう。大雑把に言えば、Prop 型の項(具体的な命題)は、それが真か偽かを決めることができる場合、決定可能であると言われる。古典論理において全ての命題は決定可能であるため、命題が決定可能かどうかという区別は構成的論理においてのみ有用である。古典論理を使っているかどうかの区別は重要である。古典論理を使って、例えば場合分けによって関数を定義すると、その関数は計算不能になる。

variable (p : Nat → Prop)

-- error : failed to synthesize instance
--   Decidable (p n)
/-
def bad_foo : Nat → Bool :=
  fun (n : Nat) =>
  if p n then true
  else false
-/

open Classical

noncomputable def foo : Nat → Bool :=
  fun (n : Nat) =>
  if p n then true
  else false

#print axioms foo
-- 'foo' depends on axioms: [Classical.choice, Quot.sound, propext]

アルゴリズム的に言えば、Decidable 型クラスは、その命題が真か偽かを効果的に決定する手続きを推論するために使うことができる。結果として、この型クラスはある定義が計算可能なときその定義をサポートすると同時に、古典論理を使った定義や推論の使用へのスムーズな移行を可能にする。

標準ライブラリでは、Decidable は次のように形式的に定義されている:

namespace Hidden
class inductive Decidable (p : Prop) where
  | isFalse (h : ¬p) : Decidable p
  | isTrue  (h : p)  : Decidable p
end Hidden

論理的に言えば、項 t : Decidable p を持つことは、証明項 t : p ∨ ¬p を持つことよりも強い。項 t : Decidable p の存在は、p の真理値に依存して任意の型の値や関数を定義することを可能にする。例えば、if p then a else b という式が意味をなすには、p が決定可能であることを知っている必要がある。ここで if p then a else bite p a b の糖衣構文である。ite は次のように定義される:

namespace Hidden
def ite {α : Sort u} (c : Prop) [h : Decidable c] (t e : α) : α :=
  Decidable.casesOn (motive := fun _ => α) h (fun _ => e) (fun _ => t)
end Hidden

標準ライブラリは dite と呼ばれる ite の変形版を持っている。dite は依存版if-then-else式である。これは次のように定義される:

namespace Hidden
def dite {α : Sort u} (c : Prop) [h : Decidable c] (t : c → α) (e : Not c → α) : α :=
  Decidable.casesOn (motive := fun _ => α) h e t
end Hidden

つまり、dite c t e において、「then」分岐では hc : c を、「else」分岐では hnc : ¬ c を仮定できる。Leanでは、dite をより使いやすくするために、dite c (λ h : c => t) (λ h : ¬ c => e) の代わりに if h : c then t else e と書くことができる。

古典論理がなければ、全ての命題が決定可能であることを証明することはできない。しかし、ある命題が決定可能であることは証明できる。例えば、自然数や整数に関する等式や不等式のような基本関係の決定可能性は古典論理なしで証明できる。さらに、決定可能性は命題結合子の適用前後で保存される:

#check @instDecidableAnd
  -- {p q : Prop} → [Decidable p] → [Decidable q] → Decidable (And p q)

#check @instDecidableOr
#check @instDecidableNot

したがって、自然数上の決定可能述語を用いた場合分けによって関数を定義することができる:

def step (a b x : Nat) : Nat :=
  if x < a ∨ x > b then 0 else 1

set_option pp.explicit true  -- 暗黙の引数を表示する
#print step

暗黙の引数の表示をオンにすると、elaboratorは適切なインスタンス instDecidableOrNat.decLt を適用しただけで、命題 x < a ∨ x > b の決定可能性を推論したことがわかる。

古典論理の公理を使うと、全ての命題が決定可能であることが証明できる。Classical 名前空間を開くと、古典論理の公理がインポートされ、全ての命題 p に対して Decidable p のインスタンスが利用できるようになる。

open Classical

したがって、古典論理的に推論したい場合、決定可能性を前提とするライブラリ内の定理は、全て自由に利用できる。12章 Axioms and Computation (公理と計算)では、排中律を使って関数を定義すると、その関数が計算に使われなくなることがあることを説明する。したがって、標準ライブラリでは Classical.propDecidable インスタンスに低い優先度を割り当てている。

namespace Hidden
open Classical
noncomputable scoped
instance (priority := low) propDecidable (a : Prop) : Decidable a :=
  choice <| match em a with
    | Or.inl h => ⟨isTrue h⟩
    | Or.inr h => ⟨isFalse h⟩
end Hidden

これは、Decidable p の型クラス解決において、Leanが他のインスタンスを優先し、他の試みが全て失敗した後にのみインスタンス propDecidable p を使うことを保証する。

Decidable 型クラスは、定理証明の小規模な自動化もいくつか提供している。標準ライブラリには、Decidable のインスタンスを使って単純なゴールを解くタクティク decide が含まれている。

example : 10 < 5 ∨ 1 > 0 := by
  decide

example : ¬ (True ∧ False) := by
  decide

example : 10 * 20 = 200 := by
  decide

theorem ex : True ∧ 2 = 1+1 := by
  decide

#print ex
-- theorem ex : True ∧ 2 = 1 + 1 :=
-- of_decide_eq_true (Eq.refl true)

#check @of_decide_eq_true
-- ∀ {p : Prop} [Decidable p], decide p = true → p

#check @decide
-- (p : Prop) → [Decidable p] → Bool

これらは次のように動作する。式 decide pp の決定可能性を用いて p の真偽決定手続きの推論を試み、成功すれば decide ptruefalse のどちらかに評価される。特に、p が正しい閉論理式である場合、decide p はdefinitionallyにブール値 true に簡約される。decide p = true が成立するという前提を受け取ると、of_decide_eq_truep の証明を出力する。ターゲット p を証明するために以上の過程をまとめたものが decide タクティクである。前述した内容により、decide は、推論された c の真偽決定手続きが、cisTrue の場合であることをdefinitionallyに評価するために十分な情報を持っていれば、いつでも成功する。

Managing Type Class Inference (型クラス推論の管理)

Leanの型クラス推論によって合成できる式 t を提供する必要があるとき、inferInstance を使うと t を推論によって合成するようLeanに依頼することができる:

def foo : Add Nat := inferInstance
def bar : Inhabited (Nat → Nat) := inferInstance

#check @inferInstance
-- {α : Sort u} → [α] → α

Leanの (t : T) 記法を使えば、今探している t がどの型クラス T のインスタンスなのかを簡潔に指定することができる:

#check (inferInstance : Add Nat)

T を引数に取る補助定義 inferInstanceAs を使うこともできる:

#check inferInstanceAs (Add Nat)

#check @inferInstanceAs
-- (α : Sort u) → [α] → α

型クラスが定義の下に埋もれているために、Leanがインスタンスを見つけられないことがある。例えば、単に inferInstance を使うだけで Inhabited (Set α) のインスタンスを見つけることはできない。この場合、定義を使って Set αα → Prop に書き下し、それを明示的に与えればよい:

def Set (α : Type u) := α → Prop

-- fails
-- example : Inhabited (Set α) :=
--  inferInstance

instance : Inhabited (Set α) :=
  inferInstanceAs (Inhabited (α → Prop))

-- 別解
example : Inhabited (Set α) :=
  @inferInstance (Inhabited (α → Prop)) _

時には、型クラス推論が期待されるインスタンスを見つけることに失敗したり、最悪の場合、無限ループに陥ってタイムアウトすることがある。このような状況でのデバッグを手伝ってもらうために、Leanにインスタンス探索の追跡を依頼することができる:

set_option trace.Meta.synthInstance true

VSCodeを使用している場合は、関連する定理や定義にカーソルを合わせるか、Ctrl-Shift-Enter によりメッセージウィンドウを開くことで、追跡結果を読むことができる。Emacsでは、C-c C-x によりファイルと独立したLeanプロセスを実行することができる。その後、出力バッファには型クラス解決手順が起きるたびに追跡内容が表示される。

次のオプションを使ってインスタンス探索を制限することもできる:

set_option synthInstance.maxHeartbeats 10000
set_option synthInstance.maxSize 400

synthInstance.maxHeartbeats オプションは、型クラス解決問題ごとのheartbeatsの最大量を指定する。heartbeatsとは(小さな)メモリ割り当ての数(1000単位)であり、synthInstance.maxHeartbeats 0 は制限がないことを意味する。synthInstance.maxSize オプションは、型クラスインスタンス合成過程で解を構築するために使われるインスタンスの最大数を指定する。

VSCodeでもEmacsでも、set_option の中でタブ補完が機能することを覚えてほしい。タブ補完は適切なオプションを探すのに役立つ。

上述したように、与えられたコンテキストでの型クラスインスタンス合成はProlog-likeなプログラムであり、これはバックトラック探索を生じさせる。プログラムの効率も発見される解も、システムがインスタンスを探索する順番に依存して変化する。探索においては、最後に宣言されたインスタンスから順番に探索される。さらに、インスタンスが他のモジュール(ファイル)で宣言されている場合、インスタンスが探索される順番は名前空間を開いた順番に依存する。後に開いた名前空間で宣言されたインスタンスほど先に探索される。

型クラスのインスタンスに「優先度」を割り当てることで、探索される順番を変更することができる。普通にインスタンスが宣言されると、そのインスタンスにはデフォルトの優先度が割り当てられる。インスタンスを定義するとき、デフォルト以外の優先度を割り当てることができる。次の例はその方法を示している:

class Foo where
  a : Nat
  b : Nat

instance (priority := default+1) i1 : Foo where
  a := 1
  b := 1

instance i2 : Foo where
  a := 2
  b := 2

example : Foo.a = 1 :=
  rfl

instance (priority := default+2) i3 : Foo where
  a := 3
  b := 3

example : Foo.a = 3 :=
  rfl

Coercions using Type Classes (型クラスを用いた強制)

最も基本的なタイプの(型)強制は、ある型の項を別の型の項にマッピングする。例えば、Nat 型から Int 型への強制は、任意の項 n : NatInt の項とみなすことを可能にする。しかし、いくつかの強制はより複雑で、パラメータに依存する。例えば、任意の型 α に対して、任意の項 as : List αSet α の項、つまりリストに出現する α の項全体からなる集合とみなすことが可能である。これに対応する強制は、α によってパラメータ化された型の「族」List α 上で定義される。

Leanでは3種類の強制を宣言することができる:

  • ある型の族から他の型の族への強制
  • ある型の族からSortのクラスへの強制
  • ある型の族から関数型のクラスへの強制

1種類目の強制は、強制元の族に属する型の任意の「項」を、強制先の族に属する対応する型の「項」として見ることを可能にする。2種類目の強制は、強制元の族に属する型の任意の「項」を「型」として見ることを可能にする。3種類目の強制は、強制元の族に属する型の任意の「項」を「関数」として見ることを可能にする。それぞれを順番に考えてみよう:

instance : Coe Bool Prop where
  coe b := b = true

この強制により、if-then-else式の条件の中でブール型の項を使うことができる:

#eval if true then 5 else 3
#eval if false then 5 else 3

List α から Set α への強制は次のように定義される:

def Set (α : Type u) := α → Prop
def Set.empty {α : Type u} : Set α := fun _ => False
def Set.mem (a : α) (s : Set α) : Prop := s a
def Set.singleton (a : α) : Set α := fun x => x = a
def Set.union (a b : Set α) : Set α := fun x => a x ∨ b x
notation "{ " a " }" => Set.singleton a
infix:55 " ∪ " => Set.union
def List.toSet : List α → Set α
  | []    => Set.empty
  | a::as => {a} ∪ as.toSet

instance : Coe (List α) (Set α) where
  coe a := a.toSet

def s : Set Nat := {1}
#check s ∪ [2, 3]
-- s ∪ List.toSet [2, 3] : Set Nat

特定の場所に明示的に強制を導入するために、 という記法を使うことができる。この記法は書き手の意図を明確にすることや、強制解決システムの制限を回避することにも役立つ。

def Set (α : Type u) := α → Prop
def Set.empty {α : Type u} : Set α := fun _ => False
def Set.mem (a : α) (s : Set α) : Prop := s a
def Set.singleton (a : α) : Set α := fun x => x = a
def Set.union (a b : Set α) : Set α := fun x => a x ∨ b x
notation "{ " a " }" => Set.singleton a
infix:55 " ∪ " => Set.union
def List.toSet : List α → Set α
  | []    => Set.empty
  | a::as => {a} ∪ as.toSet
instance : Coe (List α) (Set α) where
  coe a := a.toSet
def s : Set Nat := {1}

#check let x := ↑[2, 3]; s ∪ x
-- let x := List.toSet [2, 3]; s ∪ x : Set Nat
#check let x := [2, 3]; s ∪ x
-- let x := [2, 3]; s ∪ List.toSet x : Set Nat

Leanは CoeDep 型クラスを使った依存強制もサポートしている。例えば、Prop 型の任意の項 p をBool型の項に強制することはできないが、Decidable p 型クラスがインスタンスを持つような p だけはBool型の項に強制できる。

instance (p : Prop) [Decidable p] : CoeDep Prop p Bool where
  coe := decide p

Leanは必要に応じて(非依存)強制を連鎖させる。実際、型クラス CoeTCoe の推移的閉包である。

次に、2種類目の強制について考えよう。「Sortのクラス」とは、宇宙 Type u の集まりを意味する。2種類目の強制は次のような形をとる:

    c : (x1 : A1) → ... → (xn : An) → F x1 ... xn → Type u

ここで、F は型の族であり、F x1 ... xn は型の族に属する特定の型である。この種類の強制により、t が型 F a1 ... an の項であるときは、いつでも s : t と書くことができる。言い換えれば、この強制は F a1 ... an の項を型として見ることを可能にする。これは、構造体の1つの要素、つまり構造の台(集合) carrier が型であるような代数的構造を定義するときに非常に便利である。例えば、semigroup(半群)は次のように定義できる:

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc (a b c : carrier) : mul (mul a b) c = mul a (mul b c)

instance (S : Semigroup) : Mul S.carrier where
  mul a b := S.mul a b

つまり、半群は型 carrier、乗法 mul、「乗法は結合的である」という性質の3要素からなる。上記の instance コマンドは、a b : S.carrier があるとき、Semigroup.mul S a ba * b と略記することを可能にする。Leanは ab の型からインスタンスの引数 S を推測できることに注意してほしい。関数 Semigroup.carrier はクラス Semigroup の項(半群)を Type u の項(型)にマッピングする:

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc (a b c : carrier) : mul (mul a b) c = mul a (mul b c)
instance (S : Semigroup) : Mul S.carrier where
  mul a b := S.mul a b
#check Semigroup.carrier

この関数は強制であると宣言すれば、半群 S : Semigroup があるときはいつでも、a : S.carriera : S と略記することができる:

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc (a b c : carrier) : mul (mul a b) c = mul a (mul b c)
instance (S : Semigroup) : Mul S.carrier where
  mul a b := S.mul a b
instance : CoeSort Semigroup (Type u) where
  coe s := s.carrier

example (S : Semigroup) (a b c : S) : (a * b) * c = a * (b * c) :=
  Semigroup.mul_assoc _ a b c

上記の CoeSort Semigroup (Type u) のインスタンスは、(a b c : S) と書くことを可能にする強制である。ここで、2種類目の強制では Coe Semigroup (Type u) ではなく CoeSort Semigroup (Type u) のインスタンスを定義したことに注意してほしい。

最後に、3種類目の強制について考えよう。「関数型のクラス」とは、依存関数型(Π型) (z : B) → C の集まりを意味する。3種類目の強制は次のような形をとる:

    c : (x1 : A1) → ... → (xn : An) → (y : F x1 ... xn) → (z : B) → C

ここで、F は再び型の族であり、F x1 ... xn は型の族に属する特定の型である。BCx1, ..., xn, y に依存することができる。この種類の強制により、tF a1 ... an の項であるときは、いつでも t s と書くことができる。言い換えれば、この強制は F a1 ... an の項を関数として見ることを可能にする。上記の例の続きとして、半群 S1S2 の間のmorphism(射)あるいはhomomorphism(準同型)という概念を定義できる。射とは、S1 の台から S2 の台への、乗法を保存する(mor (a * b) = (mor a) * (mor b))関数である。次のコードでは、S1S2* に関する暗黙の強制に注意してほしい。射影 Morphism.mor は、構造体 Morphism S1 S2 の項を受け取り、射の台関数を返す:

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc (a b c : carrier) : mul (mul a b) c = mul a (mul b c)
instance (S : Semigroup) : Mul S.carrier where
  mul a b := S.mul a b
instance : CoeSort Semigroup (Type u) where
  coe s := s.carrier
structure Morphism (S1 S2 : Semigroup) where
  mor : S1 → S2
  resp_mul : ∀ a b : S1, mor (a * b) = (mor a) * (mor b)

#check @Morphism.mor

以下のコードにより、半群間の射は3種類目の強制 CoeFun の代表例となった。

structure Semigroup where
  carrier : Type u
  mul : carrier → carrier → carrier
  mul_assoc (a b c : carrier) : mul (mul a b) c = mul a (mul b c)
instance (S : Semigroup) : Mul S.carrier where
  mul a b := S.mul a b
instance : CoeSort Semigroup (Type u) where
  coe s := s.carrier
structure Morphism (S1 S2 : Semigroup) where
  mor : S1 → S2
  resp_mul : ∀ a b : S1, mor (a * b) = (mor a) * (mor b)
instance (S1 S2 : Semigroup) : CoeFun (Morphism S1 S2) (fun _ => S1 → S2) where
  coe m := m.mor

theorem resp_mul {S1 S2 : Semigroup} (f : Morphism S1 S2) (a b : S1)
        : f (a * b) = f a * f b :=
  f.resp_mul a b

example (S1 S2 : Semigroup) (f : Morphism S1 S2) (a : S1) :
      f (a * a * a) = f a * f a * f a :=
  calc f (a * a * a)
    _ = f (a * a) * f a := by rw [resp_mul f]
    _ = f a * f a * f a := by rw [resp_mul f]

この強制を使えば、f.mor (a * a * a)f (a * a * a) と略記することができる。関数型が期待される場所で Morphismf が使われると、Leanは強制を挿入する。フィールド F は強制先の関数型を指定するために使われる。F の型は強制元の型に依存しうる。

まとめると、1種類目の強制のために型クラス Coe があり、2種類目の強制のために型クラス CoeSort があり、3種類目の強制のために型クラス CoeFun がある。

The Conversion Tactic Mode (変換タクティクモード)

タクティクブロックの内部では、conv キーワードを使うとconversion mode(変換モード)に入ることができる。このモードでは、仮説やターゲットの内部、さらには関数抽象や依存関数型の内部を移動して、その部分項に書き換えや単純化のステップを適用することができる。

用語に関する注意

この節は翻訳に際して追加した節である。

変換モードにおいて、「ターゲット」とは項構築の目標となる型のことではなく、書き換え・単純化の対象のことである。また、「ゴール」は単に1つのターゲットを保持するためにある。

通常のタクティクモードでは、ターゲットは の後に書かれる。一方で、変換タクティクモードでは、ターゲットは | の後に書かれる。

Basic navigation and rewriting (基本的なナビゲーションと書き換え)

最初の例として、example (a b c : Nat) : a * (b * c) = a * (c * b) を証明してみよう(このページで挙げる例は、他のタクティクを使えばすぐに解ける可能性があるため、多少作為的である)。素朴な最初の一手は、タクティクモードに入って rw [Nat.mul_comm] を試すことである。しかし、そうするとターゲット内の一番最初に登場する乗法について可換性が適用され、ターゲットが b * c * a = a * (c * b) に変換されてしまう。この問題を解決する方法はいくつかあるが、より正確なツールである変換モードを使用することは解決法の1つである。次のコードブロックでは、タクティクブロック内の各行の後に現在のターゲットを示している。

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv =>
    -- ⊢ a * (b * c) = a * (c * b)
    lhs
    -- ⊢ a * (b * c)
    congr
    -- 2 goals: ⊢ a, ⊢ b * c
    rfl
    -- ⊢ b * c
    rw [Nat.mul_comm]

上記のスニペット(小さなコード)では次の3つのナビゲーションコマンドを使っている:

  • lhs は関係(ここでは等号)の左辺にターゲットを絞る。関係の右辺にターゲットを絞る rhs コマンドもある。
  • 現在のターゲット内の先頭の関数(ここでは乗法)が(非依存的かつ明示的な)引数をとるなら、congr は先頭の関数を分解し、各引数をターゲットとするゴールを引数の数だけ生成する。
  • rfl は現在のターゲットを反射性を使って閉じ、次のゴールに移る。

目的のターゲットに到着したら、通常のタクティクモードと同様に rw を使うことができる。

変換モードを使う2つ目の主な理由は、束縛のスコープ内で部分項を書き換えることができるからである。例えば、(fun x : Nat => 0 + x) = (fun x => x) を証明したいとしよう。素朴な最初の一手は、タクティクモードに入って rw [Nat.zero_add] を試すことである。しかし、これは失敗し、次のようなエラーメッセージを見ていらいらするだろう。

error: tactic 'rewrite' failed, did not find instance of the pattern
       in the target expression
  0 + ?n
⊢ (fun x => 0 + x) = fun x => x

正しい解法(の一例)はこうである:

example : (fun x : Nat => 0 + x) = (fun x => x) := by
  conv =>
    lhs
    intro x
    rw [Nat.zero_add]

ここで、intro xfun の束縛スコープ内に入るナビゲーションコマンドである。この例は多少作為的であることを断っておく。この例は次のように解くこともできる:

example : (fun x : Nat => 0 + x) = (fun x => x) := by
  funext x; rw [Nat.zero_add]

あるいは、simp を使うだけでよい。

example : (fun x : Nat => 0 + x) = (fun x => x) := by
  simp

conv at h を使うと、現在のコンテキスト内の仮説 h を書き換えることもできる。

Pattern matching (パターンマッチング)

上記のコマンドを使ったナビゲーションは面倒だと思うかもしれない。パターンマッチングを使えば、次のようにショートカットできる:

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv in b * c => rw [Nat.mul_comm]

これは次の糖衣構文である:

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv =>
    pattern b * c
    rw [Nat.mul_comm]

もちろん、ワイルドカードも使える:

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv in _ * c => rw [Nat.mul_comm]

Structuring conversion tactics (変換タクティク証明の構造化)

conv モード中も、タクティク証明を構造化するために波括弧と . を使うことができる。

example (a b c : Nat) : (0 + a) * (b * c) = a * (c * b) := by
  conv =>
    lhs
    congr
    . rw [Nat.zero_add]
    . rw [Nat.mul_comm]

Other tactics inside conversion mode (変換モードにおける他のタクティク)

  • arg i は現在のターゲットの i 番目の非依存的明示的引数にターゲットを絞る。
example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv =>
    -- ⊢ a * (b * c) = a * (c * b)
    lhs
    -- ⊢ a * (b * c)
    arg 2
    -- ⊢ b * c
    rw [Nat.mul_comm]
  • argscongr の別名である。

  • simp は現在のターゲットに単純化子を適用する。simp は通常のタクティクモードと同様のオプションをサポートする。

def f (x : Nat) :=
  if x > 0 then x + 1 else x + 2

example (g : Nat → Nat) (h₁ : g x = x + 1) (h₂ : x > 0) : g x = f x := by
  conv =>
    rhs
    simp [f, h₂]
  exact h₁
  • enter [1, x, 2, y] は与えられた引数を使って argintro を繰り返す。これは単なるマクロである:
syntax enterArg := ident <|> group("@"? num)
syntax "enter " "[" (colGt enterArg),+ "]": conv
macro_rules
  | `(conv| enter [$i:num]) => `(conv| arg $i)
  | `(conv| enter [@$i:num]) => `(conv| arg @$i)
  | `(conv| enter [$id:ident]) => `(conv| ext $id)
  | `(conv| enter [$arg:enterArg, $args,*]) => `(conv| (enter [$arg]; enter [$args,*]))
  • done は、もし未解決のゴールがあるなら失敗する。

  • trace_state は現在のゴールの状態を表示する。

  • whnf は現在のターゲットをWeak Head Normal Form(WHNF / 弱頭部正規形)に変換する。

  • tactic => <tactic sequence> を使うと通常のタクティクモードに戻る。これは、conv モードでサポートされていないタクティクでゴールを閉じるときや、従来の合同性や外延性の補題を適用するときに便利である。

example (g : Nat → Nat → Nat)
        (h₁ : ∀ x, x ≠ 0 → g x x = 1)
        (h₂ : x ≠ 0)
        : g x x + x = 1 + x := by
  conv =>
    lhs
    -- ⊢ g x x + x
    arg 1
    -- ⊢ g x x
    rw [h₁]
    -- 2 goals: ⊢ 1, ⊢ x ≠ 0
    . skip
    . tactic => exact h₂
  • apply <term>tactic => apply <term> の糖衣構文である。
example (g : Nat → Nat → Nat)
        (h₁ : ∀ x, x ≠ 0 → g x x = 1)
        (h₂ : x ≠ 0)
        : g x x + x = 1 + x := by
  conv =>
    lhs
    arg 1
    rw [h₁]
    . skip
    . apply h₂

Axioms and Computation (公理と計算)

Leanに実装されたCalculus of Constructionsには、依存関数型、帰納型、そして最下層にあるimpredicative(非可述的)でproof-irrelevant(証明無関係)な Prop 型から始まる宇宙の階層が含まれていることを見てきた。本章では、Leanに実装されているCalculus of Inductive Constructions(CIC)に公理と規則を追加して、CICを拡張する方法を考える。このような方法で基礎体系を拡張することは、多くの場合便利である。基礎体系を拡張することで、より多くの定理を証明することが可能になるか、そうでなければ以前は簡単に証明できなかった定理を簡単に証明できるようになる。しかし、公理を追加することで、その正しさ(無矛盾性)に関する懸念の増大以上の否定的な結果が生じることもある。特に、追加した公理の使用は、以下で紹介するように定義と定理の計算内容に影響する。

Leanは計算的推論と古典論理的推論の両方をサポートするように設計されている。望むなら、ユーザーはcomputationally pure(計算上純粋)なフラグメントだけを使うことができ、そうすればシステム内の全ての閉じた式がcanonical normal form(正規標準形)に評価されることが保証される。特に、例えば Nat 型の計算上純粋な閉じた式は、全て数字に簡約される。

Leanの標準ライブラリでは、propositional extensionality(命題外延性)という追加の公理と、function extensionality(関数外延性)の原理を含意するquotient(商)の構築が定義されている。これらの公理の拡張は、例えば集合や有限集合の理論を開発するために利用される。これらの公理やそれらに依存する定理を使うと、Leanのカーネルにおける項評価がブロックされ、Nat 型の閉項が数字に評価されなくなることがあることを以下で見る。しかし、これらの公理は新しい命題(の証明項)を(無条件に)追加するだけであり、Leanは仮想マシン評価器用のバイトコード(中間コード)に定義をコンパイルする際に型と命題の情報を消去するため、これらの公理は計算的解釈と両立する。計算に傾倒したユーザーであっても、計算における推論を行うために古典的な排中律を使いたいと思うかもしれない。排中律もカーネルでの項評価をブロックするが、排中律は定義のバイトコードへのコンパイルと両立する。

また、標準ライブラリは、計算的解釈とは全く相反するchoice principle(選択原理)も定義している。選択原理は「データ(証明以外の項)」の存在を主張する命題から魔法のようにデータを生成することができ、便利だからである。いくつかの古典的な構文を使うには選択原理が必須であり、ユーザーは必要なときに選択原理をインポートすることができる。しかし、古典的な構文を使ってデータを生成する式は、計算可能な内容を持っていない。Leanでは、その事実を示すために、このような定義を noncomputable とマークする必要がある。

(Diaconescu's theoremとして知られる)巧妙なトリックを使うと、命題外延性、関数外延性、選択原理から排中律を導くことができる。しかし、上述の通り、データを作るために使用されない限り、排中律や他の古典的な原理の使用はバイトコードコンパイルやコード抽出と両立する。(訳者注: 上記の公理たちに関して言えば、データを作るために古典的な原理を使用したときに限り計算不可能になる。)

要約すると、宇宙、依存関数型、帰納型という基本的なフレームワークの上に、標準ライブラリはさらに3つの公理を追加している:

  • 命題外延性の公理 propext
  • 商型 Quot の構築 : 関数外延性 funext を含意する
  • 選択原理 Classical.choice : 存在命題 Nonempty α からデータ a : α を生成する

ここで、最初の2つはLeanにおける項の正規化をブロックするが、バイトコード評価とは両立する。一方で、3つ目は計算的に解釈することができない。以下でこれらの詳細について述べる。

Historical and Philosophical Context (歴史的文脈と哲学的文脈)

数学の歴史の大半において、数学は本質的に計算可能なものであった: 幾何学は幾何学的なオブジェクトの作図を扱い、代数学は連立方程式の計算可能な解法と関係があり、解析学は時間発展する物理系の将来の振る舞いを計算する手段を提供した。「任意の x について、...を満たす y が存在する(∀ x, ∃ y, ...)」という定理の証明から、x が与えられたときにそのような y を計算するアルゴリズムを抽出するのは、一般的に簡単なことだった。

しかし、19世紀になり数学的議論の複雑さが増すと、数学者たちはアルゴリズム的情報を必須としない、数学的対象の具体的な表現方法の詳細を抽象化した数学的対象の記述を使う新たな推論様式を開発した。その目的は計算の細部に拘泥することなく強力な「概念的」理解を得ることだったが、結果として直観的で計算可能な体系では単にである数学的定理を認めることになった。

今日においても、計算が数学にとって重要であることはほとんど一律に合意されている。しかし、計算にまつわる問題にどのように対処するのが最善かについては、様々な見解がある。constructive(構成的)な観点からすれば、数学をその計算的ルーツから切り離すのは間違いである: 全ての意味のある数学の定理は、直観的で計算的な解釈を持つべきである。classical(古典的)な観点からすると、問題の分離を維持した方が有益である: 私たちは、プログラムについて推論するために非構成的な理論やメソッドを使う自由を維持しながら、コンピュータプログラムを書くためにある言語と付属するメソッドを使うことができる。Leanは構成的アプローチと古典的アプローチの両方をサポートするように設計されている。ライブラリのコア部分は構成的に開発されている(そのため古典的な原理を使わない選択ができる)が、システムは古典的な数学的推論を行うためのサポートも提供している。

依存型理論の最も計算上純粋な部分は Prop 型の使用を完全に避けている。帰納型と依存関数型はデータ型とみなすことができ、これらの型の項は、これ以上簡約規則を適用できなくなるまで簡約規則を適用することで「評価」することができる。原理的には、Nat 型の任意の閉項(自由変数を持たない項)は succ (succ (... (succ zero)...)) という数字に評価されるはずである。

証明無関係な Prop 型を導入し、定理に irreducible とマークすることは、問題分離への第一歩である。このマークの意図は、型 p : Prop の項は計算において何の役割も果たすべきでないということであり、その意味で項 t : p の具体的な構成は計算に「無関係」である。Prop 型の項を組み込んだ計算可能オブジェクトを定義することはできる。ポイントは、Prop 型の項は「計算結果を推論」する役に立つが、項から「コードを抽出」するときには無視できるということである。しかし、Prop 型の項はまったく無害というわけではない。Prop 型の項には任意の型 α とその項 s : αt : α に対する等式 s = t : Prop が含まれる。このような等式は項を型チェックするためにキャストとして使用される。以下では、このようなキャストがどのようにシステム内の計算をブロックしうるかの例を見ていく。しかし、命題の内容を消去し、中間の型付け制約を無視し、正規形に達するまで項を簡約する評価枠組みの中では、計算は依然として可能である。これはまさにLeanの仮想マシンが行っていることである。

証明無関係な Prop を採用した場合、任意の命題 p に対する排中律 p ∨ ¬p を使うことは正当だと考えるかもしれない。もちろん、排中律がCICの規則に従って計算をブロックする可能性はあるが、上述のようにバイトコードの評価をブロックすることはない。Leanの基礎的理論において、計算に無関係な証明と計算に関係するデータの区別を完全に消し去り、データを計算不可能にするのは、節Choice (選択原理)で説明する選択原理だけである。

Propositional Extensionality (命題外延性)

命題外延性は次のような公理である:

namespace Hidden
axiom propext {a b : Prop} : (a ↔ b) → a = b
end Hidden

propext は、2つの命題が互いを含意するとき、それらは実際に等しいと主張する公理である。これは命題の集合論的な解釈と一致する。命題の集合論的な解釈において、a : Prop は空であるか、互いに区別されたある元 * のみを含むシングルトン {*} である。この公理は、どのようなコンテキストでも命題をそれと同値な命題に置き換えることができるという効果を持つ:

theorem thm₁ (a b c d e : Prop) (h : a ↔ b) : (c ∧ a ∧ d → e) ↔ (c ∧ b ∧ d → e) :=
  propext h ▸ Iff.refl _

theorem thm₂ (a b : Prop) (p : Prop → Prop) (h : a ↔ b) (h₁ : p a) : p b :=
  propext h ▸ h₁

#print axioms thm₁  -- 'thm₁' depends on axioms: [propext]
#print axioms thm₂  -- 'thm₂' depends on axioms: [propext]

Function Extensionality (関数外延性)

命題外延性と同様に、関数外延性は、全ての入力に対して出力が一致する (x : α) → β x 型の2つの関数は等しいことを主張する。

universe u v
#check (@funext :
           {α : Type u}
         → {β : α → Type u}
         → {f g : (x : α) → β x}
         → (∀ (x : α), f x = g x)
         → f = g)

#print funext

古典的な集合論の観点からは、2つの関数が等しいというのはまさにこのことを意味する(eqfnfv - Metamath Proof Explorer)。これは関数の「外延的な」見方として知られている。しかし、構成的な観点からは、関数をアルゴリズム、あるいは何らかの明示的な方法で提示されるコンピュータプログラムと考える方が自然な場合もある。2つのコンピュータプログラムが、構文的には全く異なっているにも関わらず、全ての入力に対して同じ答えを計算できるという例は確かにある。同様に、同じ入出力動作をする2つの関数を同一視することを強制しないような関数の見方を維持したいと思うかもしれない。これは関数の「内包的な」見方として知られている。

関数外延性は商の存在から導かれる。この事実については次の節で説明する。実際、Leanの標準ライブラリでは、funext商の構築から証明されている

α : Type に対して、α の部分集合の型を表す Set α := α → Prop を定義したとする。つまり、部分集合と述語を本質的に同一視するとする。funextpropext を組み合わせることで、このような集合の「外延性の定理」setext が得られる:

def Set (α : Type u) := α → Prop

namespace Set

def mem (x : α) (a : Set α) : Prop := a x

infix:50 (priority := high) "∈" => mem

theorem setext {a b : Set α} (h : ∀ (x : α), x ∈ a ↔ x ∈ b) : a = b :=
  funext (fun x => propext (h x))

theorem setext' {a b : Set α} (h : ∀ (x : α), a x ↔ b x) : a = b :=
  funext (fun x => propext (h x))

end Set

それから、例えば空集合や集合の共通部分を定義し、集合に関する恒等式を証明することができる:

def Set (α : Type u) := α → Prop
namespace Set
def mem (x : α) (a : Set α) := a x
infix:50 (priority := high) "∈" => mem
theorem setext {a b : Set α} (h : ∀ x, x ∈ a ↔ x ∈ b) : a = b :=
  funext (fun x => propext (h x))
def empty : Set α := fun x => False

notation (priority := high) "∅" => empty

def inter (a b : Set α) : Set α :=
  fun x => x ∈ a ∧ x ∈ b

infix:70 " ∩ " => inter

theorem inter_self (a : Set α) : a ∩ a = a :=
  setext fun x => Iff.intro
    (fun ⟨h, _⟩ => h)
    (fun h => ⟨h, h⟩)

theorem inter_empty (a : Set α) : a ∩ ∅ = ∅ :=
  setext fun x => Iff.intro
    (fun ⟨_, h⟩ => h)
    (fun h => False.elim h)

theorem empty_inter (a : Set α) : ∅ ∩ a = ∅ :=
  setext fun x => Iff.intro
    (fun ⟨h, _⟩ => h)
    (fun h => False.elim h)

theorem inter.comm (a b : Set α) : a ∩ b = b ∩ a :=
  setext fun x => Iff.intro
    (fun ⟨h₁, h₂⟩ => ⟨h₂, h₁⟩)
    (fun ⟨h₁, h₂⟩ => ⟨h₂, h₁⟩)
end Set

以下は、Leanのカーネル内部で関数外延性がどのように計算をブロックするかの一例である。

def f (x : Nat) := x
def g (x : Nat) := 0 + x

theorem f_eq_g : f = g :=
  funext fun x => (Nat.zero_add x).symm

def val : Nat :=
  Eq.recOn (motive := fun _ _ => Nat) f_eq_g 0

-- does not reduce to 0
#reduce val

-- evaluates to 0
#eval val

まず、関数外延性を用いて2つの関数 fg が等しいことを示す。次に 0 の型 Nat の中に登場する fg に置き換えて 0 をキャストする。もちろん Natf に依存しないので、このキャストは実質的に何もしない。しかし、計算をブロックするにはこれで十分である: このシステムの計算規則の下で、数字に簡約されない Nat の閉項 val を手に入れた。今回の場合、val0 に簡約してほしいと思うかもしれない。しかし、自明でない例では、このようなキャストを除去すると項の型が変わり、周囲の式の型が不正確になる可能性がある。しかしながら、仮想マシンは何の問題もなく val0 に評価できる。次は propext がどのように計算をブロックするかを示す、上と似た作為的な例である。

theorem tteq : (True ∧ True) = True :=
  propext (Iff.intro (fun ⟨h, _⟩ => h) (fun h => ⟨h, h⟩))

def val : Nat :=
  Eq.recOn (motive := fun _ _ => Nat) tteq 0

-- does not reduce to 0
#reduce val

-- evaluates to 0
#eval val

observational type theorycubical type theoryを含む現在の型理論の研究プログラムは、関数外延性や商などを含む型キャストに対する簡約を許可する方法で型理論を拡張することを目指している。しかし、解決策はそれほど明確ではなく、Leanの基礎となるcalculusの規則はそのようなキャストの簡約を認めていない。

しかしながらある意味では、キャストは式の意味を変えるものではない。むしろ、キャストは式の型を推論するためのメカニズムだと言える。適切な意味論が与えられれば、簡約前後で型付けの正しさを保存するために必要な中間的な記録を無視して、項の意味を保持するやり方で項を簡約することは理にかなっている。

簡約可能性について、Prop に新しい公理を追加することは問題にならない。証明無関係により、Prop の項は何の情報も持たない。したがって、簡約手続きにおいて Prop の項は安全に無視できる。

Quotients (商)

α を任意の型とし、rα 上の同値関係とする。数学において、quotient(商) α / r、つまり「α の項の r による同値類」全体からなる型を作ることは一般的である。集合論的には、α / rα の項の r による同値類全体からなる集合とみなすことができる。このとき、∀ a b, r a b → f a = f b を満たすという意味で同値関係を尊重する任意の関数 f : α → β を、各同値類 ⟦x⟧ に対して f' ⟦x⟧ = f x で定義される関数 f' : α / r → β に「持ち上げる」ことができる。Leanの標準ライブラリは、まさにこのような構築を実行する定数(公理)をいくつか追加することで、Calculus of Constructionsを拡張している。そして、これらの最後の公理 Quot.lift をdefinitionalな除去則として導入している。

最も基本的な形では、商の構築 Quot.mkr が同値関係であることさえ要求しない。Leanには以下の定数(公理)がビルトインに(ライブラリの最初のファイル Init.Prelude より先に)定義されている:

namespace Hidden
universe u v

axiom Quot : {α : Sort u} → (α → α → Prop) → Sort u

axiom Quot.mk : {α : Sort u} → (r : α → α → Prop) → α → Quot r

axiom Quot.ind :
    ∀ {α : Sort u} {r : α → α → Prop} {β : Quot r → Prop},
      (∀ a, β (Quot.mk r a)) → (q : Quot r) → β q

axiom Quot.lift :
    {α : Sort u} → {r : α → α → Prop} → {β : Sort u} → (f : α → β)
    → (∀ a b, r a b → f a = f b) → Quot r → β
end Hidden

最初の公理 Quot は、型 αα 上の二項関係 r が与えられたときに型 Quot r を形成する。2つ目の公理 Quot.mk は、α の項を Quot r の項に写すもので、r : α → α → Propa : α があれば、Quot.mk r aQuot r の項である。3つ目の公理 Quot.ind は、全ての Quot r の項が Quot.mk r a の形をとることを示す(Quot r → PropSet (Quot r) とみなすと分かりやすい)。4つ目の公理 Quot.lift は、関数 f : α → β が与えられたとき、h が「f は関係 r を尊重する」ことの証明であれば、Quot.lift f hf に対応する Quot r 上の関数であることを主張する。この考え方は、h が「f はwell-definedである」ことを示す証明なら、関数 Quot.lift f hα の各項 a について、Quot.mk r a (a を含む r-(同値)類)を f a に写す、というものである。以下の証明で明らかなように、計算原理 Quot.Lift は除去則として宣言されている。

def mod7Rel (x y : Nat) : Prop :=
  x % 7 = y % 7

-- 商型 `Quot mod7Rel`
#check (Quot mod7Rel : Type)

-- `4` を含む `mod7Rel`-(同値)類
#check (Quot.mk mod7Rel 4 : Quot mod7Rel)

def f (x : Nat) : Bool :=
  x % 7 = 0

theorem f_respects (a b : Nat) (h : mod7Rel a b) : f a = f b := by
  simp [mod7Rel, f] at *
  rw [h]

def f' (x : Quot mod7Rel) : Bool :=
  Quot.lift f f_respects x

#check (f' : Quot mod7Rel → Bool)

-- 計算原理
example (a : Nat) : f' (Quot.mk mod7Rel a) = f a :=
  rfl

4つの定数(公理) QuotQuot.mkQuot.indQuot.lift 自体はあまり強くない。Quot r を単に α とみなし、Quot.lift を(h を無視して) α → β 上の恒等関数とみなせば、Quot.ind が満たされることが確認できる。そのため、これら4つの公理は追加の公理とはみなさない。

variable (α β : Type)
variable (r : α → α → Prop)
variable (a : α)
variable (f : α → β)
variable (h : ∀ a₁ a₂, r a₁ a₂ → f a₁ = f a₂)
theorem thm : Quot.lift f h (Quot.mk r a) = f a := rfl

#print axioms thm   -- 'thm' does not depend on any axioms

これら4つの公理は、帰納型や帰納型に関連するコンストラクタと再帰子と同様に、logical framework(論理フレームワーク)の一部とみなされる。

Quot を正真正銘の商にするのは、次の追加公理 Quot.sound である:

namespace Hidden
universe u v
axiom Quot.sound :
      ∀ {α : Type u} {r : α → α → Prop} {a b : α},
        r a b → Quot.mk r a = Quot.mk r b
end Hidden

これは「α の任意の2つの項は、r によって関係しているなら、商の中で同一視される」と主張する公理である。定義や定理 fooQuot.sound を使っている場合、コマンド #print axioms fooQuot.sound を表示する。

もちろん、商の構築は r が同値関係である場合に最もよく使われる。上記のように r が与えられたとき、r' a bQuot.mk r a = Quot.mk r b が同値になるように r' を定義すれば、r' が同値関係であることは明らかである。実際、r' は関数 a ↦ quot.mk r akernel(核)である。公理 Quot.sound は、r a br' a b を含意すると主張している。Quot.liftQuot.ind を使えば、「r を含む任意の同値関係 r'' に対して、r' a br'' a b を含意する」という意味で、r'r を含む最小の同値関係であることを証明できる。特に、r がそもそも同値関係であったならば、任意の ab に対して、r a br' a b が同値であることが証明できる。

同値関係や商の一般的なユースケースをサポートするために、標準ライブラリはsetoidという概念を定義している。これは単に同値関係を持つ型である:

namespace Hidden
class Setoid (α : Sort u) where
  r : α → α → Prop
  iseqv : Equivalence r

instance {α : Sort u} [Setoid α] : HasEquiv α :=
  ⟨Setoid.r⟩

namespace Setoid

variable {α : Sort u} [Setoid α]

theorem refl (a : α) : a ≈ a :=
  iseqv.refl a

theorem symm {a b : α} (hab : a ≈ b) : b ≈ a :=
  iseqv.symm hab

theorem trans {a b c : α} (hab : a ≈ b) (hbc : b ≈ c) : a ≈ c :=
  iseqv.trans hab hbc

end Setoid
end Hidden

αα 上の二項関係 rr が同値関係であることの証明 p が与えられたとき、Setoid.mk r p により Setoid クラスのインスタンスを定義することができる。

namespace Hidden
def Quotient {α : Sort u} (s : Setoid α) :=
  @Quot α Setoid.r
end Hidden

定数(公理) Quotient.mkQuotient.indQuotient.liftQuotient.soundQuot の対応する要素の特殊化に他ならない。型クラス推論が型 Setoid α のインスタンスを見つけることができるという事実は、多くの利点をもたらす。まず、Setoid.r a ba ≈ b (\approx と打つと入力できる)と略記することができる。ここで、Setoid.r という表記について、Setoid のインスタンスが暗黙の引数となっていることに注意してほしい。また、Setoid.reflSetoid.symmSetoid.trans という一般的な定理を使って同値関係に関する推論を行うことができる。商においては特に Quot.mk Setoid.r a の一般的な略記 ⟦a⟧ を使うことができる。ここでも Setoid.r 表記に関して Setoid のインスタンスが暗黙の引数となっている。Quotient.exact という定理もある:

universe u
#check (@Quotient.exact :
         ∀ {α : Sort u} {s : Setoid α} {a b : α},
           Quotient.mk s a = Quotient.mk s b → a ≈ b)

Quotient.exactQuotient.sound を組み合わせると、Quotient s の各項が α の項の各同値類と正確に対応することが導かれる。

標準ライブラリでは、型 α × β は型 αβ の直積を表すことを思い出してほしい。商の使い方を説明するために、型 α の項からなる非順序対の型を、型 α × α の商として定義してみよう。まず、関連する同値関係を定義する:

private def eqv (p₁ p₂ : α × α) : Prop :=
  (p₁.1 = p₂.1 ∧ p₁.2 = p₂.2) ∨ (p₁.1 = p₂.2 ∧ p₁.2 = p₂.1)

infix:50 " ~ " => eqv

次のステップは、eqv が実際に同値関係であること、つまり反射的、対称的、推移的であることを証明することである。依存パターンマッチングを使って場合分けし、仮説を分解し、それを組み立てて結論を出すことで、便利で読みやすい方法でこれら3つの事実を証明することができる。

private def eqv (p₁ p₂ : α × α) : Prop :=
  (p₁.1 = p₂.1 ∧ p₁.2 = p₂.2) ∨ (p₁.1 = p₂.2 ∧ p₁.2 = p₂.1)
infix:50 " ~ " => eqv
private theorem eqv.refl (p : α × α) : p ~ p :=
  Or.inl ⟨rfl, rfl⟩

private theorem eqv.symm : ∀ {p₁ p₂ : α × α}, p₁ ~ p₂ → p₂ ~ p₁
  | (a₁, a₂), (b₁, b₂), (Or.inl ⟨a₁b₁, a₂b₂⟩) =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (Or.inr ⟨a₁b₂, a₂b₁⟩) =>
    Or.inr (by simp_all)

private theorem eqv.trans : ∀ {p₁ p₂ p₃ : α × α}, p₁ ~ p₂ → p₂ ~ p₃ → p₁ ~ p₃
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inl (by simp_all)

private theorem is_equivalence : Equivalence (@eqv α) :=
  { refl := eqv.refl, symm := eqv.symm, trans := eqv.trans }

eqv が同値関係であることが証明されたので、Setoid (α × α) のインスタンスを構築することができ、Setoid (α × α) を使って非順序対の型 UProd α を定義することができる。

private def eqv (p₁ p₂ : α × α) : Prop :=
  (p₁.1 = p₂.1 ∧ p₁.2 = p₂.2) ∨ (p₁.1 = p₂.2 ∧ p₁.2 = p₂.1)
infix:50 " ~ " => eqv
private theorem eqv.refl (p : α × α) : p ~ p :=
  Or.inl ⟨rfl, rfl⟩
private theorem eqv.symm : ∀ {p₁ p₂ : α × α}, p₁ ~ p₂ → p₂ ~ p₁
  | (a₁, a₂), (b₁, b₂), (Or.inl ⟨a₁b₁, a₂b₂⟩) =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (Or.inr ⟨a₁b₂, a₂b₁⟩) =>
    Or.inr (by simp_all)
private theorem eqv.trans : ∀ {p₁ p₂ p₃ : α × α}, p₁ ~ p₂ → p₂ ~ p₃ → p₁ ~ p₃
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inl (by simp_all)
private theorem is_equivalence : Equivalence (@eqv α) :=
  { refl := eqv.refl, symm := eqv.symm, trans := eqv.trans }
instance uprodSetoid (α : Type u) : Setoid (α × α) where
  r     := eqv
  iseqv := is_equivalence

def UProd (α : Type u) : Type u :=
  Quotient (uprodSetoid α)

namespace UProd

def mk {α : Type} (a₁ a₂ : α) : UProd α :=
  Quotient.mk' (a₁, a₂)

notation "{ " a₁ ", " a₂ " }" => mk a₁ a₂

end UProd

非順序対 Quotient.mk' (a₁, a₂) に対する略記 {a₁, a₂} をローカルに定義していることに注意してほしい。この略記は説明を目的とするなら便利であるが、レコードや集合のような対象を表すのに波括弧を使いにくくなるので、一般的には良いアイデアではない。

既に (a₁, a₂) ~ (a₂, a₁) を証明してあるので、Quot.sound を使うことで {a₁, a₂} = {a₂, a₁} を簡単に証明することができる。

private def eqv (p₁ p₂ : α × α) : Prop :=
  (p₁.1 = p₂.1 ∧ p₁.2 = p₂.2) ∨ (p₁.1 = p₂.2 ∧ p₁.2 = p₂.1)
infix:50 " ~ " => eqv
private theorem eqv.refl (p : α × α) : p ~ p :=
  Or.inl ⟨rfl, rfl⟩
private theorem eqv.symm : ∀ {p₁ p₂ : α × α}, p₁ ~ p₂ → p₂ ~ p₁
  | (a₁, a₂), (b₁, b₂), (Or.inl ⟨a₁b₁, a₂b₂⟩) =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (Or.inr ⟨a₁b₂, a₂b₁⟩) =>
    Or.inr (by simp_all)
private theorem eqv.trans : ∀ {p₁ p₂ p₃ : α × α}, p₁ ~ p₂ → p₂ ~ p₃ → p₁ ~ p₃
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inl (by simp_all)
private theorem is_equivalence : Equivalence (@eqv α) :=
  { refl := eqv.refl, symm := eqv.symm, trans := eqv.trans }
instance uprodSetoid (α : Type u) : Setoid (α × α) where
  r     := eqv
  iseqv := is_equivalence
def UProd (α : Type u) : Type u :=
  Quotient (uprodSetoid α)
namespace UProd
def mk {α : Type} (a₁ a₂ : α) : UProd α :=
  Quotient.mk' (a₁, a₂)
notation "{ " a₁ ", " a₂ " }" => mk a₁ a₂
theorem mk_eq_mk (a₁ a₂ : α) : {a₁, a₂} = {a₂, a₁} :=
  Quot.sound (Or.inr ⟨rfl, rfl⟩)
end UProd

この例を完成させるため、a : αu : UProd α に対して、a が非順序対 u の要素の1つである場合に成立する命題 a ∈ u を定義する。まず、順序対に対して同じような命題 mem_fn a u を定義する。次に mem_fn が同値関係 eqv を尊重することを補題 mem_respects で示す。これはLeanの標準ライブラリで広く使われているイディオムである。

private def eqv (p₁ p₂ : α × α) : Prop :=
  (p₁.1 = p₂.1 ∧ p₁.2 = p₂.2) ∨ (p₁.1 = p₂.2 ∧ p₁.2 = p₂.1)
infix:50 " ~ " => eqv
private theorem eqv.refl (p : α × α) : p ~ p :=
  Or.inl ⟨rfl, rfl⟩
private theorem eqv.symm : ∀ {p₁ p₂ : α × α}, p₁ ~ p₂ → p₂ ~ p₁
  | (a₁, a₂), (b₁, b₂), (Or.inl ⟨a₁b₁, a₂b₂⟩) =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (Or.inr ⟨a₁b₂, a₂b₁⟩) =>
    Or.inr (by simp_all)
private theorem eqv.trans : ∀ {p₁ p₂ p₃ : α × α}, p₁ ~ p₂ → p₂ ~ p₃ → p₁ ~ p₃
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inl (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl ⟨a₁b₁, a₂b₂⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inl ⟨b₁c₁, b₂c₂⟩ =>
    Or.inr (by simp_all)
  | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr ⟨a₁b₂, a₂b₁⟩, Or.inr ⟨b₁c₂, b₂c₁⟩ =>
    Or.inl (by simp_all)
private theorem is_equivalence : Equivalence (@eqv α) :=
  { refl := eqv.refl, symm := eqv.symm, trans := eqv.trans }
instance uprodSetoid (α : Type u) : Setoid (α × α) where
  r     := eqv
  iseqv := is_equivalence
def UProd (α : Type u) : Type u :=
  Quotient (uprodSetoid α)
namespace UProd
def mk {α : Type} (a₁ a₂ : α) : UProd α :=
  Quotient.mk' (a₁, a₂)
notation "{ " a₁ ", " a₂ " }" => mk a₁ a₂
theorem mk_eq_mk (a₁ a₂ : α) : {a₁, a₂} = {a₂, a₁} :=
  Quot.sound (Or.inr ⟨rfl, rfl⟩)
private def mem_fn (a : α) : α × α → Prop
  | (a₁, a₂) => a = a₁ ∨ a = a₂

-- auxiliary lemma for proving mem_respects
private theorem mem_swap {a : α} :
      ∀ {p : α × α}, mem_fn a p = mem_fn a (⟨p.2, p.1⟩)
  | (a₁, a₂) => by
    apply propext
    apply Iff.intro
    . intro
      | Or.inl h => exact Or.inr h
      | Or.inr h => exact Or.inl h
    . intro
      | Or.inl h => exact Or.inr h
      | Or.inr h => exact Or.inl h


private theorem mem_respects
      : {p₁ p₂ : α × α} → (a : α) → p₁ ~ p₂ → mem_fn a p₁ = mem_fn a p₂
  | (a₁, a₂), (b₁, b₂), a, Or.inl ⟨a₁b₁, a₂b₂⟩ => by simp_all
  | (a₁, a₂), (b₁, b₂), a, Or.inr ⟨a₁b₂, a₂b₁⟩ => by simp_all; apply mem_swap

def mem (a : α) (u : UProd α) : Prop :=
  Quot.liftOn u (fun p => mem_fn a p) (fun p₁ p₂ e => mem_respects a e)

infix:50 (priority := high) " ∈ " => mem

theorem mem_mk_left (a b : α) : a ∈ {a, b} :=
  Or.inl rfl

theorem mem_mk_right (a b : α) : b ∈ {a, b} :=
  Or.inr rfl

theorem mem_or_mem_of_mem_mk {a b c : α} : c ∈ {a, b} → c = a ∨ c = b :=
  fun h => h
end UProd

利便性のため、標準ライブラリは2変数関数を「持ち上げる」ための Quotient.lift₂ と、2変数帰納法のための Quotient.ind₂ も定義している。

最後に、なぜ商の構築が関数外延性を含意するのかについて、いくつかのヒントを示してこの節を締めくくる。型 (x : α) → β x を持つ関数の外延性等式が同値関係であることを示すのは難しくない。したがって、「同値関係を足した」依存関数型 extfun α β を考えることができる。もちろん、関数適用は f₁f₂ が同値関係にあるなら、f₁ af₂ a と等しいという意味で、同値関係を尊重する。したがって、関数適用は関数 extfun_app : extfun α β → (x : α) → β x に持ち上げられる。しかし、任意の f について、extfun_app ⟦f⟧fun x => f x とdefinitionally equalであり、結果として f とdefinitionally equalである。したがって、f₁f₂ が外延的に等しいとき、次のような等号の連鎖が成り立つ:

    f₁ = extfun_app ⟦f₁⟧ = extfun_app ⟦f₂⟧ = f₂

結果として、f₁f₂ は等しい。

Choice (選択原理)

標準ライブラリで定義されている最後の公理(選択原理)を述べるには、次のように定義される Nonempty 型が必要である:

namespace Hidden
class inductive Nonempty (α : Sort u) : Prop where
  | intro (val : α) : Nonempty α
end Hidden

Nonempty α 型は Prop 型を持ち、そのコンストラクタはデータを含むので、Inductively Defined Propositions (帰納的に定義された命題)の節で見た通り、Nonempty α 型を除去しても命題を作ることしかできない。実際、Nonempty α∃ x : α, True と同値である:

example (α : Type u) : Nonempty α ↔ ∃ x : α, True :=
  Iff.intro (fun ⟨a⟩ => ⟨a, trivial⟩) (fun ⟨a, h⟩ => ⟨a⟩)

Lean版の選択公理は次のようにシンプルに表現される:

namespace Hidden
universe u
axiom choice {α : Sort u} : Nonempty α → α
end Hidden

α は空でない」ことの証明 h さえあれば、choice h は魔法のように α の項を生成する。もちろん、choice の使用は意味のある計算をブロックする: 証明無関係の考え方の下では、h はそのような項を見つける方法に関する情報を全く含んでいない。

choiceClassical という名前空間の中にあるため、この公理のフルネームは Classical.choice である。選択原理はindefinite description(不定的記述)の原理と同値である。不定的記述の原理はsubtypes(部分型)を使って次のように表すことができる:

namespace Hidden
universe u
axiom choice {α : Sort u} : Nonempty α → α
noncomputable def indefiniteDescription {α : Sort u} (p : α → Prop)
                                        (h : ∃ x, p x) : {x // p x} :=
  choice <| let ⟨x, px⟩ := h; ⟨⟨x, px⟩⟩
end Hidden

この定義は choice に依存するため、Leanは indefiniteDescription のバイトコードを生成できない。したがって、この定義を noncomputable とマークする必要がある。また、Classical 名前空間では、関数 choose とプロパティ choose_specindefiniteDescription の(2つの要素からなる)出力を分解し、各要素を抽出する:

open Classical
namespace Hidden
noncomputable def choose {α : Sort u} {p : α → Prop} (h : ∃ x, p x) : α :=
  (indefiniteDescription p h).val

theorem choose_spec {α : Sort u} {p : α → Prop} (h : ∃ x, p x) : p (choose h) :=
  (indefiniteDescription p h).property
end Hidden

また、選択原理 Choice は「空でない」という性質 Nonempty と「有項である」というより構成的な性質 Inhabited の区別をなくしてしまう:

open Classical
theorem inhabited_of_nonempty : Nonempty α → Inhabited α :=
  fun h => choice (let ⟨a⟩ := h; ⟨⟨a⟩⟩)

次節では propextfunextchoice の3つを合わせると、排中律と任意の命題の決定可能性が導かれることを説明する。これらを用いると、不定的記述の原理を次のように強化することができる:

open Classical
universe u
#check (@strongIndefiniteDescription :
         {α : Sort u} → (p : α → Prop)
         → Nonempty α → {x // (∃ (y : α), p y) → p x})

前提となる型 α が空でないとすると、p を満たす項が存在するなら、strongIndefiniteDescription pp を満たす項 x を生成する(p を満たす項が存在しないなら、strongIndefiniteDescription pchoice により生成された型 α の任意の項を返す)。この関数 strongIndefiniteDescription の出力から値要素を抽出する関数はHilbert's epsilon function(ヒルベルトのε関数)として知られている:

open Classical
universe u
#check (@epsilon :
         {α : Sort u} → [Nonempty α]
         → (α → Prop) → α)

#check (@epsilon_spec :
         ∀ {α : Sort u} {p : α → Prop} (hex : ∃ (y : α), p y),
           p (@epsilon _ (nonempty_of_exists hex) p))

関数 epsilon_spec は、p を満たす項が存在するという証明を受け取ると、p (epsilon p) の証明を返す。

The Law of the Excluded Middle (排中律)

排中律は次のように表現される:

open Classical

#check (@em : ∀ (p : Prop), p ∨ ¬p)

Diaconescuの定理は、選択公理から排中律が導かれることを述べている。より正確には、Diaconescuの定理は、Classical.choicepropextfunext から排中律が導かれることを示している。以下に標準ライブラリにあるDiaconescuの定理の証明を記す。

まず、必要な公理をインポートして、2つの述語 UV を定義する:

namespace Hidden
open Classical
theorem em (p : Prop) : p ∨ ¬p :=
  let U (x : Prop) : Prop := x = True ∨ p
  let V (x : Prop) : Prop := x = False ∨ p

  have exU : ∃ x, U x := ⟨True, Or.inl rfl⟩
  have exV : ∃ x, V x := ⟨False, Or.inl rfl⟩
  sorry
end Hidden

もし p が真なら、Prop 型の任意の項は UV の両方に属する。もし p が偽なら、U はシングルトン True であり、V はシングルトン False である。

次に、choose を使って U の元と V の元を1つ選ぶ:

namespace Hidden
open Classical
theorem em (p : Prop) : p ∨ ¬p :=
  let U (x : Prop) : Prop := x = True ∨ p
  let V (x : Prop) : Prop := x = False ∨ p
  have exU : ∃ x, U x := ⟨True, Or.inl rfl⟩
  have exV : ∃ x, V x := ⟨False, Or.inl rfl⟩
  let u : Prop := choose exU
  let v : Prop := choose exV

  have u_def : U u := choose_spec exU
  have v_def : V v := choose_spec exV
  sorry
end Hidden

UV はそれぞれ選言命題なので、u_defv_def の組は計4つのケースを表している。これらのケースのうち1つでは u = True かつ v = False であり、他の全てのケースでは p が真である。したがって、次のようになる:

namespace Hidden
open Classical
theorem em (p : Prop) : p ∨ ¬p :=
  let U (x : Prop) : Prop := x = True ∨ p
  let V (x : Prop) : Prop := x = False ∨ p
  have exU : ∃ x, U x := ⟨True, Or.inl rfl⟩
  have exV : ∃ x, V x := ⟨False, Or.inl rfl⟩
  let u : Prop := choose exU
  let v : Prop := choose exV
  have u_def : U u := choose_spec exU
  have v_def : V v := choose_spec exV
  have not_uv_or_p : u ≠ v ∨ p :=
    match u_def, v_def with
    | Or.inr h, _ => Or.inr h
    | _, Or.inr h => Or.inr h
    | Or.inl hut, Or.inl hvf =>
      have hne : u ≠ v := by simp [hvf, hut, true_ne_false]
      Or.inl hne
  sorry
end Hidden

一方、p が真であれば、関数外延性と命題外延性によって UV は等しい。uv の定義により、uv も等しいことがわかる。

namespace Hidden
open Classical
theorem em (p : Prop) : p ∨ ¬p :=
  let U (x : Prop) : Prop := x = True ∨ p
  let V (x : Prop) : Prop := x = False ∨ p
  have exU : ∃ x, U x := ⟨True, Or.inl rfl⟩
  have exV : ∃ x, V x := ⟨False, Or.inl rfl⟩
  let u : Prop := choose exU
  let v : Prop := choose exV
  have u_def : U u := choose_spec exU
  have v_def : V v := choose_spec exV
  have not_uv_or_p : u ≠ v ∨ p :=
    match u_def, v_def with
    | Or.inr h, _ => Or.inr h
    | _, Or.inr h => Or.inr h
    | Or.inl hut, Or.inl hvf =>
      have hne : u ≠ v := by simp [hvf, hut, true_ne_false]
      Or.inl hne
  have p_implies_uv : p → u = v :=
    fun hp =>
    have hpred : U = V :=
      funext fun x =>
        have hl : (x = True ∨ p) → (x = False ∨ p) :=
          fun _ => Or.inr hp
        have hr : (x = False ∨ p) → (x = True ∨ p) :=
          fun _ => Or.inr hp
        show (x = True ∨ p) = (x = False ∨ p) from
          propext (Iff.intro hl hr)
    have h₀ : ∀ exU exV, @choose _ U exU = @choose _ V exV := by
      rw [hpred]; intros; rfl
    show u = v from h₀ _ _
  sorry
end Hidden

not_uv_or_pp_implies_uv をまとめると、所望の結論が得られる:

namespace Hidden
open Classical
theorem em (p : Prop) : p ∨ ¬p :=
  let U (x : Prop) : Prop := x = True ∨ p
  let V (x : Prop) : Prop := x = False ∨ p
  have exU : ∃ x, U x := ⟨True, Or.inl rfl⟩
  have exV : ∃ x, V x := ⟨False, Or.inl rfl⟩
  let u : Prop := choose exU
  let v : Prop := choose exV
  have u_def : U u := choose_spec exU
  have v_def : V v := choose_spec exV
  have not_uv_or_p : u ≠ v ∨ p :=
    match u_def, v_def with
    | Or.inr h, _ => Or.inr h
    | _, Or.inr h => Or.inr h
    | Or.inl hut, Or.inl hvf =>
      have hne : u ≠ v := by simp [hvf, hut, true_ne_false]
      Or.inl hne
  have p_implies_uv : p → u = v :=
    fun hp =>
    have hpred : U = V :=
      funext fun x =>
        have hl : (x = True ∨ p) → (x = False ∨ p) :=
          fun _ => Or.inr hp
        have hr : (x = False ∨ p) → (x = True ∨ p) :=
          fun _ => Or.inr hp
        show (x = True ∨ p) = (x = False ∨ p) from
          propext (Iff.intro hl hr)
    have h₀ : ∀ exU exV, @choose _ U exU = @choose _ V exV := by
      rw [hpred]; intros; rfl
    show u = v from h₀ _ _
  match not_uv_or_p with
  | Or.inl hne => Or.inr (mt p_implies_uv hne)
  | Or.inr h   => Or.inl h
end Hidden

排中律の系としては、二重否定除去、場合分けによる証明、矛盾による証明などがあり、これらは全て節Classical Logic (古典論理)で説明されている。排中律と命題外延性は命題完全性を含意する:

namespace Hidden
open Classical
theorem propComplete (a : Prop) : a = True ∨ a = False :=
  match em a with
  | Or.inl ha => Or.inl (propext (Iff.intro (fun _ => ⟨⟩) (fun _ => ha)))
  | Or.inr hn => Or.inr (propext (Iff.intro (fun h => hn h) (fun h => False.elim h)))
end Hidden

選択原理と合わせると、「全ての命題は決定可能である」というより強い原理も得られる。決定可能命題のクラス Decidable は次のように定義されることを思い出してほしい:

namespace Hidden
class inductive Decidable (p : Prop) where
  | isFalse (h : ¬p) : Decidable p
  | isTrue  (h : p)  : Decidable p
end Hidden

除去により Prop 型の項しか作れない p ∨ ¬ p とは対照的に、型 Decidable p は直和型 Sum p (¬ p) と等価であり、除去により任意の型の項を作ることができる。型 Decidable p のデータはif-then-else式を書くのに必要である。

古典的推論の例と同様に、「f : α → β が単射で α が有項なら、f は左逆写像を持つ」という定理を証明するためにも choose を使う。左逆写像 linv を定義するために、依存if-then-else式を用いる。if h : c then t else edite c (fun h : c => t) (fun h : ¬ c => e) の略記であることを思い出してほしい。linv の定義の中で、選択原理は2回使われている: 選択原理は、まず (∃ a : A, f a = b) が「決定可能」であることを示すために、そして f a = b を満たす a を選ぶために使われている。propDecidable はスコープ付きインスタンスであり、open Classical コマンドによって利用可能になることに注意してほしい。このインスタンスにより、このif-then-else式の使用が正当化される(節Decidable Propositions (決定可能命題)の説明も参照のこと)。

open Classical

noncomputable def linv [Inhabited α] (f : α → β) : β → α :=
  fun b : β => if ex : (∃ a : α, f a = b) then choose ex else default

theorem linv_comp_self {f : α → β} [Inhabited α]
                       (inj : ∀ {a b}, f a = f b → a = b)
                       : linv f ∘ f = id :=
  funext fun a =>
    have ex  : ∃ a₁ : α, f a₁ = f a := ⟨a, rfl⟩
    have feq : f (choose ex) = f a  := choose_spec ex
    calc linv f (f a)
      _ = choose ex := dif_pos ex
      _ = a         := inj feq

古典的な観点からすると、linv は関数である。構成的な観点からすると、linv の定義は受け入れがたい: 一般にこのような関数を実装する方法はないため、この構築は何の情報も持たない。