Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lean by Example

プログラミング言語であるとともに定理証明支援系でもある Lean 言語と、その主要なライブラリの使い方を豊富なコード例とともに解説した資料です。

Note

誤りのご指摘、ご提案などは GitHub リポジトリからお願いします。

lean-ja の Discord サーバがあります。質問や相談などはこちらにどうぞ。招待コードは p32ZfnVawh です。https://discord.com/invite/exampleexample の部分を招待コードに置き換えてアクセスしてください。(スパム対策でURLを直接載せていません)

支援について ❤️

本書は lean-ja の管理者である北窓が執筆・開発しています。 もしも本書を気に入っていただけたのであれば、ご支援をいただけると励みになります。

  • 金銭的な支援は Pixiv Fanbox から受け付けております。
  • GitHub からスター🌟をつけていただくだけでも支援になります。

本書の特色 😎

1. 内容が正確

Lean は開発が活発に続いているソフトウェアであり、毎月のように新しいリリースが行われています。その際破壊的な変更が行われることは少なくありません。変更によって古くなってしまった記述を自動的に見つける方法がなければ、内容の信頼性が損なわれます。

本書では、この問題に対して次のように対処しています。

  • 記述内容を可能な限りコードとして表現することにより、「ビルドが通れば記述内容は正しい」という状態を維持する。
  • 内容を更新するごとにビルドが自動的に走るようにする。

これにより本書のほぼすべての記述はバージョン leanprover/lean4:v4.34.0-rc2 で実際に間違いがないということを確認済みです。

間違った記述を見つけられた際はお手数ですが issue でご報告をお願いします。

2. 情報が新しい

本書は、Lean とそのライブラリのバージョンを自動で更新するワークフロー lean-update を利用して、定期的にバージョンを最新のものに更新しています。Lean の最新情報をすべて掲載することはかないませんが、最新の情報を提供できるよう努めています。

3. コードをすぐに試せる

本書のすべての Lean コードブロックは、マウスを重ねると Lean Playground へジャンプするボタン が現れるようになっています。

またコードブロックの中には、import 文が足りないなどの理由でそのままでは実行できないものがありますが、そうした場合は画面右上の実行ボタン をクリックしていただければ、ファイル全体を実行することができます。

このようなことが可能なのは、mdgen を使って、Lean コードから markdown ファイルを生成することにより本書が制作されているからです。

4. わかりやすい

「わかりやすさ」にも種類があります。世の中には「難しい部分を隠蔽する」ことをもって「わかりやすい」としている書籍も存在しますが、本書はその立場をとりません。

では何をもってわかりやすいと考えているかというと、まず本書では「ほぼすべての記述がコード例によって検証されている」ので、言い換えれば「どの記述にもコード例が付随している」ということになります。これにより、「そもそも何を言っているのかわからない記述」が本書にはほとんど存在しないはずです。

また、「ほぼすべての記述がコード例によって検証されている」ということは、「コード例で検証できないことには言及できない」ことになります。これにより、本書の記述は「それが何であるのか」の詳しい説明が少なく、「それを使って何ができるのか」の説明が多くなっています。これにより、結果として説明がより具体的かつ実践的になり、特に初学者にとってより理解しやすくなるという効果が生じているはずです。

スポンサー

このプロジェクトは Proxima Technology 様よりご支援を頂いています。

Proxima Technology(プロキシマテクノロジー)は数学の社会実装を目指し、その⼀環としてモデル予測制御の民主化を掲げているAIスタートアップ企業です。数理科学の力で社会を変えることを企業の使命としています。

リンク集

🌐 コミュニティ

🧰 ライブラリやツール

  • Mathlib4 Lean で大学の学部程度の数学を実装したライブラリ。
  • Lean4 VSCode 拡張機能 Lean4 のための VSCode 拡張機能。
  • Lean4 Web ブラウザ上で Lean が実行できるプレイグラウンド。
  • Reservoir Lean 公式のパッケージレジストリ。

🔍 ライブラリ検索ツール

  • Loogle Mathlib などからの検索ツール。型の情報や定数名から検索ができます。vscode-lean4 から実行することもできます。
  • Lean Search 自然言語で Mathlib から定理や定義が検索できるサイト。これに関連して「A Semantic Search Engine for Mathlib4」という論文があります。日本語対応しています。
  • Lean Explore 自然言語で Mathlib などから定理や定義が検索できるサイト。日本語対応していないことに注意。これに関連して 「LeanExplore: A search engine for Lean 4 declarations」 という論文があります。
  • Moogle 自然言語でクエリできるライブラリ検索エンジン。

📖 公式ドキュメント

  • Lean Language Reference Lean 言語の公式リファレンス。Verso という Lean 製のツールで執筆されています。
  • Theorem Proving in Lean 4 Lean 4の定理証明支援系としての側面に焦点を当てた公式チュートリアルテキストです。理論的な記述が多めです。
  • Functional Programming in Lean 関数型プログラミング言語としての Lean の入門書。
  • Mathematics in Lean Lean で数学を形式化する方法を学ぶ教科書。初等整数論をはじめ、位相空間論や測度論も扱われるなど内容が充実しており、多くの演習問題があります。

📚 コミュニティによる教材・資料

  • Natural Number Game 4 Lean を使い、ペアノの公理から始めて自然数の基本的な性質を証明する初心者向けブラウザゲーム。
  • Metaprogramming in Lean 4 Lean で独自のコマンドやタクティクを作る方法を解説した本。
  • The Mechanics of Proof 大学の初級レベルの授業のための Lean を使った教科書。Mathlib にはない独自のタクティクが使用されていることに注意が必要です。
  • The Hitchhiker’s Guide to Logical Verification 「 Formal Proof and Verification」という授業の参考書。テーマは形式検証であり、形式数学の形式化だけでなくプログラミング意味論にも触れています。
  • Logic and Mechanized Reasoning Lean を使った論理学および自動証明の教科書。ソースコードもGitHubリポジトリで公開されています。
  • Scientific Computing in Lean Lean で科学計算を行うライブラリ SciLean の著者による、科学計算についての解説書。
  • From Zero to QED 型理論と関数型プログラミングを背景に、Lean を用いた定理証明について基礎から段階的に学ぶ入門書。
  • Type Checking in Lean4 Lean4 のカーネル(型検査機構)について深く理解するための資料です。
  • Lean 4 (Meta)programming Cookbook Lean4 のメタプログラミング / プログラミング用のレシピ集。「こういうことをしたいときにどう書くか」がまとめられています。

📚 日本語の書籍

  • ゼロから始めるLean言語入門 Leanで自然数に関する理論を構築することを通して、Leanの基本的な使い方を学ぶ入門書。示した結果を再利用可能にする必要性を強調しているのが特徴。

🗾 日本語の参考資料

Lean のインストール方法

Lean および関連ツールのインストール方法を説明します。VSCode の Lean 4 拡張機能が自動的にインストールしてくれるのですが、ここではコマンドラインから手動で入れる場合の手順を説明します。

OS 共通の事項

  • インストールすべきものは、elanleanlake の3つです。 elan は Lean のバージョン管理ツールで、lean は Lean 本体、lake は Lean のパッケージ管理ツールです。 elan をインストールすれば、elan 経由で leanlake もインストールできます。

  • Lean のインストールには gitcurl が必要です。 git はテキストのバージョン管理をするためのツールで、curl はデータを外部から取得したり、外部に送信したりするためのツールです。

Windows の場合

PowerShell 7 のインストール

まずは使用しているシェルを確認します。PowerShell 7 を使うことを推奨します。他のシェルでも Lean のインストール自体は可能ですが、Windows では PowerShell 7 を使うことが一般的であるためこのような手順を案内しています。

Windows PowerShell と PowerShell 7 以降は混同されがちですが、下記を実行して Major が 7 以上であれば PowerShell 7 です。

$PSVersionTable.PSVersion

PowerShell 7 をインストールするには、次を実行します。

winget install --id Microsoft.PowerShell --source winget

文字化けの修正

そのままだと PowerShell 上で Unicode 文字が文字化けすることがあるので、以下を $Profile の末尾に追記しておきます。なお「履歴ベースの予測の有効化」は Lean とは関係がありませんが、便利なので追加すると良いでしょう。

# 履歴ベースの予測を有効化
Set-PSReadLineOption -PredictionSource History

# Lean の文字化けを修正する
$utf8 = New-Object System.Text.UTF8Encoding
[console]::InputEncoding = $utf8
[console]::OutputEncoding = $utf8
$OutputEncoding = $utf8

git と curl のインストール

以下、全てのコマンドは PowerShell 7 で実行することを前提とすることにします。

gitcurl がインストール済みであるか、次のコマンドで確認します。

git --version
curl --version

おそらく初期状態の Windows では git はインストールされていないと思います。以下のコマンドでインストールします。

winget install --id Git.Git -e --source winget

curl コマンドはおそらく初めから入っていると思いますが、もし入っていない場合は次のコマンドでインストールします。

winget install --id cURL.cURL --source winget

elan のインストール

次に、elan をインストールします。次のコマンドでインストールできます。

curl -O --location https://elan.lean-lang.org/elan-init.ps1
pwsh `
  -ExecutionPolicy Bypass `
  -f elan-init.ps1 `
  -NoPrompt:$True `
  -DefaultToolchain:stable
Remove-Item elan-init.ps1

実行後、elan コマンドが使えるようになったか確認します。

elan --version

Lean と lake のインストール

次に、Lean(と同梱されている lake)をインストールします。stable を指定することで、最新の安定版をインストールできます。

elan toolchain install stable

終わったら、Lean と lake のバージョンを確認します。

lean --version
lake --version

Linux の場合

シェルは bash を使用することを想定して手順を書きます。

git と curl のインストール

まず、gitcurl がインストール済みであるか確認します。

git --version
curl --version

インストールされていなければインストールします。

sudo apt update
sudo apt install -y git curl

elan のインストール

次に elan をインストールします。

curl -O --location https://elan.lean-lang.org/elan-init.sh
sh elan-init.sh -y --default-toolchain stable
rm elan-init.sh

終わったら elan コマンドが使えるようになったか確認します。

elan --version

Lean と lake のインストール

次に、Lean(と同梱されている lake)をインストールします。stable を指定することで、最新の安定版をインストールできます。

elan toolchain install stable

終わったら、Lean と lake のバージョンを確認します。

lean --version
lake --version

MacOS の場合

シェルとして zsh を利用することを想定して手順を書きます。

git と curl のインストール

まず gitcurl が使えるか確認します。

git --version
curl --version

MacOS の場合、curl は最初から入っていると思います。git が入っていない場合でも、git --version の実行時にインストールするか聞かれるはずです。

elan のインストール

次に elan をインストールします。

curl -O --location https://elan.lean-lang.org/elan-init.sh
sh elan-init.sh -y --default-toolchain stable
rm elan-init.sh

終わったら elan コマンドが使えるようになったか確認します。

elan --version

Lean と lake のインストール

次に、Lean(と同梱されている lake)をインストールします。stable を指定することで、最新の安定版をインストールできます。

elan toolchain install stable

終わったら、Lean と lake のバージョンを確認します。

lean --version
lake --version

Mathlib4 Help

Mathlib4 Help はMathlib の #help コマンドの出力をまとめたページです。

ランダムぺージ

対話的コマンド

トップレベルコマンドのうち、# から始まるものを本書では便宜的に「対話的コマンド」と呼んでいます。これは、そうしたコマンドの多くが「式を評価した値を調べる」「式の型を調べる」など Lean から情報を得るために使用されるからです。

Warning

対話的コマンド(diagnostic command)という用語は一般的なものではありません。Lean コミュニティで通じない可能性もあります。著者は diagnostic command という用語を The Hitchhiker’s Guide to Logical Verification から拝借しましたが、同じものが hash command と呼ばれているのを聞いたこともあります。決まった呼び方がないという印象です。

#check_failure

#check_failure は、エラーが起こった時に成功し、エラーが起こらなければ失敗するコマンドです。エラーが起こるコードを意図的に構成したいときに便利です。

-- 自然数と文字列を足すことはできない
#check_failure 1 + "hello"

-- `1 = 2` を `rfl` で証明することはできない
#check_failure (by rfl : 1 = 2)

-- `1 + 4 = 5` は `contradiction` では示せない
#check_failure (by contradiction : 1 + 4 = 5)

#check

#check コマンドは、項(term) の型を表示します。Lean ではすべての項に型があるので、どんな項にも使えます。#check term という構文で、term の型を表示することができます。

-- 文字
/- info: 'a' : Char -/
#check 'a'

-- 文字列
/- info: "Hello" : String -/
#check "Hello"

-- 自然数
/- info: 1 : Nat -/
#check 1

-- 浮動小数点数
/- info: 1.0 : Float -/
#check 1.0

-- 整数
/- info: -2 : Int -/
#check -2

-- `1` はそのままだと自然数扱いになるが、整数にキャストできる
/- info: 1 : Int -/
#check (1 : Int)

-- 自然数のリスト
/- info: [1, 2, 3] : List Nat -/
#check [1, 2, 3]

-- 自然数の配列
/- info: #[1, 2, 3] : Array Nat -/
#check #[1, 2, 3]

-- 関数
/- info: fun x => x + 42 : Nat → Nat -/
#check fun x => x + 42

-- 真偽値
/- info: Bool.true : Bool -/
#check true

-- 命題
/- info: True : Prop -/
#check True

逆に term の型が T であることを確かめるには example コマンドを使用して example : T := term とします。

example : Nat := 42

example : Int := - 13

なお「すべての」項に型があるので、特に型も型を持ちます。多くの組み込み型の型は Type になっています。

-- 文字列型の型は Type
/- info: String : Type -/
#check String

-- 自然数型の型は Type
/- info: Nat : Type -/
#check Nat

#eval

#eval コマンドは、式の値をその場で評価します。

import Lean.Elab.Command -- `#eval` コマンドのフルパワーを引き出す

/- info: 2 -/
#eval 1 + 1

-- 階乗関数
def fac : Nat → Nat
  | 0 => 1
  | n + 1 => (n + 1) * fac n

/- info: 120 -/
#eval fac 5

def main : IO Unit :=
  IO.println "Hello, world!"

/- info: Hello, world! -/
#eval main

#eval による出力結果は編集することができます。代表的な方法は、Repr クラスのインスタンスを実装することです。

/-- ユーザが持つ権限 -/
inductive Role where
  | admin
  | write
  | read

/- info: Role.admin -/
#eval Role.admin

-- `#eval` の結果を強制的に上書きする
instance : Repr Role where
  reprPrec := fun _role _ => "ほげほげ!"

/- info: ほげほげ! -/
#eval Role.admin

よくあるエラー

計算不能

not computationally relevant というエラーになることがあります。

/- error: Cannot evaluate, types are not computationally relevant -/
#eval Nat

/- error: Cannot evaluate, proofs are not computationally relevant -/
#eval (rfl : 1 + 1 = 2)

これは、Lean の型や証明項は計算可能な解釈を持たないためです。

表示方法がわからない

一般に ReprToString および ToExpr のインスタンスでないような型の項は、表示方法がわからないので #eval に渡すことができません。

/-
error: Could not synthesize a `ToExpr`, `Repr`, or `ToString` instance for type
  Nat → Nat
-/
#eval (fun x => x + 1)

Repr インスタンスがあれば関数であっても #eval に渡すことができます。

-- 最初はエラーになってしまう
/-
error: Could not synthesize a `ToExpr`, `Repr`, or `ToString` instance for type
  Unit → Nat
-/
#eval (fun _ => 1 : Unit → Nat)

instance {α : Type} [Repr α] : ToString (Unit → α) where
  toString x := s!"fun (_ : Unit) => {reprStr (x ())}"

-- Repr インスタンスを定義する
instance {α : Type} [Repr α] : Repr (Unit → α) where
  reprPrec x _ := toString x

-- #eval に渡せるようになった!
/- info: fun (_ : Unit) => 1 -/
#eval (fun _ => 1 : Unit → Nat)

補足: 式の評価結果を代入する

#eval コマンドは式を評価してその結果をユーザに表示しますが、「式の評価結果を別のコードに代入する」には eval% を使います。

import Mathlib.Tactic.Eval

def bar := 1 + 1

def foo₁ := eval% bar

def foo₂ := bar

/-
info: def foo₁ : Nat :=
2
-/
#print foo₁

/-
info: def foo₂ : Nat :=
bar
-/
#print foo₂

#find

#find はライブラリ検索を行うコマンドです。

Lean 実行環境で実行せずとも、Loogle というウェブサイトがあり、ここで #find を実行することができます。そして #loogle コマンドで Loogle をエディタから利用することができるので、使用法の解説はそちらに譲ります。

また、Mathlib の検索では Moogle というサイトもあります。こちらは自然言語検索が可能です。

#grind_lint

#grind_lint コマンドは、grind タクティクのために登録した定理がインスタンスの暴発を引き起こさないかどうか検査するためのコマンドです。

inspect サブコマンド

#grind_lint inspect thm は、指定した定理を個別に詳しく調べます。

def wrap (x : Nat) : Nat := 0

theorem wrap_branch (x : Nat)
    : wrap x = wrap (2 * x) ∧ wrap x = wrap (2 * x + 1) := by
  simp [wrap]

grind_pattern wrap_branch => wrap x

-- wrap_branch を展開すると wrap が指数関数的に増えていくので、
-- 膨大なインスタンスが生成される。
/-
info: instantiating `wrap_branch` triggers more than 100 additional `grind` theorem instantiations
---
info: wrap_branch
[thm] instances
  [thm] wrap_branch ↦ 100
---
info: Try this to display the actual theorem instances:
  [apply] set_option trace.grind.ematch.instance true in
  #grind_lint inspect wrap_branch
-/
#grind_lint inspect wrap_branch

生成されたインスタンス数が閾値を超えた場合にだけ報告をします。 閾値の値は、(min := n) という構文で指定できるので、全て報告してほしい場合は min := 0 とします。

def Nat.factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * Nat.factorial n

theorem Nat.factorial_succ (n : Nat) :
    Nat.factorial (n + 1) = (n + 1) * Nat.factorial n := by
  simp [Nat.factorial]

grind_pattern Nat.factorial_succ => n.factorial

#grind_lint inspect (min := 0) Nat.factorial_succ

#guard_msgs

#guard_msgs コマンドは、あるコマンドの出力が与えられた文字列と一致するか検証します。

/-- info: 2 -/
#guard_msgs in #eval 2

/--
error: failed to synthesize instance of type class
  HAdd ℕ String String

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
#guard_msgs in #eval (2 + "hello" : String)

空白の違いを無視させるには

#guard_msgs コマンドは空白の数に敏感で、空白の長さによって通ったり通らなかったりします。しかし、whitespace という引数に lax を指定することにより、この空白に関する制限は緩めることができます。

variable (α : Type)

-- 通常の場合
/--
error: failed to synthesize instance of type class
  Inv α

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
#guard_msgs in #check (_ : α)⁻¹

-- スペースを入れてもエラーにならない
/--
  error: failed to synthesize instance of type class
    Inv α

  Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
#guard_msgs (whitespace := lax) in #check (_ : α)⁻¹

舞台裏

#guard_msgs コマンドは「与えられたコマンドを実行してその出力メッセージを取り出す」ということを行いますが、これは elabCommandTopLevel 関数で実行することができます。これを利用すると、#guard_msgs コマンドの派生コマンドを自分で定義することができます。

ここでは例として、「ドキュメントコメントに書かれた内容が出力メッセージに含まれるかどうか判定するコマンド」を定義してみます。

import Batteries.Data.String.Matcher
import Lean.Elab.Command

open Lean Elab Command

/-- コマンドの実行結果のメッセージに特定の文字列が含まれるかどうか検証するコマンド -/
syntax (docComment)? "#contain_msg" "in" command : command

elab_rules : command
  | `(command| #contain_msg in $_cmd:command) => do
    logInfo "success: nothing is expected"

  | `(command| $doc:docComment #contain_msg in $cmd:command) => do
    -- ドキュメントコメントに書かれた文字列を取得する
    let expected := String.trimAscii (← getDocStringText doc) |>.copy
    if expected.isEmpty then
      logInfo "success: nothing is expected"
      return

    -- 与えられたコマンドを実行する
    withReader ({ · with snap? := none }) do
      elabCommandTopLevel cmd

    -- コマンドの実行結果のメッセージを取得する
    let msgs := (← get).messages.toList
    let msgStrs := (← msgs.mapM (·.data.toString))
      |>.map (·.replace "\"" "")

    -- コマンドの実行結果のメッセージに expected が含まれるか検証する
    for msgStr in msgStrs do
      unless String.contains msgStr expected do
        logError "error: output string does not contain the expected string"

-- ドキュメントコメントがない場合は何もしない
#contain_msg in #eval "hello"

/- info: success: nothing is expected -/
/-- -/ #contain_msg in #eval "hello"

/- error: error: output string does not contain the expected string -/
/-- 21 -/ #contain_msg in #eval 23

#guard

#guard は与えられた Bool 値が true であることを確かめます。

-- 階乗関数
def fac : Nat → Nat
| 0 => 1
| n + 1 => (n + 1) * fac n

#guard fac 5 == 120

決定可能性

#guardBool ではなく Prop 型の項を与えた場合、エラーになることがあります。次の命題は証明があるので真ですが、 #guard は通りません。

example (α : Type) (l : List α) : [] ⊆ l := by simp

-- Prop 型を持つ
#check ((α : Type) → ∀ (l : List α), [] ⊆ l : Prop)

/-
error: Type mismatch
  ∀ (α : Type) (l : List α), [] ⊆ l
has type
  Prop
but is expected to have type
  Bool
---
error: cannot evaluate code because 'sorryAx' uses 'sorry' and/or contains errors
-/
#guard ((α : Type) → ∀ (l : List α), [] ⊆ l : Prop)

しかし、 1 + 1 = 2 等も #check で確かめてみると型は Prop です。にも関わらず #guard に渡してもエラーになりません。これは不思議に思えますが、理由は 1 + 1 = 2Decidable 型クラスのインスタンスであり、決定可能だからです。

-- 型は Prop
/- info: 1 + 1 = 2 : Prop -/
#check 1 + 1 = 2

#guard 1 + 1 = 2

-- 1 + 1 = 2 は決定可能
#synth Decidable (1 + 1 = 2)

Prop 型であっても、Decidable クラスのインスタンスであれば Bool に変換できます。それを自動で行っているので、Prop 型の項でも #guard に通せるというわけです。

-- 決定可能な Prop 型の項を Bool に変換する関数
#check (decide : (p : Prop) → [_h : Decidable p] → Bool)

-- Bool 型になっている
/- info: decide (1 + 1 = 2) : Bool -/
#check decide (1 + 1 = 2)

DIY: 差分の表示

#guard コマンドを使って A = B という式を評価して false だったとき、デフォルトでは AB がそれぞれどんな値であるかは表示されません。単に「等しくない」というメッセージが出るだけです。

/-
error: Expression
  decide (1 + 1 = 3)
did not evaluate to `true`
-/
#guard 1 + 1 = 3

この挙動は少し不便です。#guard に渡された等式が false だったときに左辺と右辺の値を表示するようなコマンドが欲しいですね。これは、以下のように自作することができます。1

import Lean
import Qq

open Lean Meta Elab Command Qq

private def failGenerically (e : Expr) : TermElabM Unit := do
  throwError "Expression{indentExpr e}\ndid not evaluate to `true`"

private def fail (e : Expr) : TermElabM Unit := do
  let_expr Decidable.decide p _ := e | failGenerically e
  let_expr Eq _ lhs rhs := p | failGenerically e
  let lhs_fmt_expr : Q(Format) ← mkAppM ``repr #[lhs]
  let rhs_fmt_expr : Q(Format) ← mkAppM ``repr #[rhs]
  let lhs_fmt ← unsafe evalExpr Format q(Format) lhs_fmt_expr
  let rhs_fmt ← unsafe evalExpr Format q(Format) rhs_fmt_expr
  throwError
    "Expression{indentExpr e}\n\
    did not evaluate to `true`\n\
    ---\n\
    {lhs} = {lhs_fmt}\n\
    {rhs} = {rhs_fmt}"

/-- 等式を評価して不成立だったときに、左辺と右辺の値を表示するような
`#guard` コマンドの派生コマンド -/
elab "#guard_diff" e:term : command => liftTermElabM do
  let e ← Term.elabTermEnsuringType e q(Bool)
  Term.synthesizeSyntheticMVarsNoPostponing
  let e ← instantiateMVars e
  let mvars ← getMVars e
  if mvars.isEmpty then
    let v ← unsafe evalExpr Bool q(Bool) e
    unless v do fail e
  else
    _ ← Term.logUnassignedUsingErrorInfos mvars

/-
error: Expression
  decide (3 * 4 = 2 + 2)
did not evaluate to `true`
---
3 * 4 = 12
2 + 2 = 4
-/
#guard_diff 3 * 4 = 2 + 2

  1. こちらのコードは Zulip の #guard with diff というトピックで Marcus Rossel さんによって提案されたコードを参考にしています。

#help

#help は、ドキュメントを確認するためのコマンドです。以下のような機能があります。

  • #help tactic で全タクティクのリストが見られます。
  • #help option で全オプションのリストが見られます。オプションは、set_option を実行することで切り替えることができます。
  • #help attr で全属性(attribute)のリストが見られます。
  • #help command で全コマンドのリストが見られます。
  • #help term ですべての term syntax の一覧が見られます。

他にも機能がありますが、詳細はBatteriesのドキュメントをご覧ください。

Note

Mathlib4 Help#help コマンドの出力結果を見ることができます。

import Batteries.Tactic.HelpCmd

-- 以下のコマンドで全 tactic のリストが見られます
#help tactic

-- 全 attribute のリストを見るには次のコマンドです
#help attr

-- 全コマンドのリストを見るには次のコマンドが使えます
#help command

#html

#html コマンドは、その名の通り html をインフォビューに表示させるコマンドです。ProofWidgets4 というライブラリで定義されています。

JSX によく似た構文を使うことができます。

import ProofWidgets

-- JSX ライクな構文が使えるようにする
open scoped ProofWidgets.Jsx

#html <p>"ここに HTML を書きます"</p>

-- いろんなHTMLタグを書くことができる
#html
  <div>
    <h3>"見出し"</h3>
    <p>
      <b>"強調されたテキスト"</b>
      <i>"斜体のテキスト"</i>
    </p>
    <a href="https://lean-lang.org/">"リンク"</a>
  </div>

-- 画像も表示できる
#html
  <img src={"https://upload.wikimedia.org/wikipedia/commons/6/6a/Julia-set_N_z3-1.png"}
    alt="julia set"/>

コンポーネント紹介

HTMLタグを使用できるだけでなく、様々なコンポーネントが定義されています。

MarkdownDisplay

<MarkdownDisplay /> コンポーネントを使用すると、Markdown や TeX を表示させることができます。

import ProofWidgets

open ProofWidgets Jsx

-- Markdown と TeX を表示する
#html <MarkdownDisplay contents={"
  ## Riemann zeta function

  The Riemann zeta function is defined as
  $$
  \\zeta(s) = \\sum_{n=1}^∞ \\frac{1}{n^s}
  $$

  for $\\mathrm{Re} (s) > 0$.
"}/>

GraphDisplay

<GraphDisplay /> コンポーネントを使用すると、有向グラフを表示させることができます。

import ProofWidgets

open ProofWidgets Jsx ForceGraphDisplay

/-- `Edge` を作る -/
def mkEdge (st : String × String) : Edge := {source := st.1, target := st.2}

/-- 文字列として与えられたラベルから `Vertex` を作る -/
def mkVertex (id : String) : Vertex := {id := id}

-- 有向グラフを表示する
#html
  <ForceGraphDisplay
    vertices={#["a", "b", "c", "d", "e"].map mkVertex}
    edges={#[("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "a")].map mkEdge}
  />

LineChart

ProofWidgets4 には Recharts ライブラリ に対するサポートがあり、<LineChart /> コンポーネントを使用すると、関数のグラフを表示させることができます。

import ProofWidgets

open Lean ProofWidgets Recharts

open scoped Jsx

/-- 与えられた関数 `fn` の `[0, 1]` 区間上での値のグラフ -/
def Plot (fn : Float → Float) (steps := 100) : Html :=
  -- `[0, 1)` 区間を `steps` 個に分割する
  let grids := Array.range steps
    |>.map (fun x => x.toFloat / steps.toFloat)

  -- データを JSON に変換
  let y := grids.map fn
  let jsonData : Array Json := grids.zip y
    |>.map (fun (x,y) => json% {x: $(toJson x), y: $(toJson y)});

  <LineChart width={400} height={400} data={jsonData}>
    <XAxis dataKey?="x" />
    <YAxis dataKey?="y" />
    <Line type={.monotone} dataKey="y" stroke="#8884d8" dot?={Bool.false} />
  </LineChart>

#html Plot (fun x => (x - 0.3) ^ 2 + 0.1)
#html Plot (fun x => 0.2 + 0.5 * Float.sin (7 * x))

使用例

#html コマンドを使うと SVG 画像を infoview 内で直接表示させることができるという点、さらに ProofWidgets が SVG 画像の作成をある程度サポートしているという点を利用すると、「二分木を画像として infoview に表示させる」ということが可能です。1

import ProofWidgets

open ProofWidgets Svg

/-- デフォルトの表示領域(Frame) -/
private def defaultFrame : Frame := {
  xmin := 0 -- 左下隅の x 座標
  ymin := 0 -- 左下隅の y 座標
  width := 1000 -- 横方向のピクセル数
  height := 1000 -- 縦方向のピクセル数
  xSize := 1000 -- width と同じ値なので、ピクセル数と座標の値は一致
}

/-- 描画に関する設定 -/
structure RenderConfig where
  /-- ノードを描画するときの円の半径 -/
  radius : Nat := 16
  /-- ノードを描画するときの円の塗りつぶし色(RGB) -/
  fillColor : (Float × Float × Float) := (0.74, 0.87, 1.0)
  /-- ノードのラベルのフォントサイズ -/
  fontsize : Nat := 14
  /-- ノードのラベルの色(RGB) -/
  textColor : (Float × Float × Float) := (0.0, 0.0, 0.0)
  /-- エッジの色(RGB) -/
  edgeColor : (Float × Float × Float) := (50.0, 50.0, 50.0)
  /-- エッジの太さ(ピクセル) -/
  edgeWidth : Nat := 2
  /-- ノード間の水平・垂直間隔の基準値 -/
  step := 30.0

/-- `RenderConfig`を読み取りできる計算文脈を表すモナド -/
abbrev RenderM := ReaderM RenderConfig

/-- 位置情報を付加したノードのデータ -/
structure NodePos where
  /-- ノードのx座標(右に行くほど大きい) -/
  x : Float
  /-- ノードのy座標(下に行くほど大きい) -/
  y : Float
  /-- ラベル -/
  label : String

/-- 上面と下面を反転して計測したy座標 -/
private def NodePos.y_inv (self : NodePos) (f : Frame) : Float :=
  f.height.toFloat - self.y

/-- ノード(円とラベル)を作成する -/
private def createNodeElements (node : NodePos) (f : Frame) : RenderM (Array (Element f)) := do
  let radius := (← read).radius
  let fillColor := (← read).fillColor
  let fontsize := (← read).fontsize
  let textColor := (← read).textColor

  let circle := circle (node.x, node.y_inv f) (.px radius)
    |>.setFill fillColor
  let adjust := fontsize.toFloat * 0.35 -- ラベルの位置調整用
  let text := text (node.x - adjust, node.y_inv f - adjust) node.label (.px fontsize)
    |>.setFill textColor
  return #[circle, text]

/-- ノードの描画テスト用の関数 -/
private def createNodeHtml (node : NodePos) (f : Frame) : RenderM Html := do
  let elements ← createNodeElements node f
  let svg : Svg f := { elements := elements }
  return svg.toHtml

#html ReaderT.run (r := {}) <|
  createNodeHtml (f := defaultFrame) (node := { x := 150, y := 30, label := "A" })

/-- エッジ(ノードの親子関係)を作成する -/
private def createEdgeElement (parent child : NodePos) (f : Frame) : RenderM (Element f) := do
  let edgeColor := (← read).edgeColor
  let edgeWidth := (← read).edgeWidth
  let element := line (parent.x, parent.y_inv f) (child.x, child.y_inv f)
    |>.setStroke edgeColor (.px edgeWidth)
  return element

/-- エッジの描画テスト用の関数 -/
private def createEdgeHtml (parent child : NodePos) (f : Frame) : RenderM Html := do
  let element ← createEdgeElement parent child f
  let svg : Svg f := { elements := #[element] }
  return svg.toHtml

#html ReaderT.run (r := {}) <| createEdgeHtml
  (parent := { x := 150, y := 30, label := "A" })
  (child := { x := 100, y := 80, label := "B" })
  (f := defaultFrame)

/-- (ラベル付きの)二分木 -/
inductive BinTree (α : Type) where
  /-- 空の木 -/
  | empty
  /-- ノード -/
  | node (val : α) (left right : BinTree α)

variable {α : Type} [ToString α]

/-- 二分木をノードの配列に変換する。 -/
def BinTree.toNodes (tree : BinTree α) : Array α :=
  match tree with
  | .empty => #[]
  | .node val left right => #[val] ++ (left.toNodes ++ right.toNodes)

/-- 二分木のエッジを配列にする。(親, 子) のペアにして返すことに注意。 -/
def BinTree.toEdges {β : Type} (tree : BinTree β) : Array (β × β) :=
  match tree with
  | .empty => #[]
  | .node a left right =>
    let leftEdges :=
      match left with
      | .empty => #[]
      | .node b _ _ => (toEdges left).push (a, b)
    let rightEdges :=
      match right with
      | .empty => #[]
      | .node c _ _ => (toEdges right).push (a, c)
    leftEdges ++ rightEdges

/-- 3つ組データを構造体の項に変換する -/
def NodePos.ofPair (p : α × Nat × Nat) (step : Float) : NodePos :=
  let (label, x, y) := p
  { x := x.toFloat * step, y := y.toFloat * step, label := toString label }

/-- 2分木の描画情報が与えられたときに、それを SVG 画像として描画する -/
def BinTree.render (tree : BinTree (α × (Nat × Nat))) (f : Frame := defaultFrame) (cfg : RenderConfig := {}) : Html :=
  let html : RenderM Html := do
    let step := (← read).step
    let nodesArray := (← tree.toNodes
      |>.map (NodePos.ofPair (step := step))
      |>.mapM (fun node => createNodeElements node f))
      |>.flatten
    let edgesArray ← tree.toEdges
      |>.map (fun (x1, x2) => (NodePos.ofPair x1 step, NodePos.ofPair x2 step))
      |>.mapM (fun (parent, child) => createEdgeElement parent child f)
    let svg : Svg f := { elements := edgesArray ++ nodesArray }
    return svg.toHtml
  ReaderT.run html cfg

/-- 二分木の葉 -/
def BinTree.leaf (val : α) : BinTree α :=
  .node val .empty .empty

-- 二分木の描画テスト
-- レイアウト情報を手動で与えて描画している
#html
  let treeLayout := BinTree.node ("A", (2, 1))
    (.leaf ("B", (1, 2)))
    (.node ("C", (4, 2))
      (.leaf ("D", (3, 3)))
      (.leaf ("E", (5, 3))))
  BinTree.render treeLayout

/-- 2分木の各要素に一様に関数を適用する -/
def BinTree.map {α β : Type} (f : α → β) (tree : BinTree α) : BinTree β :=
  match tree with
  | .empty => .empty
  | .node val left right =>
    .node (f val) (map f left) (map f right)

/-- `BinTree`は関手 -/
instance : Functor BinTree where
  map := BinTree.map

/-- 2分木のレイアウト情報が渡されたときに、各ノードのレイアウト位置を一様にずらす -/
def BinTree.shift {β γ : Type} (tree : BinTree (α × β)) (shiftFn : β → γ) : BinTree (α × γ) :=
  (fun (a, pos) => (a, shiftFn pos)) <$> tree

/-- 2分木の描画幅。二分木を描画したときに何グリッド占めるか。 -/
def BinTree.width (tree : BinTree α) : Nat :=
  tree.toNodes.size - 1

/-- 二分木のレイアウトを計算する関数 -/
def BinTree.layout (tree : BinTree α) : BinTree (α × (Nat × Nat)) :=
  match tree with
  | .empty => .empty
  | .node a .empty .empty =>
    .node (a, (1, 1)) .empty .empty
  | .node a .empty right =>
    let rightLayout := layout right
    let rightShifted := rightLayout.shift (fun (x, y) => (x + 1, y + 1))
    .node (a, (1, 1)) .empty rightShifted
  | .node a left .empty =>
    let leftLayout := layout left
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + 1))
    .node (a, (left.width + 2) * 1, 1) leftShifted .empty
  | .node a left right =>
    let leftLayout := layout left
    let rightLayout := layout right
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + 1))
    let rightShifted := rightLayout.shift (fun (x, y) => (x + (left.width + 2) * 1, y + 1))
    .node (a, ((left.width + 2) * 1, 1)) leftShifted rightShifted

-- 二分木の描画テスト
-- 二分木からレイアウト情報を計算し、それを元に描画している
#html
  let tree := BinTree.node "A"
    (BinTree.leaf "B")
    (BinTree.node "C"
      (BinTree.leaf "D")
      (BinTree.leaf "E"))
  BinTree.render tree.layout

  1. この例を作成するにあたり、lean-ja Discord サーバーで todaymint さんにご助力をいただきました。

#instances

#instances は、与えられた型クラスのインスタンスの完全なリストを出力するコマンドです。

import Batteries.Tactic.Instances -- `#instances` コマンドを使うために必要

-- 内容が何もない例示のためだけの型クラス
class Hoge (α : Type) where
  hoge : Unit

-- `Nat` を `Hoge` のインスタンスにする
instance : Hoge Nat where
  hoge := ()

-- `Bool` を `Hoge` のインスタンスにする
instance : Hoge Bool where
  hoge := ()

-- 今登録した2つのインスタンスが表示される
/-
info: 2 instances:

instHogeBool : Hoge Bool
instHogeNat : Hoge Nat
-/
#instances Hoge

#lint

#lint コマンドは、環境リンター(environment linter)を実行します。

環境リンターとは何かというと、Lean のリンター(よくない書き方のコードを検出するツール)の一種です。 環境リンターは、#lint コマンドや lake lint コマンドで実行することができます。

Lean のリンターには他に、構文リンター(syntax linter)があります。

import Batteries.Tactic.Lint

-- ドキュメントコメントのない定理
theorem hoge : True := by trivial

-- ドキュメントコメントのない定理に対して警告するリンタ
/-
The `docBlameThm` linter reports:
THEOREMS ARE MISSING DOCUMENTATION STRINGS:
-/
#lint only docBlameThm

-- ドキュメントコメントのない定義
def fuga : True := by trivial

-- 定義にドキュメントコメントがあるか確認するリンタ
-- 定理は対象外なのでスルーされる
/-
The `docBlame` linter reports:
DEFINITIONS ARE MISSING DOCUMENTATION STRINGS:
-/
#lint only docBlame

環境リンターをユーザが自作したい場合は、Batteries.Tactic.Lint.Linter 型の項を作って [env_linter] 属性を付与します。

open Lean Batteries Tactic Lint

/-- `bad` という文字が入っている宣言を検出する、意味のないリンター -/
@[env_linter]
meta def findBad : Batteries.Tactic.Lint.Linter where
  noErrorsFound := "no bad declarations found."
  errorsFound := "BAD DECLARATIONS FOUND:"
  test declName := do
    -- 自動生成された宣言などを無視したければここで除外
    if ← isAutoDecl declName then
      return none

    -- declName を調べる
    let info ← getConstInfo declName

    -- 問題なしなら none
    -- 問題ありなら some メッセージ
    if info.name.toString.contains "bad" then
      return some m!"declaration name contains `bad`"
    else
      return none

def bad := "わるいよ!"

/-
error: -- Found 1 error in 4 declarations (plus 0 automatically generated ones) in the current file with 1 linters
-/
#lint only findBad

#loogle

#loogle コマンドは、Lean で定理や関数を探すための検索エンジンである、Loogle を利用した検索をエディタ上で行うためのコマンドです。Loogle で使用できるのと同様の検索クエリが使用できます。

#find と似ていますが、#find と違って #loogle はAPIを利用するだけなのでより高速に動作します。

検索クエリの書き方

名前に特定の文字列が含まれるかどうか

二重引用符 " で文字列を囲って与えると、補題の名前にその文字列が含まれるような補題や、関数名にその文字列が含まれるような関数を検索します。

たとえば、仮に Nat.add_zero : ∀ n : Nat, n + 0 = 0 の名前をどわすれして、思い出したいとしましょう。ここで「整数の、ゼロの和に関する定理」というところまで思い出せるのであれば、定理名に入っていそうな文字列を与えることで検索に引っかけることができます。

import LeanSearchClient.LoogleSyntax

/-
info: Loogle Search Results
  [apply] #check Nat.add_zero --  (n : ℕ) : n + 0 = n
  ⏎
  [apply] #check instNeZeroNatHAdd --  {n m : ℕ} [h : NeZero n] : NeZero (n + m)
  ⏎
  [apply] #check instNeZeroNatHAdd_1 --  {n m : ℕ} [h : NeZero m] : NeZero (n + m)
  ⏎
  [apply] #check Nat.zero_add --  (n : ℕ) : 0 + n = n
  ⏎
  [apply] #check Nat.add_one_ne_zero --  (n : ℕ) : n + 1 ≠ 0
  ⏎
  [apply] #check Nat.zero_ne_add_one --  (n : ℕ) : 0 ≠ n + 1
-/
#loogle "Nat", "zero", "add"

特定の定数・関数が登場するか

たとえば List.foldl という関数についてどのような補題が存在するか知りたいとします。このような場合、List.foldl が定理の中で言及されているような定理を検索したいですが、これはそのまま識別子を与えることで検索可能です。

/-
info: Loogle Search Results
  [apply] #check List.foldl --  {α : Type u} {β : Type v} (f : α → β → α) (init : α) : List β → α
  Folds a function over a list from the left, accumulating a value starting with `init`. The
  accumulated value is combined with the each element of the list in order, using `f`.
  ⏎
  Examples:
   * `[a, b, c].foldl f z  = f (f (f z a) b) c`
   * `[1, 2, 3].foldl (· ++ toString ·) "" = "123"`
   * `[1, 2, 3].foldl (s!"({·} {·})") "" = "((( 1) 2) 3)"`
  ⏎
  ⏎
  [apply] #check List.foldl_nil --  {α✝ : Type u_1} {β✝ : Type u_2} {f : α✝ → β✝ → α✝} {b : α✝} : List.foldl f b [] = b
  ⏎
  [apply] #check List.foldl_cons --  {α : Type u} {β : Type v} {a : α} {l : List α} {f : β → α → β} {b : β} : List.foldl f b (a :: l) = List.foldl f (f b a) l
  ⏎
  [apply] #check List.foldl_eq_foldr_reverse --  {α : Type u_1} {β : Type u_2} {l : List α} {f : β → α → β} {b : β} : List.foldl f b l = List.foldr (fun x y => f y x) b l.reverse
  ⏎
  [apply] #check List.foldl_reverse --  {α : Type u_1} {β : Type u_2} {l : List α} {f : β → α → β} {b : β} : List.foldl f b l.reverse = List.foldr (fun x y => f y x) b l
  ⏎
  [apply] #check List.foldr_eq_foldl --  {α : Type u_1} {init : α} {xs : List α} {f : α → α → α} [Std.Associative f] [Std.LawfulIdentity f init] : List.foldr f init xs = List.foldl f init xs
-/
#loogle List.foldl

型による検索

探したい定理・関数が持っているはずの型で検索することができます。たとえば List.map という関数をどわすれしたとしましょう。このとき、(α → β) → List α → List β という型を持つ関数を探すことで、List.map にたどり着くことができます。それには型変数の部分をメタ変数にして検索クエリを作ります。

/-
info: Loogle Search Results
  [apply] #check List.map --  {α : Type u_1} {β : Type u_2} (f : α → β) (l : List α) : List β
  Applies a function to each element of the list, returning the resulting list of values.
  ⏎
  `O(|l|)`.
  ⏎
  Examples:
  * `[a, b, c].map f = [f a, f b, f c]`
  * `[].map Nat.succ = []`
  * `["one", "two", "three"].map (·.length) = [3, 3, 5]`
  * `["one", "two", "three"].map (·.reverse) = ["eno", "owt", "eerht"]`
  ⏎
  ⏎
  [apply] #check List.modifyHead --  {α : Type u} (f : α → α) : List α → List α
  Replace the head of the list with the result of applying `f` to it. Returns the empty list if the
  list is empty.
  ⏎
  Examples:
   * `[1, 2, 3].modifyHead (· * 10) = [10, 2, 3]`
   * `[].modifyHead (· * 10) = []`
  ⏎
  ⏎
  [apply] #check List.mapTR --  {α : Type u} {β : Type v} (f : α → β) (as : List α) : List β
  Applies a function to each element of the list, returning the resulting list of values.
  ⏎
  `O(|l|)`. This is the tail-recursive variant of `List.map`, used in runtime code.
  ⏎
  Examples:
  * `[a, b, c].mapTR f = [f a, f b, f c]`
  * `[].mapTR Nat.succ = []`
  * `["one", "two", "three"].mapTR (·.length) = [3, 3, 5]`
  * `["one", "two", "three"].mapTR (·.reverse) = ["eno", "owt", "eerht"]`
  ⏎
  ⏎
  [apply] #check List.modify --  {α : Type u} (l : List α) (i : ℕ) (f : α → α) : List α
  Replaces the element at the given index, if it exists, with the result of applying `f` to it. If the
  index is invalid, the list is returned unmodified.
  ⏎
  Examples:
   * `[1, 2, 3].modify 0 (· * 10) = [10, 2, 3]`
   * `[1, 2, 3].modify 2 (· * 10) = [1, 2, 30]`
   * `[1, 2, 3].modify 3 (· * 10) = [1, 2, 3]`
  ⏎
  ⏎
  [apply] #check List.mapTR.loop --  {α : Type u} {β : Type v} (f : α → β) : List α → List β → List β
  ⏎
  [apply] #check List.map_eq_mapTR --  : @List.map = @List.mapTR
-/
#loogle (?a → ?b) → List ?a → List ?b

パターンによる検索

パターンで検索することもできます。たとえば、n * m = 0 ↔ n = 0 ∨ m = 0 を主張する定理の名前が知りたいとしましょう。このとき、定理の中に出現するべきパターンから検索することができます。

/-
info: Loogle Search Results
  [apply] #check Nat.mul_eq_zero --  {m n : ℕ} : n * m = 0 ↔ n = 0 ∨ m = 0
-/
#loogle "Nat", "mul", (_ * _ = 0), (_ = 0 ∨ _ = 0)

#print

#print コマンドには複数の機能がありますが、単体で使うと定義を表示することができます。

/-
info: inductive Or : Prop → Prop → Prop
number of parameters: 2
constructors:
Or.inl : ∀ {a b : Prop}, a → a ∨ b
Or.inr : ∀ {a b : Prop}, b → a ∨ b
-/
#print Or

/-
info: @[defeq] theorem Nat.add_succ : ∀ (n m : Nat), n + m.succ = (n + m).succ :=
fun n m => rfl
-/
#print Nat.add_succ

/-
info: structure And (a b : Prop) : Prop
number of parameters: 2
fields:
  And.left : a
  And.right : b
constructor:
  And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
-/
#print And

利用可能な構文

#print 単体で利用できるほか、サブコマンドも定義されています。利用できるサブコマンドの全体は、エラーメッセージから確認できますが、以下の通りです。

  • axioms
  • eqns
  • equations
  • sig
  • tactic
open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

/-
error: <input>:1:7: expected 'axioms', 'eqns', 'equations', 'sig', 'tactic', identifier or string literal
-/
#eval parse `command "#print axiom"

また、このエラーメッセージから、#print コマンドに直接渡せるのは識別子(identifier)または文字列リテラル(string literal)だけであることが確認できます。識別子ではない一般の項(term)を渡すと、構文エラーになります。

open Lean in

/-
error: Application type mismatch: The argument
  a
has type
  TSyntax `term
but is expected to have type
  TSyntax [`ident, `str]
in the application
  a.raw
-/
run_meta
  let a ← `(1 + 1)
  let _ ← `(#print $a)

#print axioms: 依存公理の確認

概要

#print axioms で、与えられた証明項が依存する公理を出します。たとえば Lean では排中律は選択原理 Classical.choice を使って証明するので、排中律は選択原理に依存しています。

/-- 排中律 -/
example : ∀ (p : Prop), p ∨ ¬p := Classical.em

/- info: 'Classical.em' depends on axioms: [propext, Classical.choice, Quot.sound] -/
#print axioms Classical.em

また、#print axioms は不正な証明を見つけるのにも有用です。sorry という命題を「証明したことにする」タクティクがありますが、これは sorryAx という万能な公理を導入していることが確認できます。

theorem contra : False := by sorry

/- info: 'contra' depends on axioms: [sorryAx] -/
#print axioms contra

ただし、#print axioms で常に不正な証明が発見できるわけではありません。たとえば debug.skipKernelTC オプションを使用することですり抜けることができてしまいます。

舞台裏

Lean.collectAxioms という関数を使用することにより、依存公理を調べて何かを行うような #print axioms の類似コマンドを自作することができます。ここでは例として、elab コマンドを使用して「ある定理が選択原理 Classical.choice に依存しているかどうか調べて、依存していればエラーにする」というコマンドを作成します。

section
  open Lean Elab Command

  /-- 選択原理に依存していないことを検証するコマンド -/
  elab "#detect_classical " id:ident : command => do
    -- 識別子(ident)を Name に変換
    let constName ← liftCoreM <| realizeGlobalConstNoOverload id

    -- 依存する公理を取得
    let axioms ← collectAxioms constName

    -- 依存公理がなかったときの処理
    if axioms.isEmpty then
      logInfo m!"'{constName}' does not depend on any axioms"
      return ()

    -- Classical で始まる公理があるかどうかチェック
    -- もしあればエラーにする
    let caxes := axioms.filter fun nm => Name.isPrefixOf `Classical nm
    if caxes.isEmpty then
      logInfo m!"'{constName}' is non-classical and depends on axioms: {axioms.toList}"
    else
      throwError m!"'{constName}' depends on classical axioms: {caxes.toList}"
end

-- 依存公理がないケース
/- info: 'Nat.add_zero' does not depend on any axioms -/
#detect_classical Nat.add_zero

-- 選択公理に依存しないケース
/- info: 'Nat.div_add_mod' is non-classical and depends on axioms: [propext] -/
#detect_classical Nat.div_add_mod

-- 選択公理に依存するときはエラー
/- error: 'Classical.em' depends on classical axioms: [Classical.choice] -/
#detect_classical Classical.em

opaque コマンドによって定義された名前や、partial コマンドによって定義された関数への依存を見つけ出したいとき、#print opaque というサブコマンドが利用できます。

import Batteries.Tactic.PrintOpaques

/-- 無限ループする関数 -/
partial def endless {α : Type u} (a : α) : α :=
  endless a

def exampleFunc (a : Nat) : Nat :=
  endless a + 1

/- info: 'exampleFunc' depends on opaque or partial definitions: [endless] -/
#print opaques exampleFunc

#reduce

#reduce は、与えられた式をこれ以上簡約できなくなるまで簡約します。

/- info: 4 -/
#reduce 1 + 3

/- info: 4 -/
#reduce (fun x => x + 1) 3

#evalとの違い

これだけ見ると #eval と同じですが、#reduce は式の評価ではなくて簡約なので、関数や型も渡すことができます。

以下は、関数を渡す例です。

/-- 1を足すだけの関数 -/
def addOne (x : Nat) := x + 1

-- addOne が定義に展開されている
/- info: fun x => x.succ -/
#reduce addOne

-- 合成が計算できる
/- info: fun x => x.succ.succ -/
#reduce addOne ∘ addOne

また次は #reduce コマンドに型を渡す例です。

/-- `Nat`に値を持つ`n`引数関数の型 -/
def Natural (n : Nat) : Type :=
  match n with
  | 0 => Nat
  | n + 1 => Natural n → Nat

-- 全く簡約されない
/- info: Natural 0 -/
#reduce Natural 0

再帰除去

#reduce コマンドに再帰関数を渡してみると、出力結果に .rec などが含まれる複雑な式が返ってきます。特に、出力結果は再帰的ではありません。

/-- 常にどんな値に対してもゼロを返す関数 -/
def zero {α : Type} (xs : List α) : Nat :=
  match xs with
  | [] => 0
  | _ :: xs => zero xs

/-
info: fun xs => (List.rec ⟨0, PUnit.unit⟩ (fun head tail tail_ih => ⟨tail_ih.1, tail_ih⟩) xs).1
-/
#reduce zero

これは、Lean が裏で 再帰除去(recursion elimination) を行っているためです。

実際、Lean の型チェッカは再帰的な定義を受け付けないように設計されています。なぜかというと、再帰的な定義を(制限なしに)許すと矛盾が簡単に示せてしまうからです。

/-- Lean の型チェッカが許すべきでない再帰関数の例 -/
unsafe def bad {P : Prop} : P := bad

-- 矛盾が示せてしまう
unsafe example : False := bad

#synth

型クラス C と型 T があるとき、#synth C TTC のインスタンスになっているかチェックします。もしインスタンスでなかった場合にはエラーになります。

型クラスとは

型クラスとは、複数の型に対して共通の機能や実装を提供するものです。具体例を見てみましょう。たとえば逆数は、複数の型に対して定義されています。

-- `⁻¹` で逆数を表すことができる
#check (1 : ℚ)⁻¹

-- 実数として見ても同じ
#check (1 : ℝ)⁻¹

逆数をとる関数 fun x => x⁻¹ の定義域はどうなっているのでしょうか?定義域を一般の型 α に拡張してみて、どうなるか見てみましょう。

variable (α : Type)

/-
error: failed to synthesize instance of type class
  Inv α

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
#check (_ : α)⁻¹

一般の型に対して逆数は定義できないので、エラーになってしまいました。エラーメッセージで αInv のインスタンスではないと言われています。この Inv が型クラスです。Inv のインスタンスであるような型 T に対しては、逆数関数 (·)⁻¹ : T → T が定義できるというわけです。

インスタンスとは

例えば実数 に対して逆数は定義できるだろうと予想されますが、実際 Inv のインスタンスであることが確認できます。

/- info: Real.instInv -/
#synth Inv Real

自然数 に対しては逆数が定義されていないと予想されますが、実際 Inv のインスタンスになっていません。

-- エラーになってしまう
/-
error: failed to synthesize
  Inv ℕ

Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.
-/
#synth Inv Nat

-- Inv のインスタンスになっていない
#check_failure (inferInstance : Inv Nat)

自分で無理やり Inv のインスタンスにしてみると、通るようになります。ここでは逆数関数を常に 1 になる定数関数としてみましょう。

instance : Inv Nat := ⟨fun _ => 1⟩

#synth Inv Nat

example : (1 : ℕ)⁻¹ = 1 := by rfl

#test

#test コマンドは、Plausible ライブラリで定義されているもので、与えられた命題が成り立つかどうか、具体例をランダムに生成してチェックすることで検証します。plausible タクティクと同様の機能を持ちます。1

import Plausible

/-- 農民の掛け算と呼ばれるよくわからない関数 -/
def peasantMul (x y : Nat) : Nat := Id.run do
  let mut x := x
  let mut y := y
  let mut prod := 0
  while x > 0 do
    if x % 2 = 1 then
      prod := prod + y
    x := x / 2
    y := y * 2
  return prod

-- どうやら a * b と等しいようだが…?
-- これは本当に正しいのだろうか
#guard peasantMul 3 2 = 6
#guard peasantMul 2 4 = 8

-- 正しそう!
#test ∀ n m, peasantMul n m = n * m

#test コマンドを使う利点としては、期待される仕様をそのまま表現したものがテストとして機能するので、テストを作る労力が軽減されるというのがあります。仕様を述語論理で表現することが容易な関数のテストや、異なる実装をした関数同士が実は等しいといったテストで有用でしょう。

/-- 与えられたリストに含まれない最小の要素を求める -/
def minFree (xs : List Nat) : Nat :=
  List.range (xs.length + 1)
    |>.removeAll xs
    |>.head!

-- `minFree`の出力は元のリストに含まれない
#test ∀ xs : List Nat, minFree xs ∉ xs

-- `minFree`の出力は、元のリストに含まれない数の集合に対して下界を与える
#test ∀ xs : List Nat, ∀ x : Nat, x ∉ xs → minFree xs ≤ x

  1. 以下に紹介する peasantMul 関数の例は、Jeff Erickson「Algorithms」の0章2節で紹介されているアルゴリズムを参考にさせていただきました。

#time

#time は、コマンドの実行時間を計測するためのコマンドです。ミリ秒単位で結果を出してくれます。

-- フィボナッチ数列の遅い実装
-- `n` に関して指数関数的な時間がかかる
def fibonacci : Nat → Nat
| 0 => 0
| 1 => 1
| n + 2 => fibonacci n + fibonacci (n + 1)

-- 環境にもよるが、1000ms以上かかってしまうことも
#time #eval fibonacci 32

-- フィボナッチ数列のより速い実装
-- `n` に関して線形時間で計算できる
def fib (n : Nat) : Nat :=
  (loop n).1
where
  loop : Nat → Nat × Nat
    | 0 => (0, 1)
    | n + 1 =>
      let p := loop n
      (p.2, p.1 + p.2)

-- 10 ms 程度で終わる
#time #eval fib 32

Warning

エディタ上から #eval コマンドで実行したり、コマンドラインで lean --run で実行したときにはインタプリタが使用されます。これにより計測できる実行時間は、コンパイル後のバイナリの実行時間とは異なります。

コンパイル後のバイナリの実行時間を計測するには lean_exe という lakefile のオプションを使用してください。

舞台裏

IO.monoMsNow という関数でそのときの時刻をミリ秒単位で取得できます。これにより #time コマンドのような時間計測を行うコマンドを自作できるでしょう。また IO.monoNanosNow という関数も存在し、これはナノ秒単位で結果を取得します。これを使うと、単位がナノ秒であるような #time の派生コマンドを自作できます。

import Lean

open Lean Elab Command Term Meta in

elab "#nano_time " stx:command : command => do
  -- 実行直前に計測開始
  let start_time ← IO.monoNanosNow

  -- コマンドを実行
  elabCommand stx

  -- 実行後に計測終了
  let end_time ← IO.monoNanosNow

  -- 差分を実行時間としてナノ秒単位で出力
  logInfo m!"time: {end_time - start_time}ns"

#nano_time
  #eval 21 * 2

#version

#version コマンドは、その環境での Lean のバージョンと OS の情報を表示します。

#version

#whats_new

#whats_new コマンドを使うと、あるコマンドによって新たに導入された定理や関数などを見ることができます。

import Mathlib.Util.WhatsNew

/-- 自然数っぽい何か -/
inductive MyNat : Type where
  | zero
  | succ (n : MyNat)

def MyNat.add (m n : MyNat) : MyNat :=
  match m with
  | .zero => n
  | .succ m' => MyNat.succ (MyNat.add m' n)

instance : Add MyNat where
  add := MyNat.add

theorem MyNat.zero_add (n : MyNat) : MyNat.zero + n = n := by
  rfl

/- info: -- Lean.Meta.simpExtension extension: 1 new entries -/
#whats_new in attribute [simp] MyNat.zero_add

#whnf

#whnf は、式を弱頭正規形(weak head normal form)に簡約するコマンドです。

弱頭正規形とは、式の構成要素の最初の部分(head)がそれ以上簡約できない形になっている状態のことです。たとえば式の頭が帰納型のコンストラクタや、ラムダ式になっていれば弱頭正規形です。

import Mathlib.Tactic.Conv -- `#whnf` コマンドを使うために必要

-- 弱頭正規形
/- info: (fun x => x + 1) 1 :: List.map (fun x => x + 1) [2, 3] -/
#whnf [1, 2, 3].map (· + 1)

-- 弱頭正規形で止まらずに完全に簡約した場合
/- info: [2, 3, 4] -/
#reduce [1, 2, 3].map (· + 1)

-- 上記の式がなぜ弱頭正規形であるのかの説明
example : [1, 2, 3].map (· + 1) = [2, 3, 4] := calc
  -- フィールド記法を展開する
  _ = List.map (· + 1) [1, 2, 3] := by rfl

  -- ラムダ式に展開される
  _ = List.map (fun x => x + 1) [1, 2, 3] := by rfl

  -- List.map を使うために、引数をコンストラクタに分解する
  _ = List.map (fun x => x + 1) (1 :: [2, 3]) := by rfl

  -- List.map の定義を展開する
  _ = (fun x => x + 1) 1 :: List.map (fun x => x + 1) [2, 3] := by rfl

  -- これはコンストラクタが先頭に来ているので、弱頭正規形になっている
  _ = List.cons ((fun x => x + 1) 1) (List.map (fun x => x + 1) [2, 3]) := by rfl

  _ = [2, 3, 4] := by rfl

型クラスの実装を調べる

一見して使いどころがないようですが、型クラスの実装を調べたいときに有用です。

たとえば、以下のように Many という型を定義し、Monad 型クラスの実装を与えたとします。

/-- 遅延評価版の List もどき -/
inductive Many (α : Type) where
  | none : Many α
  | more : α → (Unit → Many α) → Many α

variable {α β : Type}

def Many.one (x : α) : Many α := Many.more x (fun () => Many.none)

def Many.union {α : Type} : Many α → Many α → Many α
  | .none, ys => ys
  | .more x xs, ys => Many.more x (fun () => union (xs ()) ys)

def Many.bind : Many α → (α → Many β) → Many β
  | .none, _ => Many.none
  | .more x xs, f =>
    (f x).union (bind (xs ()) f)

/-- Many にモナドの実装を与える -/
instance instMonadMany : Monad Many where
  pure := Many.one
  bind := Many.bind

Monad 型クラスは Functor クラスや Applicative クラスの実装を含むので、ManyFunctorApplicative のインスタンスでもあります。このインスタンスは次のように inferInstance という関数で構成することができますが、その中身を #print で出力してみても実装がわかりません。

instance instManyFunctor : Functor Many := inferInstance

/-
info: @[instance_reducible] def instManyFunctor : Functor Many :=
inferInstance
-/
#print instManyFunctor

しかし、#whnf コマンドに渡すと実装内容を表示してくれます。

-- Functor の実装を表示させることができた
/- info: { map := fun {α β} f x => (fun {α β} => Many.bind) x ((fun {α} => Many.one) ∘ f) } -/
#whnf (inferInstance : Functor Many)

-- seq の実装を表示させることができた
/-
info: {
  seq := fun {α β} f x =>
    (fun {α β} => Many.bind) f fun y => (fun {α β} => Many.bind) (x ()) ((fun {α} => Many.one) ∘ y) }
-/
#whnf (inferInstance : Seq Many)

なお、これを #reduce で行おうとすると簡約しすぎて読みづらい表示が出てきます。

/-
info: {
  map := fun {α β} f x =>
    (Many.rec ⟨fun x => Many.none, PUnit.unit⟩
          (fun a a_1 a_ih =>
            ⟨fun x =>
              (Many.rec ⟨fun x => x, PUnit.unit⟩
                    (fun a a_2 a_ih => ⟨fun x => Many.more a fun x_1 => (a_ih PUnit.unit).1 x, a_ih⟩) (x a)).1
                ((a_ih PUnit.unit).1 x),
              a_ih⟩)
          x).1
      fun x => Many.more (f x) fun x => Many.none,
  mapConst := fun {α β} x x_1 =>
    (Many.rec ⟨fun x => Many.none, PUnit.unit⟩
          (fun a a_1 a_ih =>
            ⟨fun x =>
              (Many.rec ⟨fun x => x, PUnit.unit⟩
                    (fun a a_2 a_ih => ⟨fun x => Many.more a fun x_2 => (a_ih PUnit.unit).1 x, a_ih⟩) (x a)).1
                ((a_ih PUnit.unit).1 x),
              a_ih⟩)
          x_1).1
      fun x_2 => Many.more x fun x => Many.none }
-/
#reduce (inferInstance : Functor Many)

宣言的コマンド

トップレベルコマンドには # から始まるものとそうでないものがあり、そうでないものを本書では便宜的に「宣言的コマンド」と呼んでいます。これは、そうしたコマンドが定義を行ったり属性を付与したりと、Lean に何かを宣言するために使われることが多いためです。

Warning

宣言的コマンド(declarative command)という用語は一般的なものではなく、著者が本書で使うために考案したものです。

abbrev

abbrev は、略称(abbreviation)を宣言するコマンドです。

たとえば、Nat 型に別の名前を与えたかったとしましょう。Lean では型も他の項と同様に宣言できるので、次のように書いて問題がないように見えます。1

def NaturalNumber : Type := Nat

しかし、ここで定義した Nat の別名を項に対して使用するとエラーになります。エラーメッセージには以下の通り「NaturalNumberOfNat インスタンスが見つかりません」という旨のことが書かれています。これは、Lean が NaturalNumber を定義に展開してそれが実は Nat に等しいことを知るよりも先に、42 : NaturalNumber という表記が定義されているか OfNat のインスタンスを探そうとしてエラーになったことを示唆します。

/-
error: failed to synthesize instance of type class
  OfNat NaturalNumber 42
-/
#check (42 : NaturalNumber)

ここでエラーを修正する方法の一つが、def の代わりに abbrev を使用することです。

abbrev NaturalNumber : Type := Nat

#check (42 : NaturalNumber)

舞台裏

abbrev コマンドを使用すると、裏で [reducible] 属性が付与されます。

abbrev NaturalNumber : Type := Nat

-- `[reducible]` 属性が付与されている
/-
info: @[reducible] def NaturalNumber : Type :=
Nat
-/
#print NaturalNumber

  1. 以下のコード例は、Functional Programming in Lean の1章3節の記述を参考にさせていただきました。

add_aesop_rules

add_aesop_rulesaesop タクティクに追加のルールを登録するためのコマンドです。

import Aesop

/-- 自然数 n が正の数であることを表す帰納的述語 -/
inductive Pos : Nat → Prop where
  | succ n : Pos (n + 1)

example : Pos 1 := by
  -- 最初はルールが足りなくて示せない
  fail_if_success aesop

  -- 手動でコンストラクタを `apply` することで証明できる
  apply Pos.succ

-- `Pos` 関連のルールを `aesop` に憶えさせる
add_aesop_rules safe constructors [Pos]

-- aesop で示せる
example : Pos 1 := by aesop

なお aesop をカスタマイズしたものを専用のタクティクにまとめることも可能ですが、それについてはここでは詳しく述べません。declare_aesop_rule_sets コマンドのページを参照してください。

add_aesop_rules は、add_aesop_rules <phase>? <priority>? <builder_name>? <rule_sets>? という構文で使用できます。

phase について

phasenormsafeunsafe の3通りです。「ルールを適用した後、ダメそうだとわかったら引き返せ」「常に最初に適用せよ」など試行錯誤のやり方を指示します。

open Lean Parser Category in

-- `Aesop.phase` という構文カテゴリが存在する
#check (Aesop.phase : Category)

norm

norm は正規化(normalisation)ルールを表します。最初に適用されるルール群であり、適用によりゴールが増えないようなルールだけを登録することが推奨されます。[simp] 補題と同様に使用されます。

/-- And を模倣して自作した型 -/
structure MyAnd (a b : Prop) : Prop where
  intro ::
  left : a
  right : b

/-- `P ∧ P ↔ P` に相当するルール -/
theorem erase_duplicate {P : Prop} : MyAnd P P ↔ P := by
  constructor <;> intro h
  · rcases h with ⟨h⟩
    exact h
  · exact MyAnd.intro h h

example (P : Prop) (hp : P) : MyAnd P P := by
  -- 最初は aesop で証明できない
  fail_if_success solve
  | aesop

  sorry

-- aesop に登録する
add_aesop_rules norm simp [erase_duplicate]

example (P : Prop) (hp : P) : MyAnd P P := by
  -- aesop で証明できるようになった!
  aesop

safe

safe ルールは、norm ルールの実行後に適用されます。あるゴールが証明可能であるとき、それに safe ルールを適用しても生成されるゴールは依然として証明可能であるように、safe ルールを選ぶことが推奨されます。

/-- 自前で定義したリスト -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)

/-- リストが空ではないことを表す帰納的述語 -/
inductive NonEmpty {α : Type} : MyList α → Prop where
  | cons x xs : NonEmpty (MyList.cons x xs)

example : NonEmpty (MyList.cons 1 MyList.nil) := by
  -- 最初は aesop で証明できない
  fail_if_success aesop
  sorry

local add_aesop_rules safe apply [NonEmpty.cons]

-- aesop で示せるようになった!
example : NonEmpty (MyList.cons 1 MyList.nil) := by aesop

unsafe

unsafe ルールは、すべての safe ルールが失敗した場合に適用されます。失敗したらバックトラックして他の unsafe ルールを試します。priority として成功する確率(%)を指定する必要があります。

variable (a b c d e : Nat)

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- 最初は aesop で証明できない
  fail_if_success aesop
  sorry

-- 推移律を `unsafe` ルールとして登録する
local add_aesop_rules unsafe 10% apply [Nat.le_trans]

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- aesop で証明できるようになった!
  aesop

safe ルールは適用すると後戻りができないため、特定の状況でのみ適用したいルールは unsafe とすることが推奨されます。誤って safe ルールに登録してしまうと上手く動作しないことがあります。

-- safe ルールとして推移律を登録する
local add_aesop_rules safe apply [Nat.le_trans]

variable (a b c d e : Nat)

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- aesop で証明できない
  -- 同じ命題を同じように登録したが、safe にしてしまったからダメだった
  fail_if_success aesop

  sorry

builder_name について

builder_name は、登録されるルールに対して「ゴールを分解する」「仮定から推論を進める」といった方向性を決めます。複数の選択肢がありますが、ここではその一部を紹介します。

open Lean Parser Category in

-- `Aesop.builder_name` という構文カテゴリが存在する
#check (Aesop.builder_name : Category)

apply

apply タクティクと同様にはたらくルールを登録します。

example (a b c d e : Nat)
    (h1 : a < b) (h2 : b < c) (h3 : c < d) (h4 : d < e) : a < e := by
  -- 最初は aesop で示せない
  fail_if_success aesop

  -- 手動で示すならこのように apply を繰り返すことになる
  apply Nat.lt_trans (m := d) <;> try assumption
  apply Nat.lt_trans (m := c) <;> try assumption
  apply Nat.lt_trans (m := b) <;> try assumption

-- 推移律を登録する
local add_aesop_rules unsafe 10% apply [Nat.lt_trans]

example (a b c d e : Nat)
    (h1 : a < b) (h2 : b < c) (h3 : c < d) (h4 : d < e) : a < e := by
  -- aesop で証明できるようになった
  aesop

constructors

constructors ビルダーは、帰納型 T の形をしたゴールに遭遇した際に、コンストラクタを適用するように指示します。

/-- 自前で定義した偶数を表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | succ m : Even m → Even (m + 2)

example : Even 2 := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動でコンストラクタを適用することで示せる
  apply Even.succ
  apply Even.zero

-- aesop にルールを登録する
local add_aesop_rules safe constructors [Even]

example : Even 2 := by
  -- aesop で証明できるようになった
  aesop

cases

cases ビルダーは、帰納型 T の形をした仮定がローカルコンテキストにある場合に、それに対して再帰的に cases タクティクを使用して分解するように指示します。

/-- 自前で定義した奇数を表す帰納的述語 -/
inductive Odd : Nat → Prop where
  | one : Odd 1
  | succ m : Odd m → Odd (m + 2)

example (n : Nat) (h : Odd (n + 2)) : Odd n := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動で cases を使って分解することで証明できる
  cases h
  assumption

-- aesop にルールを登録する
local add_aesop_rules safe cases [Odd]

example (n : Nat) (h : Odd (n + 2)) : Odd n := by
  -- aesop で証明できるようになった
  aesop

destruct

destruct ビルダーは、A₁ → ⋯ → Aₙ → B という形の命題を登録することで、仮定に A₁, ..., Aₙ が含まれている場合に、元の仮定を消去して B を仮定に追加します。

/-- 任意の数 n について、n か n + 1 のどちらかは偶数 -/
theorem even_or_even_succ (n : Nat) : Even n ∨ Even (n + 1) := by
  induction n with
  | zero => left; apply Even.zero
  | succ n ih =>
    rcases ih with ih | ih
    · right
      apply Even.succ
      assumption
    · left
      assumption

example {n : Nat} (h0 : ¬ Even n) (h1 : ¬ Even (n + 1)) : False := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動で補題を示すことで証明する
  have := even_or_even_succ n
  simp_all

local add_aesop_rules unsafe 30% destruct [even_or_even_succ]

example {n : Nat} (h0 : ¬ Even n) (h1 : ¬ Even (n + 1)) : False := by
  -- aesop で示せるようになった!
  aesop

tactic

tactic ビルダーは、タクティクを追加のルールとして直接利用できるようにします。

example (a b : Nat) (h : 3 ∣ (10 * a + b)) : 3 ∣ (a + b) := by
  -- aesop で証明できない
  fail_if_success aesop

  -- omega で証明できる
  omega

-- aesop にルールを登録する
local add_aesop_rules safe tactic [(by omega)]

example (a b : Nat) (h : 3 ∣ (10 * a + b)) : 3 ∣ (a + b) := by
  -- aesop で証明できるようになった!
  aesop

attribute

attribute は、属性(attribute)を付与するためのコマンドです。

次の例では、命題に [simp] 属性を付与しています。これは simp タクティクで利用される命題を増やすことを意味します。

theorem foo {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  constructor <;> intro h
  all_goals
    refine ⟨?_, by simp_all⟩
    simp_all

example {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  -- `simp` では示せない
  fail_if_success solve
  | simp

  sorry

-- `attribute` で属性を付与
attribute [simp] foo

example {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  -- `simp` で示せるようになった
  simp

属性の削除

与えた属性を削除することができることもあります。削除するには - を属性の頭に付けます。属性の削除はデバッグを意図した機能で、常にローカルにはたらき、そのセクションの外に出ると削除された属性が戻ります。

section
  -- `[simp]` 属性を削除
  attribute [-simp] foo

  -- 再び `simp` では示せなくなった
  example {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
    fail_if_success solve
    | simp

    sorry
end

-- `simp` で示せるようになった
example {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by simp

属性によっては、削除することができないこともあります。

@[irreducible] def greet := "Hello"

/- error: Attribute `[irreducible]` cannot be erased -/
attribute [-irreducible] greet

タグ

attribute コマンドを使用すると定義の後から属性を付与することができますが、定義した直後に属性を付与する場合はタグと呼ばれる @[..] という書き方が使えます。

@[simp]
theorem bar {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  constructor <;> intro h
  all_goals
    refine ⟨?_, by simp_all⟩
    simp_all

example {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  simp

有効範囲を制限する

特定の section 内でのみ付与した属性を有効にするには、local で属性名を修飾します。

example (P Q : Prop) : ((P ∨ Q) ∧ ¬ Q) ↔ (P ∧ ¬ Q) := by
  -- simp だけでは証明できない
  fail_if_success solve
  | simp

  sorry

section
  -- 補題
  theorem or_and_neg (P Q : Prop) : ((P ∨ Q) ∧ ¬ Q) ↔ (P ∧ ¬ Q) := by
    constructor <;> intro h
    · refine ⟨?_, by simp_all⟩
      have : ¬ Q := h.right
      simp_all
    · refine ⟨?_, by simp_all⟩
      simp_all

  -- local に simp 補題を登録
  attribute [local simp] or_and_neg

  -- simp で証明ができるようになった!
  example (P Q : Prop) : ((P ∨ Q) ∧ ¬ Q) ↔ (P ∧ ¬ Q) := by simp
end

example (P Q : Prop) : ((P ∨ Q) ∧ ¬ Q) ↔ (P ∧ ¬ Q) := by
  -- section を抜けると simp 補題が利用できなくなった
  fail_if_success solve
  | simp

  sorry

axiom

axiom は、公理(axiom)を宣言するためのコマンドです。公理とは、議論の前提のことで、証明を与えることなく正しいと仮定される命題です。

/-- sorryAx を真似て作った公理 -/
axiom mySorryAx {P : Prop} : P

-- 任意の命題を示すことができる
theorem FLT : ∀ x y z n : Nat, n > 2 → x^n + y^n ≠ z^n := by
  apply mySorryAx

/- info: 'FLT' depends on axioms: [mySorryAx] -/
#print axioms FLT

組み込みの公理

組み込みで用意されている公理をいくつか紹介します。

命題外延性 propext

命題外延性の公理 propext は、同値な命題は等しいという公理です。この公理があることにより、どのような状況でも常に命題をそれと同値な命題と置き換えることができます。

-- 命題外延性の公理
/- info: axiom propext : ∀ {a b : Prop}, (a ↔ b) → a = b -/
#print propext

-- 命題外延性の公理を使って命題を置換する
theorem ex_prop_ext (a b : Prop) (p : Prop → Prop) (h : a ↔ b) (h₁ : p a) : p b := by
  have := propext h
  rw [←this]
  assumption

/- info: 'ex_prop_ext' depends on axioms: [propext] -/
#print axioms ex_prop_ext

商の公理 Quot.sound

任意の型 α : Sort uα 上の2項関係 r : α → α → Prop に対して、その商(quotient)を作ることができます。商の概念は、以下に示す複数の定数から構成されます。

section
  universe u

  variable {α : Sort u}

  -- 商
  #check (Quot : (α → α → Prop) → Sort u)

  -- 商の構築
  #check (Quot.mk : (r : α → α → Prop) → α → Quot r)

  -- 帰納法の原理。
  -- 任意の部分集合 `β ⊆ Quot r` に対して、
  -- β が `Quot.mk r a` の形の項を全て含むならば、
  -- β は商 `Quot r` 全体に一致する。
  #check (Quot.ind :
    {r : α → α → Prop} → {β : Quot r → Prop}
    → (∀ a, β (Quot.mk r a)) → ∀ q, β q)

  -- 要するに商 `Quot r` の全ての項は `Quot.mk r a` の形をしている。
  -- Quot.ind から直ちに従う。
  example (r : α → α → Prop) (q : Quot r) : ∃ a : α, q = Quot.mk r a := by
    have := Quot.ind (β := fun q => ∃ a : α, q = Quot.mk r a)
    apply this
    intro a
    exists a

  -- 関数の商へのリフト。
  -- 関数 `f : α → β` が、関係 `r` に関して合同性を持つならば、
  -- `f` をリフトして関数 `Quot r → β` が得られる。
  #check (Quot.lift :
    {r : α → α → Prop} → {β : Sort u} → (f : α → β)
    → (∀ a b, r a b → f a = f b) → Quot r → β)
end

商の公理 Quot.sound は上記の「商のような」概念を本物の商にします。

-- `r a b` が成り立つならば、商に送った時に同じ値になることを主張する。
/-
info: axiom Quot.sound.{u} : ∀ {α : Sort u} {r : α → α → Prop} {a b : α},
r a b → Quot.mk r a = Quot.mk r b
-/
#print Quot.sound

商の公理はなぜ重要か

上記で挙げた商を構成する以下の定数は、いずれも他の型からは独立したオブジェクトです。

  • Quot
  • コンストラクタ Quoto.mk
  • 帰納法の原理 Quot.ind
  • 関数の商へのリフト Quot.lift
  • 商の公理 Quot.sound

この中で Quot.sound だけが「公理」と呼ばれ、特別扱いされているのは何故でしょうか。以下のように商の公理以外の部分を Lean の帰納型を使って構成してみると理解できるかもしれません。

section
  universe u
  variable {α : Type}

  /-- 標準ライブラリの Quot を真似して自作した型 -/
  inductive MyQuot (r : α → α → Prop) : Type u where
    | mk (a : α)

  -- 商型のコンストラクタ
  #check (MyQuot.mk : {r : α → α → Prop} → α → MyQuot r)

  -- 自動生成された商型の帰納法の原理
  -- Quot.ind とそっくりであることがわかる
  #check (MyQuot.rec :
    {r : α → α → Prop} → {β : MyQuot r → Sort _}
    → (mk : ∀ a : α, β (MyQuot.mk a)) → ∀ q, β q)
end

ここで、関数の商へのリフト Quot.lift に対応するものを具体的に関数として作ることができます。

/-- 商へのリフトの対応物 -/
def MyQuot.lift.{u} {α : Type}{r : α → α → Prop} {β : Sort u}
  (f : α → β) (_ : ∀ a b, r a b → f a = f b) : MyQuot r → β
  | MyQuot.mk a => f a

こうして Quot.sound を使わず、Lean に備わっている帰納型の構成だけを使って作った MyQuot は、商の公理以外のすべての点で商型にそっくりですが、全然商になっていません。コンストラクタを使って作られる項 MyQuot.mk a は、a : α が異なればすべて異なる項であり、同一視が全く入っていません。このような観点から、Quot.sound は商を商たらしめる重要なものであり、「商の公理」と呼ぶにふさわしいと考えられるわけです。

関数外延性

商の公理 Quot.sound を利用して、関数外延性を示すことができます。関数外延性とは、関数 f, g について ∀ x, f x = g x という仮定から f = g が示せるという定理です。直観的には「すべての入力に対して同じ値を返すような2つの関数は等しい」と主張しています。

実際に証明していきます。一般に証明を理解するためには、前提を確認することが大切です。「商の公理を使用しなくても、前提として等しいことが分かっているものは何か」をまず確認しましょう。前提として利用できる事実には、次の2つがあります:

  • 関数 f とラムダ式 fun x => f x は等しい。(これは η 簡約(eta reduce) と呼ばれる簡約ルールより従います)
  • 関数 f と、「関数 f を適用する関数」は等しい。

実際に、公理を一切使わずにこれは証明できます。

universe u v

variable {α : Type u} {β : α → Sort v}

/-- η 簡約。依存関数はラムダ式と等しい。 -/
theorem lambda_eq (f : (x : α) → β x) : f = (fun x => f x) := by rfl

-- 依存関数 `f` がラムダ式 `fun x => f x` に等しいことは、定義から従うので
-- 何の公理も必要としない。
/- info: 'lambda_eq' does not depend on any axioms -/
#print axioms lambda_eq

/-- 関数適用を行う高階関数 -/
def funApp (a : α) (f : (x : α) → β x) : β a := f a

/-- 「`f` を適用する関数」と `f` は等しい -/
theorem funApp_eq (f : (x : α) → β x) : funApp (f := f) = f := calc
  -- 関数とラムダ式は等しい
  _ = (fun a => funApp a f) := by rw [lambda_eq funApp]

  -- funApp の定義から等しい
  _ = (fun a => f a) := by dsimp only [funApp]

  -- 関数とラムダ式は等しい
  _ = f := by rw [lambda_eq f]

-- これも何の公理も必要としない
/- info: 'funApp_eq' does not depend on any axioms -/
#print axioms funApp_eq

この事実を使うと、商の公理から関数外延性の証明ができます。

/-- 関数外延性の定理 -/
theorem my_funext {f g : (x : α) → β x} (h : ∀ x, f x = g x) : f = g := by
  -- 外延性等式を表す二項関係を定義する
  let eqv (f g : (x : α) → β x) := ∀ x, f x = g x

  -- 二項関係 eqv で `(x : α) → β x` の商を取る
  let «[(x : α) → β x]» := Quot eqv

  -- 関数 `f` と `g` は商の公理から、商に送ると等しいことに注意する。
  -- 商の公理はここで使っている。
  have : Quot.mk eqv f = Quot.mk eqv g := Quot.sound (λ x => h x)

  -- 関数適用 `funApp` は、適用する項 `a` を固定すれば `(x : α) → β x` 上の関数と見なせる。
  -- この関数は同値関係 `eqv` を保つため、
  -- 関数適用 `funApp` を商 `«[(x : α) → β x]»` 上に持ち上げることができる。
  let «[funApp]» (a : α) (f' : «[(x : α) → β x]») : β a := by
    have lift := @Quot.lift ((x : α) → β x) eqv (β a) (funApp a)
    apply lift
    · intro f g h
      exact h a
    · exact f'

  -- `f = g` を示す問題を `«[funApp]»` をかませることで、
  -- 商での等式に帰着させることができる。
  exact show f = g from calc
    -- 「`f` を適用する関数」と `f` は等しい
    _ = funApp (f := f) := by rw [funApp_eq f]

    -- 商への関数の持ち上げの定義から等しい
    _ = «[funApp]» (f' := Quot.mk eqv f) := by rfl

    -- 関数 `f` と `g` は商の公理から、商に送ると等しい
    _ = «[funApp]» (f' := Quot.mk eqv g) := by rw [this]

    -- 商への関数の持ち上げの定義から等しい
    _ = funApp (f := g) := by rfl

    -- 「`g` を適用する関数」と `g` は等しい
    _ = g := by rw [funApp_eq g]

/- info: 'my_funext' depends on axioms: [Quot.sound] -/
#print axioms my_funext

選択原理 Classical.choice

選択原理は、ある型が空ではないという情報だけから、「魔法のように」具体的な元を構成することができると主張します。これは計算不可能な操作であるため、選択原理を使用する関数には noncomputable 修飾子が必要になります。

選択原理は数学でいうと NBG(Neumann-Bernays-Gödel) 集合論における大域選択公理(axiom of global choice)とよく似ています。

-- 選択原理は、空でない型から具体的な元を構成する
#check (Classical.choice : {α : Sort u} → Nonempty α → α)

ここで、ある型が空ではないという主張は Nonempty という型クラスで表現されています。Nonempty α∃ e : α, True と同値なので、存在命題だと思って構いません。

example (α : Type) : Nonempty α ↔ ∃ e : α, True := by
  constructor
  · intro ⟨a⟩
    exists a
  · intro ⟨a, _⟩
    exact ⟨a⟩

存在命題に対する選択

選択原理は単に項を取ってくる関数ですが、選んだ項が満たすべき性質が欲しいこともあります。言い換えれば、存在命題 ∃ x : α, P x が成り立っているとき、P x が成り立つような x : α を取ってくる関数が必要なこともあります。それは選択原理を使って次のように構成することができます。

variable {α : Sort u}

noncomputable def indefiniteDescription (p : α → Prop) (h : ∃ x, p x) : {x // p x} := by
  -- 選択原理を使用する
  apply Classical.choice

  -- p x となる x が存在することを示せばよい
  show Nonempty {x // p x}

  -- 仮定の存在命題から x を取り出す
  obtain ⟨x, px⟩ := h

  -- この x が所望の条件を満たす
  exact ⟨x, px⟩

Lean のライブラリ上では、存在命題から選択する関数には Classical.choose という名前が、選択された項が満たすべき性質には Classical.choose_spec という名前がついています。

variable {p : α → Prop}

-- 存在命題から選択する関数
#check (Classical.choose : (h : ∃ x, p x) → α)

-- 選択された要素が満たす性質
#check (Classical.choose_spec : (h : ∃ x, p x) → p (Classical.choose h))

なぜ証明無関係と矛盾しないのか

証明無関係の節で詳しく述べているように、存在命題は「何かがある」としか主張しておらず、具体的な項を復元するのに必要な情報を持っていません。したがって存在が主張されている項を取り出して関数の返り値にすることはできないはずですが、選択原理を使うと存在命題から具体的な項が得られてしまいます。これが矛盾を生まないのはなぜでしょうか?

答えは、選択原理は証明方法が異なる証明項であっても、同じ命題であれば一様に同じ項を返すからです。以下の例では、「証拠」が異なるような存在命題の証明項を2つ与えていますが、そこから選択される項は同じです。つまり、選択原理は同値な存在命題すべてに対して一斉に同じ項を取り出す方法があると主張しているのであって、「存在が主張されている項を取り出して関数の返り値にする」ことができるとまでは主張していないのです。

-- 同じ存在命題の2通りの証明
-- 2乗すると1になる整数を2通り与えた
theorem foo : ∃ x : Int, x ^ 2 = 1 := by exists 1
theorem bar : ∃ x : Int, x ^ 2 = 1 := by exists -1

open Classical in

-- 選択される項は同じ
example : choose foo = choose bar := by rfl

排中律

選択原理と命題外延性と関数外延性を併せると、排中律を証明することができます。これは Diaconescuの定理 として知られる結果です。以下にその証明を示しましょう。

まず、命題論理から始めます。排中律は任意の命題 P : Prop に対して P ∨ ¬ P が成り立つという主張ですが、これを示すにはある命題 Q : Prop に対して「P → Q かつ ¬ Q ∨ P」を示せば十分です。

variable (P Q : Prop)

/-- 排中律を証明するための主要な補題 -/
theorem lemma_em (himp : P → Q) (hor : ¬ Q ∨ P) : P ∨ ¬ P := by
  rcases hor with h | h
  · right
    intro hP
    have := himp hP
    contradiction
  · left
    exact h

-- 何の公理も使用していない
/- info: 'lemma_em' does not depend on any axioms -/
#print axioms lemma_em

選択原理を用いると命題 Q を構成することができ、関数外延性と命題外延性により、それが所望の性質を持つことを示すことができます。

/-- 排中律 -/
theorem em (P : Prop) : P ∨ ¬ P := by
  -- Prop の部分集合 U と V を考える
  let U (x : Prop) : Prop := (x = True) ∨ P
  let V (x : Prop) : Prop := (x = False) ∨ P

  -- U と V は空ではない
  have exU : ∃ x, U x := ⟨True, by simp [U]⟩
  have exV : ∃ x, V x := ⟨False, by simp [V]⟩

  -- したがって、選択原理を使って `u ∈ U` と `v ∈ V` を選ぶことができる
  let u : Prop := Classical.choose exU
  let v : Prop := Classical.choose exV
  have u_def : U u := Classical.choose_spec exU
  have v_def : V v := Classical.choose_spec exV

  -- 選択原理を使用したご利益として、u と v は一貫した方法で選択されているので、
  -- `U = V` ならば `u = v` が成り立つ。
  -- これは `u = True`, `v = False` などと構成した場合は示せないことに注意。
  have eq_of_eq (h : U = V) : u = v := by
    simp [u, v, h]

  -- `Q := u = v` として `Q` を構成する
  apply lemma_em (Q := u = v)

  case himp =>
    show P → u = v

    -- P が成り立つと仮定する
    intro (hP : P)

    -- `U = V` を示せばよい
    apply eq_of_eq

    -- U と V の定義を展開する
    dsimp [U, V]

    -- 関数外延性によってゴールを書き換える
    ext x

    -- あとは命題論理の問題になる
    -- 仮定に P があるので自明
    show x = True ∨ P ↔ x = False ∨ P
    simp [hP]

  case hor =>
    show (u ≠ v) ∨ P

    -- U と V の定義を展開する
    replace u_def : u ∨ P := by simpa [U] using u_def
    replace v_def : ¬ v ∨ P := by simpa [V] using v_def

    -- これにより4通りの場合分けが生じるが、1つを除いてすべて P が成り立つ。
    rcases u_def with hu | hu <;> rcases v_def with hv | hv
    all_goals (try right; assumption)

    -- 残りの1つでは `u ≠ v` が成り立つ。
    simp [hu, hv]

万能公理 SorryAx

sorryAx は、sorry タクティクを使用したときに使用される公理です。どんな命題でも証明することができ、どんなデータでも構成することができる万能な公理ですが、これを使用して証明を埋めるのはもちろんズルです。

/-- Fermat の最終定理 -/
theorem flt (x y z n : Nat) : n > 2 → x ^ n + y ^ n = z ^ n → x * y * z = 0 := by
  sorry

/- info: 'flt' depends on axioms: [sorryAx] -/
#print axioms flt

class

class型クラス(type class) を定義するためのコマンドです。型クラスを用いると、複数の型に対して定義され、型ごとに異なる実装を持つような関数を定義することができます。例えば「和を取る操作」のような、NatIntRat など複数の型に対して同じ名前で定義したい関数を作りたいとき、型クラスが適しています。

/-- 証明なしのバージョンのモノイド。
ただしモノイドとは、要素同士を「くっつける」操作ができて、
くっつけても変わらない要素があるようなもののこと。-/
class Monoid (α : Type) where
  /-- 単位元 -/
  e : α

  /-- 二項演算 -/
  op : α → α → α

/-- 自然数はモノイド -/
instance : Monoid Nat where
  -- ゼロを単位元とする
  e := 0

  -- 加算を二項演算とする
  op := Nat.add

/-- 連結リストはモノイド -/
instance {α : Type} : Monoid (List α) where
  -- 空リストを単位元とする
  e := []

  -- リストの連結を二項演算とする
  op := List.append

-- `Nat` に対してモノイドの演算が使える
#guard Monoid.op 0 0 = 0

-- `List Nat` に対してモノイドの演算が使える
#guard Monoid.op [1] [2, 3] = [1, 2, 3]

-- `Nat` に対して単位元を取得する関数が使える
#guard (Monoid.e : Nat) = 0

-- `List Int` に対しても単位元を取得する関数が使える
#guard (Monoid.e : List Int) = []

型クラス解決

型クラスが行っていることを class を使わずにDIYしてみると、型クラスの理解が深まるでしょう。class として上で定義したものを、もう一度 構造体として定義してみます。1

/-- 構造体でモノイドクラスを真似たもの -/
structure Monoid' (α : Type) where
  e : α
  op : α → α → α

/-- 自然数がモノイドのインスタンスであるという主張を再現したもの -/
def instMonoid'Nat : Monoid' Nat where
  e := 0
  op := Nat.add

このとき構造体 Monoid' のフィールド Monoid'.e は、「Monoid' の項に対して α の要素を返す」関数なので、次のような型を持ちます。

#check (Monoid'.e : {α : Type} → (self : Monoid' α) → α)

self : Monoid' α が暗黙の引数ではなく明示的な引数なので、型クラスのように書くことはできません。

#check_failure (Monoid'.e : Nat)

しかし、インスタンスを引数として渡せば、型クラスのように Nat の要素を取り出すことができます。

#check (Monoid'.e instMonoid'Nat : Nat)

構造体による模倣と本物の型クラスの違いがどこにあるのかおわかりいただけたでしょうか。最大の違いは、引数の instMonoid'Nat が省略できるかどうかです。

ここで(本物の)型クラスにおける単位元関数 e の型を調べてみると、self : Monoid' α が角括弧 [ .. ] で囲われていることがわかります。

#check (Monoid.e : {α : Type} → [_self : Monoid α] → α)

これは インスタンス暗黙引数(instance implicit) と呼ばれるもので、この場合 Lean に対して Monoid' α 型の項を自動的に合成するよう指示することを意味します。また、型クラスのインスタンス暗黙引数を自動的に合成する手続きのことを、 型クラス解決(type class resolution) と呼びます。

outParam

概要

足し算を表現する型クラスを自分で定義してみましょう。名前は Plus としてみます。足し算は自然数の和 Nat → Nat → Nat のように、同じ型の中で完結する操作として定義されることが多いものですが、より一般的に α → β → γ で定義されるものとしてみます。

/-- 自前で定義した足し算記法のためのクラス -/
class Plus (α β γ : Type) where
  plus : α → β → γ

-- 足し算記法を定義
-- ライブラリにある足し算記号と被るのを避けるため変な記号にしておく
scoped infixl:65 " +ₚ " => Plus.plus

-- 自然数と自然数のリストとの足し算を定義
instance : Plus Nat (List Nat) (List Nat) where
  plus n ns := List.map (fun x => n + x) ns

-- 定義した記号が使えるようになった
#check 1 +ₚ [1, 2]

この定義は上手くいっているように見えますが、返り値の型である γ を指定しないと #eval で式の値が評価できないという問題があります。

-- メタ変数の番号を表示しない
set_option pp.mvars false

-- 返り値の型がわからないので型クラス解決ができないというエラーが出ている
/-
error: failed to synthesize instance of type class
  Plus Nat (List Nat) (IO ?_)

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
#eval 1 +ₚ [1, 2]

-- 返り値の型を教えると評価できる
#eval (1 +ₚ [1, 2] : List Nat)

ここで最初の Plus の定義を書き換え、返り値の型を outParam で修飾すると、上手くいくようになります。これは、γ が未知の状態でも型クラス解決を行うようになるためです。

-- `γ` の型 `Type` を `outParam` で包んで注釈する
class Plus (α β : Type) (γ : outParam Type) where
  plus : α → β → γ

scoped infixl:65 " +ₚ " => Plus.plus

instance : Plus Nat (List Nat) (List Nat) where
  plus n ns := List.map (fun x => n + x) ns

-- 返り値の型を教えなくても評価できるようになった!
#eval 1 +ₚ [1, 2]

使用例: カリー化の計算

outParam の使用例として、多引数関数のカリー化を計算する型クラスを定義する例を紹介します。

なおこの例ではインスタンス優先度(priority := low という指定の部分)も使用していますが、それについては instance のページを参照してください。

/-- 多引数関数の「カリー化」を表現する型クラス -/
class Curry (Xs Y : Type) (F : outParam Type) where
  /-- `Xs → Y` 型の関数を、「カリー化」された形 `F` に変換する。 -/
  curry : (Xs → Y) → F

-- `curry` という関数名を名前空間の指定なしで直接使えるようにする
export Curry (curry)

variable {X Y Xs F : Type}

/-- ベースケース: `Xs` が単一引数 `X` のとき、`F = X → Y` としてそのまま返す。

より具体的なインスタンスが優先されるように、`priority := low` を指定している。
-/
instance (priority := low) : Curry X Y (X → Y) where
  curry := id

/-- 再帰ケース: `f : (X × Xs) → Y` をカリー化して `curry f : X → Xs → Y` という関数を得る。
`x : X` が与えられたとき、部分適用 `f (x, ·) : Xs → Y` を帰納的にカリー化して返す。-/
instance [Curry Xs Y F] : Curry (X × Xs) Y (X → F) where
  curry := fun f x => curry (fun xs => f (x, xs))

-- 動作テスト
example : curry (fun a : Nat => a) = (fun a => a) := rfl
example : curry (fun ((a, b) : Nat × Nat) => a + b) = (fun a b => a + b) := rfl
example :
  let actual := curry fun ((a, b, c) : Nat × Nat × Nat) => a + b + c
  let expected := fun a b c => a + b + c
  actual = expected := rfl

使用例: アンカリー化の計算

アンカリー化(カリー化の逆の操作)も同様に計算することができます。

/-- アンカリー化を計算する型クラス。
`f : F` を `uncurry f : Xs → Y` に変換する。-/
class Uncurry (F : Type) (Xs Y : outParam Type) where
  uncurry : F → Xs → Y

-- `Uncurry.uncurry` の代わりに `uncurry` と直接書けるようにエクスポートしておく
export Uncurry (uncurry)

variable {X Y Xs F : Type}

/-- ベースケース:
`f : X → Y` をアンカリー化しても何も変わらない。
ただし、より具体的なインスタンスが利用できる場面で
このインスタンスが使用されないように `priority` を低く設定している -/
instance (priority := low) : Uncurry (X → Y) X Y where
  uncurry := id

/-- 帰納ケース:
`f : F` をアンカリー化して `uncurry f : Xs → Y` を得ることができるとする。
このとき `g : X → F` をアンカリー化して `uncurry g : X × Xs → Y` を得ることができる。-/
instance [Uncurry F Xs Y] : Uncurry (X → F) (X × Xs) Y where
  uncurry := fun f (x, xs) => uncurry (f x) xs

-- 動作テスト
example : uncurry (fun a : Nat => a) = (fun a => a) := rfl
example : uncurry (fun a b : Nat => a + b) = (fun (a, b) => a + b) := by rfl
example :
  let actual := uncurry (fun a b c : Nat => a + b + c)
  let expected := (fun (a, b, c) => a + b + c)
  actual = expected := by rfl

class inductive

基本的に型クラスの下部構造は構造体ですが、一般の帰納型を型クラスにすることも可能です。それには class inductive というコマンドを使います。

/-- 全単射があるという同値関係 -/
structure Equiv (α : Type) (β : Type) where
  toFun : α → β
  invFun : β → α
  left_inv : invFun ∘ toFun = id
  right_inv : toFun ∘ invFun = id

@[inherit_doc] infixl:25 " ≃ " => Equiv

/-- その型の濃度を知っていることを意味する型クラス。
証明無関係の制約により、返り値の型を Prop にしてはいけない。-/
class inductive HasCardinal (X : Type) : Type where
  /-- 有限集合は濃度が計算できる -/
  | finite (n : Nat) (f : X ≃ Fin n)

  /-- 可算無限集合は濃度が計算できる -/
  | countable (f : X ≃ Nat)

/-- Bool の濃度は計算できる -/
instance : HasCardinal Bool := by
  apply HasCardinal.finite (n := 2)
  constructor

  -- Bool → Fin 2 を作る
  case toFun => exact (fun b => if b then 1 else 0)

  -- Fin 2 → Bool を作る
  case invFun => exact (fun ⟨x, _⟩ => x == 1)

  -- invFun ∘ toFun は Bool 上の恒等関数
  case left_inv =>
    ext b
    cases b <;> simp

  -- toFun ∘ invFun は Fin 2 上の恒等関数
  case right_inv =>
    ext ⟨x, h⟩
    simp only [Fin.isValue, Function.comp_apply, beq_iff_eq, id_eq]
    match x with
    | 0 => simp
    | 1 => simp

/-- 可算無限までしかない順序数もどき -/
inductive Ordinal : Type where
  | nat (n : Nat)
  | omega
deriving DecidableEq

def Ordinal.toString : Ordinal → String
  | Ordinal.nat n => ToString.toString n
  | Ordinal.omega => "ω"

instance : ToString Ordinal := ⟨Ordinal.toString⟩

/-- X の濃度が計算できる場合、X の濃度を返す関数 -/
def HasCardinal.card (X : Type) [h : HasCardinal X] : Ordinal :=
  match h with
  | finite n _ => Ordinal.nat n
  | countable _ => Ordinal.omega

-- 単に card という名前でアクセスできるようにする
export HasCardinal (card)

-- Bool の濃度が計算できた
#guard card Bool = Ordinal.nat 2

  1. この説明は Mathematics in Leanを参考にしています。

declare_aesop_rule_sets

declare_aesop_rule_sets コマンドは、aesop タクティクで使用させるための特定のルールセットを宣言します。

Warning

このページの内容は ボタンから Lean 4 Web で実行することができません。

基本的な使い方

declare_aesop_rule_sets で宣言されたルールセットは、宣言したそのファイルの中では有効になりません。import する必要があります。前提として以下の内容のファイルを import しているとしましょう。

-- import されているファイルの内容
import Aesop

declare_aesop_rule_sets [HogeRules]

このとき、以下のように aesoprule_setsHogeRules を渡すことで、HogeRules に登録されたルールセットを使用することができます。

import LeanByExample.Declarative.DeclareAesopRuleSets.Lib -- インポートで有効になる

example : True := by
  aesop (rule_sets := [HogeRules])

タクティク作成

このコマンドの主な用途は、aesop をカスタマイズして aesop と同じように使えるタクティクを自作することです。特定のルールセットに対して登録されたルールは単に aesop を実行しただけでは適用されないので、用途ごとにタクティクを分けることができるという理屈です。

たとえば、macro コマンドを使ったシンプルな方法で、aesop が持つデフォルトのルールセットはそのままにaesop をベースに新たに hoge というタクティクを作ることができます。

/-- aesop ラッパー -/
macro "hoge" : tactic => do `(tactic| aesop (rule_sets := [HogeRules]))

example : True := by
  hoge

aesop? と同等の機能も、同じようにして実現することができます。

/-- `hoge` が使用したルールを生成する -/
macro "hoge?" : tactic => `(tactic| aesop? (rule_sets := [HogeRules]))

/-
info: Try this:

  [apply]   simp_all only
-/
example : True := by
  hoge?

hoge タクティク用のルールを登録することもできます。

[aesop] 属性と同等の機能を持つ [hoge] 属性を作成したい場合は、たとえば次のようにします。

/-- `hoge` タクティク用のルールを追加する -/
macro "hoge" e:Aesop.rule_expr : attr =>
  `(attr| aesop (rule_sets := [HogeRules]) $e)


/-- `True` を模して自作した命題 -/
inductive MyTrue : Prop where
  | intro

example : MyTrue := by
  -- 最初は証明できない
  fail_if_success hoge

  apply MyTrue.intro

attribute [local hoge safe constructors] MyTrue

example : MyTrue := by
  -- `hoge` で証明できるようになった!
  hoge

example : MyTrue := by
  -- 依然として `aesop` では証明できない
  fail_if_success aesop

  apply MyTrue.intro

add_aesop_rules コマンドと同様の機能を持つ add_hoge_rules コマンドを作成したい場合は、たとえば次のようにします。

/-- `hoge` タクティク用のルールを追加する -/
macro attrKind:attrKind "add_hoge_rules" e:Aesop.rule_expr : command =>
  `(command| $attrKind:attrKind add_aesop_rules (rule_sets := [HogeRules]) $e)


/-- `True` を模して自作した命題 -/
inductive MyTrue : Prop where
  | intro

example : MyTrue := by
  -- 最初は証明できない
  fail_if_success hoge

  apply MyTrue.intro

-- `MyTrue` に関するルールを `hoge` に登録する
local add_hoge_rules safe constructors [MyTrue]

example : MyTrue := by
  -- `hoge` で証明できるようになった!
  hoge

example : MyTrue := by
  -- 依然として `aesop` では証明できない
  fail_if_success aesop

  apply MyTrue.intro

declare_syntax_cat

declare_syntax_cat コマンドは、新しい構文カテゴリを宣言するためのコマンドです。構文カテゴリを宣言することで、宣言した構文を再利用可能にして冗長な構文宣言を減らすことができます。

例として、集合の内包表記 {x : T | P x} を定義するコードを示します。

/-- `α` を全体集合とする部分集合の全体 -/
def Set (α : Type u) := α → Prop

variable {α : Type u}

/-- 項 `x : α` が `X : Set α` に属する -/
def Set.mem (X : Set α) (x : α) : Prop := X x

/-- `x ∈ X` と書けるようにする -/
instance : Membership α (Set α) where
  mem := Set.mem

/-- 述語 `p : α → Prop` から構成される集合 -/
def setOf (p : α → Prop) : Set α := p

/-- binder という構文カテゴリ。
これは変数束縛を表していて、
`{x : X | P x}` の `x : X` の部分とか
`{x ∈ X | P x}` の `x ∈ X` の部分とかを表している -/
declare_syntax_cat binder

/-- `{x : T | P x}` の `: T` の部分。
あってもなくても良いので `( )?` で囲う -/
syntax ident (" : " term)? : binder

/-- `{x ∈ T | P x}` の `∈ T` の部分。
あってもなくても良いので `( )?` で囲う -/
syntax ident (" ∈ " term)? : binder

/-- 集合の内包表記 -/
syntax "{" binder "|" term "}" : term

-- 合法な構文として認識される
-- 実装は与えていないのでエラーにはなる
#check_failure { x | x = 0}
#check_failure { x : Nat | x > 0 }
#check_failure { x ∈ T | x = 0}

/-- `{x : T | P x}` と `{x ∈ T | P x}` の形の式を `setOf` の式に変換する -/
macro_rules
  | `({ $var:ident | $body:term }) => `(setOf (fun $var => $body))
  | `({ $var:ident : $ty:term | $body:term }) => `(setOf (fun ($var : $ty) => $body))
  | `({ $var:ident ∈ $s:term | $body:term }) => `(setOf (fun $var => $var ∈ $s ∧ $body))

-- 内包表記が使えるようになった
#check { x : Nat | x > 0 }

#check
  let Even := { x : Nat | x % 2 = 0 }
  { x ∈ Even | x > 0 }

def

def は、関数や定数など、グローバルに項を定義するための基本的なコマンドです。

/-- 1を足す関数 -/
def addOne (n : Nat) : Nat := n + 1

-- 関数だけでなく定数も定義できる
def foo := "hello"

構文ルール

def は以下のような構文ルールを持ちます。

  • def の後に関数名・定数名を書きます。
  • 関数名の後に引数を指定します。引数なしだと定数になります。
  • 引数の後に : によって返り値の型を指定します。返り値の値が関数本体などから推論できる場合は省略できます。
  • := の後に関数の本体を書きます。

なお theorem コマンドも同様の構文ルールを持ちます。

再帰関数

再帰関数も同様の構文で定義することができます。

/-- 階乗関数 -/
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

#guard factorial 7 = 5040

このとき、裏で Lean は再帰関数が停止することの証明を生成しようとし、証明が失敗するとエラーになります。これについて詳しくは以下のページを参照してください。

引数の指定

関数を定義するとき、隣接する同じ型の引数に対しては : を使いまわすことができます。

def add (n m : Nat) : Nat := n + m

def threeAdd (n m l : Nat) : Nat := n + m + l

また丸括弧 () ではなくて波括弧で引数を指定すると、その引数は暗黙の引数になり、Lean が推論して埋めてくれるようになります。

/-- リストの長さを返す -/
def List.myLength {α : Type} (xs : List α) : Nat :=
  match xs with
  | [] => 0
  | _ :: xs => 1 + myLength xs

-- `α` を引数として与えなくても呼び出すことができる
#guard List.myLength [1, 2, 3] = 3

名前付き引数

通常は引数をスペース区切りで渡しますが、それだと引数を埋める順番が固定されます。引数の名前を指定して値を渡すこともできて、そうすれば引数の順番を気にする必要はありません。これを 名前付き引数(named arguments) といいます。

/-- a に 10 を掛けてから b を足す -/
def addAndMul (a b : Int) : Int :=
  10 * a + b

-- 何も指定しないと a, b の順番で引数を渡すことになる
#guard addAndMul 3 2 == 32

-- 引数名を指定して渡すと、引数を好きな順番で渡せる
#guard addAndMul (b := 3) (a := 2) == 23

引数のデフォルト値

関数の引数には、デフォルト値を設定することができます。デフォルト値を設定すると、引数を省略して呼び出したときにその値が自動的に使われます。

/-- `name` から挨拶文を生成する -/
def greetWithDefault (name : String) (punct := "!") : String :=
  name ++ "さん、こんにちは" ++ punct

-- `punct` を省略しているが、デフォルトの `!` が使われている
#guard greetWithDefault "山本" == "山本さん、こんにちは!"

明示的に指定したくなった場合は、名前付き引数として渡します。

#guard greetWithDefault "山本" (punct := "!!!") == "山本さん、こんにちは!!!"

deriving

deriving は、型クラスのインスタンスを自動的に生成します。

以下に示すように、deriving instance C for T とすると型 T に対して型クラス C のインスタンスを生成します。

inductive Color : Type where
  | red
  | green
  | blue

-- 暗黙的に Repr インスタンスを生成しない
set_option eval.derive.repr false

-- `Repr` が定義されていないので `eval` できない
/-
error: could not synthesize a `Repr` or `ToString` instance for type
  Color
-/
#eval Color.red

-- インスタンス生成
deriving instance Repr for Color

-- `eval` できるようになった
#eval Color.red

deriving 句

対象の型の直後であれば、省略して deriving C だけ書けば十分です。

def StrList := List String deriving Inhabited

#check (default : StrList)

また複数の型クラスに対してインスタンスを生成するには、クラス名をカンマで続けます。

structure People where
  name : String
  age : Nat
deriving Inhabited, Repr

-- 両方のインスタンスが生成されている
#eval (default : People)

よくあるエラー

なお、deriving で実装を生成できるのは、DerivingHandler が登録されていて実装方法が用意されている型クラスのみです。実装方法が指定されていなければ使うことはできません。

/-- 自前で定義した型クラス -/
class Callable (α : Type) where
  call : α → String

/- error: No deriving handlers have been implemented for class `Callable` -/
deriving instance Callable for People

例外として、定義を展開して既存のインスタンスが見つかる場合は deriving が通ります。

/-- 適当に定義した自前の型クラス -/
class Foo (α : Type) where
  foo : Unit

/-- String の Foo インスタンスを定義する -/
instance : Foo String where
  foo := ()

-- `Foo` の deriving handler は作っていないが、String と bar は同じなので
-- String のインスタンスが使用される
def bar := String deriving Foo

-- String のインスタンスが使われている
#guard Foo.foo bar = ()

dsimproc

dsimproc は、simproc を宣言するためのコマンドの一つです。

simproc は、ある式 expr にマッチする部分を見つけたときに、より単純な式 result を動的に計算し、expr = result の証明も同時に構成するような、simp タクティクから呼び出される書き換え規則のことです。

dsimproc は simproc を定義するものですが、定義的に等しい(definitionally equal) 変形だけを行います。 したがって、expr = result の証明が不要です。(rfl を使うだけで済むので)

以下は、具体的な数値に対する関数呼び出しを計算して単純化するようなシンプルな simproc を定義する例です。

import Lean

def Nat.fib (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fib (n + 1) + fib n

example (P : Nat → Prop) (h : P (Nat.fib 5)) : P 5 := by
  -- 最初は simp が使えない
  fail_if_success simp at h

  have : Nat.fib 5 = 5 := by rfl
  simp [this] at h
  assumption

dsimproc computeFib (Nat.fib _) := fun e => do
  -- パターンマッチ。`e : Expr` が `Nat.fib n` の形であることを確認する
  -- そうでなければ即終了する
  let_expr Nat.fib m ← e
    | return .continue

  -- `m` が自然数リテラルであることを確認する
  -- もしそうでなければ即終了する
  let some n := m.nat?
    | return .continue

  -- `Nat.fib n` を評価する
  let l := Nat.fib n

  return .visit <| Lean.toExpr l

-- 単純化をやってくれる
example (P : Nat → Prop) (h : P (Nat.fib 5)) : P 5 := by
  dsimp at h
  assumption

example (P : Nat → Prop) (h : P (Nat.fib 5)) : P 5 := by
  -- `simp_all`からも呼び出される
  simp_all

用途

simp 補題による書き換えでは無数に多くの補題を必要としそうな場合であっても、simproc を使うことで綺麗に解決できることがあります。1

open Lean Meta Qq in

/-- 数値リテラル `n` を `1 + 1 + ⋯ + 1` という形に展開する

**注意**: このsimprocが常に適用されると困るので、実際には`dsimproc_decl`を使った方が良い。
-/
dsimproc unfoldNat ((_)) := fun e => do
  -- 自然数リテラルでなければ即終了する
  let_expr OfNat.ofNat _ num inst := e
    | return .continue
  let some n := num.rawNatLit?
    | return .continue

  -- その自然数リテラルが自然数を表しているのでなければ即終了する
  let num : Q(Nat) := num
  unless ← isDefEq inst q(instOfNatNat $num) do
    return .continue

  -- `1 + 1 + ⋯ + 1` の形に展開する
  let mut result : Q(Nat) := q(1)
  let mut n := n
  while n > 1 do
    result := q($result + 1)
    n := n - 1
  return .done result

example (a : Nat) : 3 * a = 1 * a + 2 * a := by
  dsimp only [unfoldNat]
  guard_target =ₛ (1 + 1 + 1) * a = 1 * a + (1 + 1) * a
  grind

example (a : Nat) : (2 : Int) * a = a + a := by
  -- 自然数を表す自然数リテラルは存在しないので失敗する
  fail_if_success dsimp only [unfoldNat]
  grind

  1. このコード例は、Robin Arnez さんに教えていただいたものを参考にしています。

elab

elab コマンドは、構文とその解釈を同時に定義するためのコマンドです。マクロと似ていますが、マクロとは違ってコードの置換ではなく手続き的な処理に向いています。このコマンドを使うと、コマンドやタクティクや項エラボレータを手軽に定義することができます。

より詳しく書くと、elab コマンドは構文とそのエラボレータを同時に定義するためのコマンドです。ただしエラボレータとは、おおざっぱに言えば SyntaxExpr に変換する処理のことです。

使用例

タクティク

elab コマンドを使わずにタクティクを定義しようとすると、以下の手続きを踏む必要があります。

  1. 構文を定義する。
  2. Tactic 型の関数を定義する。
  3. 構文と実装を [tactic] 属性で結びつける。
import Lean

/-- 挨拶をするだけのタクティク -/
syntax (name := greetStx) "greet₁" : tactic

open Lean Elab Tactic in

@[tactic greetStx]
def evalGreet : Tactic := fun _ =>
  logInfo "Hello, world!"

/- info: Hello, world! -/
example : True := by
  greet₁
  trivial

elab コマンドを使用すると、これらを一括で行うことができます。

open Lean Elab Tactic in

elab "greet₂" : tactic =>
  logInfo "Hello, world!"

/- info: Hello, world! -/
example : True := by
  greet₂
  trivial

コマンド

elab コマンドを使わずにコマンドを定義しようとすると、以下の手続きを踏む必要があります。

  1. 構文を定義する。
  2. CommandElab 型の関数を定義する。
  3. 構文と実装を [command_elab] 属性で結びつける。
/-- 挨拶をするだけのコマンド -/
syntax (name := greetCmdStx) "#greet₁" : command

open Lean Elab Command in

@[command_elab greetCmdStx]
def evalGreetCmd : CommandElab := fun _ =>
  logInfo "Hello, world!"

/- info: Hello, world! -/
#greet₁

elab コマンドを使用すると、これらを一括で行うことができます。

open Lean Elab Command in

elab "#greet₂" : command =>
  logInfo "Hello, world!"

/- info: Hello, world! -/
#greet₂

項エラボレータ

elab コマンドを使わずに項エラボレータを定義しようとすると、以下の手続きを踏む必要があります。

  1. 構文を定義する。
  2. TermElab 型の関数を定義する。
  3. 構文と実装を [term_elab] 属性で結びつける。
syntax (name := greetTermStx) "<<" "greet₁" ">>" : term

open Lean Elab Term in

@[term_elab greetTermStx]
def evalGreetTerm : TermElab := fun _ _ => do
  return (toExpr "hello world")

/- info: "hello world" -/
#eval << greet₁ >>

elab コマンドを使用すると、これらを一括で行うことができます。

open Lean Elab Term in

elab "<<" "greet₂" ">>" : term =>
  return (toExpr "hello world")

/- info: "hello world" -/
#eval << greet₂ >>

example

example は名前を付けずに命題の証明をすることができるコマンドです。

-- `1 + 1 = 2` は `rfl` で証明できる
example : 1 + 1 = 2 := rfl

-- `n + 0 = n` は `rfl` で証明できる
example {n : Nat} : n + 0 = n := rfl

より正確には、example : T := tt が型 T を持っていることを確かめます。特に T の型が Prop であるときには、最初に述べた通り tT の証明だとみなすことができます。

-- `[1, 2, 3]` は `List Nat` 型の項
example : List Nat := [1, 2, 3]

-- `#[1, 2, 3]` は `Array Nat` 型の項
example : Array Nat := #[1, 2, 3]

-- `true` は `Bool` 型の項
example : Bool := true

export

export コマンドは、他の名前空間から定義を現在の名前空間に追加します。

具体的には、Some.Namespace の配下に nameᵢ がある状態で export Some.Namespace (name₁ name₂ ...) を実行することにより、以下の両方の処理が行われます。

  • open と同様に、nameᵢSome.Namespace の接頭辞なしで現在の名前空間 N 上で見えるようになります。
  • 現在の名前空間 N の外部の名前空間から N.nameᵢ としてアクセスできるようになります。
namespace N -- export コマンドが実行される名前空間

  inductive Sample : Type where
    | foo
    | bar
    | baz

  -- foo は名前空間 Sample 上にあるので、
  -- 短い名前ではアクセスできない
  #check_failure foo
  #check Sample.foo

  -- foo を現在の名前空間 N に追加する
  export N.Sample (foo)

  -- 短い名前でもアクセス可能になった
  #check foo

end N

-- 名前空間 `N` の外部からアクセスするには、
-- 普通はフルネームが必要
#check_failure N.bar
#check N.Sample.bar

-- しかし foo は名前空間 N の内部で export されているので、
-- N を指定するだけでアクセス可能
#check N.foo

grind_pattern

grind_pattern コマンドは、定理を grind タクティクに再利用させるためのパターンを指定するコマンドです。

/-- 何らかの二項関係 -/
opaque R : Nat → Nat → Prop

/-- R は推移的 -/
axiom Rtrans {x y z : Nat} : R x y → R y z → R x z

-- ローカルコンテキストに `R x y` と `R y z` が現れたとき、
-- `Rtrans` をインスタンス化しなさいと grind に指示している
grind_pattern Rtrans => R x y, R y z

example (x y z : Nat) (h₁ : R x y) (h₂ : R y z) : R x z := by
  -- grind で証明できる
  grind

[grind] 属性では登録できない例

多くの場合、わざわざ grind_pattern コマンドでパターンを手動指定しなくても [grind] 属性にパターンを自動で推測させることができますが、自動推測に失敗することがあります。このような場合は grind_pattern コマンドでパターンを明示する必要があります。

variable {α : Type}

/-- 二項関係 `R` がリストの隣接要素に対して成立するという述語。 -/
inductive IsChain (R : α → α → Prop) : List α → Prop
  | nil : IsChain R []
  | single (a : α) : IsChain R [a]
  | cons_cons {a b : α} {l : List α} (hab : R a b) (hchain : IsChain R (b :: l)) :
    IsChain R (a :: b :: l)

attribute [grind cases] IsChain
attribute [simp] IsChain.nil IsChain.single
attribute [grind <=] IsChain.cons_cons IsChain.single IsChain.nil

variable {R : α → α → Prop}

/-- リストの最後の要素を取得する。 -/
@[simp]
def last? (xs : List α) : Option α :=
  match xs with
  | [] => none
  | [a] => some a
  | _ :: b :: bs => last? (b :: bs)

theorem last?_cons_exists (xs : List α) (x : α) :
    ∃ y : α, some y = last? (x :: xs) := by
  induction xs generalizing x with simp_all

-- `[grind]` 属性は、登録すべきパターンを見つけられない。
/-
error: invalid `grind` theorem, failed to find an usable pattern using different modifiers
-/
attribute [grind] last?_cons_exists

-- `grind_pattern` コマンドなら、`last? (x :: xs)` を見たときに
-- `last?_cons_exists` を使うよう明示的に指定できる。
grind_pattern last?_cons_exists => last? (x :: xs)

@[grind =]
theorem IsChain.append {xs ys : List α} :
    IsChain R (xs ++ ys) ↔
      match last? xs, ys with
      | none, _ => IsChain R ys
      | _, [] => IsChain R xs
      | some x, b :: _ => IsChain R xs ∧ R x b ∧ IsChain R ys := by
  fun_induction last? xs with grind

ワイルドカード

パターンの中でワイルドカード _ を使用することができます。この機能は、条件を厳しすぎず緩すぎない、ちょうどよい強さに調整するのに役立ちます。

/-- 何らかの関数 -/
opaque f : Nat → Nat

/-- f は単調増加 -/
axiom f_monotone {a b : Nat} (h : a ≤ b) : f a ≤ f b

section Part1
  /- ## 厳しすぎるパターン指定の例 -/

  grind_pattern f_monotone => f a, f b, f a ≤ f b

  example (a b : Nat) (h : a ≤ b) : f a ≤ f b := by
    -- grind の中の前処理等で `f a ≤ f b` の形が崩れるため、
    -- grind が `f a ≤ f b` のパターンを見つけられない。
    -- そのため grind で証明できない
    fail_if_success grind

    apply f_monotone <;> assumption

end Part1

section Part2
  /- ## ワイルドカードでパターンを緩める例 -/

  grind_pattern f_monotone => f a, f b, f _ ≤ f _

  example (a b : Nat) (h : a ≤ b) : f a ≤ f b := by
    -- grind で証明できるようになった!
    grind

end Part2

where による制御

grind_pattern コマンドは、grind タクティクに定理を再利用させることができるという点では [grind] 属性とできることが同じですが、grind_pattern コマンドの方がより細かい制御を行えます。

典型的な例として、grind_pattern コマンドでは where で制約を追加することができ、不要なインスタンスが暴発することを防ぐことができます。

=/= 制約

=/= 制約を追加すると、両辺が 定義的に等しい(definitionally equal) かどうかの場合分けが追加され、等しくない場合のみ定理がインスタンス化されるようになります。

variable {α : Type} [Mul α] [One α]

/-- リストの積 -/
@[grind =]
def List.myprod (xs : List α) : α :=
  xs.foldr (· * ·) 1

-- 注意: 既に定義されているものが存在する
#check List.prod

-- α がモノイドという仮定を足す
variable [@Std.LawfulIdentity α (· * ·) 1]
variable [@Std.Associative α (· * ·)]

@[simp, grind =]
theorem List.myprod_cons (x : α) (xs : List α) :
    (x :: xs).myprod = x * xs.myprod := by
  grind

theorem List.myprod_append (xs ys : List α) :
    (xs ++ ys).myprod = xs.myprod * ys.myprod := by
  induction xs with
  | nil => grind
  | cons x xs ih =>
    simp_all [Std.Associative.assoc]

section
  -- 単に「左辺を見つけたらインスタンス化」というルールにすると...
  local grind_pattern List.myprod_append => (xs ++ ys).myprod

  /-
  trace: [grind.ematch.instance] List.myprod.eq_1: ([] ++ []).myprod = List.foldr (fun x1 x2 => x1 * x2) 1 ([] ++ [])
  [grind.ematch.instance] List.myprod_append: ([] ++ []).myprod = [].myprod * [].myprod
  [grind.ematch.instance] List.append_nil: [] ++ [] = []
  [grind.ematch.instance] List.nil_append: [] ++ [] = []
  [grind.ematch.instance] List.foldr_append: List.foldr (fun x1 x2 => x1 * x2) 1 ([] ++ []) =
        List.foldr (fun x1 x2 => x1 * x2) (List.foldr (fun x1 x2 => x1 * x2) 1 []) []
  [grind.ematch.instance] List.foldr_nil: List.foldr (fun x1 x2 => x1 * x2) 1 [] = 1
  -/
  example : ([] ++ []).myprod = 1 := by
    -- `[] ++ []` に対しても暴発して余計なインスタンス化が実行される
    set_option trace.grind.ematch.instance true in

    grind
end

-- `=/=` 制約を追加して、空リストが混ざっているときはインスタンス化しないように指示
grind_pattern List.myprod_append => (xs ++ ys).myprod where
  xs =/= []
  ys =/= []

/-
trace: [grind.ematch.instance] List.myprod.eq_1: ([] ++ []).myprod = List.foldr (fun x1 x2 => x1 * x2) 1 ([] ++ [])
[grind.ematch.instance] List.append_nil: [] ++ [] = []
[grind.ematch.instance] List.nil_append: [] ++ [] = []
[grind.ematch.instance] List.foldr_append: List.foldr (fun x1 x2 => x1 * x2) 1 ([] ++ []) =
      List.foldr (fun x1 x2 => x1 * x2) (List.foldr (fun x1 x2 => x1 * x2) 1 []) []
[grind.ematch.instance] List.foldr_nil: List.foldr (fun x1 x2 => x1 * x2) 1 [] = 1
-/
example : ([] ++ []).myprod = 1 := by
  -- `[] ++ []` に対して暴発しなくなる
  set_option trace.grind.ematch.instance true in

  grind

guard 制約

guard 制約を追加すると、与えられた条件が成り立つときにだけ定理がインスタンス化されるようになります。

grind_pattern コマンドでは様々なパターンが登録できるのですが、等式を登録することはできません。 したがって A = B が成り立つときにインスタンス化してほしい定理がある場合は、guard 制約を追加するのが良いでしょう。

theorem Nat.zero_of_divide_lt_right {a b c : Nat} (hac : c < a) (h : a * b = c) :
    b = 0 ∧ c = 0 := by
  cases b with grind

-- invalid pattern というエラーになり、登録できない
-- これは等式をパターンとして登録しようとしたため
/-
error: invalid pattern, (non-forbidden) application expected
  @Eq (Nat) (@HMul.hMul (Nat) (Nat) (Nat) (@instHMul (Nat) (instMulNat)) #4 #3) #2
-/
grind_pattern Nat.zero_of_divide_lt_right => c < a, a * b = c

-- 等式を避ければパターンとして登録できる
grind_pattern Nat.zero_of_divide_lt_right => c < a, a * b where
  guard a * b = c

/-- 割り算と剰余の定理の、一意性パート -/
theorem Int.division_with_remainder_unique {a b q r q' r' : Int} (hb : 0 < b)
    (h : a = b * q + r) (hr : 0 ≤ r) (hrb : r < b)
    (h' : a = b * q' + r') (hr' : 0 ≤ r') (hrb' : r' < b) :
    q = q' ∧ r = r' := by

  -- 両辺引いて、以下の等式を得る。
  have h_abs_eq : b.natAbs * (q - q').natAbs = (r' - r).natAbs := by
    grind only [natAbs_eq_iff_mul_eq_zero]

  -- `0 ≤ r < b` と `0 ≤ r' < b` から、`|r' - r| < b` が成り立つ
  have : (r' - r).natAbs < b.natAbs := by grind

  -- `0 < b` と `|r' - r| < b` から、`q - q' = 0` が成り立つ
  have : (q - q').natAbs = 0 := by
    -- ここで grind_pattern で登録した上記の定理を使用している
    grind only [usr Nat.zero_of_divide_lt_right]
  have q_eq_q' : q - q' = 0 := by grind

  -- さらに `r = a - b * q` と `r' = a - b * q'` より `r = r'` が成り立つ。
  have r_eq_r' : r = r' := by
    grind

  grind only

また、不等式 A ≤ B 等はパターンとしては登録できるのですが、動作が不安定になるためお勧めできません。なるべく guard 制約などとして扱うことが望ましいでしょう。

-- 整数全体を表す記号
notation:max "ℤ" => Int

/-- 絶対値。値は整数(自然数ではない) -/
def Int.abs (a : ℤ) : ℤ := max a (- a)

@[inherit_doc]
notation:max "|" a "|" => Int.abs a

/-- 0 以上の数については、絶対値は元の数に等しい -/
theorem Int.abs_of_nonneg {a : ℤ} (ha : 0 ≤ a) : |a| = a := by
  grind only [abs, = Int.max_def]

section
  -- `0 ≤ a` を探索パターンとして登録してみると
  local grind_pattern Int.abs_of_nonneg => |a|, 0 ≤ a

  -- 何故か `ha : 0 ≤ a` という仮定を見つけてくれず、
  -- `grind` で証明できない。
  example {a : ℤ} (ha : 0 ≤ a) : |a| = a := by
    fail_if_success grind
    sorry
end

-- `guard` 制約としていれてやると上手く動作する
grind_pattern Int.abs_of_nonneg => |a| where
  guard 0 ≤ a

example {a : ℤ} (ha : 0 ≤ a) : |a| = a := by
  grind

inductive

inductive コマンドは、帰納型(inductive type) を定義することができます。

帰納型とは、Lean においてユーザが新しい型を定義するための主要な方法です。帰納型はその型の項を得るための有限個の関数(コンストラクタ)を組み合わせることによって定まります。コンストラクタがこれから定義しようとしている型自身を引数に含むことも許されるので、帰納型は再帰的な構造を持つことができます。

帰納型の例

列挙型

帰納型の最も基本的な例は、次のような列挙型です。列挙型とは、固定された値のどれかを取るような型です。

/-- 真か偽のどちらかの値をとる型 -/
inductive MyBool where
  | true
  | false

#check (MyBool.true : MyBool)

列挙型は、帰納型の中でもすべてのコンストラクタが引数を持たないような特別な場合といえます。

再帰的なデータ構造

一般には、帰納型のコンストラクタは引数を持つことができます。コンストラクタの引数の型が定義しようとしているその帰納型自身であっても構いません。これにより、連結リストや二分木といった再帰的な構造を持つデータ型を定義することができます。

/-- 連結リスト -/
inductive LinkedList (α : Type) where
  /-- 空のリスト -/
  | nil

  /-- リスト `xs : LinkedList α` の先頭に `head : α` をつけたものはリスト -/
  | cons (head : α) (tail : LinkedList α)

/-- 2分木 -/
inductive BinTree (α : Type) where
  /-- 空の木 -/
  | empty

  /-- ノード `value : α` の左と右に木を付け加えたものは木 -/
  | node (value : α) (left right : BinTree α)

帰納族

ある添字集合 Λ : Type の要素 λ : Λ のそれぞれに対して、型 T λ : Sort u を独立した型として定義することができます。簡単な例として、長さを型の情報として持つリストがあります。

/-- 長さを型の情報として持つリスト -/
inductive Vec (α : Type) : Nat → Type where
  | nil : Vec α 0
  | cons (a : α) {n : Nat} (v : Vec α n) : Vec α (n + 1)

これで帰納型の族 {Vec α 0, Vec α 1, Vec α 2, …} を定義したことになります。このように定義される帰納型の族を 帰納族(inductive family) と呼びます。

inductive コマンドはパラメータと添字(index)を区別するため、次のように書くとエラーになることに注意してください。

/-
error: Mismatched inductive type parameter in
  BadVec α 0
The provided argument
  0
is not definitionally equal to the expected parameter
  n

Note: The value of parameter `n` must be fixed throughout the inductive declaration. Consider making this parameter an index if it must vary.
-/
inductive BadVec (α : Type) (n : Nat) : Type where
  | nil : BadVec α 0
  | cons (a : α) {n : Nat} (v : Vec α n) : BadVec α (n + 1)

このようにコードを書くと n はパラメータとして扱われます。パラメータは、各コンストラクタの返り値の型に一様に現れなくてはいけません。 したがって、nil の右辺には BadVec α 0 ではなく BadVec α n が来なければならないというエラーが出ているわけです。実際に、このエラーはコンストラクタの返り値の型を変更すれば消すことができます。

/-- 良くない方法でエラーを消した `BadVec` -/
inductive BadVec' (α : Type) (n : Nat) : Type where
  | nil : BadVec' α n
  | cons (a : α) (v : Vec α n) : BadVec' α n

#check (BadVec'.nil : BadVec' Nat 3)

しかし、元々の Vec とは似て非なるものになってしまいます。上記の BadVec' では、nil : BadVec α 0 だけでなく nil : BadVec α 1nil : BadVec α 2 なども存在すると宣言したことになってしまいます。

帰納的述語

inductive コマンドを使って述語を定義することもできます。すなわち、「この条件が成立するなら、この性質が成り立つ」という十分条件を網羅的に列挙することによって、ある性質を定義することができます。このようにして定義される述語のことを 帰納的述語(inductive predicate) と呼びます。

使用例: 自然数における順序関係

Lean の標準ライブラリにある簡単な例として、自然数における順序関係の定義が挙げられます。(infix コマンドは見やすくするために使用しています。)

/-- 標準ライブラリの定義を真似て構成した順序関係 -/
inductive Nat.myle (n : Nat) : Nat → Prop where
  /-- 常に `n ≤ n` が成り立つ -/
  | refl : myle n n

  /-- `n ≤ m` ならば `n ≤ m + 1` が成り立つ -/
  | step {m : Nat} : myle n m → myle n (m + 1)

@[inherit_doc] infix:50 " ≤ₘ " => Nat.myle

この場合 inductive コマンドを使用せずに、def を使って Bool 値の再帰関数として定義しても意味的に同じものができます。

/-- 自然数における順序関係を計算する関数 -/
def Nat.myble (m n : Nat) : Bool :=
  match m, n with
  -- 常に `0 ≤ n` が成り立つ
  | 0, _ => true
  -- 常に `¬ m + 1 ≤ 0` が成り立つ
  | _ + 1, 0 => false
  -- `m + 1 ≤ n + 1 ↔ m ≤ n` が成り立つ
  | m + 1, n + 1 => Nat.myble m n

両者の違いは何でしょうか?双方にメリットとデメリットがあります。

Bool 値の再帰関数として定義するメリットとして、計算可能になることが挙げられます。再帰関数として定義されていれば #eval コマンドなど、Lean に組み込まれた機能を使って計算を行うことができます。一方で帰納的述語として定義されていると、計算をさせるための準備をこちらで行う必要があります。

-- すぐに計算できる
#guard Nat.myble 12 24

/-
error: failed to synthesize
  Decidable (2 ≤ₘ 4)

Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.
-/
#eval 2 ≤ₘ 4

帰納的述語として定義するメリットとしては、たとえば証明に使う論理モデルと計算に使うモデルを分離できることがあります。計算可能でないというデメリットは Decidable クラスのインスタンスにすることで補うことができるならば、大きな問題にはならないと考えられます。計算を気にせずに証明時に楽なシンプルなモデルを採用できるのは、帰納的述語のメリットです。

-- 論理モデルに基づいて帰納法を回して証明をする例
example {m n k : Nat} (h₁ : m ≤ₘ n) (h₂ : n ≤ₘ k) : m ≤ₘ k := by
  induction h₂ with
  | refl => assumption
  | @step l h₂ ih =>
    apply Nat.myle.step (by assumption)

使用例: 回文判定

Prop 値の再帰関数として定義することと比較した、帰納的述語として定義することのメリットとして「定義における場合分けが簡潔になる」ことが挙げられます。以下は、リストが回文であることを主張する帰納的述語の例です。

variable {α : Type}

/-- 回文を表す帰納的述語 -/
@[grind]
inductive Palindrome : List α → Prop
  /-- 空リストは回文 -/
  | nil : Palindrome []
  /-- 要素が一つだけのリストは回文 -/
  | single (a : α) : Palindrome [a]
  /-- 回文の両端に同じ要素を追加しても回文 -/
  | sandwich {a : α} {as : List α} (ih : Palindrome as) : Palindrome ([a] ++ as ++ [a])

これと同様の定義を再帰関数によって行うことは可能ですが、sandwich のケースが少し複雑になってしまいます。これは、match 式が受け入れるパターンマッチの形式が限定されているためです。(as.revers = as で判定することもできますが、それが Palindrome と同値であることは自明ではないことです)

-- `DecidableEq` を仮定したくないので古典論理を利用する
open scoped Classical in

/-- 回文判定を行う再帰関数 -/
def isPalindrome (as : List α) : Prop :=
  match as with
  | [] => true
  | [a] => true
  | a₁ :: a₂ :: as =>
    let xs := (a₂ :: as).dropLast
    let x := (a₂ :: as).getLast (by simp)

    if a₁ = x then
      isPalindrome xs
    else
      false
termination_by as.length

使用例: プログラムの BigStep 意味論

帰納的述語として定義することの更なるメリットとしては、再帰が停止することを保証しなくて良いことが挙げられます。実際、帰納的述語は本質的に停止する保証がない再帰的な操作でも扱うことができます。以下は、少し複雑ですがプログラムの BigStep 意味論を表現する例です。1

/-- 変数。ここでは簡単のために、すべての文字列が変数として存在するものとする -/
abbrev Variable := String

/-- プログラムの状態。すべての変数の値を保持したもの。
ここでは簡単にするために、変数の値はすべて自然数だとしている。 -/
def State := Variable → Nat

/-- `1 + x` のような算術式。変数の値を決めるごとに値が決まる。-/
def Arith := State → Nat

/-- 抽象化された単純なプログラミング言語のプログラム -/
inductive Stmt : Type where
  /-- 何もしないコマンド -/
  | skip
  /-- `x := a` のような代入文。-/
  | assign (v : Variable) (expr : Arith)
  /-- 2つのコマンドを続けて実行する。`;;` で表される。-/
  | seq (S T : Stmt)
  /-- while 文 -/
  | whileDo (B : State → Prop) (S : Stmt)

@[inherit_doc] infix:60 ";; " => Stmt.seq

-- 技術的な理由で必要
set_option quotPrecheck false in

/-- 状態 `s : State` があったとき、変数 `x` に対してだけ値を更新したものを表す記法 -/
notation s:70 "[" x:70 "↦" n:70 "]" => (fun v ↦ if v = x then n else s v)

open Stmt

/-- プログラムの BigStep 意味論。`BigStep c s t` は、
プログラム `c` を状態 `s` の下で実行すると状態が `t` になって停止することを表す。
-/
inductive BigStep : Stmt → State → State → Prop where
  /-- skip コマンドの意味論。-/
  | skip (s : State) : BigStep skip s s

  /-- 代入文 `x := a` の実行前に状態が `s` であったなら、
  代入文の実行後には状態は変数 `x` についてだけ更新される。-/
  | assign (x : Variable) (a : State → Nat) (s : State) :
    BigStep (assign x a) s (s[x ↦ a s])

  /-- seq コマンドの意味論。-/
  | seq {S T : Stmt} {s t u : State} (hS : BigStep S s t) (hT : BigStep T t u) :
    BigStep (S;; T) s u

  /-- while 文の、条件式が真のときの意味論。
  `whileDo B S` は、開始時に `B` が成り立っているなら、
  `S` を実行してから `whileDo B S` を実行するのと同じ意味になる。-/
  | while_true {B S s t u} (hcond : B s) (hbody : BigStep S s t)
    (hrest : BigStep (whileDo B S) t u) : BigStep (whileDo B S) s u

  /-- while 文の、条件式が偽のときの意味論。-/
  | while_false {B S s} (hcond : ¬ B s) : BigStep (whileDo B S) s s

「プログラムを評価する」という操作は while 文の評価が終わらない可能性があるため、証明可能な再帰関数として定義することができません。そのため上記の BigStep 意味論の例は帰納的述語が真に有用なケースであるといえます。

使用例: 反射的閉包

α 上の二項関係 R : α → α → Prop が与えられたとします。このとき、R が反射的であるとは、∀ x, R x x が成り立つことを意味します。典型的な例として、等号 = や不等号 は反射的です。

一般の二項関係は反射的とは限りませんが、任意の二項関係 R に対して、R を含むような最小の反射的な二項関係が存在します。これを R反射的閉包 と呼びます。ここでは仮に、R の反射的閉包を #R と書くことにします。ただし、二項関係 RS に対して、SR を含むとは、∀ x y, R x y → S x y が成り立つことであると定義します。

このとき #R を帰納的述語として表現することができます。一見しただけでは「~を含むような最小の…」という表現を帰納的述語として表現できることは見えてこないかもしれません。しかし、冷静に考えてみると #R については、以下の性質が成り立ちます。

  1. #RR を含む。つまり ∀ x y, R x y → #R x y が成り立つ。
  2. #R は反射的である。つまり ∀ x, #R x x が成り立つ。

そして、#R はこれを満たすような最小の二項関係であるわけです。それを考えると、反射的閉包は次のように定義することができます。

variable {α : Type} (R : α → α → Prop)

/-- 反射的閉包 -/
inductive ReflCl : α → α → Prop where
  /-- `R` を含む -/
  | base {x y : α} (h : R x y) : ReflCl x y

  /-- 反射的 -/
  | refl {x : α} : ReflCl x x

実際ここで定義した ReflCl は、R を含むような最小の反射的二項関係であることを証明できます。

variable {α : Type} (R : α → α → Prop)

/-- `α`上の二項関係の全体 -/
abbrev BinRel (α : Type) := α → α → Prop

/-- `R` が `S` に含まれるという関係を `R ≤ S` と書けるようにする -/
instance : LE (BinRel α) where
  le := fun R S => ∀ x y, R x y → S x y

/-- `R`の反射的閉包は`R`を含む -/
theorem le_reflCl (R : BinRel α) : R ≤ ReflCl R := by
  dsimp [(· ≤ ·)]
  grind [ReflCl]

/-- `ReflCl R` は反射的二項関係 -/
theorem reflCl_refl (R : BinRel α) : ∀ x : α, ReflCl R x x := by
  grind [ReflCl]

/-- `ReflCl R` は `R` を含むような反射的二項関係の中で最小 -/
theorem reflCl_is_minimal (R S : BinRel α) (hle : R ≤ S) (hrefl : ∀ x, S x x) :
    ReflCl R ≤ S := by
  dsimp [(· ≤ ·)] at *
  intro x y h
  induction h with grind

使用例: アクセス可能性

ある二項関係 r : α → α → Prop が与えられたとします。このとき、r に関して x : α がアクセス可能であるとは、x から始まる無限降下列が存在しないことをいいます。

つまり、r y₀ x, r y₁ y₀, r y₂ y₁, … と無限に続くような列 y₀, y₁, y₂, ... が存在しないということです。わかりやすさのために r = (· < ·) とすると、これは ... < y₂ < y₁ < y₀ < x のような無限降下列が存在しないことを意味します。

この「アクセス可能性」を帰納型として表現することができます。少し見えづらいと思いますが、実際にアクセス可能性述語は次のように帰納的に定義することができます。

  1. x : α に対して、r y x となる y : α が存在しないのであれば、x はアクセス可能。
  2. r y x を満たす任意の y : α がアクセス可能であるならば、x もアクセス可能。

技術的には r y x を満たす y が存在しないケースは ∀ y, r y x が成り立つケースに含めることができるため、アクセス可能性述語は次のように帰納的述語として定義することができます。

/-- `r` に関するアクセス可能性述語 -/
inductive Acc {α : Type} (r : α → α → Prop) : α → Prop where
  /-- `r y x` が成り立つような任意の `y : α` がアクセス可能であるならば、`x` もアクセス可能。-/
  | intro (x : α) (h : (y : α) → r y x → Acc r y) : Acc r x

帰納型の仕様

再帰子

inductive コマンドで帰納型 T を定義すると、再帰子(recursor) T.rec が自動生成されます。再帰子は induction タクティクで使用されるほか、[induction_eliminator] 属性にも関係があります。

たとえば列挙型である Bool の場合、再帰子は次のようになっています。

/-
info: Bool.rec.{u} {motive : Bool → Sort u} (false : motive false) (true : motive true) (t : Bool) : motive t
-/
#check Bool.rec

この再帰子の型をよく見ると、Bool から型 motive _ への依存関数 (t : Bool) → motive t を構成する手段を提供していることがわかります。

Nat の場合はもっとわかりやすいです。

/-
info: Nat.rec.{u} {motive : Nat → Sort u} (zero : motive Nat.zero) (succ : (n : Nat) → motive n → motive n.succ) (t : Nat) :
  motive t
-/
#check Nat.rec

Nat から型 motive _ への依存関数 (t : Nat) → motive t を構成する手段を提供しているのは同じなのですが、よく見ると帰納法の原理そのものの形をしています。

motive t が棲んでいる宇宙 Sort uProp を代入してみましょう。このとき motive Nat.zero 型の引数というのは、命題 motive Nat.zero : Prop の証明にほかならず、(n : ℕ) → motive n → motive n.succ 型の引数というのは、命題 ∀ n, motive n → motive (n + 1) の証明にほかなりません。この条件の下で関数 (t : Nat) → motive t つまり ∀ t, motive t の証明が得られると主張しているのですから、帰納法の原理そのものであることがわかります。

noConfusion

帰納型のコンストラクタは必ず単射になり、異なるコンストラクタの像は決して重なりません。このルールは、injection タクティクで利用することができます。

/-- Peano の公理によって定義された自然数 -/
inductive MyNat : Type where
  | zero
  | succ (n : MyNat)

/-- コンストラクタの像は重ならない -/
example (n : MyNat) : .succ n ≠ MyNat.zero := by
  intro h
  injection h

/-- コンストラクタは必ず単射である -/
example (n m : MyNat) : MyNat.succ n = MyNat.succ m → n = m := by
  intro h
  injection h

show_term を使用して証明項を出してみると、injection タクティクにより noConfusion という名前の定理が呼ばれていることがわかります。

/-
info: Try this:
  [apply] fun h => False.elim (noConfusion_of_Nat MyNat.ctorIdx h)
-/
example (n : MyNat) : .succ n ≠ MyNat.zero := show_term by
  intro h
  injection h

/-
info: Try this:
  [apply] MyNat.succ.noConfusion h fun n_eq => n_eq
-/
example (n m : MyNat) (h : MyNat.succ n = MyNat.succ m) : n = m := show_term by
  injection h

strictly positive 要件

帰納型を定義しようとした際に、次のようなエラーになることがあります。

/-
error: (kernel) arg #1 of 'Foo.mk' has a non positive occurrence of the datatypes being declared
-/
inductive Foo where
  | mk (f : Foo → Nat)

エラーメッセージには「Foo.mk の引数の中に、定義しようとしている型が non positive に現れている」と書かれています。この positive とは、引数における位置のことで、一般に関数型 X → Y があるとき X負の位置(negative position) であり、Y正の位置(positive position) であると呼びます。ただし関数の型が入れ子になっていると正負が変わります。たとえば (X → Y) → Z という型の場合、X は負の位置の負の位置にあるので、正の位置と見なされます。上記の Foo のコンストラクタには Foo 自身が現れていますが、コンストラクタの引数の型の中で正の位置に現れていないので、それがルール違反であるとエラーメッセージは言っているわけです。

次の例では、Bar のコンストラクタの引数の型の中で Bar 自身が正の位置に現れていますが、これも同じエラーになります。

/-
error: (kernel) arg #1 of 'Bar.mk' has a non positive occurrence of the datatypes being declared
-/
inductive Bar where
  | mk (f : (Bar → Nat) → Nat)

どの から見ても正の位置にあるときには狭義の正の位置(strictly positive position)と呼ばれるのですが、Lean は実際には狭義の正の位置でなければ定義を拒否します。帰納型 T のコンストラクタの引数の中に T 自身が現れる場合、狭義の正の位置つまり A → T の形で現れるのは許容されますが T → A の形で現れるのは許されません。これを strictly positive 要件と本書では呼びます。

仮に strictly positive 要件に違反するような帰納型をなんでも定義できたとすると、矛盾が導かれてしまいます。unsafe 修飾子で実際に試してみましょう。2

import Mathlib.SetTheory.Cardinal.Basic

-- 任意に型 A が与えられたとして固定する
opaque A : Type

/-- strictly positive 要件を破っている帰納型 -/
unsafe inductive Bad where
  | mk (f : Bad → A)

section
  /- ## A が空の場合

  `A` が空なら、`A` は `False` と同じなので矛盾が導かれる。
  -/

  unsafe def selfApply (b : Bad) : A :=
    match b with
    | Bad.mk f => f b

  -- A の項が A の情報を使わずに構成できてしまった
  unsafe def ω : A := selfApply (Bad.mk selfApply)

  /-- `A` が空なら矛盾が導かれる -/
  unsafe example [IsEmpty A] : False :=
    IsEmpty.false ω
end

section
  /- ## A に2つ以上の要素があるとき -/

  /-- `Bad` と `Bad → A` の間に全単射がある -/
  unsafe def equiv : Bad ≃ (Bad → A) where
    toFun := fun ⟨f⟩ => f
    invFun := Bad.mk
    left_inv := by
      intro ⟨f⟩
      rfl
    right_inv := by
      intro f
      rfl

  open scoped Cardinal

  unsafe example [Nontrivial A] : False := by
    -- `Bad` と `Bad → A` は全単射があるので濃度(`#`)が同じ
    have h : # Bad = # (Bad → A) :=
      Cardinal.mk_congr equiv

    -- 特に、`# Bad = # A ^ # Bad` が成り立つ
    replace h : # Bad = #A ^ # Bad := by
      simpa [Cardinal.mk_pi, Cardinal.prod_const, Cardinal.lift_id] using h

    -- しかしカントール(Cantor)の定理により、`# A ≥ 2` ならば
    -- `# Bad < # A ^ # Bad` が成り立つ
    have : # Bad < #A ^ # Bad := by
      apply Cardinal.cantor' (a := # Bad) (b := # A)
      rw [Cardinal.one_lt_iff_nontrivial]
      infer_instance

    -- これは矛盾
    apply ne_of_lt this h
end

-- ω を簡約すると ω 自身が出てくる
-- つまり無限ループしている
/-
error: maximum recursion depth has been reached
use `set_option maxRecDepth <num>` to increase limit
use `set_option diagnostics true` to get diagnostic information
-/
#reduce ω

  1. The Hitchhiker’s Guide to Logical Verification を参考にいたしました。

  2. 以下の証明は、Lean 公式 Zulip の strictly positive requirement というトピックで Markus Himmel さんが示した証明を参考にしています。

infix

infix は、中置記法を定義するコマンドです。

-- 中置記法の2項演算子を定義。中身はただの掛け算
infix:60 " ⋄ " => Nat.mul

#guard 2 ⋄ 3 = 6

: の後に付けている数字は パース優先順位(parsing precedence) で、高いほど結合するタイミングが早くなります。等号 = のパース優先順位は 50 であることを覚えておくと良いかもしれません。

-- 等号より微妙にパース優先順位が高い
infix:51 " strong " => Nat.add

-- きちんと 1 + (2 strong 3) = 6 と解釈される。
-- これは、 等号のパース優先順位が 51 未満であることを意味する
#check 1 + 2 strong 3 = 6

-- パース優先順位を 50 より低くすると等号より低くなる
-- したがってエラーになる
infix:49 " weak " => Nat.add

#check_failure 1 + 2 weak 3 = 6

infix で定義される記法は左結合でも右結合でもなく、必ず括弧が必要です。

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- 単に連結するとパース不可でエラーになる
/- error: <input>:1:6: expected end of input -/
#eval parse `term "1 ⋄ 2 ⋄ 3"

-- 括弧を付ければOK
#eval parse `term "1 ⋄ (2 ⋄ 3)"

舞台裏

infixnotation コマンドに展開されるマクロとして実装されています。

def lxor (l r : Bool) : Bool := !l && r

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:50 lhs✝:51 " LXOR " rhs✝:51 => lxor lhs✝ rhs✝ -/
#expand (infix:50 " LXOR " => lxor)

infixl

infixl は、左結合の中置記法を定義するためのコマンドです。

-- 中置記法の2項演算子を定義。中身はただの引き算
infixl:60 " ⋄ " => fun x y => x - y

-- 左結合になっている
#guard (16 ⋄ 5) ⋄ 4 = 7
#guard 16 ⋄ (5 ⋄ 4) = 15
#guard 16 ⋄ 5 ⋄ 4 = 7

舞台裏

infixlnotation コマンドに展開されるマクロとして実装されています。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:60 lhs✝:60 " ⋄ " rhs✝:61 => (fun x y => x - y) lhs✝ rhs✝ -/
#expand (infixl:60 " ⋄ " => fun x y => x - y)

infixr

infixr は、右結合の中置記法を定義するためのコマンドです。

-- 中置記法の2項演算子を定義。中身はただの引き算
infixr:60 " ⋄ " => fun x y => x - y

-- 右結合になっている
#guard (16 ⋄ 5) ⋄ 4 = 7
#guard 16 ⋄ (5 ⋄ 4) = 15
#guard 16 ⋄ 5 ⋄ 4 = 15

舞台裏

infixrnotation コマンドに展開されるマクロとして実装されています。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:60 lhs✝:61 " ⋄ " rhs✝:60 => (fun x y => x - y) lhs✝ rhs✝ -/
#expand (infixr:60 " ⋄ " => fun x y => x - y)

instance

instance は、型クラスのインスタンスを定義するためのコマンドです。

/-- 平面 -/
structure Point (α : Type) where
  x : α
  y : α

/-- 原点 -/
def origin : Point Int := { x := 0, y := 0 }

-- 数値のように足し算をすることはできない
#check_failure (origin + origin)

/-- 平面上の点の足し算ができるようにする -/
instance {α : Type} [Add α] : Add (Point α) where
  add p q := { x := p.x + q.x, y := p.y + q.y }

-- 足し算ができるようになった
#check (origin + origin)

インスタンスの連鎖

インスタンスは連鎖させることができます。言い換えると、「aC のインスタンスならば、f aC のインスタンスである」というようなインスタンス宣言ができます。Lean コンパイラは再帰的にインスタンスを探します。

/-- 偶数 -/
inductive Even : Type where
  | zero
  | succ (n : Even)
deriving DecidableEq

/-- 偶数から自然数への変換 -/
def Even.toNat : Even → Nat
  | zero => 0
  | succ n => 2 + (Even.toNat n)

/-- Even を文字列に変換することを可能にする。
同時に #eval も可能になる。-/
instance : ToString Even where
  toString := toString ∘ Even.toNat

/-- Even.zero を 0 と書けるようにする -/
instance : OfNat Even 0 where
  ofNat := Even.zero

-- 実際に Even.zero を 0 と書けるようになった
#guard (0 : Even) = Even.zero

/-- インスタンス連鎖を利用して OfNat を実装。
n について OfNat の実装があれば、n + 2 についても OfNat の実装を得る。-/
instance {n : Nat} [OfNat Even n] : OfNat Even (n + 2) where
  ofNat := Even.succ (OfNat.ofNat n)

#guard (2 : Even) = Even.succ Even.zero

-- 奇数については OfNat の実装はない
#check_failure (3 : Even)

なお、インスタンス連鎖の回数には上限があります。

-- ギリギリセーフ
#eval (254 : Even)

-- 上限を超えてしまった
#check_failure (256 : Even)

インスタンス優先度

概要

Lean では、同じ型と型クラスの組に対して複数のインスタンスを定義することができます。たとえば、次のようにモノイドという型クラスを定義したとします。

/-- モノイド -/
class Monoid (α : Type) where
  /-- 単位元 -/
  unit : α

  /-- 演算 -/
  op : α → α → α

  /-- 結合法則 -/
  assoc : ∀ x y z : α, op (op x y) z = op x (op y z)

このとき Nat という一つの型に対して、2つの Monoid のインスタンスを定義することができ、後から定義した方が優先されます。

section
  /- ## 優先度を指定しないインスタンスを2つ重ねた例 -/

  local instance : Monoid Nat where
    unit := 0
    op x y := x + y
    assoc x y z := by ac_rfl

  local instance : Monoid Nat where
    unit := 1
    op x y := x * y
    assoc x y z := by ac_rfl

  -- 後から宣言した方が優先される
  #guard (Monoid.unit : Nat) = 1
end

しかし、priority 構文を使うとインスタンス優先度を設定することができます。たとえば、priority := low とするとインスタンス優先度が low に設定されて、優先されなくなります。

section
  /- ## priority := low の使用例 -/

  local instance : Monoid Nat where
    unit := 0
    op x y := x + y
    assoc x y z := by ac_rfl

  local instance (priority := low) : Monoid Nat where
    unit := 1
    op x y := x * y
    assoc x y z := by ac_rfl

  -- 後から宣言した方が優先されなくなる!
  #guard (Monoid.unit : Nat) = 0
end

逆に priority := high とするとインスタンス優先度が high に設定されて、優先されるようになります。

section
  /- ## priority := high の使用例 -/

  local instance (priority := high) : Monoid Nat where
    unit := 0
    op x y := x + y
    assoc x y z := by ac_rfl

  local instance : Monoid Nat where
    unit := 1
    op x y := x * y
    assoc x y z := by ac_rfl

  -- 後から宣言した方が優先されなくなる!
  #guard (Monoid.unit : Nat) = 0
end

使用例: アリティの計算

インスタンス優先度を活用する例として、関数型のアリティ(引数の数)を計算する型クラスを定義する例をご紹介します。1

/-- `Arity α` は「型 `α` のアリティ(引数の数)」を与える型クラス。 -/
class Arity (α : Type) where
  /-- `α` のアリティ -/
  arity : Nat

/--
汎用の(デフォルト)インスタンス:
あらゆる型を「関数でない」とみなし、アリティを `0` とする。
関数型に対しては、このインスタンスより **優先度の高い** 関数用インスタンスが使われる。
-/
instance (priority := low) (α : Type) : Arity α where
  arity := 0

/--
関数型 `α → β` のインスタンス:
帰結型 `β` のアリティに 1 を足す。
(`β` に対して再帰的に `Arity` を使うことで、多引数カリー化を数え上げる)
-/
instance (α β : Type) [Arity β] : Arity (α → β) where
  arity := 1 + (Arity.arity β)

-- 関数でない型のアリティは 0
#guard Arity.arity Nat = 0

-- 1引数関数型のアリティは 1
#guard Arity.arity (Nat → Bool) = 1

-- 2引数関数型のアリティは 2
#guard Arity.arity (Nat → Bool → String) = 2

-- タプルを受け取る関数型は「引数が1つ」なので1
#guard Arity.arity (Nat × Bool → String) = 1

舞台裏

instance[instance] 属性を付与された def と同じようにはたらきます。ただし instance はインスタンス名を省略することができるという違いがあります。

-- `List` 同士を足すことはできない
#check_failure [1] + [2]

-- インスタンスを宣言する
instance instListAdd {α : Type} : Add (List α) where
  add := List.append

-- リスト同士を足すことができるようになった
-- 実装としては、上で指定した通り `List.append` が使われる
#guard [1] + [2] = [1, 2]

-- インスタンスを削除する
attribute [-instance] instListAdd

-- リスト同士を足すことができなくなった
#check_failure [1] + [2]

  1. ルールとして、より具体的なインスタンスが優先されるので、この関数型のアリティについての例では priority := low を指定する必要はありません。しかし、コードの意図を明確にするためには指定したほうがわかりやすいでしょう。

macro_rules

macro_rulesマクロ展開を定義するためのコマンドです。類似のコマンドに macro コマンドがあります。

/-- `#hello` コマンドの構文の定義。
オプションの引数を受け取るようにしておく。 -/
syntax "#hello" (str)? : command

macro_rules
  | `(#hello) => `(command| #eval "Hello, Lean!")
  | `(#hello $name) => `(command| #eval s!"Hello, {$name}!")

/- info: "Hello, Lean!" -/
#hello

/- info: "Hello, world!" -/
#hello "world"

macro_rules コマンドは上記の例のように、=> 記号を境に分かれており、左辺の構文を右辺の構文に変換するというルールを定義します。

展開ルールの上書きと追加

一つの構文に対して macro_rules で複数の展開ルールを宣言することができます。このとき、最後に宣言されたルールから先に適用されます。

/-- 挨拶するコマンド -/
syntax "#greet" : command

-- `#greet` という構文に2つの展開ルールを定義

macro_rules
  | `(command| #greet) => `(#eval "Hello, Lean!")

macro_rules
  | `(command| #greet) => `(#eval "Good morning, Lean!")

-- 最後に宣言されたルールが適用される
/- info: "Good morning, Lean!" -/
#greet

このとき古い方の展開ルールは常に上書きされて消えるわけではありません。macro_rules で宣言されたルールは最後に宣言されたものから順に試され、展開に失敗するとスキップされ、最初に展開に成功したルールが採用されます。具体例でこの挙動を確認してみましょう。

/-- `trivial` というタクティクの類似物 -/
syntax "my_trivial" : tactic

-- `assumption` タクティクを呼び出す
macro_rules
  | `(tactic| my_trivial) => `(tactic| assumption)

-- `rfl` タクティクを呼び出す
macro_rules
  | `(tactic| my_trivial) => `(tactic| rfl)

-- 後から追加されたルールが先に適用されるので、Lean はまず `rfl` に展開しようとする。
-- しかし `rfl` はゴールの形が不適切なので失敗する。
-- その後 `assumption` が試され、それは通る。
example (P : Prop) (h : P) : P := by
  my_trivial

-- `rfl` が使われて通る
example {α : Type} (x : α) : x = x := by
  my_trivial

再帰的展開

macro_rules の右辺に、これから解釈しようとしている構文自身を含めることができます。これにより、再帰的なマクロ展開を定義することができます。

example {α : Type} {P : Prop} (x : α) (h : P) : x = x ∧ P := by
  -- 最初は示せない
  fail_if_success my_trivial

  -- 手動で示す
  apply And.intro
  · rfl
  · assumption

-- 再帰的なマクロ展開を定義。
-- `P` と `Q` が両方 `my_trivial` で示せるなら、
-- `P ∧ Q` が `my_trivial` で示せるようになる。
macro_rules
  | `(tactic| my_trivial) => `(tactic| apply And.intro <;> my_trivial)

example {α : Type} {P : Prop} (x : α) (h : P) : x = x ∧ P := by
  -- `my_trivial` で示せるようになった!
  my_trivial

使用例

macro_rules を理解する一番の近道は、具体例をたくさん見ることです。以下に、macro_rules のシンプルな使用例をいくつか紹介します。

集合の波括弧記法

macro_rules を使用して、集合の波括弧記法 {{ a₁, a₂, ..., aₙ }} を解釈するマクロを定義する例を以下に示します。

/-- 部分集合。`α` の部分集合 `A ⊆ α` は、任意の要素 `x : α` に対して
それが `A` の要素かどうか判定する述語 `A : α → Prop` と同一視できる。-/
def Set (α : Type) := α → Prop

namespace Set

  variable {α : Type}

  /-- 1つの要素だけからなる集合 -/
  def singleton (a : α) : Set α := fun x => x = a

  /-- 集合に1つ要素を追加する -/
  def insert (a : α) (s : Set α) := fun x => x = a ∨ s x

end Set

-- 集合の波括弧記法の定義。
-- 既存の記号と被らないようにするために二重にしている。
-- なお `term,*` は `term` が `,` 区切りで0個以上続く列を表す。
syntax "{{" term,* "}}" : term

-- `syntax` コマンドは記法の解釈方法を決めていないので、エラーになる
#check_failure {{2, 3}}

-- 集合の波括弧記法をどう解釈するかのルールを定める
macro_rules
  | `(term| {{$x}}) => `(Set.singleton $x)
  | `(term| {{$x, $xs:term,*}}) => `(Set.insert $x {{$xs,*}})

-- 集合の波括弧記法が使えるようになった
#check ({{2, 3, 4, 5}} : Set Nat)

入れ子リスト

Lean 標準の List : Type → Type はリストの要素が同じ型であることを要求しており、「リストのリスト」にリストではない要素を混入させることを許しませんが、それを許すような入れ子リストを定義して、そのための構文を用意する例を示しましょう。1

/-- 入れ子になったリスト -/
inductive NestedList (α : Type) : Type
  /-- 空リスト -/
  | nil : NestedList α
  /-- NestedList に要素を追加する -/
  | conse : α → NestedList α → NestedList α
  /-- NestedList に NestedList を追加する -/
  | consl : NestedList α → NestedList α → NestedList α
deriving DecidableEq

namespace NestedList
  /- ## NestedList を定義する構文を作る -/

  /-- NestedList を定義するための構文。 -/
  syntax "《" term,* "》" : term

  -- `syntax` コマンドは記法の解釈方法を決めていないので、エラーになる
  #check_failure 《1, 《2, 3》, 4》

  macro_rules
    | `(《》) => `(NestedList.nil)
    | `(《《$xs,*》》) => `(NestedList.consl 《$xs,*》 NestedList.nil)
    | `(《《$xs,*》, $ys,*》) => `(NestedList.consl 《$xs,*》 《$ys,*》)
    | `(《$x》) => `(NestedList.conse $x NestedList.nil)
    | `(《$x, $xs,*》) => `(NestedList.conse $x 《$xs,*》)

  -- NestedList を見やすく定義できるようになった!
  #check 《1, 《2, 3》, 4》

  #guard
    let xs := 《1, 2》
    let ys := NestedList.conse 1 <| .conse 2 NestedList.nil

    -- 両者は同じものを表している!
    xs = ys

end NestedList

リスト内包表記

マクロとして リスト内包表記(list comprehension) を導入する例を以下に示します。2

namespace ListComp
  /- # リスト内包表記 -/

  /-- リスト内包表記 -/
  declare_syntax_cat compClause

  syntax "for " term " in " term : compClause
  syntax "if " term : compClause
  syntax "[" term " | " compClause,* "]" : term

  -- `syntax` コマンドは記法の解釈方法を決めていないので、エラーになる
  #check_failure [x | for x in [1, 2, 3, 4, 5]]
  #check_failure [x | if x < 2]
  #check_failure [x | for x in [1, 2, 3], if x < 2]

  macro_rules
    | `([$t |]) => `([$t])
    | `([$t | for $x in $xs]) => `(List.map (fun $x => $t) $xs)
    | `([$t | if $x]) => `(if $x then [$t] else [])
    | `([$t | $c, $cs,*]) => `(List.flatten [[$t | $cs,*] | $c])

  -- for 構文のテスト
  #guard [x ^ 2 | for x in [1, 2, 3, 4, 5]] = [1, 4, 9, 16, 25]

  -- 2重の for 構文のテスト
  #guard
    let lhs := [(x, y) | for x in [1, 2, 3], for y in [4, 5]]
    let rhs := [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]
    lhs = rhs

  -- if 構文のテスト
  #guard [x | for x in [1, 2, 3], if x < 2] = [1]

end ListComp

数式を Lean に埋め込む

1 + 2 * 3 のような数式(値ではなくて式そのもの)を Lean の式として解釈するマクロを以下に示します。

/-- 2項演算の集合 -/
inductive Op where
  /-- 加法 -/
  | add
  /-- 乗法 -/
  | mul
deriving DecidableEq

/-- 数式 -/
inductive Arith where
  /-- 数値リテラル -/
  | val (n : Nat) : Arith
  /-- 演算子の適用 -/
  | app (op : Op) (lhs rhs : Arith) : Arith
deriving DecidableEq

section arith_syntax

  /-- `Arith` のための構文カテゴリ -/
  declare_syntax_cat arith

  /-- `Arith` を見やすく定義するための構文 -/
  syntax "[arith| " arith "]" : term

  -- 数値リテラルは数式
  syntax:max num : arith

  -- 数式を `+` または `*` で結合したものは数式
  -- `+` と `*` のパース優先順位を指定しておく
  syntax:30 arith:30 " + " arith:31 : arith
  syntax:35 arith:35 " * " arith:36 : arith

  -- 数式を括弧でくくったものは数式
  syntax:max "(" arith ")" : arith

  -- `syntax` コマンドは記法の解釈方法を決めていないのでエラーになるが、
  -- パースはできるようになった
  #check_failure [arith| 1 + 2]
  #check_failure [arith| 1 * 2]
  #check_failure [arith| (1 + 2) * 3]

end arith_syntax

section arith_macro

  macro_rules
    | `([arith| $n:num]) => `(Arith.val $n)
    | `([arith| $l:arith + $r:arith]) => `(Arith.app Op.add [arith| $l] [arith| $r])
    | `([arith| $l:arith * $r:arith]) => `(Arith.app Op.mul [arith| $l] [arith| $r])
    | `([arith| ($e:arith)]) => `([arith| $e])

  open Arith

  -- 足し算は左結合になる
  #guard
    let expected := app Op.add (app Op.add (val 1) (val 2)) (val 3)
    let actual := [arith| 1 + 2 + 3]
    actual = expected

  -- 掛け算は左結合になる
  #guard
    let expected := app Op.mul (app Op.mul (val 1) (val 2)) (val 3)
    let actual := [arith| 1 * 2 * 3]
    actual = expected

  -- 足し算と掛け算が混在する場合は、掛け算が優先される
  #guard
    let expected := app Op.add (app Op.mul (val 1) (val 2)) (val 3)
    let actual := [arith| 1 * 2 + 3]
    actual = expected
end arith_macro

IMP 言語の構文

簡単な命令型プログラミング言語(ここでは IMP と呼ばれるものを使用します)を Lean の中に埋め込んでしまうことができます。3

namespace IMP
  /- ## IMPのASTを定義する -/

  /-- リテラル -/
  inductive Lit where
    /-- 数値リテラル -/
    | nat (n : Nat)
    /-- 真偽値リテラル -/
    | bool (b : Bool)

  /-- 単項演算子 -/
  inductive UnOp where
    /-- 否定演算子 -/
    | not

  inductive BinOp where
    /-- 論理積 -/
    | and
    /-- 和 -/
    | add
    /-- 順序関係 `<` -/
    | less

  /-- IMPの式 -/
  inductive Expr where
    /-- リテラル式 -/
    | lit (l : Lit)
    /-- 変数 -/
    | var (x : String)
    /-- 単項演算子の適用 -/
    | un (op : UnOp) (e : Expr)
    /-- 二項演算子の適用 -/
    | bin (op : BinOp) (e1 e2 : Expr)

  inductive Program where
    /-- 何もしないプログラム -/
    | skip
    /-- 変数代入 `v := e` -/
    | assign (v : String) (e : Expr)
    /-- 逐次実行 `p1; p2` -/
    | seq (p1 p2 : Program)
    /-- 条件分岐 `if e then p1 else p2` -/
    | ite (e : Expr) (p1 p2 : Program)
    /-- ループ `while e do p` -/
    | while (e : Expr) (p : Program)

end IMP


namespace IMP
  /- ## リテラルのための構文を定義する -/

  /-- IMPのリテラルのための構文 -/
  declare_syntax_cat imp_lit
  syntax num : imp_lit
  syntax "true" : imp_lit
  syntax "false" : imp_lit

  syntax "[imp_lit|" imp_lit "]" : term

  macro_rules
    | `([imp_lit| $n:num]) => `(Lit.nat $(n))
    | `([imp_lit| true]) => `(Lit.bool $(Lean.mkIdent ``Bool.true))
    | `([imp_lit| false]) => `(Lit.bool $(Lean.mkIdent ``Bool.false))

  #check [imp_lit| 42]
  #check [imp_lit| true]
  #check [imp_lit| false]
end IMP


namespace IMP
  /- ## 単項演算子のための構文を定義する -/

  /-- 単項演算子のための構文 -/
  declare_syntax_cat imp_unop
  syntax "!" : imp_unop

  syntax "[imp_unop|" imp_unop "]" : term

  macro_rules
    | `([imp_unop| !]) => `(UnOp.not)

  #check [imp_unop| !]
end IMP


namespace IMP
  /- ## 2項演算子のための構文を定義する -/

  /-- 2項演算子のための構文 -/
  declare_syntax_cat imp_binop
  syntax "+" : imp_binop
  syntax "&&" : imp_binop
  syntax "<" : imp_binop

  syntax "[imp_binop|" imp_binop "]" : term

  macro_rules
    | `([imp_binop| +]) => `(BinOp.add)
    | `([imp_binop| &&]) => `(BinOp.and)
    | `([imp_binop| <]) => `(BinOp.less)

  #check [imp_binop| +]
  #check [imp_binop| &&]
  #check [imp_binop| <]
end IMP

namespace IMP
  /- ## 式のための構文を定義する -/
  open Lean

  declare_syntax_cat imp_expr
  syntax imp_lit : imp_expr
  syntax ident : imp_expr
  syntax imp_unop imp_expr : imp_expr
  syntax imp_expr imp_binop imp_expr : imp_expr
  syntax "(" imp_expr ")" : imp_expr

  syntax "[imp_expr|" imp_expr "]" : term

  -- 余計な警告が出るので消す
  set_option linter.unusedVariables false in

  macro_rules
    | `([imp_expr| $lit:imp_lit]) => `(Expr.lit [imp_lit| $lit])
    | `([imp_expr| $x:ident]) => `(Expr.var $(quote x.getId.toString))
    | `([imp_expr| !$e]) => `(Expr.un UnOp.not [imp_expr| $e])
    | `([imp_expr| $e1 $op:imp_binop $e2]) => `(Expr.bin [imp_binop| $op] [imp_expr| $e1] [imp_expr| $e2])
    | `([imp_expr| ($e)]) => `([imp_expr| e])

  #check [imp_expr| a]
  #check [imp_expr| a + 5]
  #check [imp_expr| 1 + true]
end IMP

namespace IMP
  /- ## IMPプログラムのための構文を定義する -/

  declare_syntax_cat imp_program
  syntax "skip" : imp_program
  syntax ident ":=" imp_expr : imp_program
  syntax imp_program ";" imp_program : imp_program
  syntax "if" imp_expr "then" imp_program "else" imp_program : imp_program
  syntax "while" imp_expr "do" imp_program : imp_program

  syntax "[IMP|" imp_program "]" : term

  open Lean

  macro_rules
    | `([IMP| skip]) => `(Program.skip)
    | `([IMP| $x:ident := $e]) => `(Program.assign $(quote x.getId.toString) [imp_expr| $e])
    | `([IMP| $p1; $p2]) => `(Program.seq [IMP| $p1] [IMP| $p2])
    | `([IMP| if $e then $p1 else $p2]) => `(Program.ite [imp_expr| $e] [IMP| $p1] [IMP| $p2])
    | `([IMP| while $e do $p]) => `(Program.while [imp_expr| $e] [IMP| $p])

  #check [IMP|
    a := 5;
    if ! a && 3 < 4 then
      c := 5
    else
      a := a + 1;
    b := 10;
    while 1 < 2 do
      b := b + 1
  ]
end IMP

  1. ここで紹介しているコード例は、Lean 公式 Zulip の “macro parser for nested lists” というトピックで Kyle Miller さんが挙げていたコードを参考にしています。

  2. ここで紹介しているコード例は、 lean4-samplesリポジトリのListComprehension.leanというファイルの内容を参考にしています。

  3. ここで紹介しているコード例は、Metaprogramming in Lean 4 の Embedding DSLs By Elaboration という章の記述を参考にしています。

macro

macro は、その名の通りマクロを定義するためのコマンドです。ただしマクロとは、構文を構文に変換する機能のことです。

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- 最初は `#greet` が未定義なので、合法的なLeanのコマンドとして認識されない
/- error: <input>:1:0: expected command -/
#eval parse `command "#greet"

-- `#greet` コマンドを定義する
macro "#greet " : command => `(command| #eval "Hello World!")

-- `#greet` コマンドが使用可能になった
/- info: "Hello World!" -/
#greet

舞台裏

syntax コマンドとmacro_rules コマンドを使用すれば、macro コマンドと同様のことが実現できます。macro_rules コマンドと比較すると、macro コマンドはマクロのための構文と展開ルールを同時に定義しているところが異なります。

namespace Macro

  scoped macro "#hello " : command => `(command| #eval "Hello Lean!")

  -- `#hello` コマンドが使用可能になった
  /- info: "Hello Lean!" -/
  #hello

end Macro

namespace MacroRules

  -- 構文の定義
  scoped syntax "#hello " : command

  -- 構文は認識されるが、解釈方法が定義されていないのでエラーになる
  /- error: elaboration function for `MacroRules.«command#hello»` has not been implemented -/
  #hello

  scoped macro_rules
    | `(#hello) => `(command| #eval "Hello Lean!")

  -- `#hello` コマンドが使用可能になった
  /- info: "Hello Lean!" -/
  #hello

end MacroRules

マクロ作例

マクロでどのようなことができるのかを理解するには、例を見るのが最高の近道です。以下に、macro コマンドでマクロを定義する例をいくつか示します。なお macro_rules コマンドを使用すれば、より複雑なマクロも定義できます。

引数を取るマクロ

冒頭の例の #greet コマンドは引数を持ちませんが、引数を取るようなものも定義できます。引数は $ を付けることでマクロ内で展開することができます。

-- 引数を取って、引数に対して挨拶するコマンドを定義する
-- 引数は `$` を付けると展開できる
macro "#hello " id:term : command => `(command| #eval s!"Hello, {$id}!")

/- info: "Hello, Lean!" -/
#hello "Lean"

タクティクを自作する

マクロを使用すると、コマンドだけでなくタクティクの定義も行うことができます。

-- 平方根の計算
example : √4 = 2 := by
  rw [Real.sqrt_eq_cases]
  norm_num

-- 平方根の簡約
example : √18 = 3 * √ 2 := by
  rw [Real.sqrt_eq_cases]
  ring_nf
  norm_num

-- 新たなタクティクを定義する
macro "norm_sqrt" : tactic => `(tactic| focus
    rw [Real.sqrt_eq_cases]
    try ring_nf
    norm_num
  )

-- 新しいタクティクにより一発で証明が終わるようになった!
example : √4 = 2 := by norm_sqrt
example : √18 = 3 * √ 2 := by norm_sqrt

do 構文を追加する

マクロで do 構文を追加することができます。

/-- 自前で定義した累積代入構文 -/
macro x:ident " += " e:term : doElem => `(doElem| ($x) := ($x) + $e)

/-- 0からnまでの和 -/
def sum (n : Nat) : Nat := Id.run do
  let mut sum := 0
  for i in [0:n+1] do
    sum += i
  return sum

#guard sum 3 = 6
#guard sum 4 = 10

引数の値ではなく名前を参照

Quote と組み合わせることで、マクロ展開時に、マクロの引数として与えられた変数の値だけでなく名前も参照することができます。

-- 通常の dbg_trace の挙動。
-- 与えられた式の値だけを返し、与えられた式が何だったかは教えてくれない
/- info: 1 -/
#eval
  let x := 1
  dbg_trace x
  return ()

/-- 与えられている変数の名前も出力するような `dbg_trace` の変種 -/
macro "dbg_trace!" x:ident body:term : term =>
  -- 与えられている引数 `x` の名前を取得する
  let ident := Lean.quote x.getId.toString
  `(term| dbg_trace s!"{$ident} = {$x}"; $body)

-- 与えられた変数の名前を出力するようになった!
/- info: y = 1 -/
#eval
  let y := 1
  dbg_trace! y
  return ()

mutual

mutual は、相互再帰を定義するために使用されます。

以下は、作為的ではあるものの簡単な例です。

mutual

/-- 偶数であることを表す述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | succ {n : Nat} (h : Odd n) : Even (n + 1)

/-- 奇数であることを表す述語 -/
inductive Odd : Nat → Prop where
  | succ {n : Nat} (h : Even n) : Odd (n + 1)
end

相互再帰を使って定義された概念に対して証明を行う場合、定理も相互再帰的にする必要が生じることがあります。

mutual

theorem Even.exists {n : Nat} (h : Even n) : ∃ a, n = 2 * a := by
  cases h with
  | zero => exists 0
  | @succ n h =>
    obtain ⟨a, ha⟩ := h.exists
    exists (a + 1)
    grind

theorem Odd.exists {n : Nat} (h : Odd n) : ∃ a, n = 2 * a + 1 := by
  cases h with
  | @succ n h =>
    obtain ⟨a, ha⟩ := Even.exists h
    exists a
    grind

end

使用例

作為的でない相互再帰の使用例としては、以下のようなものがあります。

namespace

namespace は、定義に階層構造を与えて整理するためのコマンドです。名前空間 Foo の中で bar を定義すると、それは Foo.bar という名前になり、名前空間 Foo の中では短い名前 bar でアクセスできますが、名前空間を出るとアクセスにフルネームが必要になります。

なおここでは説明のために namespace の中をインデントしていますが、これは一般的なコード規約ではありません。

-- namespace の外で定義された関数
def greet (name : String) := "Hello, " ++ name

namespace Nat

  def isEven (n : Nat) : Bool := n % 2 == 0

  -- 同じ名前空間の中なら短い名前でアクセスできる
  #check isEven

  -- 名前空間の外にある名前にアクセスできる
  #check greet

end Nat

-- 名前空間の外に出ると、短い名前ではアクセスできない
#check_failure isEven

-- フルネームならアクセスできる
#check Nat.isEven

名前空間は入れ子にすることができます。

namespace Nat

  namespace Even

    def thirty := 30

  end Even

  #check Even.thirty

end Nat

#check Nat.Even.thirty

名前空間を一時的に抜ける

名前空間を一時的に抜けたいとき、_root_ が使用できます。名前空間 Hoge の中で foo を定義すると Hoge.foo という名前になりますが、_root_.foo と定義すればこの挙動を避けて foo という名前にすることができます。

たとえば以下のように名前空間の中で List 配下の関数を定義し、フィールド記法を使おうとしてもうまくいきません。こういう場合に _root_ を使用すると、名前空間を閉じることなくエラーを解消できます。

namespace Root -- 名前空間 `Root` の宣言

variable {α : Type}

def List.unpack (l : List (List α)) : List α :=
  match l with
  | [] => []
  | x :: xs => x ++ unpack xs

/-
error: Invalid field `unpack`: The environment does not contain `List.unpack`, so it is not possible to project the field `unpack` from an expression
  [[1, 2], [3]]
of type `List (List Nat)`
-/
#check ([[1, 2], [3]] : List (List Nat)).unpack

-- エラーになる原因は、名前空間 `Root` の中で宣言したので
-- 関数名が `Root.List.unpack` になってしまっているから
#check Root.List.unpack

-- `_root_` を頭につけて再度定義する
def _root_.List.unpack (l : List (List α)) : List α :=
  match l with
  | [] => []
  | x :: xs => x ++ unpack xs

-- 今度は成功する
#eval [[1, 2], [3]].unpack

end Root

notation

notation は、新しい記法を導入するためのコマンドです。

/-- 各 `k` に対して、二項関係 `a ≃ b mod k` を返す -/
def modulo (k a b : Int) : Prop :=
  k ∣ (a - b)

-- mod という記法を導入する
notation:60 a:60 " ≃ " b:60 " mod " k:60 => modulo k a b

-- 定義した記法が使える
#check (3 ≃ 7 mod 4)

notation 記号で定義した記法が実際にどのように展開されているのか確かめるには、pp.notation というオプションを無効にします。

section

  /-- 階乗関数 -/
  def factorial : Nat → Nat
    | 0 => 1
    | n+1 => (n+1) * factorial n

  /-- 階乗関数を表す記法 -/
  notation:100 a:100 "!" => factorial a

  -- 表示する際に導入された記法を無効にする
  set_option pp.notation false

  /- info: factorial 5 : Nat -/
  #check 5!

end

パース優先順位

notation コマンドで記法を定義するときに、その記法の他の演算子などと比べた パース優先順位(parsing precedence) を数値で指定することができます。パース優先順位が高い演算子ほど、他の演算子より先に適用されます。言い換えれば、パース優先順位を正しく設定することにより、括弧を省略しても意図通りに式が解釈されるようにすることができます。

/-- 結合が弱い方。中身は足し算 -/
notation:min a:min " weak " b:min => Nat.add a b

/-- 結合が強い方。中身は掛け算 -/
notation:70 a:70 " strong " b:70 => Nat.mul a b

example : (3 weak 1 strong 2) = 5 := calc
  -- weak と strong のパース優先順位は strong の方が高いので、
  -- まず 1 strong 2 が計算されて 2 になり、
  _ = (3 weak 2) := rfl
  -- その後 3 weak 2 が計算されて 5 になる。
  _ = 5 := rfl

example : (2 strong 2 weak 3) = 7 := calc
  _ = (4 weak 3) := rfl
  _ = 7 := rfl

パース優先順位についての構文は syntax コマンドでも同様です。

プレースホルダのパース優先順位

プレースホルダのパース優先順位の数字は、「この位置に来る記号は、指定された数字以上のパース優先順位を持たなければならない」ことを意味します。下記の例の場合、仮に右結合になるとすると LXOR 自身のパース優先順位が 60 でしかなくて 61 以上という制約を満たさないため、右結合になることがありえないことがわかります。

-- 右側のプレースホルダのパース優先順位を1だけ高くした
notation:60 a:60 " LXOR " b:61 => !a && b

-- 左結合だった場合の値
#guard (true LXOR false) LXOR true = true
-- 右結合だった場合の値
#guard true LXOR (false LXOR true) = false

-- 左結合になることがわかる
#guard true LXOR false LXOR true = true

逆にすると右結合になります。

notation:60 a:61 " RXOR " b:60 => a && !b

-- 左結合だった場合の値
#guard (true RXOR false) RXOR true = false
-- 右結合だった場合の値
#guard true RXOR (false RXOR true) = true

-- 右結合になることがわかる
#guard true RXOR false RXOR true = true

パース優先順位を省略する場合

パース優先順位を省略することもできます。Lean は結合順序に指定がなければ右結合になるようにするようです。

/-- パース優先順位を全く指定しないで定義した記法。中身はべき乗 -/
notation a " -^ " b => Nat.pow a b

-- この場合は `weak` (優先順位最低)が先に適用される
example : (2 -^ 1 weak 3) = 16 := calc
  _ = (2 -^ 4) := rfl
  _ = 16 := rfl

-- 一方で次の書き方だと `-^` の方が先に適用される
example : (2 weak 1 -^ 3) = 3 := calc
  _ = (2 weak 1) := rfl
  _ = 3 := rfl

-- ここでも右結合になっている
example : (2 weak 3 -^ 1 weak 2) = 29 := calc
  _ = (2 weak 3 -^ 3) := rfl
  _ = (2 weak 27) := rfl
  _ = 29 := rfl

パース優先順位を一部だけ指定することもできます。

/-- パース優先順位を部分的に省略した足し算 -/
notation:min a " -+ " b => Nat.add a b

/-- パース優先順位を部分的に省略した掛け算 -/
notation a " -* " b:70 => Nat.mul a b

-- この場合だと、`-*` が先に適用される
-- 仮に `-+` が先に適用されたとすると、
-- `-*` の `b:70` の部分に `(2 -+ 3):min` が来ることになるのでおかしい。
-- だから `-*` が先に適用される。
example : (2 -* 2 -+ 3) = 7 := calc
  _ = (4 -+ 3) := rfl
  _ = 7 := rfl

記法の重複問題

notation を使って定義した記法のパース優先順位が意図通りに反映されないことがあります。

section

  -- 排他的論理和の記号を定義
  local notation:60 x:60 " ⊕ " y:61 => xor x y

  -- 等号(パース優先順位 50)より優先順位が低いという問題でエラーになる
  -- 上では60で定義しているのに、なぜ?
  #check_failure true ⊕ true = false

  -- 括弧を付けるとエラーにならない
  #check (true ⊕ true) = false

end

ここでのエラーの原因は、記法が被っていることです。 という記法は型の直和に対して既に使用されており、直和記法のパース優先順位が等号より低いためにエラーが発生していました。

-- 集合の直和の記号と被っていた。
-- 集合の直和記号は等号よりパース優先順位が低いからエラーになっていた
#check Nat ⊕ Fin 2

このケースの場合、priority で記法の優先度を指定しても解決されません。

section

  -- 排他的論理和の記号を定義
  local notation:60 (priority := high) x:60 " ⊕ " y:61 => xor x y

  -- やっぱりエラーになる
  #check_failure true ⊕ true = false

  -- 括弧を付けるとエラーにならない
  #check (true ⊕ true) = false

end

この問題の解決策は、まず第一に括弧を付けることですが、裏技として記法を上書きしてしまうこともできます。

section
  -- ⊕ という記号をオーバーライドする
  -- local コマンドを使っているのは、セクション内でだけ有効にするため
  local macro_rules
  | `($x ⊕ $y) => `(xor $x $y)

  -- もう ⊕ が Sum として解釈されることはなく、エラーにならない
  #guard true ⊕ true = false
  #guard true ⊕ false = true
  #guard false ⊕ true = true
  #guard false ⊕ false = false

  -- 上書きされたので、 Sum の意味で ⊕ を使うことはできなくなった
  #check_failure Nat ⊕ Fin 2
end

半角スペースの扱い

なお、notation を定義する際に半角スペースを入れることがしばしばありますが、これは表示の際に使われるだけで記法の認識には影響しません。

section

  -- 足し算を ⋄ で表す
  -- local コマンドを付けているのは、この記法をセクション内でだけ有効にするため
  local notation a "⋄" b => Add.add a b

  -- ⋄ の左右に半角スペースが入っていない!
  -- 違いはそれだけで、記法としては同様の書き方で認識される
  /- info: 1⋄2 : Nat -/
  #check 1 ⋄ 2

end

opaque

opaque は、定義に展開できない名前を宣言するコマンドです。具体的な実装を与えずに特定の型を持つ項が「とりあえず何かある」という表現ができます。

def との違い

def コマンドと異なり opaque コマンドを使用して宣言した名前は、#reduce コマンドで簡約できません。

-- def を使って定義
def greet : String := "hello world!"

-- reduce の結果と eval の結果が一致する

/- info: "hello world!" -/
#eval greet

/- info: "hello world!" -/
#reduce greet


-- opaque を使った定義
opaque opaque_greet : String := "hello world!"

-- 簡約できないので eval と一致しなくなる

/- info: "hello world!" -/
#eval opaque_greet

/- info: opaque_greet -/
#reduce opaque_greet

opaque で宣言された名前は partial で修飾された名前と同様に証明の中で簡約できなくなり、コンパイラを信頼しないとそれに関する証明ができなくなります。

-- 等しいものだという判定はできる
#eval opaque_greet == greet

example : opaque_greet = greet := by
  -- 等しいことの証明が rfl ではできない
  fail_if_success rfl

  -- greet は展開できるが
  dsimp [greet]

  -- opaque はできない。
  fail_if_success dsimp [opaque_greet]

  -- コンパイラを信頼することにすれば証明ができる
  native_decide

opaque で名前を宣言するとき、型だけを指定して値を指定しないということができます。このとき、その型が Inhabited 型クラスのインスタンスであることを暗黙のうちに使用します。

-- 値なしの宣言ができる
opaque some_string : String

-- 値は空文字列
/- info: "" -/
#eval some_string

-- 適当な構造体を用意する
structure Something where
  val : String

-- Inhabited インスタンスがないのでエラーになる
/-
error: failed to synthesize 'Inhabited' or 'Nonempty' instance for
  Something

If this type is defined using the 'structure' or 'inductive' command, you can try adding a 'deriving Nonempty' clause to it.
-/
opaque something : Something

variable との違い

opaque と同じく variable コマンドも「特定の型を持つ項がとりあえず何かある」という表現をするために使用されることがありますが、両者には重要な違いがあります。

たとえば、Lean の型システムにおいて「自分自身に適用することができる関数 f : (A → B) → C は存在しない」ということをコードで確かめたかったとします。このとき「何でもいいので」型 A, B, C と関数 f が存在すると仮定を置きたくなります。

ここで型の宣言に variable を使用すると上手くいきません。

variable {A B C : Type} [Inhabited C]

opaque f : (A → B) → C

-- エラーにならない!
#check f f

これは、2つの f のそれぞれの引数としての A, B, C が異なる値を持ちうるためです。型を opaque で宣言すると固定されるので、想定通りエラーになります。

opaque A : Type
opaque B : Type
opaque C : Type

-- 実装は与えないが C は Inhabited のインスタンスだと仮定
variable [Inhabited C]

opaque f : (A → B) → C

-- 想定通りエラーになる
#check_failure f f

open

open は名前空間を開くためのコマンドです。

名前空間 N の中にある定義 S を使いたいとき、通常はフルネームの N.S を使う必要がありますが、open N とすることで短い別名 S でアクセスできるようになります。

namespace Hoge

  def foo := "hello"

end Hoge

-- 名前空間の外からだと `foo` という短い名前が使えない
#check_failure foo

section
  -- 名前空間 `Hoge` をオープン
  open Hoge

  -- `open` することで `foo` という短い名前が使えるようになる
  #check foo
end

-- セクションが終わると再び短い名前は使えなくなる
#check_failure foo

入れ子になった名前空間

名前空間 N₁N₂ が入れ子になっているとき、その下にある定義に短い名前でアクセスするには、open N₁ N₂ とすればよいです。

namespace Foo

  namespace Bar

    def baz := "world"

  end Bar

end Foo

-- 入れ子の名前空間を開く
-- `Foo` の後に `Bar` を開く必要がある
open Foo Bar

#check baz

名前空間と公理

また名前空間 Foo 内に bar という公理(axiom で宣言されたもの)が存在する場合、Foo を開くと同時に公理 Foo.bar もインポートされます。

open Classical

-- 選択原理
#print choice

variable (P : Prop)

-- 選択原理が仮定された状態になっているため、
-- 任意の命題が決定可能になっている
#synth Decidable P

postfix

postfix は、後置記法を定義するコマンドです。

/-- 階乗 -/
def factorial : Nat → Nat
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

-- 後置記法を定義する
postfix:200 "!" => factorial

-- 定義した記法が使える
#guard 5! = 120

舞台裏

postfixnotation コマンドに展開されるマクロとして実装されています。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:200 arg✝:200 "!" => factorial arg✝ -/
#expand (postfix:200 "!" => factorial)

prefix

prefix は、前置記法を定義するためのコマンドです。

-- 前置記法を定義
-- 中身は Nat.succ
prefix:90 "⋄" => Nat.succ

-- 上で定義した記法が使える
#guard ⋄3 = 4

舞台裏

prefixnotation コマンドに展開されるマクロとして実装されています。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:90 "⋄" arg✝:90 => Nat.succ arg✝ -/
#expand (prefix:90 "⋄" => Nat.succ)

proof_wanted

proof_wanted はその名の通り、証明を公募するためのコマンドです。

sorry とよく似た機能を持つコマンドです。構文的には theorem に似ていますが、証明を書く必要がありません。証明を書かないで済むという点で sorry と同様です。

import Batteries.Util.ProofWanted

variable (n : Nat)

-- proof_wanted で証明を省略できる
proof_wanted result : n + 0 = n

-- sorry で同様のことができる
theorem another_result : n + 0 = n := by sorry

register_label_attr

register_label_attr コマンドを使用すると、ラベル属性を定義することができます。

Warning

このページの内容は ボタンから Lean 4 Web で実行することができません。

使用例

ラベル属性を作るシンプルな例を示しましょう。

まず、以下のような内容のファイルを作成します。仮に RegisterLabelAttr/Lib.lean というファイル名だとします。

import Lean

register_label_attr my_tag

これで、このファイルを import しているファイルの中で @[my_tag] タグを使用することができます。

import LeanByExample.Declarative.RegisterLabelAttr.Lib

@[my_tag]
def greet := "Hello, world!"

もちろんタグとしてだけでなく attribute コマンドによっても使用可能です。

def foo := 42

attribute [my_tag] foo

タグに対する基本的な操作である、「タグが付与されている宣言をすべて集める」という操作は Lean.labelled 関数で実行することができます。

/- info: #[`greet, `foo] -/
#eval Lean.labelled `my_tag

さらに応用として、「[my_tag] 属性が付与された定理を順に apply してゴールを閉じるタクティク」を自作する例も挙げておきます。

open Lean Elab Tactic

elab "apply_my_tagged" : tactic => do
  let taggedDecls ← labelled `my_tag
  if taggedDecls.isEmpty then
    throwError "`[my_tag]`が付与された定理はありません。"
  for decl in taggedDecls do
    let declStx : TSyntax `term := mkIdent decl
    try
      evalTactic <| ← `(tactic| apply $declStx)

      -- 成功したら終了する
      return ()
    catch _ =>
      -- 失敗したら単に次の候補に進む
      pure ()
  throwError "ゴールを閉じることができませんでした。"

@[my_tag]
theorem testThm (P Q : Prop) : P → Q → P ∧ Q := by
  simp_all

example (P Q : Prop) : P → Q → P ∧ Q := by
  apply_my_tagged

register_option

register_option は、オプションを自作するためのコマンドです。自作したオプションは set_option から設定できるようになります。

Warning

このページの内容は ボタンから Lean 4 Web で実行することができません。

使用例

たとえば、RegisterOption/Lib.lean というファイルを作成して、以下のように記述したとします。

import Lean

open Lean

register_option greeting : String := {
  defValue := "Hello World"
  descr := "just a friendly greeting"
}

このファイルを読み込めば、greeting というオプションを使用することができます。たとえば、以下のように使用することができます。

import LeanByExample.Declarative.RegisterOption.Lib

open Lean in

elab "#greet" : command => do
  let opts ← getOptions
  logInfo s!"{opts.get greeting.name greeting.defValue}"

-- デフォルト値が表示される
/- info: Hello World -/
#greet

-- オプションを設定すると
set_option greeting "Hi there"

-- 表示も変更される
/- info: Hi there -/
#greet

設定したオプションを活用する例は、Linter のページを参照のこと。

register_simp_attr

register_simp_attr は、simp タクティクで使うルールセットとタグを登録することができるコマンドで、simp ラッパとして新しいタクティクを作るのに使うことができます。

Warning

このページの内容は ボタンから Lean 4 Web で実行することができません。

使用例

以下では、型クラスによって導入される記法を定義に展開するタクティクを自作する例を示します。

このコマンドは属性タグを作るので、まず別のファイルを作って以下のように書き込みます。ファイル名は何でも良いのですが、仮に RegisterSimpAttr/Lib.lean であるとします。

import Lean

/-- `+`や`≤`など、演算子や記法を定義に展開するためのルールを登録する -/
register_simp_attr notation_simp

その後、このファイルをインポートすれば @[notation_simp] タグとルールセットが利用可能になります。以下のように、macro_rules コマンドを使用すれば simp ラッパを作成することができます。

import LeanByExample.Declarative.RegisterSimpAttr.Lib -- インポートで有効になる

section
  open Lean Meta Parser.Tactic Elab.Tactic

  /-- `+`や`≤`など、演算子や記法を定義に展開する -/
  syntax (name := notation_simp_stx) "notation_simp" (simpArgs)? (location)? : tactic

  macro_rules
  | `(tactic| notation_simp $[[$simpArgs,*]]? $[at $location]?) =>
    let args := simpArgs.map (·.getElems) |>.getD #[]
    `(tactic| simp only [notation_simp, $args,*] $[at $location]?)
end

これで、たとえば以下のように使用することができます。

example {n m : Nat} (h : n < m) : n + 1 ≤ m := by
  -- notation_simp を使わない場合の方法。
  -- これを `(· < ·)` を展開したいときに毎回書く。
  dsimp [(· < ·), Nat.lt] at h
  assumption

@[notation_simp]
theorem Nat.lt_def (n m : Nat) : n < m ↔ (n + 1) ≤ m := by rfl

example {n m : Nat} (h : n < m) : n + 1 ≤ m := by
  -- これだけで展開が行えるようになった!
  notation_simp at h
  assumption

notation_simp? タクティクも自作するには、次のようにすればできます。

section
  open Lean Meta Parser.Tactic Elab.Tactic

  @[inherit_doc notation_simp_stx]
  syntax (name := notation_simp?) "notation_simp?" (simpArgs)? (location)? : tactic

  macro_rules
  | `(tactic| notation_simp? $[[$simpArgs,*]]? $[at $location]?) =>
    let args := simpArgs.map (·.getElems) |>.getD #[]
  `(tactic| simp? only [notation_simp, $args,*] $[at $location]?)
end

/-
info: Try this:
  [apply] simp only [Nat.lt_def] at h
-/
example {n m : Nat} (h : n < m) : n + 1 ≤ m := by
  notation_simp? at h
  assumption

section

section は、有効範囲を制限するためのコマンドです。以下に挙げるような効果があります。

  • variable で定義された引数の有効範囲を制限する。
  • open で開いた名前空間の有効範囲を制限する。
  • set_option で設定したオプションの有効範囲を制限する。
  • local で修飾されたコマンドの有効範囲を制限する。

section コマンドで開いたセクションは end で閉じることができますが、end は省略することもできます。end を省略した場合はそのファイルの終わりまでが有効範囲となります。

なお以下の例ではセクションの中をインデントしていますが、インデントするのは一般的なコード整形ルールではありません。

以下は variable の有効範囲を区切る例です。

section
  variable (a : Type)

  -- 宣言したので有効
  #check a
end

-- `section` の外に出ると無効になる
#check_failure a

次は open の有効範囲を区切る例です。

section
  open Classical

  -- open されているのでアクセスできる
  #check choice
end

-- `end` 以降は無効になる
#check_failure choice

次は set_option の有効範囲を区切る例です。

section
  set_option autoImplicit true

  -- α が暗黙引数になる
  def nilList : List α := []
end

-- `end` 以降は無効になり、α が未定義だというエラーになる
/-
error: Unknown identifier `α`

Note: It is not possible to treat `α` as an implicitly bound variable here because the `autoImplicit` option is set to `false`.
-/
def nilList' : List α := []

次は local で修飾されたコマンドの有効範囲を区切る例です。

section
  -- Nat の inhabited インスタンスを上書きする
  local instance : Inhabited Nat := ⟨1⟩

  -- 上記のインスタンスが有効
  #guard (default : Nat) = 1
end

-- セクションが終わると上記のインスタンスが無効になる
#guard (default : Nat) = 0

また、セクションに名前を付けることもできます。名前を付けた場合は、閉じるときにも名前を指定する必要があります。

section hoge
  variable (a : Type)

  #check a
end hoge

セクションは入れ子にすることもできます。

section parent
  variable (a : Type)

  section child
    variable (b : Type)

    -- 親セクションで定義された引数は子セクション内でも有効
    #check a
  end child

  -- child セクションの外なので無効
  #check_failure b
end parent

-- parent セクションの外なので無効
#check_failure a

set_option

set_option は、オプションを変更・設定するために使われるコマンドです。

set_option option_name value という構文で使用すれば option_name という名前のオプションの値を value に設定することができます。使用できる option_name#help コマンドまたは Mathlib4 Help で確認できます。

有効範囲はそのセクションの内部またはそのファイルの最後までです。

/- info: ¬1 + 1 = 2 : Prop -/
#check ¬ (1 + 1 = 2)

section

  -- 定義された記法を使わないように設定する
  set_option pp.notation false

  -- 表示される内容が変わった!
  /- info: Not (Eq (HAdd.hAdd 1 1) 2) : Prop -/
  #check ¬ (1 + 1 = 2)

end

-- `section` を抜けると元に戻る
/- info: ¬1 + 1 = 2 : Prop -/
#check ¬ (1 + 1 = 2)

simproc

simproc コマンドは、simproc を宣言するコマンドの一つです。

simproc は、ある式 expr にマッチする部分を見つけたときに、より単純な式 result を動的に計算し、expr = result の証明も同時に構成するような、simp タクティクから呼び出される書き換え規則のことです。

使用例

たとえば、simp タクティクには if 式に対する単純化機能が標準で備わっていますが、これを行う simproc を自前で定義してみましょう。

import Lean
import Qq

example : (if 1 = 1 then 2 else 3) = 2 := by
  -- if式の単純化を行うことができる
  simp

まず前段階として、標準の if と混ざらないように if 式そのものを自前で構成します。

section

  variable {α : Type}

  /-- 標準の`ite`(if式の内部実装)を真似て自作したもの -/
  def myIte (cond : Prop) [h : Decidable cond] (t e : α) : α :=
    match decide cond with
    | true => t
    | false => e

  @[inherit_doc myIte]
  notation "mif " cond " then " t " else " e => myIte cond t e

  -- 動作テスト
  #guard (mif 1 < 2 then 3 else 4) == 3
  #guard (mif 1 = 2 then 3 else 4) == 4

  variable (cond : Prop) [h : Decidable cond]

  /-- 条件式が真なら、mif式は`then`部分に等しい -/
  theorem myIte_cond_eq_true (t e : α) (i : cond = True) : myIte cond t e = t := by
    dsimp [myIte]
    have : decide cond = true := by simp_all
    rw [this]

  /-- 条件式が偽なら、mif式は`else`部分に等しい -/
  theorem myIte_cond_eq_false (t e : α) (i : cond = False) : myIte cond t e = e := by
    dsimp [myIte]
    have : decide cond = false := by simp_all
    rw [this]

end

続いて、simproc の本体を構成します。単に単純化の結果を返せばよいわけではなく、元の式との等価性を証明する必要があることに気を付けてください。1

open Lean Meta Simp Qq

/-- `mif`式を単純化するsimproc。 -/
simproc ↓reduceMyIte (myIte _ _ _) := .ofQ fun u α expr => do
  -- パターンマッチ。`myIte _ _ _`の形であることを確認する。
  -- そうでなければ即終了する。
  match u, α, expr with
  | 1, _, ~q(@myIte _ $cond $h $t $e) =>
    -- 条件式の部分を単純化する
    have simp_cond : Result := ← simp cond -- 右辺が`Qq`を使用していないので`have`を使う
    have simp_cond_prop : Q(Prop) := simp_cond.expr
    trace[debug] "条件式の部分が {simp_cond_prop} に単純化されました。"

    -- 単純化された結果`cond`が`True`または`False`として評価されていなければ、
    -- その時点で終了する
    if !simp_cond_prop.isTrue && !simp_cond_prop.isFalse then
      return .continue

    -- 単純化なので`cond = simp_cond_prop`という命題が成り立つ。
    -- その証明を取得して名前を付けておく。
    have cond_eq_simp_cond : Q($cond = $simp_cond_prop) := ← simp_cond.getProof
    trace[debug] "条件式に対して {← inferType cond_eq_simp_cond} という単純化が行われました。"

    -- 条件式が真と評価された場合
    if simp_cond_prop.isTrue then
      -- このとき`simp_cond_prop`は`True`であることを`Qq`に教えておく
      have : $simp_cond_prop =Q True := ⟨⟩

      -- このとき`myIte cond t e = t`が成り立つので、結果の`Expr`としては`t`を返すべき。
      have result_expr := t
      trace[debug] "{expr} を単純化した結果は {result_expr} であるべきです。"

      -- 結果の`Expr`が元の`Expr`と同じであることの証明も必要。
      -- つまり`myIte cond t e = t`の証明が必要。
      let target : Q(Prop) := q(myIte $cond $t $e = $t) -- 証明したい命題
      trace[debug] "単純化のために {target} を証明する必要があります。"

      -- `target`の証明を構成する。
      -- これは定理`myIte_cond_eq_true`を使って構成できる。
      let target_proof : Q($target) := q(myIte_cond_eq_true $cond $t $e $cond_eq_simp_cond)
      trace[debug] "{← inferType target_proof} の証明を構成しました。"
      return .visit { expr := result_expr, proof? := target_proof }

    -- 条件式が偽と評価された場合(説明は省略)
    if simp_cond_prop.isFalse then
      have : $simp_cond_prop =Q False := ⟨⟩
      have result_expr := e
      let target_proof := q(myIte_cond_eq_false $cond $t $e $cond_eq_simp_cond)
      return .visit { expr := result_expr, proof? := target_proof }

    return .continue

  | _, _, _ =>
    return .continue

-- これで`[debug]`とマークされたトレースメッセージが表示される
set_option trace.debug true

/-
trace: [debug] 条件式の部分が True に単純化されました。
[debug] 条件式に対して (1 < 1 + 1) = True という単純化が行われました。
[debug] mif 1 < 2 then 3 else 4 を単純化した結果は 3 であるべきです。
[debug] 単純化のために (mif 1 < 2 then 3 else 4) = 3 を証明する必要があります。
[debug] (mif 1 < 2 then 3 else 4) = 3 の証明を構成しました。
-/
example : (mif 1 < 2 then 3 else 4) = 3 := by
  simp

example : (mif 1 = 2 then 3 else 4) = 4 := by
  simp

  1. こちらのコードを書くにあたり、Zulip 上で Eric Wieser 氏に多くの助言をいただきました。ありがとうございました。

structure

structure は構造体を定義するためのコマンドです。構造体とは、大雑把に説明すれば複数のデータをまとめて一つの型として扱えるようにしたものです。

/-- 2次元空間の点 -/
structure Point (α : Type) : Type where
  x : α
  y : α

構造体を定義すると、自動的に作られる関数があります。代表的なものは以下の2つです。

  • フィールドにアクセスするための関数。
  • コンストラクタ。

フィールドへのアクセサはフィールドの名前で、コンストラクタは mk という名前で作られます。

-- アクセサ
#check (Point.x : {α : Type} → (Point α) → α)
#check (Point.y : {α : Type} → (Point α) → α)

def origin : Point Int := { x := 0, y := 0 }

/-- アクセサを使用する -/
example : Point.x origin = 0 := by rfl

-- コンストラクタ
#check (Point.mk : {α : Type} → α → α → Point α)

コンストラクタに mk 以外の名前を使いたい場合、:: を使って次のようにします。

structure MyProd (α : Type) (β : Type) where
  gen ::
  fst : α
  snd : β

-- コンストラクタの名前が gen になっている
#check MyProd.gen

項を定義する様々な構文

構造体の項を定義したい場合、複数の方法があります。構造体インスタンス記法(structure instance notation) が好まれますが、フィールド名が明らかな状況であれば無名コンストラクタを使用することもあります。

-- コンストラクタを使う
def sample0 : Point Int := Point.mk 1 2

-- 構造体インスタンス記法を使う
def sample1 : Point Int := { x := 1, y := 2 }

-- 無名コンストラクタを使う
def sample2 : Point Int := ⟨1, 2⟩

-- `where` を使う
def sample : Point Int where
  x := 1
  y := 2

値の部分的変更

既存の構造体のフィールドの一部だけを変更した新しい構造体の項を作ることができます。

variable {α : Type} [Add α]

/-- `p : Point` の x 座標を 2 倍にする -/
def Point.doubleX (p : Point α) : Point α :=
  { p with x := p.x + p.x}

#check Point.doubleX origin

継承

既存の構造体に新しいフィールドを追加した新しい構造体を定義することができます。多重継承(複数の親を持つ構造体を作ること)も行うことができます。

structure Point3D (α : Type) extends Point α where
  z : α

structure RGBValue where
  red : Nat
  green : Nat
  blue : Nat

structure ColorPoint3D (α : Type) extends Point α, RGBValue where
  z : α

舞台裏

構造体は、コンストラクタが一つしかない帰納型であると見なすことができます。

帰納型による模倣

structure コマンドを使って定義した上記の Point を、inductive コマンドで模倣してみましょう。まず mk という単一のコンストラクタだけを持つ帰納型を定義します。このコンストラクタの各引数がフィールドに相当します。

inductive Point' (α : Type) : Type where
  | mk (x y : α)

アクセサ関数が自動的に作られませんが、自分で作ることができます。

-- アクセサ関数が利用できない
#check_failure Point'.x

/-- 自前で定義した `Point'` へのフィールドへのアクセサ -/
def Point'.x {α : Type} (p : Point' α) : α :=
  match p with
  | Point'.mk x _ => x

-- アクセサ関数が使えるようになった
#eval
  let p := Point'.mk 1 2
  p.x

用途

この structure コマンドの代わりに inductive コマンドを用いる方法は、定義しようとしている構造体が命題をパラメータに持っているときに必要になります。Prop の Large Elimination が許可されていないことにより、この場合はアクセサ関数が生成できないので structure コマンドが使用できず、エラーになります。

-- `w` はデータなので、アクセサ関数が生成できなくてエラーになる
/-
error: failed to generate projection `MyExists.w` for the 'Prop'-valued type `MyExists`, field must be a proof, but it has type
  α
-/
structure MyExists.{v} {α : Sort v} (p : α → Prop) : Prop where
  intro ::

  w : α
  h : p w

syntax

syntax コマンドは新しい構文を定義することができます。

import Lean

open Lean Parser

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- 最初は `#greet` などというコマンドは定義されていないので
-- そもそも Lean の合法な構文として認められない。
/- error: <input>:1:0: expected command -/
#eval parse `command "#greet"

-- `#greet` というコマンドのための構文を定義
syntax "#greet" : command

-- まだエラーになるが、少なくとも `#greet` というコマンドが Lean に認識されるようにはなった。
-- エラーメッセージは、`#greet` コマンドの解釈方法がないと言っている。
/- error: elaboration function for `«command#greet»` has not been implemented -/
#greet

Lean に構文を認識させるだけでなく、解釈させるには macro_rules などの別のコマンドが必要です。

-- `#greet` コマンドの解釈方法を定める
macro_rules
  | `(command| #greet) => `(#eval "Hello, Lean!")

/- info: "Hello, Lean!" -/
#greet

パース優先順位

syntax コマンドは Lean に新しい構文解析ルールを追加しますが、導入した構文が意図通りに解釈されないことがあります。

section

  /-- `a = b as T` という構文を定義 -/
  local syntax term " = " term " as " term : term

  /-- `a = b as T` という構文を、型 `T` 上で `a = b` が成り立つと解釈させる -/
  local macro_rules
    | `(term| $a = $b as $c) => `(@Eq (α := $c) $a $b)

  -- メタ変数の番号を表示しない
  set_option pp.mvars false

  -- `1 + (1 = 2)` だと認識されてしまっている
  /- info: 1 + (1 = 2) : ?_ -/
  #check (1 + 1 = 2 as Nat)

end

パース優先順位(parsing precedence) を設定することで、どの構文から順に解釈されるかを指定することができ、問題を修正できることがあります。このあたりは notation コマンドと同様です。

section

  -- 十分低いパース優先順位を設定する
  local syntax:10 term:10 " = " term:10 " as " term:10 : term

  local macro_rules
    | `(term| $a = $b as $c) => `(@Eq (α := $c) $a $b)

  -- 意図通りに構文解析が通るようになる
  #guard (1 + 1 = 2 as Nat)

  #guard (3 - 5 + 4 = 2 as Int)

  -- Nat だと 3 - 5 = 0 となるので結果が変わる
  #guard (3 - 5 + 4 = 4 as Nat)
end

name 構文

(name := ...) という構文により、名前を付けることができます。名前を付けると、その名前で Lean.ParserDescr の項が生成されます。実際、syntax コマンドは Lean のパーサーを拡張するコマンドであるといえます。

-- 最初は存在しない
#check_failure (hogeCmd : ParserDescr)

-- `#hoge` というコマンドを定義する
-- `name` 構文で名前を付けることができる
syntax (name := hogeCmd) "#hoge" : command

-- 構文に対して付けた名前で、ParserDescr 型の項が生成されている
#check (hogeCmd : ParserDescr)

Lean のパーサーを流用する

syntax コマンドや declare_syntax_cat コマンドで生成された Lean のパーサーを利用して、String を引数に取るようなパーサーを定義することができます。1

具体例を挙げましょう。まず Syntax.lean というファイルを作成して次のように書きます。

-- # Syntax.lean ファイル

/-- 2項演算の集合 -/
inductive Op where
  /-- 加法 -/
  | add
  /-- 乗法 -/
  | mul
deriving DecidableEq, Repr

/-- 数式 -/
inductive Arith where
  /-- 数値リテラル -/
  | val (n : Nat) : Arith
  /-- 演算子の適用 -/
  | app (op : Op) (lhs rhs : Arith) : Arith
deriving DecidableEq, Inhabited, Repr

section arith_syntax

  /-- `Arith` のための構文カテゴリ -/
  declare_syntax_cat arith

  /-- `Arith` を見やすく定義するための構文 -/
  syntax "[arith| " arith "]" : term

  -- 数値リテラルは数式
  syntax:max num : arith

  -- 数式を `+` または `*` で結合したものは数式
  -- `+` と `*` のパース優先順位を指定しておく
  syntax:30 arith:30 " + " arith:31 : arith
  syntax:35 arith:35 " * " arith:36 : arith

  -- 数式を括弧でくくったものは数式
  syntax:max "(" arith ")" : arith

end arith_syntax

macro_rules
  | `([arith| $n:num ]) => `(Arith.val $n)
  | `([arith| $l:arith + $r:arith]) => `(Arith.app Op.add [arith| $l] [arith| $r])
  | `([arith| $l:arith * $r:arith]) => `(Arith.app Op.mul [arith| $l] [arith| $r])
  | `([arith| ($e:arith) ]) => `([arith| $e ])

次に、Environment.lean というファイルを作成して次のように書きます。

-- # Environment.lean ファイル

import LeanByExample.Declarative.Syntax.Syntax
import Lean

open Lean

/-- import先のファイル名 -/
private def fileName : Name := `LeanByExample.Declarative.Syntax.Syntax

/-- `Arith`のための構文とマクロの定義が終わった直後の状態の`Environment` -/
initialize env_of_arith_stx : Environment ← do
  -- コンパイル時にLeanにoleanファイルを見つけさせるために必要
  initSearchPath (← findSysroot)

  -- importModulesの前で初期化を許可する必要がある
  unsafe Lean.enableInitializersExecution

  importModules #[{module := fileName}] {} (loadExts := true)

そうすると、以下のように Lean のパーサーとマクロ展開ルールを利用して String 上のパーサーを定義することができます。

-- Parser.lean ファイル

import LeanByExample.Declarative.Syntax.Environment

open Lean Elab Term Command

/-- Leanのマクロ展開ルールを使って、`input : String` から `Arith` の項を生成する -/
def TermElabM.parseArith (input : String) : TermElabM (Except String Arith) := do
  let stx := Parser.runParserCategory env_of_arith_stx `term s!"[arith| {input}]"
  match stx with
  | .error err => return Except.error err
  | .ok stx =>
    let result ← unsafe evalTerm Arith (.const ``Arith []) stx
    return Except.ok result

/-- `TermElabM` に包まれた値をむりやり取り出す -/
unsafe def TermElabM.unsafeRun {α : Type} [Inhabited α] (env : Environment) (m : TermElabM α) : α :=
  let core := liftCommandElabM <| liftTermElabM m
  let ctx : ContextInfo := { env := env, fileMap := ⟨"", #[]⟩, ngen := { } }
  let io := ContextInfo.runCoreM ctx core
  match unsafeIO io with
  | .error e => panic! s!"{e}"
  | .ok a => a

/-- 文字列をパースして`Arith`の項を生成する -/
def parseArith (s : String) : Except String Arith := unsafe
  TermElabM.unsafeRun env_of_arith_stx (TermElabM.parseArith s)


-- パーサーのテストのためのコード
section ParserTest

open Arith

instance : ToString Op where
  toString := fun op =>
    match op with
    | Op.add => "+"
    | Op.mul => "*"

/-- `Arith`を文字列化する。ただし、全体を括弧で囲う。-/
protected def Arith.toStringAux (arith : Arith) : String :=
  match arith with
  | Arith.val n => toString n
  | Arith.app op lhs rhs =>
    "(" ++ Arith.toStringAux lhs ++ s!" {op} " ++ Arith.toStringAux rhs ++ ")"

protected def Arith.toString (arith : Arith) : String :=
  match arith with
  | Arith.val n => toString n
  | Arith.app op lhs rhs =>
    Arith.toStringAux lhs ++ s!" {op} " ++ Arith.toStringAux rhs

instance : ToString Arith where
  toString := Arith.toString

private def testParseArith (input : String) (expected : Arith) : IO Unit := do
  let .ok actual := parseArith input
    | throw <| .userError s!"{input} のパースに失敗しました"

  if expected != actual then
    throw <| .userError s!"{input} のパース結果が期待値と一致しません。期待値: {expected}, 実際の値: {actual}"
  IO.println s!"✅ テスト成功!"

#eval testParseArith "1 + 2" (app Op.add (val 1) (val 2))

#eval testParseArith "3 * (4 + 5)" (app Op.mul (val 3) (app Op.add (val 4) (val 5)))

end ParserTest

  1. このコード例を用意するにあたって、伊勢村哲司さんおよび Adam Topaz さんにご助力をいただきました。ありがとうございました。

theorem

theorem は名前付きで命題を証明するためのコマンドです。より正確には、theorem は証明項を定義するためのコマンドだといえます。

/-- 自然数に右から0を足しても変わらない -/
theorem add_zero {n : Nat} : n + 0 = n := by simp

-- `add_zero` という項の型が、命題の内容になっている
#check (add_zero : ∀ {n : Nat}, n + 0 = n)

def との違い

theorem コマンドは特定の型を持つ項を定義するという意味で、def と同じです。実際、def を使っても証明項を定義することは可能です。しかし theorem を使っても関数などを定義することはできません。theorem で宣言できる項は命題のみです。

def add_zero' {n : Nat} : n + 0 = n := by simp

/-
error: type of theorem `frac` is not a proposition
  Nat → Nat
-/
theorem frac (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * frac n

universe

universe は、宇宙レベルを表す変数を宣言するコマンドです。universe u と宣言すると、そのスコープ内で u を宇宙変数として使えるようになります。

universe u

#check (Type u)

ここで 宇宙(universe) とは、項が再び型であるような型のことです。たとえば NatBool は型ですが、Lean ではそのような型も項として扱われ、Type という型を持ちます。

#check (Nat : Type)
#check (Bool : Type)

Type 自身も型ですが、Type : Type ではなく Type : Type 1 です。Type 1 の型は Type 2 です。以下、Type 2 の型は Type 3 等と無限に続きます。

#check (Type : Type 1)
#check (Type 1 : Type 2)
#check (Type 2 : Type 3)

一般に Type u の型は Type (u + 1) になります。

#check (Type u : Type (u + 1))

宇宙多相

宇宙変数 u を明示的に宣言することによって、定義を 宇宙多相(universe polymorphic) にすることができます。複数の型宇宙で同じように動作する定義が得られるということです。Type u を使えば Type 上で宇宙多相にできますし、Sort u を使えば Prop も含めることができます。

例: Size 型クラス

たとえば、size という関数を提供する以下のような型クラスを考えてみます。1

class Size (A : Type u) where
  size : A → Nat

export Size (size)

配列やリストなどのコレクションの大きさを取得する統一的な手段を提供する型クラスというイメージです。

instance : Size (List A) where
  size l := l.length

instance : Size (Array A) where
  size a := a.size

#guard size [1, 2, 3] = 3
#guard size #["a", "b", "c"] = 3

この size 関数を型に対しても適用できるようにしたいと思ったとします。size Bool = 2size Unit = 1 といった具合にです。

この場合最も直接的な実装は Type に対して Size のインスタンスを定義することですが、任意の A : Type に対してそのサイズを計算することは不可能であるため、この方法では実装できません。

instance : Size (Type u) where
  size A := sorry -- 実装を与える方法がない

Size の実装を与える型を選択できるようにしなければいけないので、size 関数の引数となる型を Size の定義に含めるように書き直します。

/-- 書き直した Size 型クラス -/
class Size {A : Type u} (a : A) where
  size : Nat

export Size (size)

instance (l : List A) : Size l where
  size := l.length

instance (a : Array A) : Size a where
  size := a.size

#guard size [1, 2, 3] = 3
#guard size #["a", "b", "c"] = 3

このようにすると、size 関数を型に対しても定義できるようになります。

instance : Size Bool where
  size := 2

instance : Size Unit where
  size := 1

#guard size Bool = 2
#guard size Unit = 1

上記の Size 型クラスの定義では、宇宙多相であることが必要です。実際に推論された宇宙レベルを表示してみると、異なる宇宙レベルが使用されていることがわかります。.{u} と表示されている部分が宇宙レベルを表しています。

-- 推論された宇宙レベルを表示する
set_option pp.universes true

/- info: size.{1} Bool : Nat -/
#check size Bool

/- info: size.{0} (List.cons.{0} 1 List.nil.{0}) : Nat -/
#check size [1]

例: *.rec

Lean の標準ライブラリにある例でいうなら、帰納型を定義したときに自動生成される *.rec という関数も宇宙多相です。定義を見ると、.{u} と書かれており宇宙多相であることがわかります。

/-
info: Nat.rec.{u} {motive : Nat → Sort u} (zero : motive Nat.zero) (succ : (n : Nat) → motive n → motive n.succ) (t : Nat) :
  motive t
-/
#check Nat.rec

単に宇宙多相であるだけでなく、この関数は宇宙多相であることが必要な例になっています。

以下のように、u = 0 とすれば Sort 0 = Prop なので証明に使うことができます。

theorem Nat.add_zero' (n : Nat) : n + 0 = n :=
  Nat.rec
    (motive := fun (n : Nat) => n + 0 = n)
    (by simp)
    (fun n ih => by simp)
    n

そして、u = 1 とすれば Sort 1 = Type なのでデータを扱う普通の関数が定義できます。

/-- 階乗関数 -/
def Nat.factorial (n : Nat) : Nat :=
  Nat.rec
    (motive := fun (_ : Nat) => Nat)
    1
    (fun n ih => (n + 1) * ih)
    n

#guard Nat.factorial 5 = 120

なお帰納法と再帰が両方とも *.rec という同じ関数で実現できるのは偶然ではなく、カリー・ハワード同型対応によって「帰納法は再帰」と言えるからです。


  1. この Size 型クラスを実装して宇宙多相にする例は、Scientific Computing in Leanの TypeClasses as Interfaces and Function Overloading という章で紹介されている例をそのまま使用しています。

variable

variable は、定理や関数の引数を宣言するためのコマンドです。

たとえば以下の関数と命題に対して、引数の α : Typel : List α は共通です。

/-- リストが空であるか判定する -/
def isNil {α : Type} (l : List α) : Bool :=
  match l with
  | [] => true
  | _ => false

theorem nng_list_length {α : Type} (l : List α) : l.length ≥ 0 := by simp

variable コマンドを利用すると、共通の引数を宣言してまとめておくことができます。

variable {α : Type} (l : List α)

def isNil : Bool :=
  match l with
  | [] => true
  | _ => false

theorem nng_list_length : l.length ≥ 0 := by simp

theorem と def での取り込み基準の違い

variable コマンドの挙動は theoremdef で異なります。

theorem コマンドに対しては、定理の型(つまり主張)に現れる変数だけを自動で引数にします。 一方で、def コマンドに対しては、本体に現れる変数も自動で引数にします。

variable (n : Nat)

-- 定理の主張には `m` しか現れていないので、`n` は引数として取り込まれない
/-
error: Unknown identifier `n`
-/
theorem foo (m : Nat) : m = m :=
  have : n = n := by rfl
  rfl

-- 引数は1つだけ
#check (foo : ∀ m : Nat, m = m)

-- `n` は証明の中にしか現れないが、引数として取り込まれる
def foo' (m : Nat) : m = m :=
  have : n = n := by rfl
  rfl

-- その証拠に `foo'` には引数が2つある
#check (foo' : ∀ (_n m : Nat), m = m)

明示的に引数に含めるには、include コマンドを使用します。

variable (n : Nat)

include n

theorem foo_included (m : Nat) : m = m :=
  have : n = n := by rfl
  rfl

-- `n` が引数として含まれた結果
-- 引数が2つになっている
#check (foo_included : ∀ (_n m : Nat), m = m)

再帰と variable

variable コマンドで宣言された引数は、再帰呼び出しの中でも同じ値が使用されます。 したがって、再帰関数の再帰呼び出しで変化する引数を variable で共通化することはできません。

variable {α : Type} (l : List α)

def List.myLength : Nat :=
  match l with
  | [] => 0
  | _ :: xs => by
    -- `List.myLength` の型が `List α → Nat` ではなくて、
    -- `Nat` になってしまっている。
    -- 引数の `List α` の部分には固定された値が入ってしまっているため
    guard_hyp List.myLength : Nat

    sorry

修飾子

本書では、必ず前後にトップレベルコマンドが続いて、そのコマンドに影響を与える構文要素のことを便宜的に修飾子と呼んでいます。

ドキュメントコメント

Lean では、/---/ で囲まれた部分がドキュメントコメントとして扱われます。

ドキュメントコメントは、コードをわかりやすくするのに役立ちます。直後に来る関数や定義を修飾して、そこに書かれた内容がマウスオーバーしたときに表示されます。

/-- 階乗関数 -/
def Nat.factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * Nat.factorial n

なお、「コメント」を「コードの動作に影響を与えない人間向けの説明文」と定義するのであれば、Lean のドキュメントコメントはコメントではありません。誤った場所にドキュメントコメントを配置するとエラーになります。

import Lean

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- ドキュメントコメントで `namespace` を修飾しようとすると構文エラーになる
/-
error: <input>:1:21: expected '#guard_msgs', 'abbrev', 'add_decl_doc', 'axiom', 'binder_predicate', 'builtin_cbv_simproc', 'builtin_cbv_simproc_decl', 'builtin_dsimproc', 'builtin_dsimproc_decl', 'builtin_grind_propagator', 'builtin_initialize', 'builtin_simproc', 'builtin_simproc_decl', 'cbv_simproc', 'cbv_simproc_decl', 'class', 'coinductive', 'declare_command_config_elab', 'declare_command_config_elab_legacy', 'declare_config_elab', 'declare_config_elab_legacy', 'declare_core_config_elab', 'declare_simp_like_tactic', 'declare_syntax_cat', 'declare_term_config_elab', 'def', 'def_eval_config_item', 'dsimproc', 'dsimproc_decl', 'elab', 'elab_rules', 'example', 'grind_propagator', 'inductive', 'infix', 'infixl', 'infixr', 'initialize', 'instance', 'macro', 'macro_rules', 'notation', 'opaque', 'postfix', 'prefix', 'recommended_spelling', 'register_builtin_option', 'register_error_explanation', 'register_grind_attr', 'register_label_attr', 'register_linter_set', 'register_option', 'register_simp_attr', 'register_sym_simp_attr', 'register_tactic_tag', 'register_try?_tactic', 'simproc', 'simproc_decl', 'structure', 'syntax', 'tactic_extension', 'theorem' or 'unif_hint'
-/
#eval parse `command "/-- これはドキュメントコメント -/ namespace Foo"

ドキュメントコメントを扱う例

ドキュメントコメントは Lean に無視されるわけではなく、他の構文要素と同様にパースされます。したがって特にコードの中で扱うことができます。

たとえば、ドキュメントコメントを取得して内容を表示するコマンドを書くことができます。

import Lean.Elab.Command

open Lean Elab Command in

/-- ドキュメントコメントを取得して表示するコマンド -/
elab "#doc " x:ident : command => do
  let name := x.getId
  if let some s ← findDocString? (← getEnv) name then
    logInfo m!"{s}"

/-
info: Linked lists: ordered lists, in which each element has a reference to the next element.

Most operations on linked lists take time proportional to the length of the list, because each
element must be traversed to find the next element.

`List α` is isomorphic to `Array α`, but they are useful for different things:
* `List α` is easier for reasoning, and `Array α` is modeled as a wrapper around `List α`.
* `List α` works well as a persistent data structure, when many copies of the tail are shared. When
  the value is not shared, `Array α` will have better performance because it can do destructive
  updates.
-/
#doc List

補足:コメントはパースされている

なお、ドキュメントコメントに限らず、Lean のコメントはパーサに無視されません。ただ実行内容を持たず、コードの動作に影響を与えないだけです。

import Lean.Elab.Command

open Lean Elab Command Parser

/-
info: def foo :
    -- ここにコメント
    /- ここにもコメント -/
    True :=
  trivial
-/
run_cmd liftTermElabM do
  let s := "def foo : \n\
    -- ここにコメント\n\
    /- ここにもコメント -/\n\
    True := trivial"
  let cmd : Command := ⟨← ofExcept <| runParserCategory (← getEnv) `command s⟩
  logInfo (← PrettyPrinter.ppCommand cmd)

decreasing_by

decreasing_by は、再帰関数などの停止性(計算が無限に続くことがなく、値が一意に決まること)を示し、Lean に定義を受け入れさせるために使われます。

使用例

たとえば、以下の関数はそのままでは Lean が停止性を示せないためエラーになってしまいます。

-- エラーになってしまう
/-
error: fail to show termination
-/
def Nat.toListNat (n : Nat) : List Nat :=
  if n == 0 then
    []
  else
    Nat.toListNat (n / 10) ++ [(n % 10)]

decreasing_by に続けて停止することの証明を与えれば Lean に受け入れられるようになります。再帰のたびに(無限には減少できない)引数が真に減少することを示せば十分です。

def Nat.toListNat (n : Nat) : List Nat :=
  if n == 0 then
    []
  else
    Nat.toListNat (n / 10) ++ [(n % 10)]
decreasing_by
  -- `n ≠ 0 → n / 10 < n` を示す
  guard_target =ₛ n / 10 < n

  grind

-- 動作テスト
#guard Nat.toListNat 1234 = [1, 2, 3, 4]

あるいは、decreasing_by を使用しなくても、関数内で have などを使って停止性を証明しておいても良いです。

def Nat.toListNat (n : Nat) : List Nat :=
  if h : n == 0 then
    []
  else
    have : n / 10 < n := by
      grind
    Nat.toListNat (n / 10) ++ [(n % 10)]

部分関数を禁止するのはなぜか?

そもそもなぜ Lean では関数の停止性を示す必要があるのでしょうか?それは、Lean の論理体系の健全性を保つためです。1

カリー・ハワード同型対応によれば「帰納法は再帰」なので、停止しない再帰を許すことは直接的に「終了しない帰納法」を許すことに繋がり、矛盾をもたらすことになってしまいます。

-- unsafe を使うと終わらない帰納法が使えるので、何でも証明できる
unsafe def loop_thm (P : Prop) : P :=
  loop_thm P

unsafe example : False := loop_thm False

定理ではなくて通常の関数の場合も、停止しない関数を野放図に許すと矛盾を導くことができます。例として、以下の「Option 値の点列から some であるようなものを探す関数」を考えてみましょう。この関数は引数の点列の f がいつか some になる保証がないので、無限に計算が終わらない可能性があります。

variable {α : Type}

/-- 点列 `f : Nat → Option α` が `some` を返すような最初の `f n` を返す -/
unsafe def search (f : Nat → Option α) (start : Nat) : α :=
  match f start with
  | .some x => x
  | .none => search f (start + 1)

ここで問題なのは、この関数 search を使うと任意の型の項が構成できてしまうということです。

-- `α` は任意なので、どんな型の項でも作れることになる
unsafe def anything : α := search (fun _ => none) 0

したがって特に Empty 型の項を作ることもできて、カリー・ハワード同型対応により「型の項を作る」ことは「命題を証明する」ことに対応するので、矛盾が導かれたことになってしまいます。

unsafe example : False := by
  have _ : Empty := anything
  contradiction

  1. ここで紹介している例は Joachim Breitner さんによる Recursive definitions in Lean というブログ記事から引用しています。

local

local はコマンドをそのセクションの内部でだけ有効にするための修飾子です。

section foo
  -- local を付けて新しい記法を定義
  local notation " succ' " => Nat.succ

  -- section の中では使用できる
  #check succ'
end foo

-- section を抜けると使えなくなる
#check_failure succ'

section foo
  -- 同じ名前の section を再度開いても使えない
  #check_failure succ'
end foo

コマンドの有効範囲を namespace の内部に限定するのにも使えます。ただし、下記のコードで示しているように、local で修飾したコマンドの効果は同じ名前空間の中で永続するのではなく、end でその名前空間が閉じられたときに消失します。

namespace hoge
  -- local を付けて新しい記法を定義
  local notation " succ' " => Nat.succ

  -- 定義した namespace の中では使用できる
  #check succ' 2
end hoge

-- namespace の外では使用できない
#check_failure succ' 2

-- 再び同じ名前の namespace をオープンする
namespace hoge
  -- 使用できない!
  #check_failure succ'
end hoge

修飾可能なコマンド

local で有効範囲を限定できるコマンドには、次のようなものがあります。

リストの全体は、local の後に修飾できないコマンドを続けたときのエラーメッセージで確認できます。

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- `def` は有効範囲を制限できないのでエラーになる
/-
error: <input>:1:6: expected 'binder_predicate', 'builtin_cbv_simproc', 'builtin_dsimproc', 'builtin_simproc', 'cbv_simproc', 'def_eval_config_item', 'dsimproc', 'elab', 'elab_rules', 'grind_pattern', 'infix', 'infixl', 'infixr', 'instance', 'macro', 'macro_rules', 'notation', 'postfix', 'prefix', 'simproc', 'syntax' or 'unif_hint'
-/
#eval parse `command "local def"

数が多いためすべての例を挙げることはしませんが、いくつか紹介します。たとえば instance の場合、local を付けて登録したインスタンスがそのセクションの内部限定になります。

inductive MyNat : Type where
  | zero : MyNat
  | succ : MyNat → MyNat

section
  -- local を付けてインスタンスを定義
  local instance : OfNat MyNat 0 where
    ofNat := MyNat.zero

  -- その section の中では使用できる
  #check (0 : MyNat)
end

-- section を抜けると使えなくなる
#check_failure (0 : MyNat)

属性に対する local

属性付与の効果範囲を限定するためには、attribute コマンドを local で修飾するのではなく、attribute コマンドの中で local を使います。

def MyNat.add (n m : MyNat) : MyNat :=
  match m with
  | zero => n
  | succ m => succ (MyNat.add n m)

theorem MyNat.zero_add (n : MyNat) : MyNat.add .zero n = n := by
  induction n with
  | zero => rfl
  | succ n ih => simp [MyNat.add, ih]

section
  -- [simp] 属性をローカルに付与する
  attribute [local simp] MyNat.zero_add

  -- その section の中では使用できる
  example : MyNat.add .zero .zero = .zero := by
    simp
end

-- section を抜けると simp 補題が利用できなくなる
example : MyNat.add .zero .zero = .zero := by
  -- 使えないのでエラーになる
  fail_if_success simp
  rw [MyNat.zero_add]

構文的な性質

attrKind のドキュメントコメントに次のように書かれている通り、localscoped はともに構文的には attrKind に相当します。

attrKind matches ("scoped" <|> "local")?, used before an attribute like @[local simp].

/-- 例示のための意味のないコマンド。直前に `attrKind` のみを受け付ける。-/
macro attrKind "#greet " : command => `(#eval "hello")

-- パース出来るので、`local` と `scoped` は同じカテゴリに属する
local #greet
scoped #greet

noncomputable

noncomputable は、宣言された関数が計算可能でないことを Lean に伝えるために使われます。

Lean は、def コマンドで定義された関数はすべて計算可能であると想定しています。したがって、計算可能でない関数を定義すると、エラーが発生します。

計算可能でない関数が生じるのは、選択原理 Classical.choice を使用したときです。選択原理は、型が「空ではない」という証明だけから、その型の項を魔法のように構成できると主張している公理です。Prop証明無関係という特性により、空ではないという情報から具体的な項の情報は得られないため、選択原理を使用した関数は計算不能になります。

variable {X Y : Type}

-- 写像 `f : X → Y` が全射であること
def Surjective (f : X → Y) : Prop := ∀ y, ∃ x, f x = y

-- 全射な関数の逆写像を構成する
-- しかし、全射という情報だけからは逆写像を具体的に作ることはできないので、
-- 計算不能になりエラーになってしまう
/-
error: `Classical.choice` not supported by code generator; consider marking definition as `noncomputable`
-/
def inverse' (f : X → Y) (hf : Surjective f) : Y → X := by
  -- `y : Y` が与えられたとする
  intro y

  -- `f` は全射なので `{x // f x = y}` は空ではない
  have : Nonempty {x // f x = y} := by
    let ⟨x, hx⟩ := hf y
    exact ⟨⟨x, hx⟩⟩

  -- 選択原理を用いて、`f x = y` なる `x : X` を構成する
  have x := Classical.choice this
  exact x.val

-- `noncomputable` という修飾子を付ければ、エラーは回避できる
noncomputable def inverse (f : X → Y) (hf : Surjective f) : Y → X := by
  intro y

  have : Nonempty {x // f x = y} := by
    let ⟨x, hx⟩ := hf y
    exact ⟨⟨x, hx⟩⟩

  have x := Classical.choice this
  exact x.val

noncomputable とマークされた式を含む式は文字通り評価不能になり、#eval に渡すことができなくなります。

-- 補助として `id` という恒等写像が全射であることを示しておく
theorem id_surjective : Surjective (id : Nat → Nat) := by
  intro y
  exists y

-- `id` の逆写像を構成する
noncomputable def id_inverse := inverse (id : Nat → Nat) id_surjective

-- 逆写像の `3` での値を評価しようとするとエラーになる
/-
error: failed to compile definition, consider marking it as 'noncomputable' because it depends on 'id_inverse', which is 'noncomputable'
-/
#eval id_inverse 3

partial_fixpoint

partial_fixpoint は、partial と同様に「すべての入力に対して必ずしも停止しないような関数」を定義することを可能にしますが、partial とは異なり定義した関数を証明に使うことが可能です。

variable {α : Type}

/-- `partial` で定義された検索関数 -/
partial def searchP (f : Nat → Option α) (start : Nat) : Option Nat :=
  match f start with
  | some _ => some start
  | none => searchP f (start + 1)

/-- `partial_fixpoint` で定義された検索関数 -/
@[grind]
def searchF (f : Nat → Option α) (start : Nat) : Option Nat :=
  match f start with
  | some _ => some start
  | none => searchF f (start + 1)
partial_fixpoint


-- `partial` で定義した関数は証明に使うことができない
example (f : Nat → Option α) (n : Nat) (h : (f n).isSome) : (searchP f n).isSome := by
  induction n with
  | zero =>
    -- 全く展開することができず、上手くいかない
    fail_if_success unfold searchP
    sorry
  | succ n ih =>
    sorry

-- `searchF` に関しては証明ができる
example (f : Nat → Option α) (n : Nat) (h : (f n).isSome) : (searchF f n).isSome := by
  induction n with
  | zero =>
    unfold searchF
    grind
  | succ n ih => grind

名前の由来

partial_fixpoint という名前を見て、partial の部分は納得がいくと思います。部分関数(partial function)を定義するための修飾子だからです。では fixpoint の部分は何でしょうか?

これを説明するためには、再帰関数について考える必要があります。たとえば、階乗関数を考えてみましょう。

/-- 階乗関数 -/
def Nat.factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

「引数として n + 1 を渡すと、自分自身を再帰的に呼びだす」という構造を持っています。#print equations コマンドを使うと、ここで定義された関数がどのような関数等式を満たしているかを確認することができます。

/-
info: equations:
@[backward_defeq] theorem Nat.factorial.eq_1 : Nat.factorial 0 = 1
@[backward_defeq] theorem Nat.factorial.eq_2 : ∀ (n_2 : Nat), n_2.succ.factorial = (n_2 + 1) * n_2.factorial
-/
#print equations Nat.factorial

「重要なのはこの関数等式であって、定義の仕方自体は表面的なものである」という視点に立つことができます。Nat.factorial とは、上記の関数等式を満たす何者かであればよいということです。そう考えると、関数等式さえ表現できるのであれば別の定義の仕方がありえることになります。

そこで、たとえば次のような高階関数を考えてみます。

def factBody (f : Nat → Nat) : Nat → Nat :=
  fun n =>
    match n with
    | 0 => 1
    | n + 1 => (n + 1) * f n

このようにすると、Nat.factorial が満たしていた関数等式 f 0 = 1 ∧ f (n + 1) = (n + 1) * f n は、f = factBody f という等式に置き換えることができます。

example (f : Nat → Nat) (h : f = factBody f) : f 0 = 1 := calc
  _ = factBody f 0 := by rw [h]; grind
  _ = 1 := rfl

example (f : Nat → Nat) (h : f = factBody f)
    : ∀ n : Nat, f (n + 1) = (n + 1) * f n := by
  intro n
  calc
    _ = factBody f (n + 1) := by rw [h]; grind
    _ = (n + 1) * f n := rfl

つまり、階乗関数を「f = factBody f を満たす f」として定義することができるというわけです。

ここで f = factBody f という等式をよく見ると、ffactBody という高階関数の不動点(fixpoint)であると主張していることがわかります。ここでは階乗関数を具体例にしましたが、一般に再帰関数は関数の不動点として捉えられることが知られています。partial_fixpoint の名前は、まさにこの「再帰関数は不動点である」という考え方に由来します。

健全性が保たれるための条件

partial_fixpoint で修飾すれば停止性を証明しなくても許される関数と、そうではない関数があります。どんな関数でも partial_fixpoint で修飾すれば見逃してもらえるわけではありません。

/-
error: failed to compile definition 'search' using `partial_fixpoint`, could not prove that the type
  {α : Type} → (Nat → Option α) → Nat → α
is nonempty.
-/
/-- 点列 `f : Nat → Option α` が `some` を返すような最初の `f n` を返す -/
def search (f : Nat → Option α) (start : Nat) : α :=
  match f start with
  | .some x => x
  | .none => search f (start + 1)
partial_fixpoint

許される関数とそうでない関数の違いはどこにあるのでしょうか?実は、partial_fixpoint で修飾することができる関数は、大まかに次の2つの条件のどちらかを満たすものです。1

  1. 返り値の型が Inhabited であるような、末尾再帰関数
  2. Option モナドのような、適切なモナドに包まれた値を返す関数

1-A 返り値の型が Inhabited とは

返り値の型が Inhabited でなければならない、という条件がなぜ必要なのかを見るには、以下のような例を考えると良いでしょう。

unsafe def empty_loop : Empty := empty_loop

unsafe example : False := by
  exact empty_loop.elim

停止性の保証なしに再帰関数 f : A → B の定義を許すと、f を使って B の項を作ることができてしまう可能性があります。したがって、BInhabited でなければ、矛盾が導かれる可能性があります。これを禁止するのはもっともなことでしょう。

なお、例外として定義域の型も空である場合は、返り値の型が Inhabited でなくても partial_fixpoint で修飾することができるようです。これはカリー・ハワード同型対応から言えば「矛盾を仮定すれば矛盾が示せる」ことに相当し、論理的には妥当なものの応用はないかもしれません。

/-- 停止しないし返り値の型は空だが、`partial_fixpoint` で修飾できる関数 -/
def f : Empty → Empty :=
  fun x => f x
partial_fixpoint

1-B 末尾再帰的とは

再帰関数 f : A → B が末尾再帰的でなければならない、という条件がなぜ必要なのか見るために、以下の例を見てください。

/-- 末尾再帰的でない、停止しない関数 -/
unsafe def nonTR_loop (n : Nat) : Nat :=
  1 + nonTR_loop n

-- nonTR_loop の定義から、以下の関数等式が成り立つはず
-- (deep recursion になるので証明できず、やむを得ず axiom としている)
unsafe axiom nonTR_loop.def (n : Nat) : nonTR_loop n = 1 + nonTR_loop n

-- 矛盾が導かれる
unsafe example : False := by
  have lem : ∀ x, x ≠ 1 + x := by
    intro x
    grind

  let y := nonTR_loop 0
  have : y = 1 + y := by
    dsimp [y]
    rw [← nonTR_loop.def 0]

  exact lem y this

返り値の型 Nat は当然ながら Inhabited ですが、末尾再帰的でなかったために、関数等式 f x = 1 + f x を満たす関数 f が存在することになってしまい、そんな自然数 f x は存在しないので矛盾が導かれてしまいました。

もしこれが末尾再帰的であれば、どうだったか?を考えるとより明確になります。たとえば以下の例を考えてみましょう。

/-- 末尾再帰的な、停止しない再帰関数 -/
def tr_loop (n : Nat) : Nat :=
  tr_loop (n + 1)
partial_fixpoint

f 0 = f 1 = f 2 = ... という関数等式を満たす f は確かに一意には定まらない(再帰が停止しないため)のですが、この関数の存在を仮定しても矛盾が導かれることはありません。なぜなら、定数関数という解がきちんと存在するからです。partial_fixpoint は、この関数 tr_loop の値を具体的に特定することなく、ただ tr_loop n = tr_loop (n + 1) という関数等式を満たす何者かであるとするだけなので、矛盾が導かれることはありません。

-- tr_loop の定義から得られるのは以下の関数等式だけ
/-
info: equations:
theorem tr_loop.eq_1 : ∀ (n : Nat), tr_loop n = tr_loop (n + 1)
-/
#print equations tr_loop

これだけでは「なぜ末尾再帰なら大丈夫なのか」の完全な説明にはなっていませんが、とりあえず以上の例から「末尾再帰なら、f x = f y という形の等式が得られるだけなので、関数が停止しなくても値が一意に定まらなくなるだけで矛盾は生じない」ということが窺えるのではないでしょうか。

用途

通常 Lean で再帰関数を定義する際には「停止性を証明できるチャンスは定義したその瞬間だけ」ですが、partial_fixpoint を利用すると「定義した後で停止性を証明する」ことができます。2

/-- McCarthy の 91 関数の返り値を `Option` で包んだもの。
返り値を `Option` で包んであるので、`partial_fixpoint` で定義を通すことができる。-/
def f91? (n : Nat) : Option Nat :=
  if n > 100 then
    some (n - 10)
  else do
    let m ← f91? (n + 11)
    f91? m
partial_fixpoint

@[simp]
theorem f91?_91 : f91? 91 = some 91 := by cbv

@[simp]
theorem f91?_spec_true (n : Nat) (h : 100 < n) :
    f91? n = some (n - 10) := by
  unfold f91?
  simp [h]

@[simp]
theorem f91?_spec_false (n : Nat) (h : n ≤ 100) :
    f91? n = some 91 := by
  unfold f91?
  rw [ite_eq_right (by omega)]
  by_cases hn : n < 90
  · rw [f91?_spec_false (n + 11) (by omega)]
    simp
  · simp [f91?_spec_true (n + 11) (by omega)]
    by_cases h100 : n = 100
    · simp [h100]
    · exact f91?_spec_false (n + 1) (by omega)
termination_by 100 - n

theorem f91?_spec (n : Nat) :
    f91? n = some (if n ≤ 100 then 91 else n - 10) := by
  grind only [= f91?_spec_false, = f91?_spec_true]

/-- `f91?` は必ず停止する -/
theorem f91?_total (n : Nat) : f91? n |>.isSome := by
  simp [f91?_spec]

-- 停止性が保証できたので、`Option` を外すことができる
def f91 (n : Nat) : Nat :=
  (f91? n).get (f91?_total n)

  1. Lean 公式リファレンスの「Partial Fixpoint Recursion」の項目を参照:https://lean-lang.org/doc/reference/latest/Definitions/Recursive-Definitions/#partial-fixpoint

  2. この例と証明は、F91 in Lean という Joachim Breitner さんのブログ記事を参考にしました。

partial

partial は部分関数(partial function)を定義するための修飾子です。部分関数とは、全ての入力に対しては必ずしも停止しないような関数のことです。

Lean では再帰関数も再帰的でない関数と同様に定義できますが、扱いは異なります。再帰的な関数 f を定義すると、Lean は f がすべての入力に対して必ず有限回の再帰で停止することを証明しようとします。自動的な証明が失敗すると、エラーになってしまいます。(そもそもなぜ部分関数が許可されないのかについては部分関数を禁止するのはなぜか?を参照のこと。)

-- 何も指定しないと、停止することが Lean にはわからないのでエラーになる
/-
error: fail to show termination for
  WithoutPartial.M
-/
/-- McCarthy の 91 関数 -/
def M (n : Nat) : Nat :=
  if n > 100 then
    n - 10
  else
    M (M (n + 11))

エラーを消すには、以下のどちらかの対応が必要です。

  • 停止することを手動で証明するか。
  • 停止性は保証しないと明示的にマークするか。

関数の定義に partial とマークすると、停止性は保証しないとマークしたことになります。

-- `partial` を使うと、停止性の証明が不要になる
partial def M (n : Nat) : Nat :=
  if n > 100 then
    n - 10
  else
    M (M (n + 11))

#eval M 91

舞台裏

なお、partial とマークされた定義を元に新たに関数を定義するとき、再度 partial とマークする必要はありません。

/-- 階乗っぽいが停止しない関数 -/
partial def forever (x : Int) : Int :=
  if x = 0 then 1
  else x * forever (x - 1)

-- more 関数も停止しないが、partial は不要である
def more := @forever

これは意外に思えます。partial とマークされた関数は停止性が保証されていないので、それを使用した関数も停止性を保証できなくなるはずだからです。なぜこうなるのかというと、partial はそもそも「停止性が保証されていない」ことを表すものではなく、opaque コマンドと同様に「名前が定義に展開できない」ことを表すものだからです。

-- 実際に #print してみると、`opaque` と表示される
/- info: opaque forever : Int → Int -/
#print forever

/-- 正しい階乗関数 -/
def factorial (x : Nat) : Nat :=
  match x with
  | 0 => 1
  | x + 1 => (x + 1) * factorial x

-- 正しい階乗関数と比較してみると、
-- 正しい方は `opaque` ではなく、中身が表示されることがわかる
/-
info: def factorial : Nat → Nat :=
fun x => Nat.brecOn x factorial._f
-/
#print factorial

ある関数・定数が partial な関数に依存しているかチェックしたい場合は、#print opaque コマンドを使用することでできます。

import Batteries.Tactic.PrintOpaques

/-- 無限ループする関数 -/
partial def endless {α : Type u} (a : α) : α :=
  endless a

def exampleFunc (a : Nat) : Nat :=
  endless a + 1

/- info: 'exampleFunc' depends on opaque or partial definitions: [endless] -/
#print opaques exampleFunc

例外的な挙動

再帰的でない関数を partial とマークしても何も起こりません。

partial def square (x : Int) := x * x

-- 簡約が実行される
/- info: Int.ofNat 1024 -/
#reduce square 32

private

private は、その定義があるファイルの中でだけ参照可能になるようにする修飾子です。他のファイルからはアクセス不能になります。不安定なAPIなど、外部に公開したくないものに対して使うのが主な用途です。

Warning

このページの内容は ボタンから Lean 4 Web で実行することができません。

たとえば、以下のように書かれているファイル Private/Lib.lean があったとしましょう。

structure Point where
  x : Nat
  y : Nat

namespace Point

  protected def sub (p q : Point) : Point :=
      { x := p.x - q.x, y := p.y - q.y }

  private def private_sub := Point.sub

end Point

このとき、モジュール PrivateLib を読み込んでいるファイルからは、protected で修飾された名前はアクセス可能ですが、private で修飾された名前はアクセスできません。

import LeanByExample.Modifier.Private.Lib -- private が使用されているモジュールをインポート

-- private を使わずに定義した内容にはアクセスできる
#check Point.sub

-- private とマークした定義にはアクセスできない
#check_failure Point.private_sub

なお private コマンドで定義した名前は、同じファイル内であればそのセクションや名前空間を出ても普通にアクセスすることができます。特に、privateprotected の効果を持ちません。

namespace Hoge
  section
    -- private とマークした定義
    private def addOne (n : Nat) : Nat := n + 1

  end
end Hoge

open Hoge

-- 外からでもアクセスできる
#check addOne

補足: privateで隠された定義に一時的にアクセスする

private で隠された定義にアクセスする方法はあります。手軽なのは、open private コマンドを使う方法です。

import LeanByExample.Modifier.Private.Lib
import Batteries.Tactic.OpenPrivate

-- 最初はアクセスできない
#check_failure Point.private_sub

-- `open private` コマンドを使用する
open private Point.private_sub from LeanByExample.Modifier.Private.Lib

-- アクセスできるようになった
#check Point.private_sub

protected

protected は、ある名前空間 Hoge にある定義 foo に対して、短い名前 foo でアクセスすることを禁止するものです。

namespace Playground

  /-- protected が付いていない定義 -/
  def ordianal_hoge := "hoge"

  /-- protected が付いている定義 -/
  protected def protected_hoge := "hoge"

end Playground

namespace Playground

  -- 名前空間の中なので、短い名前でアクセスできる
  -- (通常の挙動)
  #check ordianal_hoge

  -- 名前空間を開いているが、短い名前ではアクセスできない
  #check_failure protected_hoge

  -- 名前空間名を補えばアクセスできる
  #check Playground.protected_hoge

end Playground

section
  open Playground

  -- 名前空間を `open` しているので、
  -- 短い名前でアクセスできる(通常の挙動)
  #check ordianal_hoge

  -- 名前空間を `open` しているが、短い名前ではアクセスできない
  #check_failure protected_hoge
end

構文

def コマンドに対してだけでなく、indudctive コマンドで生成されるコンストラクタに対しても使用可能です。

/-- 2分木 -/
inductive BinTree (α : Type) where
  | empty : BinTree α
  | protected node : α → BinTree α → BinTree α → BinTree α

section

  -- 名前空間を開く
  open BinTree

  -- 名前空間を open しているが、
  -- コンストラクタに短い名前でアクセスできない
  #check_failure node
  #check BinTree.node

  -- protected でない方は短い名前でアクセスできる
  #check empty

end

また structure コマンドで生成されるアクセサ関数やコンストラクタに対しても使用可能です。

structure Sample where
  -- コンストラクタも protected にできる
  protected mk ::

  bar : Nat
  protected hoge : String

section

  open Sample

  -- 名前空間を open しているので bar には短い名前でアクセスできる
  #check bar

  -- hoge には短い名前でアクセスできない
  #check_failure hoge

end

用途

機能から想像がつくと思いますが、protected は混同を避けるために使用されます。

protected を使うべき典型的な状況は、型クラスのメソッドが export されている場合です。

/-- 文字列をパースして `α` 型の項を得る方法を提供する型クラス -/
class OfString (α : Type) where
  ofString : String → Option α

export OfString (ofString)

型クラスのメソッドを用意するときに、関数名は往々にしてそのメソッドと同じ名前にするので、紛らわしさが生じます。

namespace Bool

  -- `Bool` 名前空間の中にいると、
  -- `Bool.ofString` と `OfString.ofString` が紛らわしい
  def ofString (s : String) : Option Bool :=
    match s with
    | "true" => some true
    | "false" => some false
    | _ => none

  instance : OfString Bool where
    ofString := ofString

  -- `Bool.ofString` の方を指している
  /- info: Bool.ofString (s : String) : Option Bool -/
  #check ofString

end Bool

-- `OfString.ofString` の方を指している
/- info: OfString.ofString {α : Type} [self : OfString α] : String → Option α -/
#check ofString

protected を使用すると、「型クラスのメソッドの具体的な実装の方を指したいときは、明示的に名前空間を補う」というルールにできるので、紛らわしさが改善されます。

namespace Unit

  protected def ofString (s : String) : Option Unit :=
    match s with
    | "()" => some ()
    | _ => none

  instance : OfString Unit where
    ofString := Unit.ofString

  -- `OfString.ofString` の方を指している
  /- info: OfString.ofString {α : Type} [self : OfString α] : String → Option α -/
  #check ofString

end Unit

名前空間がネストしているとき

名前空間がネストしていないとき、protected は「フルネームを強制する」と説明しても問題ありません。しかし、名前空間がネストしているときにはその説明は誤りになります。

以下に示すように、Outer.Inner.sample という名前を protected で修飾すると、最も近い名前空間の修飾名である Inner を省略することができなくなります。

namespace Outer
  namespace Inner

    protected def sample := "sample"

    -- フルネームを強制するのであれば失敗するべきだが、成功する。
    #check Inner.sample

    -- Inner 名前空間を省略することはできない
    #check_failure sample

  end Inner
end Outer

scoped

scoped は、コマンドの有効範囲を現在の名前空間に限定します。

-- #greet をコマンドとして認識させる
-- 実装は与えない
syntax "#greet" : command

namespace Scoped
  -- scoped を付けて greet コマンドをマクロとして定義
  scoped macro "#greet" : command => `(#eval "hello, world!")

  -- その名前空間の中では greet コマンドが利用できる
  #greet

end Scoped

-- 名前空間を抜けると使えなくなる
/- error: elaboration function for `«command#greet»` has not been implemented -/
#greet

-- 再び同じ名前で名前空間を開く
namespace Scoped
  -- その名前空間の中では greet コマンドが利用できる
  #greet

end Scoped

section
  open Scoped

  -- 単に open するだけでも利用できるようになる
  #greet
end

修飾可能なコマンド

scoped で有効範囲を限定できるコマンドには、次のようなものがあります。

リストの全体は、scoped の後に修飾できないコマンドを続けたときのエラーメッセージで確認できます。

open Lean Parser

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- `def` は有効範囲を制限できないのでエラーになる
/-
error: <input>:1:7: expected 'binder_predicate', 'builtin_cbv_simproc', 'builtin_dsimproc', 'builtin_simproc', 'cbv_simproc', 'def_eval_config_item', 'dsimproc', 'elab', 'elab_rules', 'grind_pattern', 'infix', 'infixl', 'infixr', 'instance', 'macro', 'macro_rules', 'notation', 'postfix', 'prefix', 'simproc', 'syntax' or 'unif_hint'
-/
#eval parse `command "scoped def"

open scoped

open scoped コマンドを利用すると、特定の名前空間にある scoped が付けられた名前だけを有効にすることができます。単に open コマンドを利用するとその名前空間にあるすべての名前が有効になります。

namespace Foo
  -- Foo の中でのみ有効な add' という名前を定義
  scoped infix:55 " add' " => Nat.add

  -- 動作する
  #guard 30 add' 12 = 42

  -- Foo の中で greet も定義
  def greet := "hello"

end Foo

section
  -- 単に open した場合、どちらも使用可能
  open Foo

  #check (30 add' 12)

  #check greet
end

section
  -- open scoped とした場合
  open scoped Foo

  -- scoped がついた宣言は使用可能
  #check (30 add' 12)

  -- greet は使えないまま
  #check_failure greet
end

属性に対する scoped

attribute コマンドの中で scoped を使用すると、属性付与の効果範囲を限定することができます。

namespace Bar

  @[scoped simp]
  def bar : Nat := 12

  example : bar = 12 := by
    simp

end Bar

example : Bar.bar = 12 := by
  -- `scoped`が付いているので、使えない
  fail_if_success simp

  dsimp [Bar.bar]

section
  -- 名前空間を開く
  open Bar

  example : bar = 12 := by
    -- 名前空間を開いたので、`simp`が使える
    simp

end

構文的な性質

attrKind のドキュメントコメントに次のように書かれている通り、localscoped はともに構文的には attrKind に相当します。

attrKind matches ("scoped" <|> "local")?, used before an attribute like @[local simp].

/-- 例示のための意味のないコマンド。直前に `attrKind` のみを受け付ける。-/
macro attrKind "#greet " : command => `(#eval "hello")

-- パース出来るので、`local` と `scoped` は同じカテゴリに属する
local #greet
scoped #greet

termination_by

termination_by 句(termination_by clause)は、再帰関数が有限回の再帰で停止することを Lean にわかってもらうために、「再帰のたびに減少する指標」を指定します。

-- 何も指定しないと、停止することが Lean にはわからないのでエラーになる
/-
error: fail to show termination for
  M
-/
/-- McCarthy の 91 関数 -/
def M (n : Nat) : Nat :=
  if n > 100 then
    n - 10
  else
    M (M (n + 11))

以下のように、termination_by で「再帰適用で減少していくもの」を指定することができ、うまくいけばエラーがなくなります。1

/-- McCarthy の 91 関数 -/
def Mc91 (n : Nat) : Nat :=
  (M n).val
where
  M (n : Nat) : { m : Nat // m ≥ n - 10 } :=
    if h : n > 100 then
      ⟨n - 10, by omega⟩
    else
      have : n + 11 - 10 ≤ M (n + 11) := (M (n + 11)).property
      have lem : n - 10 ≤ M (M (n + 11)) := calc
        _ ≤ (n + 11) - 10 - 10 := by omega
        _ ≤ (M (n + 11)) - 10 := by omega
        _ ≤ M (M (n + 11)) := (M (M (n + 11)).val).property

      ⟨M (M (n + 11)), lem⟩
  -- 再帰のたびに n が 101 に近づいていくことを Lean に教えてあげる
  termination_by 101 - n

構造的再帰と整礎再帰

構造的再帰

再帰関数を定義しようとすると、Lean はその関数が「どんな入力に対しても有限回の再帰で停止すること」を証明しようとします。関数によって、その自動証明が簡単なことと難しいことがあります。

簡単な場合の代表例は、引数が帰納型 T の項になっていて、再帰呼び出しによって T の「より小さい」項を引数として渡す場合です。ここで、「より小さい」というのは帰納型の基底ケースからのコンストラクタの適用回数によって測ります。このような再帰は 構造的再帰(structural recursion) と呼ばれます。

構造的再帰の場合は停止性の証明は簡単なので、基本的に Lean が自動で証明してくれます。

/-- 構造的再帰の例。階乗関数。-/
def Nat.factorial (n : Nat) : Nat :=
  -- Nat の帰納的定義の構造に基づいて再帰しているので、
  -- 引数が「より小さくなる」ことは明らかで、 Lean は自動で停止性を証明できる
  match n with
  | 0 => 1
  | m + 1 => (m + 1) * Nat.factorial m

example : Nat.factorial 5 = 120 := by rfl

構造的再帰の場合 Lean は何も指定しなくても構造的再帰であることを理解してくれるのですが、構造的再帰であると明示することもできて、その場合は termination_by structural と書きます。

/-- 構造的再帰の例。 -/
def swapAlt {α : Type} (xs : List α) : List α :=
  match xs with
  | [] => []
  | [x] => [x]
  | x :: y :: zs => y :: x :: swapAlt zs
termination_by structural xs

example : swapAlt [1, 2, 3, 4] = [2, 1, 4, 3] := by
  rfl

整礎再帰

構造的再帰では停止性は証明できなくても、別の方法で停止性が自動的に保証できる場合があります。

例えば、配列のインデックスを左から見ていく操作のように、「再帰呼び出しのたびに引数が増加していてもある上限を超えないことが分かっている場合」は停止性が保証できます。

/-- `Array.search`のための補助関数。
再帰呼び出しのたびに `i` が増加するが、増加に上限があるので必ず停止する -/
def Array.searchAux {α : Type} (as : Array α) (i : Nat) (P : α → Bool) : Bool :=
  if h : i < as.size then
    if P (as[i]) then
      true
    else
      Array.searchAux as (i + 1) P
  else
    true
-- 再帰のたびに i は増加するが、
-- 配列のサイズを超えることはないので停止することを Lean に教えてあげる
termination_by as.size - i

/-- 配列の要素であって、述語 `P` を満たすものを探す -/
def Array.search {α : Type} (as : Array α) (P : α → Bool) : Bool :=
  Array.searchAux as 0 P

#guard #[1, 2, 3].search (· % 2 = 0)

一般に(無限に小さくなり続けることはない)ある指標が、再帰呼び出しのたびに減少していく場合、停止性を保証するのに利用することができます。このような再帰は 整礎再帰(well-founded recursion) と呼ばれます。termination_by 句は、整礎再帰のためにどの指標を利用するかを Lean に伝えるための構文であると言うことができます。

Lean は賢いので、多くのシンプルなケースについては termination_by 句を省略することができますが、その場合でも整礎再帰であることに変わりはありません。

/-- 整除関数。
構造的再帰ではないが、Lean が自動的に停止性を証明できる -/
def div (x y : Nat) : Nat :=
  if h : y = 0 then
    0
  else if x < y then
    0
  else
    1 + div (x - y) y

#guard div 10 3 = 3

整礎再帰と [irreducible] 属性

整礎再帰を使って定義した関数は自動的に [irreducible] 属性が付与されます。

/-- 文字列の左側を指定文字で埋めて、指定長にそろえる(整礎再帰バージョン)-/
def String.padLeftWF (input : String) (padChar : Char) (length : Nat) : String :=
  if input.length ≥ length then
    input
  else
    String.padLeftWF (padChar.toString ++ input) padChar length
termination_by length - input.length

/-
info: @[irreducible] def String.padLeftWF : String → Char → Nat → String
-/
#print String.padLeftWF

したがって、その関数について何か証明しようとしても rfl タクティクや decide タクティクが使えません。

example : String.padLeftWF "42" '0' 5 = "00042" := by
  -- `rfl` では示せない
  fail_if_success rfl

  -- `decide` でも示せない
  fail_if_success decide

  sorry

もしも整礎再帰を回避して同じことができるのであれば、回避すると良いでしょう。

/-- 文字列の左側を指定文字で埋めて、指定長にそろえる(非再帰バージョン)-/
def String.padLeftSimple (input : String) (padChar : Char) (length : Nat) : String :=
  if input.length ≥ length then
    input
  else
    let padding : String := String.pushn "" padChar (length - input.length)
    padding ++ input

-- [irreducible] ではない
/-
info: def String.padLeftSimple : String → Char → Nat → String
-/
#print String.padLeftSimple

example : String.padLeftSimple "42" '0' 5 = "00042" := by
  -- こちらは定義に展開できるので `rfl` が通る
  rfl

帰納法と整礎再帰

Lean では関数と証明項の間に大きな違いはないため、termination_by を再帰関数ではなくて theorem に対して使うこともできます。この場合、「再帰のたびに小さくなる指標」は「帰納法のステップごとに大きくなっていく指標」に相当します。これを利用することにより、証明の中で複雑な帰納法を書くことができます。

以下は、整数に対して「絶対値に関する帰納法」をこの手法で実装している例です。

/-- 素数 -/
@[grind =]
def Int.Prime (p : Int) : Prop :=
  p > 1 ∧ ∀ a : Int, a > 0 → a ∣ p → a = 1 ∨ a = p

grind_pattern Int.le_of_dvd => a ∣ b where
  guard 0 < b

/-- 1 より大きい整数には素因数が存在する -/
theorem Int.exists_prime_factor (n : Int) (hn : 1 < n) :
    ∃ p : Int, p.Prime ∧ p ∣ n := by
  -- `n : Int` の絶対値に関する帰納法で示す。
  -- 絶対値として `Nat` 値の `natAbs` を使用していることに注意

  -- n 自身が素数であれば明らかなので、n は素数でないとして良い。
  by_cases hprime : n.Prime
  case pos =>
    exists n
    simp_all

  -- n は素数ではないので、非自明な約数 k が存在する
  have ⟨k, hk⟩ : ∃ k : Int, 1 < k ∧ k < n ∧ k ∣ n := by
    have : ∃ x, 0 < x ∧ x ∣ n ∧ ¬x = 1 ∧ ¬x = n := by
      simp [Int.Prime] at hprime
      exact hprime hn
    replace ⟨x, hx⟩ := this
    exists x
    grind

  -- `|k| < |n|` なので、帰納法の仮定を `k` に適用できる。
  -- (証明中の定理自身を呼びだしていることに注意。これは再帰呼び出しの構文と同じ)
  -- したがって `k` の素因数 `p` が存在する。
  have hlt : k.natAbs < n.natAbs := by grind
  obtain ⟨p, hp, hpdvd⟩ := Int.exists_prime_factor k (by grind)

  -- p は k の約数なので、p は n の約数でもある
  grind only [Int.dvd_trans]

-- ここで `natAbs` を再帰の進捗指標に指定している。
-- この部分が「絶対値に関する帰納法」に対応している。
termination_by Int.natAbs n

  1. このコード例は、Lean 公式 Zulip の how to show termination of McCarthy M というトピックにおける Timo Carlin-Burns さんの投稿を参考にしています。

unsafe

unsafe は、Leanのルールを破るような操作を許可するのに使う修飾子です。

たとえば、次のような帰納型は strictly positive 要件というLeanのルールに反するのでエラーになり、定義することができません。

/-
error: (kernel) arg #1 of 'Bad'.mk' has a non positive occurrence of the datatypes being declared
-/
inductive Bad' where
  | mk : (Bad' → Bad') → Bad'

unsafe で修飾すれば定義が通るようになります。

unsafe inductive Bad where
  | mk : (Bad → Bad) → Bad

ptrAddrUnsafe

ptrAddrUnsafe 関数は、値のメモリ上での位置を返す関数ですが、unsafe で修飾されています。

/- info: unsafe opaque ptrAddrUnsafe.{u} : {α : Type u} → α → USize -/
#print ptrAddrUnsafe

これは、ptrAddrUnsafe参照透過性(referential transparency) を壊してしまうからです。参照透過性とは、「引数の値が同じならば関数の値も同じ」という性質です。ptrAddrUnsafe は等しい引数でも異なる値を返すことがあります。

/- info: true -/
#eval show Bool from Id.run do
  let u := ptrAddrUnsafe ([1, 2, 3])
  let v := ptrAddrUnsafe ([1, 2] ++ [3])

  -- 引数の値は同じだが
  assert! [1, 2, 3] == [1, 2] ++ [3]

  -- 返り値が異なる!
  return u != v

Functional but in-place

unsafe が付与されていない通常の関数は参照透過性を持ち、したがって特に Lean ではグローバル変数の値を変更することは通常できません。

-- foo という変数を定義する
def foo := "hello"

-- 再定義しようとするとエラーになる
/- error: `foo` has already been declared -/
def foo := "hello world"

しかし、ローカル変数は破壊的に変更することができます。

/-- フィボナッチ数列を計算する -/
def fibonacci (n : Nat) : Array Nat := Id.run do
  -- 可変な配列 `fib` を宣言する
  let mut fib : Array Nat := Array.mkEmpty n
  fib := fib.push 0
  fib := fib.push 1
  for i in [2:n] do
    -- 更新前の配列 `fib` のメモリアドレスを取得
    let old := unsafe ptrAddrUnsafe fib

    -- `fib` を更新
    fib := fib.push (fib[i-1]! + fib[i-2]!)

    -- 更新後のメモリアドレスを取得
    let new := unsafe ptrAddrUnsafe fib

    -- メモリアドレスが一致する
    assert! old = new
  return fib

-- 値がコピーされていれば panic するはずだが...?
/- info: #[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] -/
#eval fibonacci 15

なぜこのようなことができるのかというと、Lean が値の不要なコピーを行わないからです。具体的には、Lean は 参照カウント がちょうど1であるような値を更新する際に、コピーして新しい値を生成する代わりに破壊的変更を行います。これを指して、Lean 言語のパラダイムのことを Functional but in-place と呼ぶことがあります。

where

where 句(where clause)を使うと、定義をする前に変数を使用することができます。主に、ヘルパー関数を宣言するために使用されます。

/-- 自然数 `n` の素因数とその重複度のリストを返す -/
partial def primeFactorsMult (n : Nat) : List (Nat × Nat) :=
  loop 2 n [] |>.reverse
where
  /-- 自然数 `d` に対して、`n` の重複度 `μ` と `d` のペア `(d, μ)` を返す。-/
  extract (d n : Nat) : Nat × Nat :=
    if d ≤ 1 then
      (1, 0)
    else if n % d != 0 then
      (d, 0)
    else
      let (d, m) := extract d (n / d)
      (d, m + 1)

  /-- ヘルパー関数 -/
  loop (d target : Nat) (acc : List (Nat × Nat)) : List (Nat × Nat) :=
    if target ≤ 1 then
      acc
    else
      let (d, m) := extract d target
      if m = 0 then
        loop (d + 1) target acc
      else
        loop (d + 1) (target / (d ^ m)) ((d, m) :: acc)

属性

属性は、attribute コマンドや @[...] という構文で扱うことができるものです。

本書では属性の完全なリストは示しません。Mathlib において利用可能な属性の一覧は Mathlib4 Help で確認できます。

aesop

[aesop]aesop タクティクに追加のルールを登録するための属性です。同様の機能を持つコマンドに add_aesop_rules があります。

import Aesop


/-- 自然数 n が正の数であることを表す帰納的述語 -/
inductive Pos : Nat → Prop where
  | succ n : Pos (n + 1)

example : Pos 1 := by
  -- 最初はルールが足りなくて示せない
  fail_if_success aesop

  -- 手動でコンストラクタを `apply` することで証明できる
  apply Pos.succ

-- `Pos` 関連のルールを `aesop` に憶えさせる
attribute [aesop safe constructors] Pos

-- aesop で示せるようになった!
example : Pos 1 := by aesop

なお aesop をカスタマイズしたものを専用のタクティクにまとめることも可能ですが、それについてはここでは詳しく述べません。declare_aesop_rule_sets コマンドのページを参照してください。

[aesop] 属性は、[aesop <phase>? <priority>? <builder_name>? <builder_option>? <rule_sets>?] という構文で使用できます。

phase について

phasenormsafeunsafe の3通りです。「ルールを適用した後、ダメそうだとわかったら引き返せ」「常に最初に適用せよ」など試行錯誤のやり方を指示します。

open Lean Parser Category in

-- `Aesop.phase` という構文カテゴリが存在する
#check (Aesop.phase : Category)

norm

norm は正規化(normalisation)ルールを表します。最初に適用されるルール群であり、適用によりゴールが増えないようなルールだけを登録することが推奨されます。[simp] 補題と同様に使用されます。

/-- And を模倣して自作した型 -/
structure MyAnd (a b : Prop) : Prop where
  intro ::
  left : a
  right : b

/-- `P ∧ P ↔ P` に相当するルール -/
theorem erase_duplicate {P : Prop} : MyAnd P P ↔ P := by
  constructor <;> intro h
  · rcases h with ⟨h⟩
    exact h
  · exact MyAnd.intro h h

example (P : Prop) (hp : P) : MyAnd P P := by
  -- 最初は aesop で証明できない
  fail_if_success solve
  | aesop
  sorry

-- aesop に登録する
attribute [aesop norm simp] erase_duplicate

example (P : Prop) (hp : P) : MyAnd P P := by
  -- aesop で証明できるようになった!
  aesop

safe

safe ルールは、norm ルールの実行後に適用されます。あるゴールが証明可能であるとき、それに safe ルールを適用しても生成されるゴールは依然として証明可能であるように、safe ルールを選ぶことが推奨されます。

/-- 自前で定義したリスト -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)

/-- リストが空ではないことを表す帰納的述語 -/
inductive NonEmpty {α : Type} : MyList α → Prop where
  | cons x xs : NonEmpty (MyList.cons x xs)

example : NonEmpty (MyList.cons 1 MyList.nil) := by
  -- 最初は aesop で証明できない
  fail_if_success aesop
  sorry

attribute [aesop safe apply] NonEmpty.cons

-- aesop で示せるようになった!
example : NonEmpty (MyList.cons 1 MyList.nil) := by aesop

unsafe

unsafe ルールは、すべての safe ルールが失敗した場合に適用されます。失敗したらバックトラックして他の unsafe ルールを試します。priority として成功する確率(%)を指定する必要があります。

variable (a b c d e : Nat)

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- 最初は aesop で証明できない
  fail_if_success aesop
  sorry

-- 推移律を `unsafe` ルールとして登録する
attribute [local aesop unsafe 10% apply] Nat.le_trans

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- aesop で証明できるようになった!
  aesop

safe ルールは適用すると後戻りができないため、特定の状況でのみ適用したいルールは unsafe とすることが推奨されます。誤って safe ルールに登録してしまうと上手く動作しないことがあります。

-- 同じ推移律を今度は `safe` ルールとして登録する
attribute [local aesop safe apply] Nat.le_trans

variable (a b c d e : Nat)

example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) (h4 : d ≤ e) : a ≤ e := by
  -- aesop で証明できない
  -- 同じ命題を同じように登録したが、safe にしてしまったからダメだった
  fail_if_success aesop

  sorry

builder_name について

builder_name は、登録されるルールに対して「ゴールを分解する」「仮定から推論を進める」といった方向性を決めます。複数の選択肢がありますが、ここではその一部を紹介します。

open Lean Parser Category in

-- `Aesop.builder_name` という構文カテゴリが存在する
#check (Aesop.builder_name : Category)

apply

apply タクティクと同様にはたらくルールを登録します。

variable (a b c d e : Nat)

example (h1 : a < b) (h2 : b < c) (h3 : c < d) (h4 : d < e) : a < e := by
  -- 最初は aesop で示せない
  fail_if_success aesop

  -- 手動で示すならこのように apply を繰り返すことになる
  apply Nat.lt_trans (m := d) <;> try assumption
  apply Nat.lt_trans (m := c) <;> try assumption
  apply Nat.lt_trans (m := b) <;> try assumption

-- 推移律を登録する
attribute [aesop unsafe 10% apply] Nat.lt_trans

example (h1 : a < b) (h2 : b < c) (h3 : c < d) (h4 : d < e) : a < e := by
  -- aesop で証明できるようになった!
  aesop

constructors

constructors ビルダーは、帰納型 T の形をしたゴールに遭遇した際に、コンストラクタを適用するように指示します。

/-- 自前で定義した偶数を表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | succ m : Even m → Even (m + 2)

example : Even 2 := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動でコンストラクタを適用することで示せる
  apply Even.succ
  apply Even.zero

-- aesop にルールを登録する
attribute [aesop safe constructors] Even

example : Even 2 := by
  -- aesop で証明できるようになった!
  aesop

cases

cases ビルダーは、帰納型 T の形をした仮定がローカルコンテキストにある場合に、それに対して再帰的に cases タクティクを使用して分解するように指示します。

/-- 自前で定義した奇数を表す帰納的述語 -/
inductive Odd : Nat → Prop where
  | one : Odd 1
  | succ m : Odd m → Odd (m + 2)

example (n : Nat) (h : Odd (n + 2)) : Odd n := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動で cases を使って分解することで証明できる
  cases h
  assumption

-- aesop にルールを登録する
attribute [aesop safe cases] Odd

example (n : Nat) (h : Odd (n + 2)) : Odd n := by
  -- aesop で証明できるようになった
  aesop

destruct

destruct ビルダーは、A₁ → ⋯ → Aₙ → B という形の命題を登録することで、仮定に A₁, ..., Aₙ が含まれている場合に、元の仮定を消去して B を仮定に追加します。

/-- 自前で定義した偶数を表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | succ m : Even m → Even (m + 2)

/-- 任意の数 n について、n か n + 1 のどちらかは偶数 -/
theorem even_or_even_succ (n : Nat) : Even n ∨ Even (n + 1) := by
  induction n with
  | zero => left; apply Even.zero
  | succ n ih =>
    rcases ih with ih | ih
    · right
      apply Even.succ
      assumption
    · left
      assumption

example {n : Nat} (h0 : ¬ Even n) (h1 : ¬ Even (n + 1)) : False := by
  -- 最初は aesop で証明できない
  fail_if_success aesop

  -- 手動で補題を示すことで証明する
  have := even_or_even_succ n
  simp_all

attribute [aesop unsafe 30% destruct] even_or_even_succ

example {n : Nat} (h0 : ¬ Even n) (h1 : ¬ Even (n + 1)) : False := by
  -- aesop で示せるようになった!
  aesop

app_unexpander

[app_unexpander] 属性を Unexpander 型の関数に付与すると、#check コマンドおよびinfoviewでの表示のされ方を変更することができます。

一般に、[app_unexpander c] 属性を Unexpander 型の関数に付与することで、c の(関数適用の)表示を変更することができます。

/-- 人に挨拶をする関数 -/
def greet (x : String) := s!"Hello, {x}!"


open Lean PrettyPrinter in

/-- すべての挨拶の表示を強制的に hello world に変えてしまう -/
@[app_unexpander greet]
def unexpGreet : Unexpander := fun stx =>
  match stx with
  | `(greet $x) => `("hello world")
  | _ => throw ()

-- #check の表示が上書きされて変わる
/- info: "hello world" : String -/
#check greet "Alice"

-- infoview における表示も変わってしまう
/-
trace: s : String
h : s = "hello world"
⊢ String
-/
example (s : String) (h : s = greet "Alice" ) : String := by
  trace_state

  exact "hello"

-- #eval の表示は変わらない
/- info: "Hello, Alice!" -/
#eval greet "Alice"

使用例

集合の内包表記

macro_rules コマンドで定義した集合の内包記法の表示のされ方を制御する例を挙げます。

/-- α を全体集合として、その部分集合の全体。
α の部分集合と α 上の述語を同一視していることに注意。 -/
def Set (α : Type) := α → Prop

/-- 述語 `p : α → Prop` に対応する集合 -/
def setOf {α : Type} (p : α → Prop) : Set α := p


-- ## 集合の内包表記

/-- 内包表記 `{ x : α | p x }` の `x : α` の部分のための構文。
`: α` の部分はあってもなくてもよいので `( )?` で囲っている。-/
syntax extBinder := ident (" : " term)?

/-- 内包表記 `{ x : α | p x }` の `{ | }` の部分のための構文。 -/
syntax (name := setBuilder) "{" extBinder " | " term "}" : term

/-- 内包表記の意味をマクロとして定義する -/
macro_rules
  | `({ $x:ident : $type | $p }) => `(setOf (fun ($x : $type) => $p))
  | `({ $x:ident | $p }) => `(setOf (fun ($x : _) => $p))

-- 内包表記が使えるようになったが、#check コマンドの出力では
-- いま定義した記法が使われないという問題がある
/- info: setOf fun n => ∃ m, n = 2 * m : Set Nat -/
#check {n : Nat | ∃ m, n = 2 * m}


open Lean PrettyPrinter in

/-- #check コマンドの出力でも内包表記を使用するようにする -/
@[app_unexpander setOf]
def setOf.unexpander : Unexpander := fun stx =>
  match stx with
  | `($_ fun $x:ident => $p) => `({ $x:ident | $p })
  | `($_ fun ($x:ident : $ty:term) => $p) => `({ $x:ident : $ty:term | $p })
  | _ => throw ()


-- ## app_unexpander のテスト

/- info: {n | ∃ m, n = 2 * m} : Set Nat -/
#check {n | ∃ m, n = 2 * m}

/- info: {n | ∃ m, n = 2 * m} : Set Nat -/
#check {n : Nat | ∃ m, n = 2 * m}

リストリテラル

List 型の項を作る構文である、リストリテラルを自作して、リストリテラルの表示を調整する例を挙げます。

/-- 自前で定義した`List` -/
inductive MyList (α : Type) where
  /-- 空のリスト -/
  | nil
  /-- リストの先頭に要素を追加する -/
  | cons (head : α) (tail : MyList α)
deriving DecidableEq

/-- 空の`MyList` -/
notation:max "⟦⟧" => MyList.nil

/-- `MyList`に要素を追加する -/
infixr:70 " ::: " => MyList.cons

/-- 自作のリストリテラル構文。なお末尾のカンマは許可される -/
syntax "⟦" term,*,? "⟧" : term

macro_rules
  | `(⟦ ⟧) => `(⟦⟧)
  | `(⟦ $x ⟧) => `($x ::: ⟦⟧)
  | `(⟦ $x, $xs,* ⟧) => `($x ::: (⟦ $xs,* ⟧))

-- 構文は正しく動作しているが、`#check` コマンドの出力に構文が反映されていない
/- info: 1 ::: 2 ::: 3 ::: ⟦⟧ : MyList Nat -/
#check ⟦1, 2, 3⟧

namespace MyList

  open Lean PrettyPrinter

  @[app_unexpander MyList.cons]
  def unexpandCons : Unexpander := fun stx =>
    match stx with
    | `($(_) $head $tail) =>
      match tail with
      | `(⟦⟧) => `(⟦ $head ⟧)
      | `(⟦ $xs,* ⟧) => `(⟦ $head, $xs,* ⟧)
      | `(⋯) => `(⟦ $head, $tail ⟧)
      | _ => throw ()
    | _ => throw ()

  /- info: ⟦1, 2, 3⟧ : MyList Nat -/
  #check ⟦1, 2, 3⟧

  /- info: ⟦1⟧ : MyList Nat -/
  #check ⟦1, ⟧

end MyList

cases_eliminator

[cases_eliminator] 属性は、cases タクティクで場合分けをした際の枝を変更します。

より詳しくいうと、cases タクティクの using キーワードのデフォルトの引数を変更することができます。デフォルトでは、帰納型 T に対して T.casesOn という定理が自動生成されてそれが暗黙の裡に using キーワードの引数として使われますが、[cases_eliminator] 属性で別な定理を指定すると、それが使われるようになります。

/-- 遅延評価のリストもどき -/
inductive Many (α : Type u) where
  | none
  | more (x : α) (xs : Unit → Many α)

example (xs : Many α) : True := by
  cases xs with
  | none => trivial
  | more x xs =>
    -- xs の型が Unit → Many α になっており、
    -- 必要なとき毎回 xs () で取り出さなければならず面倒
    guard_hyp xs : Unit → Many α
    trivial

/-- Unit が登場しないように工夫した関数 -/
def Many.cons (x : α) (xs : Many α) : Many α :=
  .more x (fun () => xs)

-- Many を定義したときに自動生成される定理
/-
info: Many.casesOn.{u_1, u} {α : Type u} {motive : Many α → Sort u_1} (t : Many α) (none : motive Many.none)
  (more : (x : α) → (xs : Unit → Many α) → motive (Many.more x xs)) : motive t
-/
#check Many.casesOn

/-- Many.casesOn の more を cons に置き換えたバージョン。
この定理に `[cases_eliminator]` 属性を与えることで、
`casesOn` の代わりにこれがデフォルトで使われるようになる。 -/
@[cases_eliminator]
protected def Many.cons_casesOn {motive : Many α → Sort u} (t : Many α) (none : motive Many.none)
    (cons : (a : α) → (b : Many α) → motive (Many.cons a b)) : motive t := by
  match t with
  | .none => assumption
  | .more x xs =>
    exact cons x (xs ())

example (xs : Many α) : True := by
  cases xs

  case none => trivial

  -- case で分割したときのデフォルトのコンストラクタが cons に変わる
  case cons x xs =>
    -- xs の型が Many α になっている!
    guard_hyp xs : Many α
    trivial

デフォルトの挙動に戻すには、using キーワードに .casesOn を渡します。

example (x : Many α) : True := by
  cases x using Many.casesOn
  case none => trivial
  case more x => trivial

なお rcases には影響しません。

example (xs : Many α) : True := by
  rcases xs with _ | _

  case none => trivial

  case more _ xs =>
    -- xs の型が Unit → Many α になっている
    guard_hyp xs : Unit → Many α
    trivial

coe

[coe] 属性は、特定の関数を型強制を行う関数として登録し、 記号で表示されるようにします。

/-- 自前で定義した自然数 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

-- `MyNat.succ` を(意味をなしていないが)型強制として登録する
attribute [coe] MyNat.succ

-- `MyNat.succ` ではなく `↑` と表示されるようになった
/- info: ↑MyNat.zero : MyNat -/
#check (MyNat.succ .zero : MyNat)

また、[coe] 属性は norm_cast タクティクとも関係があります。

用途

型強制を Coe 型クラスで登録しただけでは、必ずしも型強制を行う関数が 記号で表示されるわけではありません。[coe] 属性を付与することにより、より「型強制らしく」表示されるようになります。

例として、以下に Quotient によって構成した整数への変換を挙げます。

/-- 自然数の組 -/
def IntBase := Nat × Nat

/-- 自然数の組に対する同値関係 -/
def IntBase.equiv : IntBase → IntBase → Prop :=
  fun (a₁, b₁) (a₂, b₂) => a₁ + b₂ = b₁ + a₂

/-- `IntBase` 上の同値関係として `IntBase.equiv` を登録する -/
instance IntBase.sequiv : Setoid IntBase where
  r := IntBase.equiv
  iseqv := by
    constructor
    case refl =>
      intro ⟨x, y⟩
      dsimp [IntBase.equiv]
      ac_rfl
    case symm =>
      intro ⟨x, y⟩ ⟨x', y'⟩ h
      dsimp [IntBase.equiv] at *
      omega
    case trans =>
      intro ⟨x, y⟩ ⟨x', y'⟩ ⟨x'', y''⟩ hxy hyz
      dsimp [IntBase.equiv] at *
      omega

/-- 整数を表す型 -/
abbrev myInt := Quotient IntBase.sequiv

/-- 自然数を `myInt` に変換する -/
def myInt.ofNat (n : Nat) : myInt := Quotient.mk' (n, 0)

/-- `Nat → myInt` という型強制 -/
instance : Coe Nat myInt where
  coe := myInt.ofNat

-- この時点では、`myInt.ofNat` が型強制として表示されない
/- info: myInt.ofNat Nat.zero : myInt -/
#check (Nat.zero : myInt)

-- `[coe]` 属性を付与する
attribute [coe] myInt.ofNat

-- `myInt.ofNat` ではなく `↑` と表示されるようになった!
/- info: ↑Nat.zero : myInt -/
#check (Nat.zero : myInt)

command_elab

[command_elab] 属性は、コマンドの実装である CommandElab 型の関数とコマンドの構文を結び付け、コマンドとして動作するようにします。

import Lean

open Lean Elab Command in

/-- 挨拶をするコマンド -/
syntax (name := helloCommand) "#hello" : command

open Lean Elab Command in

def evalHello : CommandElab := fun _stx => do
  let msg := s!"Hello, Lean!"
  logInfo msg

-- 実装がないので #hello コマンドはまだ使えない
/- error: elaboration function for `helloCommand` has not been implemented -/
#hello

-- 属性を使って実装と構文を紐づける
attribute [command_elab helloCommand] evalHello

-- コマンドが使えるようになった
/- info: Hello, Lean! -/
#hello

csimp

[csimp] 属性は、コンパイラに単純化を指示します。

A = B という形の定理に付与することでコンパイラに A の計算を B の計算に置き換えさせることができます。非効率な関数を効率的な実装に置き換えるために使用されます。

/-- フィボナッチ数列の非効率な実装 -/
def fibonacci : Nat → Nat
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci n + fibonacci (n+1)

-- `#eval fibonacci 32` は1秒以上かかる
#eval fibonacci 32

/-- フィボナッチ数列のより高速な実装 -/
def fib (n : Nat) : Nat :=
  (loop n).1
where
  -- ヘルパー関数
  loop : Nat → Nat × Nat
    | 0 => (0, 1)
    | n + 1 =>
      let p := loop n
      (p.2, p.1 + p.2)

/-- `fib` は `fibonacci` と同じ漸化式を満たす -/
theorem fib_add (n : Nat) : fib (n + 2) = fib n + fib (n + 1) := by rfl

/-- `fibonacci` と `fib` は同じ結果を返す。
`[csimp]` 属性を与えることで、`fibonacci` の計算を `fib` の計算に置き換えることができる。-/
@[csimp]
theorem fib_eq_fibonacci : fibonacci = fib := by
  ext n
  induction n using fibonacci.induct
  case case1 => rfl
  case case2 => rfl
  case case3 n ih1 ih2 =>
    rw [fib_add, fibonacci]
    simp [ih1, ih2]

-- `fibonacci` の計算が1秒以内に終わるようになった
#eval fibonacci 32
#eval fibonacci 132

注意: [csimp] 属性による公理の隠蔽

[csimp] 属性が付与された定理の証明にどんな公理を使用しているかを、#print axioms コマンドで追跡することはできません。

def one := 1
def two := 2

/-- 何かの公理 -/
axiom my_axiom : False

@[csimp] theorem one_eq_two : one = two := by
  exact my_axiom.elim

theorem false_theorem : 1 = 2 := by
  rw [show 1 = one from rfl]
  native_decide

-- `my_axiom` に依存しているはずだが、`native_decide` により導入された別の公理の陰に隠れて見えなくなっている
/- info: 'false_theorem' depends on axioms: [false_theorem._native.native_decide.ax_1_1] -/
#print axioms false_theorem

default_instance

[default_instance] 属性は、型クラス解決が(型情報の不足などで)上手くいかない場合に、最後の手段としてインスタンス解決に使われるインスタンスを指定します。

-- `Int` は `Mul` 型クラスのインスタンスで、
-- インスタンス名は `Int.instMul`
/- info: Int.instMul -/
#synth Mul Int

-- メタ変数名を表示しない
set_option pp.mvars false

-- エラー。`x, y` の型がわからないので `x * y` をどう解釈すればいいか Lean はわからない。
#check_failure fun y => (fun x => x * y)

-- `y` の型を明示的に与えればエラーは消える
#check fun (y : Nat) => (fun x => x * y)

-- `Mul` のインスタンスを探して見つからなかった際に `Int.instMul` を使うように指定する
attribute [default_instance] Int.instMul

-- エラーが消えた!
#check fun y => (fun x => x * y)

用途

典型的な使用方法は、OfNat 型クラスの(型を指定しない場合の)解釈のされ方を変更することでしょう。

/-- 自然数の対 -/
def NatPair := Nat × Nat

/-- `(0, 0)` を単に `0 : NatPair` と書けるようにする -/
instance instNatPair : OfNat NatPair 0 := ⟨(0, 0)⟩

-- 期待される型を指定しない場合、数値リテラルは `Nat` の項であると解釈される
/- info: 0 : Nat -/
#check 0

-- 期待される型を指定しなければ、数値リテラルを `NatPair` の項として解釈する
attribute [default_instance] instNatPair

-- `NatPair` の項として解釈されるようになった!
/- info: 0 : NatPair -/
#check 0

-- 期待される型を指定したときの挙動は変わらない。
-- 相変わらず `Nat` の項として解釈される
/- info: 0 : Nat -/
#check (0 : Nat)

インスタンス優先度との違い

インスタンス優先度(priority) という機能もあります。これも型クラス解決の優先度に関係する機能ですが、[default_instance] 属性とは挙動が異なります。Lean はインスタンス解決の際にまずインスタンス優先度の高いものから試し、すべて失敗した場合にのみ [default_instance] 属性を参照します。つまり、考慮される順番が異なるということです。

/-- ⋄ 記法のための型クラス -/
class Diamond (α : Type) where
  dia : α

notation "⋄" => Diamond.dia

/-- `⋄ : Nat` が `0` を表すものとする -/
instance instDiaZero : Diamond Nat where
  dia := 0

/-- `⋄ : Nat` が `1` を表すものとする -/
instance instDiaOne : Diamond Nat where
  dia := 1

-- 二つのインスタンスが衝突しているが、
-- 後に定義された方が優先される
#guard (⋄ : Nat) = 1

-- `0` と解釈する方をデフォルトにする
attribute [default_instance] instDiaZero

-- デフォルトインスタンスにしても効果がなく、相変わらず `1` の方が優先される!
-- これは、期待される型がわかっていて型クラス解決が早期に成功するから
#guard (⋄ : Nat) = 1

-- 期待される型が不明な状況であれば、
-- デフォルトインスタンスが使用される
/- info: 0 -/
#eval ⋄

/-- `⋄ : Nat` が `2` を表すものとする。優先度を `high` にしておく -/
instance (priority := high) instDiaTwo : Diamond Nat where
  dia := 2

/-- `⋄ : Nat` が `3` を表すものとする -/
instance instDiaThree : Diamond Nat where
  dia := 3

-- `2` と解釈する方が優先されるようになった!
#guard (⋄ : Nat) = 2

elab_as_elim

[elab_as_elim] 属性を付与すると、除去子(eliminator)としてエラボレートされるようになります。

/-- 標準の再帰子である`Nat.rec`を真似て作った再帰子もどき -/
def Nat.rec' {motive : Nat → Sort u}
    (zero : motive 0) (succ : ∀ n, motive n → motive (n + 1))
    (t : Nat) : motive t := Nat.rec zero succ t

-- メタ変数を具体的に表示しない
set_option pp.mvars false in

-- 何も付けないとこういうエラーメッセージ
/-
error: Function expected at
  Nat.rec' ?_ (fun x b => b + 1) ?_
but this term has type
  ?_ ?_

Note: Expected a function because this term is being applied to the argument
  a
-/
def add (a b : Nat) := Nat.rec' _ (zero := b) (succ := fun _ b => b + 1) a

-- `elab_as_elim` 属性を付与する
attribute [elab_as_elim] Nat.rec'

-- 「eliminatorがエラボレートできません」というエラーメッセージに変わる。
-- eliminatorだと認識されている!
/- error: failed to elaborate eliminator, expected type is not available -/
def add (a b : Nat) := Nat.rec' _ (zero := b) (succ := fun _ b => b + 1) a

implemented_by

[implemented_by] は、仕様(満たしてほしい性質)と実装を分離するための属性です。

[implemented_by] 属性を使うと、実行時の実装を証明なしに置き換えてしまうことができます。

def bar := "world"

-- 実行する際には`bar`を参照してくださいとLeanに指定している
@[implemented_by bar]
def foo := "hello"

/- info: "world" -/
#eval foo

[implemented_by] で置換されるのは実行時の実装であって、証明時のふるまいは変わりません。

-- 証明においては`foo`は`"hello"`のまま
example : foo = "hello" := rfl

native_decide を使用するなどしてコンパイラを信頼することにすれば、置換した内容を証明に利用することもできます。 しかし、当然ながらこれによって証明の健全性は保証されません。

example : foo = "world" := by
  native_decide

使用例

典型的な使用場面として、「unsafe だが高速なコード」を安全に使用したい場面というのがあります。 たとえば、2分木を考えます。1

/-- 2分木 -/
inductive Tree (α : Type) where
  /-- 空の木 -/
  | empty
  /-- 左右の部分木とノードの値から、新しい木を得る -/
  | branch (left : Tree α) (value : α) (right : Tree α)

2分木t₁ t₂に対して、t₁ == t₂ であるかどうかを判定する関数を考えます。 再帰的に判定すればできるのですが、t₁t₂ のメモリアドレスが等しい場合にはそんなことをするのはムダになります。 そこで、次のような関数を定義します。

/-- 高速だが unsafe な BEq の実装 -/
unsafe def Tree.fastBEq {α : Type} [BEq α] (t₁ t₂ : Tree α) : Bool :=
  -- メモリアドレスが等しいならば、同じ木である
  if ptrEq t₁ t₂ then
    true
  else
    -- それ以外の場合は再帰的に判定する
    match t₁, t₂ with
    | .empty, .empty => true
    | .branch l₁ v₁ r₁, .branch l₂ v₂ r₂ =>
      v₁ == v₂ && fastBEq l₁ l₂ && fastBEq r₁ r₂
    | _, _ => false

しかし、ptrEqunsafe なので、Tree.fastBEq 全体が unsafe になってしまいます。 [implemented_by] 属性を使うと、実行時には Tree.fastBEq を使うというのは維持したまま、証明時には安全な定義を使うことができます。

@[implemented_by Tree.fastBEq]
def Tree.beq {α : Type} [BEq α] (t₁ t₂ : Tree α) : Bool :=
  match t₁, t₂ with
  | .empty, .empty => true
  | .branch l₁ v₁ r₁, .branch l₂ v₂ r₂ =>
    v₁ == v₂ && Tree.beq l₁ l₂ && Tree.beq r₁ r₂
  | _, _ => false

instance {α : Type} [BEq α] : BEq (Tree α) where
  beq := Tree.beq

  1. このコード例と解説は、The Lean Language Reference を参考にしています。

induction_eliminator

[induction_eliminator] 属性は、帰納法の枝を変更することを可能にします。

より詳しくいうと、induction タクティクの using キーワードのデフォルトの引数を変更することができます。デフォルトでは、帰納型 T に対して T.rec (および T.recOn )という定理が自動生成されてそれが暗黙の裡に using キーワードの引数として使われますが、[induction_eliminator] 属性で別な定理を指定すると、それが使われるようになります。

/-- 遅延評価のリストもどき -/
inductive Many (α : Type u) where
  | none
  | more (x : α) (xs : Unit → Many α)

/-- Many に対する合併関数 -/
def Many.union : Many α → Many α → Many α
  | .none, ys => ys
  | .more x xs, ys => Many.more x (fun () => union (xs ()) ys)

-- Many に関する定理を帰納法で示す例
example (xs : Many α) : Many.union xs Many.none = xs := by
  induction xs with
  | none => rfl
  | more x xs ih =>
    -- ここで xs の型は Unit → Many α になっている
    -- 必要なとき毎回 xs () で取り出さなければならず面倒
    guard_hyp xs : Unit → Many α

    simp [Many.union, ih]

/-- Unit が登場しないように工夫した関数 -/
def Many.cons (x : α) (xs : Many α) : Many α :=
  .more x (fun () => xs)

-- Many を定義したときに自動生成される定理
/-
info: Many.rec.{u_1, u} {α : Type u} {motive : Many α → Sort u_1} (none : motive Many.none)
  (more : (x : α) → (xs : Unit → Many α) → ((a : Unit) → motive (xs a)) → motive (Many.more x xs)) (t : Many α) :
  motive t
-/
#check Many.rec

-- Many.rec の `Many.more` の部分を `Many.cons` に置き換えた定理を作る。
-- これに `[induction_eliminator]` 属性を与えることで、
-- コンストラクタ `Many.more` の代わりに `Many.cons` が使えるようになる
@[induction_eliminator]
protected def Many.cons_rec {motive : Many α → Sort u}
  (none : motive Many.none)
  (cons : (a : α) → (b : Many α) → (motive b) → motive (Many.cons a b))
    : (t : Many α) → motive t
  | .none => none
  | .more x xs => cons x (xs ()) (Many.cons_rec none cons (xs ()))

example (xs : Many α) : Many.union xs Many.none = xs := by
  induction xs with
  | none => rfl
  | cons x xs ih =>
    -- ここで xs の型が Many α になっている!
    -- 毎回 `xs ()` などと書く必要がなくなった
    guard_hyp xs : Many α

    simp [Many.union, Many.cons, ih]

[induction_eliminator] を設定する前の挙動に戻すには、using キーワードに明示的に .rec 定理を与えます。

example (xs : Many α) : True := by
  -- 明示的に指定すれば, 元の挙動に戻せる
  induction xs using Many.rec with
  | none => trivial
  | more _ xs _ =>
    -- 元のように Many.more が使われる
    guard_hyp xs : Unit → Many α

    trivial

inherit_doc

[inherit_doc] 属性を指定すると、既存の定数などのドキュメントコメントを使いまわすことができます。

import Lean

/-- 最初に与えた doc コメント -/
def greet := "hello"

-- `greet` のドキュメントコメントを引き継ぐ
@[inherit_doc greet] abbrev greet' := greet

open Lean Elab Command in

/-- ドキュメントコメントを取得して表示するコマンド -/
elab "#doc " x:ident : command => do
  let name := x.getId
  if let some s ← findDocString? (← getEnv) name then
    logInfo m!"{s}"

/- info: 最初に与えた doc コメント -/
#doc greet'

[inherit_doc] 属性を使用するのは、記法を導入する際であることが多いでしょう。この場合、ドキュメントコメントの継承元を指定する必要がありません。

/-- `⊔` という記号のための型クラス -/
class Sup (α : Type) where
  /-- 最小上界、上限 -/
  sup : α → α → α

@[inherit_doc] infixl:68 " ⊔ " => Sup.sup

irreducible

[irreducible] 属性を付与すると、その名前が定義本体に 定義的に等しい(definitionally equal) ということが隠されて、rfl タクティクや dsimp タクティクが通らなくなります。

/-- 階乗関数 -/
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | m + 1 => (m + 1) * factorial m

example : factorial 5 = 120 := by
  -- 最初は rfl が通る
  rfl

example : factorial 5 = 120 := by
  -- dsimp も通る
  dsimp [factorial]

-- [irreducible]属性を与える
attribute [irreducible] factorial

example : factorial 5 = 120 := by
  -- rfl が通らなくなる
  fail_if_success rfl
  -- dsimp も通らない
  fail_if_success dsimp [factorial]

  cbv

-- 補足: 名前を定義に展開しなくてもわかる等式は、引き続き rfl で示せる
example : factorial 5 = factorial 5 := by
  rfl

なお unfold タクティクは [irreducible] 属性が付与されていても使えます。

example : factorial 5 = 120 := by
  repeat unfold factorial
  rfl

また、名前からは #reduce コマンドが使えなくなるような印象を受けますが、#reduce は相変わらず使用可能です。

-- `#reduce` は相変わらずできる
/- info: 6 -/
#reduce factorial 3

用途

ユーザが明確に、自分で定義した関数・定数に対して [irreducible] 属性を付与すべき場面はそんなに多くないと筆者は思います。 Lean は整礎再帰な関数partial_fixpoint により修飾された関数に対して自動的に [irreducible] 属性を付与するのですが、おそらくこの属性を見かけるのはそういう場面が多いでしょう。

#print コマンドを使うと裏で属性が付与されている様子を確認することができます。

def searchF {α : Type} (f : Nat → Option α) (start : Nat) : Option Nat :=
  match f start with
  | some _ => some start
  | none => searchF f (start + 1)
partial_fixpoint

-- `partial_fixpoint` を使ったので irreducible になっている
/-
info: @[irreducible] def searchF : {α : Type} → (Nat → Option α) → Nat → Option Nat :=
fun {α} f =>
  Lean.Order.fix
    (fun f_1 start =>
      match f start with
      | some val => some start
      | none => f_1 (start + 1))
    ⋯
-/
#print searchF

/-- 文字列の左側を指定文字で埋めて、指定長にそろえる(整礎再帰バージョン)-/
def String.padLeftWF (input : String) (padChar : Char) (length : Nat) : String :=
  if input.length ≥ length then
    input
  else
    String.padLeftWF (padChar.toString ++ input) padChar length
termination_by length - input.length

-- 整礎再帰を使ったので `[irreducible]` になっている
/-
info: @[irreducible] def String.padLeftWF : String → Char → Nat → String :=
fun input padChar length => String.padLeftWF._unary padChar length input
-/
#print String.padLeftWF

macro_inline

[macro_inline] 属性は、関数に付与することでその関数をマクロのように展開させることができます。これにより、関数の引数の評価が、関数の評価前からそれが最初に使われた時点に延期されます。

用途

多くの場合、[macro_inline] 属性は短絡評価(short-circuit evaluation)を実装するのに使われます。短絡評価とは、「引数を全て評価するまでもなく値がわかるときには、残りの引数を評価しない」という評価戦略のことです。この評価戦略が実装されている典型的な関数として、論理演算子 &&|| が挙げられます。

-- 第二引数は評価されていない。
/- info: false -/
#eval false && (dbg_trace "hello"; true)

-- 第二引数は評価されていない。
/- info: true -/
#eval true || (dbg_trace "hello"; true)

実際に Bool.and(つまり &&)を真似て関数を自作してみて、[macro_inline] 属性の挙動を確認してみましょう。

/-- `Bool.and` を真似て自作した関数
(わざと冗長な定義を採用している) -/
def Bool.myAnd (a b : Bool) : Bool :=
  match a, b with
  | false, _ => false
  | _, false => false
  | _, _ => true

-- 第二引数が評価されており、短絡評価になっていない
/-
info: hello
---
info: false
-/
#eval Bool.myAnd false (dbg_trace "hello"; true)

-- `[macro_inline]` 属性を付与する
attribute [macro_inline] Bool.myAnd

-- 短絡評価になった!!
/- info: false -/
#eval Bool.myAnd false (dbg_trace "hello"; true)

macro

[macro] 属性は、マクロの実装である Macro 型の関数とマクロの構文を結び付け、マクロとして動作するようにします。

import Lean

open Lean

syntax:10 (name := lxor) term:10 " LXOR " term:11 : term

def expandLxor : Macro := fun stx =>
  match stx with
  | `($l:term LXOR $r:term) => `(term| !$l && $r)
  | _ => Macro.throwUnsupported

-- 解釈不能というエラーになる
#check_failure true LXOR false

-- マクロとして登録する
attribute [macro lxor] expandLxor

-- マクロ展開されるので、`!true && false` になる
#guard (true LXOR false) = false

[macro] 属性は低レベルな機能です。多くの用途では macro_rules コマンドや macro コマンドで用が足りることでしょう。

match_pattern

[match_pattern] 属性を付与すると、元々コンストラクタしか使用できない match 式でのパターンマッチの枝に、指定した関数を使うことができるようになります。

/-- 自前で定義したリスト -/
inductive MyList (α : Type u) where
  | nil
  | cons (head : α) (tail : MyList α)

/-- MyList の `cons` コンストラクタに対するラッパー。中身は同じ -/
def MyList.myCons (a : α) (as : MyList α) : MyList α :=
  MyList.cons a as

-- 最初は `match` の中で `MyList.myCons` を使うことはできない
/- error: Invalid pattern: Expected a constructor or constant marked with `[match_pattern]` -/
def badLength : MyList α → Nat
  | MyList.nil => 0
  | MyList.myCons _ as => 1 + badLength as

-- `[match_pattern]` 属性を付与する
attribute [match_pattern] MyList.myCons

-- これで `match` の中で `MyList.myCons` を使うことができる
def goodLength : MyList α → Nat
  | MyList.nil => 0
  | MyList.myCons _ as => 1 + goodLength as

simp

[simp] 属性を定理に付与すると、simp タクティクによって単純化ルールとして使用されるようになります。

/-- 標準の`Nat`を真似て自作した型 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

/-- `MyNat`上の足し算 -/
def MyNat.add (m n : MyNat) : MyNat :=
  match n with
  | .zero => m
  | .succ n => succ (add m n)

instance : Add MyNat where
  add := MyNat.add

instance : Zero MyNat where
  zero := MyNat.zero

theorem MyNat.add_zero (n : MyNat) : n + 0 = n := by
  rfl

-- 最初は`simp`で示すことができない
example (n : MyNat) : (n + 0) + 0 = n := by
  fail_if_success simp

  rw [MyNat.add_zero n]
  rw [MyNat.add_zero n]

-- `[simp]`属性を付与する
attribute [simp] MyNat.add_zero

-- `simp`で示せるようになった!
example (n : MyNat) : (n + 0) + 0 = n := by
  simp

関数に付与した場合

[simp] 属性は定理だけでなく関数定義などにも付与することができます。関数定義に付与した場合、その関数について定義からただちに従う関数等式が自動的に見出だされて simp 補題として登録されます。

/-- フィボナッチ数列 -/
def Nat.fib (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => Nat.fib (n + 1) + Nat.fib n

-- `rfl` ですぐ示せる関数等式があるが、`simp` では示せない
example : Nat.fib 0 = 0 := by
  fail_if_success simp
  rfl

example : Nat.fib 1 = 1 := by
  fail_if_success simp
  rfl

example (n : Nat) : Nat.fib (n + 2) = Nat.fib (n + 1) + Nat.fib n := by
  fail_if_success simp
  rfl

-- `Nat.fib` に対する `simp` 補題を生成する
attribute [simp] Nat.fib

-- `simp` で示せるようになる!
example : Nat.fib 0 = 0 := by
  simp

example : Nat.fib 1 = 1 := by
  simp

example (n : Nat) : Nat.fib (n + 2) = Nat.fib (n + 1) + Nat.fib n := by
  simp

simp 補題の優先順位

simp 補題による単純化は、おおむね「内側から外側へ」という順序で行われます。 これはオプションでsimpのトレース情報を出力させてみると確認できます。

/-- フィボナッチ数列 -/
def Nat.fib (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => Nat.fib (n + 1) + Nat.fib n

@[simp]
theorem Nat.fib_zero : Nat.fib 0 = 0 := by
  rfl

-- `simp`の行った書き換えを追跡する
set_option trace.Meta.Tactic.simp.rewrite true in

-- 内側から外側へという順序で単純化を行っていることがわかる。
/-
trace: [Meta.Tactic.simp.rewrite] Nat.fib_zero:1000:
      Nat.fib 0
    ==>
      0
[Meta.Tactic.simp.rewrite] Nat.add_zero:1000:
      0 + 0
    ==>
      0
[Meta.Tactic.simp.rewrite] Nat.mul_zero:1000:
      37 * 0
    ==>
      0
[Meta.Tactic.simp.rewrite] eq_self:1000:
      0 = 0
    ==>
      True
-/
example : 37 * (Nat.fib 0 + 0) = 0 := by
  simp

[simp←]

simp 補題は通常「左辺を右辺に」単純化するために使用されますが、逆方向に使用したい場合は [simp←] とします。

/-- モノイド -/
class Monoid (α : Type u) extends Mul α, One α where
  mul_one : ∀ a : α, a * 1 = a
  one_mul : ∀ a : α, 1 * a = a
  mul_assoc : ∀ a b c : α, (a * b) * c = a * (b * c)

variable [Monoid α]

@[simp←]
theorem mul_one_rev (a : α) : a = 1 * a := by
  rw [Monoid.one_mul a]

-- `simp?` で確認してみると、該当する補題に `←` が付与されている
/-
info: Try this:
  [apply] simp only [← mul_one_rev]
-/
example (a : α) : 1 * (1 * a) = a := by
  simp?

[simp↓]

simp はデフォルトでは部分式をすべて単純化した後に全体の式に単純化を適用しています。simp タクティクに、部分式が単純化されるよりも先に前処理として単純化したいルールがある場合は、[simp↓] を使用します。

/-- 整数のような何か -/
opaque MyInt : Type

variable [LE MyInt] [Add MyInt] [Zero MyInt]

@[simp]
axiom MyInt.add_zero (a : MyInt) : a + 0 = a

@[simp]
axiom MyInt.simp_le_add (a b c : MyInt) : a + b ≤ a + c ↔ b ≤ c


example (a b : MyInt) (h : a + 0 ≤ a + b) : 0 ≤ b := by
  simp at h

  -- `simp` により `add_zero` が先に適用されてしまう
  -- 本当は `simp_le_add` が先に適用されて欲しい…
  guard_hyp h : a ≤ a + b

  sorry

-- `[simp↓]` 属性を付与する
attribute [simp↓] MyInt.simp_le_add

-- `simp`の行った書き換えを追跡する
set_option trace.Meta.Tactic.simp.rewrite true

/-
trace: [Meta.Tactic.simp.rewrite] ↓ MyInt.simp_le_add:1000:
      a + 0 ≤ a + b
    ==>
      0 ≤ b
-/
example (a b : MyInt) (h : a + 0 ≤ a + b) : 0 ≤ b := by
  -- ちゃんと `simp_le_add` が先に適用されるようになった!
  simp at h
  assumption

優先度指定

simp 補題の中でも早い段階で適用してほしい補題や、遅い段階で適用してほしい補題があるとき、優先度を指定することができます。

simp high

[simp high] とすると優先度が高まり、他の simp 補題よりも先に適用されるようになります。

/-- 謎の自然数 -/
opaque foo : Nat

/-- 謎の自然数 -/
opaque bar : Nat

/-- 謎の自然数 -/
opaque baz : Nat

@[simp]
axiom foo_eq_bar : foo = bar

@[simp]
axiom foo_eq_baz : foo = baz


example : foo = baz := by
  -- 先に宣言された方の `foo_eq_bar` が適用されてしまって、
  -- 証明することができない
  simp

  guard_target =ₛ bar = baz

  sorry

attribute [simp high] foo_eq_baz

example : foo = baz := by
  -- `foo_eq_baz` の方が優先度が高いため、こちらが先に適用される
  simp

simp low

[simp low] とすると優先度が低くなり、他の simp 補題よりも後に適用されるようになります。

/-- 謎の自然数 -/
opaque foo : Nat

/-- 謎の自然数 -/
opaque bar : Nat

/-- 謎の自然数 -/
opaque baz : Nat

@[simp]
axiom foo_eq_bar : foo = bar

@[simp]
axiom foo_eq_baz : foo = baz


example : foo = baz := by
  -- 先に宣言された方の `foo_eq_bar` が適用されてしまって、
  -- 証明することができない
  simp

  guard_target =ₛ bar = baz

  sorry

-- `foo_eq_bar` の優先度を下げる
attribute [simp low] foo_eq_bar

example : foo = baz := by
  -- `foo_eq_baz` の方が優先度が高いため、こちらが先に適用される
  simp

simps

補題を simp で使えるようにするのは [simp] 属性を付与することで可能ですが、[simps] 属性(または @[simps] タグ)を利用すると simp で使用するための補題を自動的に生成してくれます。

例えば、ユーザが Point という構造体を定義し、Point 上の足し算を定義したところを考えましょう。このとき、足し算はフィールドの値の足し算で定義されているため、「Point の和の x 座標」は x 座標の和ですが、これはそのままでは simp で示すことができません。[simps] 属性を Point.add 関数に付与することで、simp で示せるようになります。

import Mathlib.Tactic.Simps.Basic -- [simps] 属性を使うため

@[ext]
structure Point where
  x : Int
  y : Int

/-- Point の和 -/
def Point.add (p q : Point) : Point :=
  { x := p.x + q.x, y := p.y + q.y }

/-- 和の x 座標は x 座標の和 -/
example (a b : Point) : (Point.add a b).x = a.x + b.x := by
  -- この状態だと `simp` で示せない
  fail_if_success simp

  rfl

-- `Point.add` に `[simps]` 属性を付与する
attribute [simps] Point.add

example (a b : Point) : (Point.add a b).x = a.x + b.x := by
  -- simp 補題が自動的に生成されて、simp で示せるようになった
  simp

上記の例では attribute コマンドで属性を付与していますが、タグも使用できます。

@[simps]
def Point.sub (p q : Point) : Point :=
  { x := p.x - q.x, y := p.y - q.y }

example (a b : Point) : (Point.sub a b).x = a.x - b.x := by simp

@[simps?] に換えると、生成された補題を確認することができます。

/-
trace: [simps.verbose] The projections for this structure have already been initialized by a previous invocation of `initialize_simps_projections` or `@[simps]`.
    Generated projections for Point:
    Projection x: fun x => x.x
    Projection y: fun x => x.y
[simps.verbose] adding projection Point.mul_x:
      ∀ (p q : Point), (p.mul q).x = p.x * q.x
[simps.verbose] adding projection Point.mul_y:
      ∀ (p q : Point), (p.mul q).y = p.y * q.y
-/
@[simps?] def Point.mul (p q : Point) : Point :=
  { x := p.x * q.x, y := p.y * q.y }

tactic

[tactic] 属性は、タクティクの実装である Tactic 型の関数とタクティクの構文を結び付け、タクティクとして動作するようにします。

import Lean
import Qq
import Batteries.Tactic.Exact

/-- True というゴールだけを閉じる trivial タクティクの制限版 -/
syntax (name := trivialStx) "my_trivial" : tactic

open Lean Elab Tactic Qq in

def trivialImpl : Tactic := fun _stx => do
  -- 現在のゴールを取得する
  let goal : MVarId ← getMainGoal
  try
    -- ゴールが `True.intro` で閉じられるか試す
    goal.assignIfDefEq q(True.intro)
  catch _error =>
    -- 失敗した場合はゴールの型を取得してエラーメッセージを表示する
    let goalType ← goal.getType
    throwError "my_trivialタクティクが失敗しました。ゴールの型は`{goalType}`であって`True`ではありません。"

-- 未実装というエラーになってしまう
/- error: Tactic `trivialStx` has not been implemented -/
example : True := by
  my_trivial

-- [tactic] 属性を使って実装と構文を結びつける
attribute [tactic trivialStx] trivialImpl

-- タクティクとして使えるようになった!
example : True := by
  my_trivial

option

オプションは、set_option コマンドで設定することができ、主にデバッグに利用されます。

autoImplicit

autoImplicit オプションは、自動束縛暗黙引数(auto bound implicit arguments)という機能を制御します。類似オプションとして relaxedAutoImplicit があります。

有効にすると、宣言が省略された引数が1文字であるとき、それを暗黙引数として自動的に追加します。

-- `autoImplicit` が無効の時
set_option autoImplicit false in

-- `nonempty` の定義には `α` という未定義の識別子が含まれるため、エラーになる
/-
error: Unknown identifier `α`

Note: It is not possible to treat `α` as an implicitly bound variable here because the `autoImplicit` option is set to `false`.
-/
def nonempty : List α → Bool
  | [] => false
  | _ :: _ => true

-- `autoImplicit` が有効の時
set_option autoImplicit true in

-- `α` という未定義の識別子を含んでいてもエラーにならない。
-- 勝手に暗黙引数として追加されている
def head : List α → Option α
  | [] => none
  | x :: _ => some x

1文字の未束縛の識別子であればなんでも対象になるようです。

section autoImpl

-- `autoImplicit` が有効の時
set_option autoImplicit true

  -- ギリシャ文字ではなくて1文字の小文字でも暗黙引数として追加される
  def nonempty₂ : List a → Bool
    | [] => false
    | _ :: _ => true

  -- `ℱ` も暗黙引数になる
  def nonempty₃ : List ℱ → Bool
    | [] => false
    | _ :: _ => true

  -- 2文字の識別子は暗黙引数として追加されない
  /-
  error: Unknown identifier `AB`

  Note: It is not possible to treat `AB` as an implicitly bound variable here because it has multiple characters while the `relaxedAutoImplicit` option is set to `false`.
  -/
    def nonempty₄ : List AB → Bool
      | [] => false
      | _ :: _ => true

end autoImpl

hygiene

hygiene オプションは、マクロ衛生機能を有効にするかどうかを制御します。

プログラミング言語に対して、マクロが 衛生的(hygienic) であるとは、マクロ処理の過程で名前衝突の問題が発生しないことを指します。Lean のマクロはデフォルトで衛生的ですが、hygiene オプションは一時的にこれを無効にすることができます。

/-- 定数関数を定義するマクロ -/
macro "const" e:term : term => `(fun x => $e)

def x : Nat := 42
def y : Nat := 42

-- マクロ衛生がきちんと働いているときの挙動
#guard (const x) 0 = 42
#guard (const y) 0 = 42

-- マクロ衛生を保つ機能を無効にする
set_option hygiene false

/-- マクロ衛生が無効になった定数関数マクロ -/
macro "const'" e:term : term => `(fun x => $e)

-- 引数の値が同じでも、識別子の名前によって値が変わるようになってしまった。
-- これはマクロの中で使用されている変数名も `x` であるため。
#guard (const' x) 0 = 0
#guard (const' y) 0 = 42

タクティクにおけるマクロ衛生

タクティクで導入される識別子についても、実行時の環境にある識別子と衝突しないようにする機能が Lean にはあります。

macro "my_intro" : tactic => `(tactic| intro h)

example (P : Prop) : P → P := by
  my_intro

  -- `h : P` がマクロ展開によって導入されはするが、
  -- 死んでいるので使えない
  fail_if_success exact h

  assumption

-- マクロ衛生を保つ機能を無効にする
set_option hygiene false

macro "my_intro'" : tactic => `(tactic| intro h)

example (P : Prop) : P → P := by
  my_intro'

  -- `h : P` が使えてしまう
  exact h

また、(驚くべきことに)タクティクマクロの中で導入した識別子だけでなく、「参照した」識別子についても Lean が自動的に衝突を回避します。

/-- exfalso タクティクを真似て自作したタクティク -/
macro "my_exfalso" : tactic => `(tactic| apply False.elim)

namespace Foo

  /-- `False.elim` とめっちゃよく似た名前の紛らわしい定理 -/
  theorem False.elim : 1 + 1 = 2 := by rfl

  example (_h : False) : 1 + 1 = 2 := by
    -- 普通に `apply` すると上の紛らわしい定理の方が使われてしまう
    apply False.elim

    done

  example (h : False) : 1 + 1 = 2 := by
    -- 実行した環境ではなくて宣言した環境における `False.elim` が使われる。
    -- 紛らわしい方の定理は使われない!
    my_exfalso

    show False
    contradiction

end Foo

-- マクロ衛生を保つ機能を無効にする
set_option hygiene false

/-- マクロ衛生が無効になったバージョンの `my_exfalso` -/
macro "my_exfalso'" : tactic => `(tactic| apply False.elim)

namespace Bar

  /-- `False.elim` とめっちゃよく似た名前の紛らわしい定理 -/
  theorem False.elim : 1 + 1 = 2 := by rfl

  example (_h : False) : 1 + 1 = 2 := by
    -- 紛らわしい方の `False.elim` が使われてしまう。
    my_exfalso'

    done

end Bar

用途:定義すると同時に記法を使う

通常、notation コマンドなどによって導入された記法は定義の途中では使うことができません。定義が終了した後で記法を宣言するのが一般的です。

namespace Ordinary

  inductive Nat.myle (n : Nat) : Nat → Prop where
    | refl : myle n n
    | step {m : Nat} : myle n m → myle n (m + 1)

  scoped notation:50 a:50 " ≤ₘ " b:50 => Nat.myle a b

end Ordinary

もし定義の途中で記法を使いたければ、hygiene オプションと local 修飾子を利用して次のようにすれば可能です。

section
  set_option hygiene false

  -- 定義の間だけ有効になるように記法を一時的に与える
  local notation:50 a:50 " ≤ₘ " b:50 => Nat.myle a b

  inductive Nat.myle (n : Nat) : Nat → Prop where
    | refl : n ≤ₘ n
    | step {m : Nat} : n ≤ₘ m → n ≤ₘ m + 1

end

-- 改めて記法を付与する
notation:50 a:50 " ≤ₘ " b:50 => Nat.myle a b

-- 通常通り証明が行える
example {n m k : Nat} (hnm : n ≤ₘ m) (hmk : m ≤ₘ k) : n ≤ₘ k := by
  induction hmk with
  | refl => assumption
  | step hmk ih =>
    apply Nat.myle.step
    assumption

linter.flexible

linter.flexible オプションは、ライブラリの変更に対して頑強な証明を作るためのリンタの有効/無効を切り替えます。

背景

証明を書く際のベストプラクティスとして、simp タクティクのような柔軟性のあるタクティクは、証明の末尾以外で使わないほうがよいということが知られています。1 simp タクティクはどういう補題が [simp] 属性で登録されているかに応じて挙動が変わるため、引用しているライブラリに simp 補題が新たに追加されたり、削除されたりするたびに挙動が変わってしまうためです。

たとえば、以下のような証明を考えてみます。

opaque foo : Nat
opaque bar : Nat

/-- `foo` と `bar` が等しいという定理 -/
axiom foo_eq_bar : foo = bar

example {P : Nat → Prop} (hbar : P bar) : True ∧ (P foo) := by
  simp
  rw [foo_eq_bar]
  assumption

ここで、定理 foo_eq_bar は今のところ [simp] 属性が与えられていませんが、与えられると rw タクティクが失敗するようになります。

attribute [simp] foo_eq_bar

example {P : Nat → Prop} (hbar : P bar) : True ∧ (P foo) := by
  -- 元々はゴールを `P foo` に変えていた
  simp

  -- ゴールが変わってしまった
  guard_target =ₛ P bar

  -- `rw` タクティクが失敗するようになった
  fail_if_success rw [foo_eq_bar]

  assumption

ここで問題は、simp 補題が追加される前の証明において、元々 simp タクティクがゴールを何から何に変形していたのかわからないということです。この例では foo_eq_bar が追加されたことが分かっていますが、一般にはライブラリにどのような変更があったのかはすぐにわかることではありません。ライブラリの差分のことを知らなくても、証明が壊れる前の simp タクティクが何を行っていたか知る方法が必要です。

これは simp が柔軟(flexible)なタクティクであることが原因で、対策として次のような方法が知られています。

  • 証明末でしか simp タクティクを使わない。証明末であれば、「壊れる前の証明において simp が何を行っていたか」は常に「残りのゴールを閉じる」であり明確で、ライブラリの変更があってもどう修正すればいいかがわかりやすいからです。
  • have タクティクや suffices タクティクを使って証明末そのものを増やす。
  • simp only 構文を使って、ライブラリ側に変更があっても simp タクティクの挙動が変わらないようにする。
  • simpa タクティクのような、ゴールを閉じなければならないという制約を持つタクティクで書き換える。

このリンタについて

このリンタを有効にすると、上記のようなライブラリの変更に対して脆弱な証明を自動で検出して、警告を出してくれます。

import Mathlib.Tactic.Linter.FlexibleLinter

set_option linter.flexible true

/-
warning: `simp` is a flexible tactic modifying `⊢`. Try `simp?` and use the suggested `simp only [...]`. Alternatively, use `suffices` to explicitly state the simplified form.

Note: This linter can be disabled with `set_option linter.flexible false`
-/
example {n m : Nat} (h : n = m) : True ∧ (n = m) := by
  simp
  exact h

example {n m : Nat} (h : n = m) : True ∧ (n = m) := by
  -- 書き換えると警告が消える
  simpa

  1. https://leanprover-community.github.io/extras/simp.html#non-terminal-simps を参照のこと。

linter.style.multiGoal

linter.style.multiGoal は、よくない証明の書き方を指摘するリンターの一つです。

Lean では、複数のサブゴールがあるときにタクティクを実行すると最初のゴールに対して実行されるのですが、証明を構造化するという観点からは、ゴールの一つにフォーカスする(infoview に一つしかゴールが表示されないようにする)べきです。 このリンタはそのような問題を指摘します。

import Mathlib.Tactic.Linter.Multigoal

set_option linter.style.multiGoal true

/-
warning: The following tactic starts with 2 goals and ends with 1 goal, 1 of which is not operated on.
  exact hP
Please focus on the current goal, for instance using `·` (typed as "\.").

Note: This linter can be disabled with `set_option linter.style.multiGoal false`
-/
example {P Q : Prop} (hP : P) (hQ : Q) : P ∧ Q := by
  -- ゴールが2つ生成される
  constructor

  -- この時点でサブゴールが2つあるのに、
  -- フォーカスせずにタクティクを実行しているので警告が出る
  exact hP
  exact hQ

-- 良い証明の例
example {P Q : Prop} (hP : P) (hQ : Q) : P ∧ Q := by
  constructor
  · exact hP
  · exact hQ

linter.missingDocs

linter.missingDocs オプションを有効にすると、ドキュメントコメントが与えられていない定義に対して警告が表示されます。

set_option linter.missingDocs true

/-
warning: missing doc string for public def hoge

Note: This linter can be disabled with `set_option linter.missingDocs false`
-/
def hoge := 42

-- ドキュメントコメントを付与すると警告が消える
/- なにがしかのコメント -/
def foo := 11

なお private とマークされた定義には警告が出ません。

private def bar := 99

theorem で宣言された定理や補題に対しても警告が出ません。

theorem foo_hoge : foo + hoge = 53 := by
  rfl

補足

上記で述べたように linter.missingDocs オプションは theorem を無視しますが、Linter を自作することによって、theorem に対してドキュメントを要求するようなリンターを自作できます。

まず、以下のようなファイルを作成します。

import Lean

open Lean Elab Command Name

/--
ドキュメントコメントが付与されていない定理に対して警告を発するリンター。
デフォルトでは有効。
-/
register_option linter.docBlameThm : Bool := {
  defValue := true
  descr := "docBlameThmリンターを有効にする"
}

/--
ある位置 `pos` 以降にソースコード内で登場するすべての宣言名を収集する
-/
private def getNamesFrom (pos : String.Pos.Raw) : CommandElabM (Array Syntax) := do
  let drs := declRangeExt.toPersistentEnvExtension.getState (asyncMode := .local) (← getEnv)
  let fm ← getFileMap
  let mut nms := #[]
  for (nm, rgs) in drs do
    if pos ≤ fm.ofPosition rgs.range.pos then
      let ofPos1 := fm.ofPosition rgs.selectionRange.pos
      let ofPos2 := fm.ofPosition rgs.selectionRange.endPos
      nms := nms.push (mkIdentFrom (.ofRange ⟨ofPos1, ofPos2⟩) nm)
  return nms

/-- ドキュメントコメントが付与されているかどうか判定する -/
def Lean.Name.hasDocString (c : Name) (env : Environment) : CoreM Bool := do
  let doc? ← findDocString? env c
  return doc?.isSome

/-- ある宣言が `theorem` で宣言されているかどうか判定する -/
def Lean.Name.isTheorem (c : Name) : CommandElabM Bool := do
  let info ← getConstInfo c
  match info with
  | .thmInfo _ => return true
  | _  => return false

@[inherit_doc linter.docBlameThm]
def docBlameThmLinter : Linter where
  run := withSetOptionIn fun stx ↦ do
    -- リンターが有効になっていなければ何もしない
    unless Linter.getLinterValue linter.docBlameThm (← Linter.getLinterOptions) do
      return

    -- どこかにエラーがあれば何もしない
    if (← get).messages.hasErrors then
      return

    -- ユーザが定義した名前を取得する
    let names := (← getNamesFrom (stx.getPos?.getD default))
      |>.filter (! ·.getId.isInternal)

    let env ← getEnv
    for constStx in names do
      let constName := constStx.getId
      -- 定理でなければ無視する
      if ! (← constName.isTheorem) then
        continue
      -- ドキュメントコメントがなければ警告を出す
      let hasDocStr ← liftCoreM <| constName.hasDocString env
      if ! hasDocStr then
        Linter.logLint linter.docBlameThm constStx
          m!"`{constName}`にドキュメントコメントを与えてください。"

initialize addLinter docBlameThmLinter

このファイルを読み込むと、次のように使用できます。

import LeanByExample.Option.MissingDocs.DocBlameThm

/-
warning: `ex`にドキュメントコメントを与えてください。

Note: This linter can be disabled with `set_option linter.docBlameThm false`
-/
theorem ex : True := by trivial

-- `private` な定理に対しては warning が出ない
private theorem ex2 : True := by trivial

-- example に対しては warning が出ない
example : True := by trivial

-- `def`に対しては warning が出ない
def hoge := 42

pp.macroStack

pp.macroStack オプションをtrueにすると、マクロの展開過程がステップごとに表示されるようになります。

たとえば、以下のようなマクロを定義したとします。

/-- 自前で定義したリスト型 -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)
deriving DecidableEq

/-- 空リスト。標準の`List`のための記法と被るのを避けている。 -/
notation:max " ⟦⟧ " => MyList.nil

/-- `MyList`に要素を追加する。標準の`List`のための記法と被るのを避けている。 -/
infixr:80 " ::: " => MyList.cons

/-- 自作のリストリテラル構文。なお末尾のカンマは許可される。
なお標準の`List`のための記法と被るのを避けている。 -/
syntax "⟦" term,*,? "⟧" : term

macro_rules
  | `(⟦$x⟧) => `($x ::: ⟦⟧)
  | `(⟦$x, $xs,*⟧) => `($x ::: (⟦$xs,*⟧))

これは一見正しそうに見えますが、実際に使ってみるとエラーになってしまいます。

#check_failure ⟦1, ⟧

こうしたときに、マクロがどういう順に展開されていってどこでエラーになったかを確認するのに pp.macroStack オプションは役に立ちます。

set_option pp.macroStack true

/-
info: elaboration function for `«term⟦_⟧»` has not been implemented
  ⟦ ⟧
with resulting expansion
  ⟦ ⟧
while expanding
  (⟦ ⟧)
while expanding
  1 ::: (⟦ ⟧)
while expanding
  ⟦1, ⟧
-/
#check_failure ⟦1, ⟧

relaxedAutoImplicit

relaxedAutoImplicit オプションは、autoImplicit オプションの派生オプションであり、自動束縛の対象を広げます。

-- `autoImplicit` は有効にしておく
set_option autoImplicit true

section
  -- `relaxedAutoImplicit` が無効の時
  set_option relaxedAutoImplicit false

  -- 二文字の識別子は自動束縛の対象にならないのでエラーになる
  /-
  error: Unknown identifier `AB`

  Note: It is not possible to treat `AB` as an implicitly bound variable here because it has multiple characters while the `relaxedAutoImplicit` option is set to `false`.
  -/
  def nonempty₁ : List AB → Bool
    | [] => false
    | _ :: _ => true
end

section
  -- `relaxedAutoImplicit` が有効の時
  set_option relaxedAutoImplicit true

  -- 二文字の識別子も自動束縛の対象になるのでエラーにならない
  def nonempty₂ : List AB → Bool
    | [] => false
    | _ :: _ => true
end

debug.skipKernelTC

debug.skipKernelTC を有効にすると、カーネルによる型検査が行われなくなります。結果として、不正な証明を Lean に受け入れさせることができてしまいます。

import Lean

section
  open Lean Elab Tactic

  set_option debug.skipKernelTC true

  elab "so_sorry" : tactic => do
    closeMainGoal `so_sorry (Lean.mkConst ``trivial)

  def bad_proof : False := by so_sorry

end

-- Fermat の最終定理が証明できてしまう
theorem easy_proof (x y z n : Nat) : n > 2 → x ^ n + y ^ n = z ^ n → x * y * z = 0 := by
  exact bad_proof.elim

debug.skipKernelTC を使った証明は、#print axioms コマンドを使用しても正当な証明と見分けがつかないことに注意が必要です。

/- info: 'easy_proof' does not depend on any axioms -/
#print axioms easy_proof

補足: skipKernelTC を禁止すれば不正な証明を防げるか?

debug.skipKernelTC オプションを禁止しても、以下のように、代わりに addDeclCore 関数を使うなどして同様のことが実現できてしまいます。

section
  open Lean

  def myBadTheorem : TheoremVal where
    name := `bad_proof₂
    levelParams := []
    type := Lean.mkConst ``False
    value := Lean.mkConst ``trivial
    all := []

  #eval show CoreM Unit from do
    let currentEnv ← getEnv
    match currentEnv.addDeclCore (doCheck := false) 0 0 (.thmDecl myBadTheorem) none with
    | .ok env => setEnv env
    | .error _ => throwError "didn't work"
end

-- Fermat の最終定理が証明できてしまう
theorem easy_proof₂ (x y z n : Nat) : n > 2 → x ^ n + y ^ n = z ^ n → x * y * z = 0 := by
  exact bad_proof₂.elim

/- info: 'easy_proof₂' does not depend on any axioms -/
#print axioms easy_proof₂

型クラス

型クラス(type class) とは、和や積、逆数を取る演算など、複数の型に対してそれぞれのやり方で実装されるような演算を定義するものです。

型クラスは主に class コマンドで定義され、インスタンスを宣言するには instance コマンドを使用します。

Add

Add+ という二項演算子のための型クラスです。ここまで HAdd と同じですが、HAdd は異なるかもしれない型 α, β, γ に対して足し算 (· + ·) : α → β → γ を定義することができる一方で、Add は同じ型 α に対して足し算 (· + ·) : α → α → α を定義することしかできません。

+ 記法が何を意味するかについては制約はありませんが、足し算で表される演算は可換であることが期待されるので、例外はあるものの a + b = b + a が成り立つような実装をすることが推奨されます。

/-- 自前で定義した自然数 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

namespace MyNat
  /- ## MyNat の足し算を定義する -/

  def add (m n : MyNat) : MyNat :=
    match n with
    | zero => m
    | succ n => succ (add m n)

  -- 記法が定義されていないので使えない
  #check_failure MyNat.zero + MyNat.zero

  -- `add` 関数を `+` の実装とする
  instance : Add MyNat where
    add := add

  -- 足し算記号が使えるようになった
  #check MyNat.zero + MyNat.zero
end MyNat

舞台裏

Add 型クラスは、内部的には HAdd に展開されています。

-- #check コマンドの出力で記法を使わないようにする
set_option pp.notation false in

/- info: HAdd.hAdd MyNat.zero MyNat.zero : MyNat -/
#check MyNat.zero + MyNat.zero

Alternative

Alternative 関手は、回復可能な失敗を表現します。あるいは、選択的な計算を表すと表現することもできます。

定義

Alternative クラスは、Applicative 型クラスを継承して概ね次のように定義されています。

class Alternative (f : Type u → Type v) extends Applicative f where
  /--
  空のコレクションまたは回復可能な失敗を生成する。
  -/
  failure : {α : Type u} → f α
  /--
  `Alternative`インスタンスに応じて、値を拾ったり最初に成功した結果を返すことで
  `failure`から回復したりする。
  -/
  orElse : {α : Type u} → f α → (Unit → f α) → f α

構文

Alternative のインスタンスにすると、<|> という二項演算子が使えるようになります。<|>orElse とほぼ対応していますが、少し型が異なります。

section
  variable {α : Type}

  -- `F` は `Alternative` 関手
  variable {F : Type → Type} [Alternative F]

  -- `orElse` と `<|>` は基本的には同じもの
  example (a : F α) (b : Unit → F α) : Alternative.orElse a b = (a <|> b ()) := rfl

  -- 型が少し `orElse` と異なる
  example : F α → F α → F α := fun x y => (x <|> y)

end

インスタンス

Option

重要なインスタンスとして、OptionAlternative のインスタンスです。failurenone として実装されていて、(· <|> ·) は最初の none でない値を選択するような処理として実装されています。

#guard (failure : Option Nat) = none

#guard (some 2 <|> none <|> some 5) = some 2

#guard (none <|> none <|> some 5 <|> some 4) = some 5

Option(· <|> ·) は評価を順に行っていって、失敗したら単に次に進むということを繰り返しながら、成功するまで評価を続けます。早期に成功した場合、後続の項は評価されません。

/- info: some "hello" -/
#eval some "hello" <|> (dbg_trace "foo!"; some "world")

List

ListAlternative のインスタンスにすることができます。

-- モナドのインスタンスにする
instance : Monad List where
  pure x := [x]
  bind l f := l.flatMap f
  map f l := l.map f

-- Alternative のインスタンスにする
instance : Alternative List where
  failure := @List.nil
  orElse l l' := List.append l (l' ())

#guard ([] <|> [1, 2, 3]) = [1, 2, 3]

#guard ([1, 2, 3] <|> [4, 5, 6]) = [1, 2, 3, 4, 5, 6]

guard 関数

「条件 p が真ならば何もしない。そうでなければ失敗とする」という処理のために guard 関数が用意されています。

section
  /- ## filter の再帰を使う定義と、使わない定義を比較する -/

  variable {α : Type}

  /-- do 構文による filter の実装 -/
  def List.doFilter (p : α → Bool) (xs : List α) : List α := do
    -- リスト `xs` から要素 `x` を取り出す
    let x ← xs

    -- `p x` が真なら残す
    guard <| p x

    return x

  -- 再帰を使う標準の定義と一致する
  #test
    ∀ {α : Type} (p : α → Bool) (xs : List α),
    List.doFilter p xs = List.filter p xs
end

Append

Append++ という二項演算子のための型クラスです。“append” という名前の通り、リストや文字列などを「連結させる」操作を表すのに使われます。

#guard "hello" ++ " world!" = "hello world!"

#guard [1, 2] ++ [3, 4] = [1, 2, 3, 4]

#guard #[1, 2] ++ #[3, 4] = #[1, 2, 3, 4]

ここまで HAppend と同じですが、HAppend は異なるかもしれない型 α, β, γ に対して連結 (· ++ ·) : α → β → γ を定義することができる一方で、Append は同じ型 α に対して連結 (· ++ ·) : α → α → α を定義することしかできません。

Append インスタンスを実装する

以下は、自前で定義した型 MyList に対して Append インスタンスを実装する例です。

/-- 自前で定義したリスト -/
inductive MyList where
  | nil
  | cons (head : Nat) (tail : MyList)

namespace MyList

  def append (xs ys : MyList) : MyList :=
    match xs with
    | nil => ys
    | cons x xs => cons x (append xs ys)

  -- 記法が定義されていないので使えない
  #check_failure MyList.nil ++ MyList.nil

  -- `append` 関数を `++` の実装とする
  instance : Append MyList where
    append := append

  -- 連結記号が使えるようになった
  #check MyList.nil ++ MyList.nil

end MyList

舞台裏

Append 型クラスは、内部的には HAppend に展開されています。

-- #check コマンドの出力で記法を使わないようにする
set_option pp.notation false in

/- info: HAppend.hAppend MyList.nil MyList.nil : MyList -/
#check MyList.nil ++ MyList.nil

Add との使い分け

+ で表される演算は可換(a + b = b + a)であることが期待されます。しかしたとえば、文字列やリストの連結は順序に依存するため非可換です。このような非可換な連結に + を使うと混乱を招くため、Lean では ++ という別の記法を用意しています。

-- 文字列の連結は非可換: 順序が違うと結果が異なる
#guard ("Hello, " ++ "world!" ≠ "world!" ++ "Hello, ")

-- リストの連結も非可換
#guard ([1, 2] ++ [3, 4] ≠ [3, 4] ++ [1, 2])

-- 一方、自然数の足し算は可換
#guard 2 + 3 = 3 + 2

Applicative

Applicative 型クラスは、Functor 型クラスの拡張であり、Monad 型クラスよりは制限された中間的な構造です。関数適用を一般化であり、計算効果をエンコードするものと見なすことができます。

定義

Applicative 型クラスは、大雑把に書けば次のように定義されています。(実際の定義はもっと複雑です)

class Applicative.{u, v} (f : Type u → Type v) where
  /-- `a : α` が与えられたとき、`pure a : f α` は「何もせずに `a` を返すアクション」を表す。 -/
  pure {α : Type u} : α → f α

  /--
  `<*>` 演算子の実装。
  モナドにおいては、`mf <*> mx` は `do let f ← mf; x ← mx; pure (f x)` と同じになる。
  つまり、まず関数を評価し、次に引数を評価して適用する。
  予期しない順序で評価されることを避けるために、`mx` は `Unit → f α` という関数を使って遅延的に取得される。
  -/
  seq : {α β : Type u} → f (α → β) → (Unit → f α) → f β

すなわち、関数 F : Type → TypeApplicative 型クラスのインスタンスにするということは、pure : α → F α(· <*> ·) : F (α → β) → F α → F β を定義するということです。

/-- 標準にある`Option`を真似て構成した関手 -/
inductive MyOption (α : Type) where
  | none
  | some (a : α)

/-- `MyOption`を`Applicative`のインスタンスにする -/
instance : Applicative MyOption where
  pure a := MyOption.some a
  seq f a :=
    match f, a () with
    | .some f, .some a => .some (f a)
    | _, _ => .none

Functor との関係

Applicative のインスタンスであるならば、自動的に Functor のインスタンスにもなります。これは、Functor.map を次のように実装することができるからです。

instance {F : Type → Type} [Applicative F]: Functor F where
  map f x := pure f <*> x

なぜ「関数適用の一般化」なのか

Functor.map メソッドは (α → β) → F α → F β という型を持ちます。これは、F = Id の場合を考えてみると分かるように、1引数の関数適用を一般化したものだと考えることができます。では2引数、3引数の時はどうなるでしょうか?

単純に拡張すると、2引数の時は (α → β → γ) → F α → F β → F γ という型になり、3引数の時は (α → β → γ → δ) → F α → F β → F γ → F δ という型になります。これらを Functor.map を使って表現するのは困難です。

しかし、FApplicative 型クラスのインスタンスになっていれば、n 引数の場合でも表現することができます。1

variable {α β γ δ : Type}
variable {F : Type → Type} [Applicative F]

/-- 1引数の場合 -/
example : (α → β) → F α → F β := fun f x =>
  pure f <*> x

/-- 2引数の場合 -/
example : (α → β → γ) → F α → F β → F γ := fun f x y =>
  pure f <*> x <*> y

/-- 3引数の場合 -/
example : (α → β → γ → δ) → F α → F β → F γ → F δ := fun f x y z =>
  pure f <*> x <*> y <*> z

  1. ここでの説明は 「プログラミングHaskell 第2版」(Graham Hutton 著、山本和彦訳)を参考にしました。

BEq

BEq は、==!= による Bool 値の比較を提供する型クラスです。 たとえば Nat には BEq のインスタンスがあるので、2つの自然数を == で比較することができます。

#guard 2 == 2
#guard !(2 == 3)
#guard (2 != 42)

DecidableEq との使い分け

単に Bool 値の比較をしたいだけであれば、DecidableEq のインスタンスでも可能です。しかも DecidableEq なら単に Bool 値の比較ができるだけでなく、∀ x : α, x = x といった「等号が満たすべきルール」もついてくるので、証明に使いたくてかつ DecidableEq が使える場合はそちらを使うべきでしょう。

-- BEq は単なる関数なのでルールは付属しておらず、
-- x == x などを証明するには LawfulBEq が必要
example {α : Type} [BEq α] [LawfulBEq α] (x : α) : x == x := by
  simp

-- DecidableEq を仮定すると自動的に BEq と LawfulBEq のインスタンスが生成される
example {α : Type} [DecidableEq α] (x : α) : x == x := by
  let _ : LawfulBEq α := by infer_instance
  simp

敢えて BEq を使うべきケースもあります。典型的なのは Float です。

Float には NaN という値があります。これは「数値ではない」ことを表す特別な値で、isNaN という関数で判定できます。

-- 0.0 / 0.0 は NaN
#guard (0.0 / 0.0).isNaN

NaN は「自分自身と比較しても等しくない」という特殊な仕様があります。

-- 自分自身と比較しても false になる
#guard !((0.0 / 0.0) == (0.0 / 0.0))

-- 通常の数値の比較は true になる
#guard 3.2 == 3.2
#guard 1.45 == 1.45

一方で命題としては Float に対しても ∀ x, x = x が成り立っています。

example (x : Float) : x = x := by rfl

Coe

Coe は、型強制(coercion) と呼ばれる仕組みをユーザが定義するための型クラスです。

ある型 T が期待される場所に別の型の項 s : S を見つけると、Lean は型エラーにする前に自動的に型変換を行うことができないか試します。ここで行われる「自動的な型変換」が型強制です。型強制を明示的に指定するには、 記号をつけて ↑x などのようにします。なおどの関数を 記号で表示させるかは [coe] 属性で制御することができます。

具体的には Coe S T という型クラスのインスタンスが定義されているとき、型 S の項が型 T に変換されます。

例えば、正の自然数からなる型 Pos を定義したとします。包含関係から Pos → Nat という変換ができるはずです。この変換を関数として定義するだけでは、必要になるごとに毎回書かなければなりませんが、型強制を使うと自動化することができます。

/-- 正の自然数 -/
inductive Pos where
  | one
  | succ (n : Pos)

def one : Pos := Pos.one

/-- 階乗関数 -/
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

-- `factorial` の引数は `Nat` なのに、`Pos` を渡したのでエラーになる
/-
error: Application type mismatch: The argument
  one
has type
  Pos
but is expected to have type
  Nat
in the application
  factorial one
-/
#check factorial one

/-- `Pos` から `Nat` への変換 -/
def Pos.toNat : Pos → Nat
  | one => 1
  | succ n => n.toNat + 1

-- 明示的に変換すればエラーにならないが、
-- `Pos ⊆ Nat` と見なして自動で変換してほしくなる
#eval factorial <| one.toNat

/-- `Pos` から `Nat` への型強制 -/
instance : Coe Pos Nat where
  coe n := n.toNat

-- 自動的に `Pos` から `Nat` への変換が行われるようになった!
#guard factorial one = 1

CoeDep

CoeDep型強制を行うための型クラスですが、Coe と異なり「項に依存する型強制」(dependent coercion)を行うことができます。

たとえば空でないリストからなる型 NonEmptyList を定義したとします。空リストを変換する方法がないため、List α → NonEmptyList α という変換を定義する自然な方法はありません。しかし CoeDep を使えば空でないリストに限って NonEmptyList に変換するという型強制を定義することができます。

/-- 空でないリスト -/
structure NonEmptyList (α : Type) : Type where
  head : α
  tail : List α

-- 型強制がないのでエラー
#check_failure ([1, 2] : NonEmptyList Nat)

variable {α : Type}

/-- 型強制。`x :: xs` という形をしている `List` の要素だけを `NonEmptyList` の項に変換する -/
instance {x : α} {xs : List α} : CoeDep (List α) (x :: xs) (NonEmptyList α) where
  coe := {head := x, tail := xs}

-- 型強制が定義された
#check ([1, 2] : NonEmptyList Nat)

CoeFun

CoeFun クラスのインスタンスを与えると、関数ではない項を関数型に強制することができます。

/-- 加法的な関数の全体 -/
structure AdditiveFunction : Type where
  /-- 関数部分 -/
  toFun : Nat → Nat

  /-- 加法を保つ -/
  additive : ∀ x y, toFun (x + y) = toFun x + toFun y

/-- 恒等写像 -/
def identity : AdditiveFunction := ⟨id, by intro _ _; rfl⟩

-- `identity` の型は `AdditiveFunction` であって、関数ではないのでこれはエラーになる
#check_failure (identity 1)

-- 関数に変換してからならOK
#check (identity.toFun 1)

-- `CoeFun` を使って関数への変換を自動化する
local instance : CoeFun AdditiveFunction (fun _ => Nat → Nat) where
  coe f := f.toFun

-- まるで関数のように使えるようになる
#check (identity 1)

上記の例ではどんな t : AdditiveFunction も同じ型 Nat → Nat に強制していますが、実際には依存関数型に強制することができます。

Coe との違い

上記の例を、Coe を使って再現しようとして下記のようにしても上手くいかないことに注意して下さい。

-- `Coe` で `Nat → Nat` への変換を自動化しようとしている例
local instance : Coe AdditiveFunction (Nat → Nat) where
  coe f := f.toFun

-- `Nat → Nat` への型強制が呼び出されず、エラーになってしまう
-- これは、期待されている型が `Nat → Nat` ではなく、単に `Nat → ?_` であるため。
#check_failure (identity 1)

-- 期待される型を明記すればエラーにならない
#check ((identity : Nat → Nat) 1)

CoeSort

CorSortCoe と同じく型強制を定義するための型クラスですが、違いとして型宇宙(TypeProp など、項が再び型であるような型)への変換を専門に行う点が挙げられます。

import Mathlib.Data.Fintype.Basic -- `Fintype` を使うため

/-- 有限集合の圏 -/
structure FinCat where
  /-- 台集合 -/
  base : Type

  /-- 台集合が有限集合であるという性質 -/
  fin : Fintype base

/-- 要素が2つの集合。有限集合なので `FinCat` のオブジェクト。-/
def Two : FinCat := { base := Fin 2, fin := inferInstance }

-- `Two` は有限集合の圏のオブジェクトなので集合っぽいものであってほしいが、
-- `Two` の型は `FinCat` であって `Type` などの型宇宙ではないので、
-- `a : Two` という書き方ができない。
/-
info: type expected, got
  (Two : FinCat)
-/
#check_failure ((1 : Fin 2) : Two)

-- `Two → Two` という書き方もできない。
-- `A → A` も `A` の型が `Type` などの型宇宙であることを要求する。
/-
info: type expected, got
  (Two : FinCat)
-/
#check_failure (Two → Two)

-- 台集合はあくまで `Two.base` なので、
-- `.base` をつける必要がある。
#check ((1 : Fin 2) : Two.base)
#check (Two.base → Two.base)

section

  /-- `FinCat` から `Type` への型強制。
  `S : FinCat` を、`S.base : Type` に変換する。-/
  local instance : CoeSort FinCat Type := ⟨fun S ↦ S.base⟩

  -- 型強制があるのでこういう書き方ができる
  #check ((1 : Fin 2) : Two)
  #check Two → Two

end

Coe との違い

Coe で同様のコードを書いても、上記の FinCat の例はうまくいきません。

section

  local instance : Coe FinCat Type := ⟨fun S ↦ S.base⟩

  #check_failure ((1 : Fin 2) : Two)
  #check_failure (Two → Two)

end

しかし、これは「Coe では型宇宙への変換は扱えないから」ではありません。CoeCoeSort では型強制が呼ばれるタイミングが異なるからです。Coe は「ある型の項が期待される場所に、異なる型の項が来た時」にトリガーされますが、CoeSort は「型が期待される場所に型が来ていないとき」にトリガーされます。

実際、Coe を使っても Type への型強制は定義することができます。

/-- 型を受け取ってゼロを返す関数 -/
def zero (_ : Type) : Nat := 0

/-- `Type` のラッパー -/
structure AltType where
  base : Type

def A : AltType := ⟨Nat⟩

-- `zero` は `Type` を期待しているのでエラーになる
#check_failure zero A

section

  /-- `AltType` を `Type` に型強制する -/
  local instance : Coe AltType Type := ⟨fun S ↦ S.base⟩

  -- 成功するようになった!
  #check zero A

end

Decidable

Decidable は、命題が決定可能であることを示す型クラスです。

ここで命題 P : Prop が決定可能であるとは、(後述する例外を除き)その真偽を決定するアルゴリズムが存在することを意味します。具体的には P : PropDecidable のインスタンスであるとき、decide 関数を適用することにより decide P : Bool が得られます。

-- 決定可能な命題を決定する関数 decide が存在する
#check (decide : (P : Prop) → [Decidable P] → Bool)

/- info: true -/
#eval decide (2 + 2 = 4)

/- info: false -/
#eval decide (2 + 2 = 5)

Decidable 型クラスのインスタンスに対しては、decide タクティクにより証明が可能です。

/-- 自前で定義した偶数を表す述語 -/
def Even (n : Nat) : Prop := ∃ m : Nat, n = 2 * m

example : Even 4 := by
  -- 最初は decide で示すことができない
  fail_if_success decide

  exists 2

theorem even_impl (n : Nat) : n % 2 = 0 ↔ Even n := by
  constructor <;> intro h
  case mp =>
    exists (n / 2)
    omega
  case mpr =>
    obtain ⟨m, rfl⟩ := h
    omega

/-- Even が決定可能であることを示す -/
instance (n : Nat) : Decidable (Even n) :=
  decidable_of_iff (n % 2 = 0) (even_impl n)

-- decide で証明ができるようになった!
example : Even 4 := by decide

class inductive

Decidable 型クラスの定義は少し特殊です。コンストラクタが複数あり、構造体ではなく帰納型の構造をしています。これは Decidableclass inductive というコマンドで定義されているためです。

class inductive Decidable (p : Prop) where
  /-- `¬ p` であることが分かっているなら、`p` は決定可能 -/
  | isFalse (h : Not p) : Decidable p
  /-- `p` であることが分かっているなら、`p` は決定可能 -/
  | isTrue (h : p) : Decidable p

排中律と決定可能性

命題 P : Prop が決定可能というのは、実際のところ「P の証明または ¬ P の証明を持っている」ということを意味します。したがって、P の証明または ¬ P の証明のいずれかが手に入っているのであれば、そこから Decidable P のインスタンスを構築することができ、P は決定可能であるといえます。

def inst_decidable_lem {P : Prop} (h : P ⊕' (¬ P)) : Decidable P := by
  cases h with
  | inl h =>
    apply Decidable.isTrue
    exact h
  | inr h =>
    apply Decidable.isFalse
    exact h

もっと言えば、排中律を利用してよければ任意の命題 P : Prop に対して P の証明または ¬ P の証明を得ることができるので、すべての命題は決定可能になります。

これはもちろん、「どんな命題に対しても、それを決定できるアルゴリズムが作れる」という意味ではありません。逆です。Decidable 型クラスが意味を失ってしまうということです。

/-- 排中律を利用すれば任意の命題について、肯定または否定の証明が手に入る -/
noncomputable example (P : Prop) : P ⊕' (¬ P) := by
  by_cases h : P
  case pos => exact PSum.inl h
  case neg => exact PSum.inr h

/-- 排中律を仮定すれば、任意の命題は決定可能 -/
noncomputable instance (P : Prop) : Decidable P := by
  exact Classical.propDecidable P

実際、排中律を使って得られた Decidable インスタンスは計算的な解釈を持たないため、そうやって Decidable インスタンスを得ても decide タクティクで証明をすることはできません。

/-- 奇数を表す述語 -/
def Odd (n : Nat) : Prop := ∃ m : Nat, n = 2 * m + 1

-- 排中律を利用して決定可能にしている
noncomputable instance (n : Nat) : Decidable (Odd n) := by
  classical
  infer_instance

example : Odd 3 := by
  -- decide で証明ができない
  fail_if_success decide

  exists 1

Functor

Functor は圏論における 関手(functor) という概念からその名がある型クラスです。

型を受け取って型を返す関数 F : Type u → Type v が関手であるとは、型 α : Type u を写すことができるだけでなく、関数 f : α → βF α → F β 型の関数に写す方法があることを意味します。つまり、関手とは「型を写す」だけでなく同時に「関数も写す」ことができるようなものです。

シンプルにするために宇宙レベルを無視し、デフォルト値が持たされたフィールドも無視すると、Functor 型クラスは次のように定義されています。

/-- 関手。標準ライブラリにある `Functor` を真似て定義した型 -/
class Functor (F : Type → Type) where
  /-- 関数 `f : α → β` を関数 `F α → F β` に変換する -/
  map : (α → β) → F α → F β

つまり、関手は Functor.map という高階関数を持っており、f : α → β という関数を Functor.map f : F α → F β という関数に移すことができます。この関数は「普通の関数 f : α → β」を「F に包まれた Type 間の関数 F α → F β」に写していることから、持ち上げ(lift) であると呼ばれることがあります。

Functorによる関数の持ち上げ

Functor.map はよく使われる操作であるため、<$> という専用の記法が用意されています。

-- `F` は Functor であると仮定
variable (F : Type → Type) [Functor F]

-- `<$>` は `map` と同じ
example (x : F α) (g : α → β) : g <$> x = Functor.map g x :=
  rfl

型が合っているだけでは予想外の挙動をすることがあるので、関手が満たすべきルールが存在し、それは LawfulFunctor というクラスにまとめられています。

典型的なインスタンス

Id

Id : Type u → Type u は「何もしない」関手です。

/-- `Id` の `Functor.map` の実装を真似て作った関数 -/
def Id.myMap {α β : Type} (f : α → β) (x : Id α) : Id β := f x

/-- `Id` の `Functor.map` は `Id.myMap` のように定義されている -/
example {α β : Type} (f : α → β) (x : Id α) : Id.myMap f x = f <$> x := rfl

List

Functor 型クラスの典型的なインスタンスのひとつが List です。これにより「リストの各要素に関数を適用する」ための簡単な方法が提供されます。

/-- 標準にある `List.map` の実装を真似て実装した `map` 処理 -/
def List.myMap {α β : Type} (f : α → β) : List α → List β
  | [] => []
  | x :: xs => f x :: List.myMap f xs

/-- `List` の `Functor.map` は `List.myMap` のように定義されている -/
example {α β : Type} (f : α → β) (xs : List α) : List.myMap f xs = f <$> xs := by
  induction xs with
  | nil => rfl
  | cons x xs ih =>
    simp_all [List.myMap, Functor.map]

-- リストの各要素を2倍する例
#guard (· * 2) <$> [1, 2, 3, 4, 5] = [2, 4, 6, 8, 10]

Option

OptionFunctor 型クラスのインスタンスになっています。これにより「x? : Optionsome x の場合にだけ関数を適用し、none なら none を返す」という操作のための簡単な方法が提供されます。

/-- 標準にある `Option.map` の実装を真似て実装した `map` 処理 -/
def Option.myMap {α β : Type} (f : α → β) : Option α → Option β
  | some x => some (f x)
  | none => none

/-- `Option` の `Functor.map` は `Option.myMap` のように定義されている -/
example {α β : Type} (f : α → β) (x : Option α) : Option.myMap f x = f <$> x := by
  cases x <;> rfl

#guard (· * 2) <$> some 2 = some 4
#guard (· * 2) <$> [1, 2, 3][4]? = none

/- info: 20 -/
#eval (· * 2) <$> (10 : Id Nat)

(A → ·)

任意の型 A : Type u に対して、fun X => (A → X) という対応は関手になります。

/-- 型から、その型への関数型を返す -/
abbrev Hom (A : Type) (X : Type) := A → X

/-- 関数合成を map として `Hom A` は関手になる -/
instance {A : Type} : Functor (Hom A) where
  map f g := f ∘ g

#guard
  let doubleLength : Hom String Nat := (· * 2) <$> String.length
  doubleLength "hello" = 10 && doubleLength "world!" = 12

上記で定義した Hom は Lean の標準ライブラリでは ReaderM と呼ばれます。

/-- 上で定義した Hom は ReaderM に等しい -/
example (A : Type) : ReaderM A = Hom A := rfl

((· → A) → A)

任意の型 A : Type u に対して、fun X => (X → A) → A という対応は関手になります。1

abbrev Cont (A : Type) (X : Type) := (X → A) → A

instance {A : Type} : Functor (Cont A) where
  map f g := fun h => g (h ∘ f)

(A × ·)

任意の型 A : Type に対して、A との直積を取る対応 fun X => A × X は関手になります。

section
  -- 最初 Functor インスタンスは用意されていない
  #check_failure (· * 2) <$> ("hello", 20)

  abbrev prodWith (A : Type) (X : Type) := A × X

  local instance {A : Type} : Functor (prodWith A) where
    map f := fun (a, x) => (a, f x)

  #guard (· * 2) <$> ("hello", 20) = ("hello", 40)
end

(A ⊕ ·)

任意の型 A : Type に対して、A との直和を取る対応 fun X => A ⊕ X は関手になります。

section
  -- 最初 Functor インスタンスは用意されていない
  #check_failure (· * 2) <$> (Sum.inr 20 : String ⊕ Nat)

  abbrev sumWith (A : Type) (X : Type) := Sum A X

  local instance {A : Type} : Functor (sumWith A) where
    map f := fun z =>
      match z with
      | .inl a => Sum.inl a
      | .inr x => Sum.inr (f x)

  #eval (· * 2) <$> (Sum.inr 20 : String ⊕ Nat)
end

Functor 則

Functor 型クラスのインスタンスには満たすべきルールがあります。このルールを破っていても Functor 型クラスのインスタンスにすることは可能ですが、避けるべきです。Functor 型クラスがルールを満たしていることを証明するためには、LawfulFunctor 型クラスを使います。


  1. この関手は、 継続モナド(continuation monad) として知られているものです。詳細は、たとえば Andrzej Filinski 「Representing monads」(1994) などを参照のこと。

GetElem

GetElem 型クラスは、リストや配列などの「データの直線的な集まり」を表す型 Col の項 as : Col に対して、その i 番目の要素を取得する方法を提供します。GetElem 型クラスを実装すると、as[i] というインデックスアクセスの構文が使用できるようになります。

/-- 標準にある List を真似て作ったデータ構造 -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)

namespace MyList
  /- ## MyList のための外延記法 -/

  /-- MyList のための外延記法 -/
  syntax "my[" term,* "]" : term

  macro_rules
    | `(my[]) => `(MyList.nil)
    | `(my[$e]) => `(MyList.cons $e MyList.nil)
    | `(my[$e, $elems,* ]) => `(MyList.cons $e (my[$elems,*]))

  #check my[1, 2, 3]

end MyList

namespace MyList
  /- ## GetElem 型クラスのインスタンスを定義 -/

  -- 作ったばかりで実装していないため、
  -- インデックスアクセスの構文 `as[i]` が使えない
  #check_failure my[1, 2, 3][2]

  variable {α : Type}

  /-- リストの長さを返す関数 -/
  def length (l : MyList α) : Nat :=
    match l with
    | nil => 0
    | cons _ t => 1 + length t

  /-- `as : MyList` の `idx` 番目の要素を取得する。
  `idx` の型は `Fin` としてある。 -/
  def get (as : MyList α) (idx : Fin as.length) : α :=
    match as, idx with
    | .cons head _, ⟨0, _⟩ => head
    | .cons _ as, ⟨i + 1, h⟩ =>
      -- インデックスアクセスが妥当であることを証明する
      have bound : i < as.length := by
        simp [MyList.length] at h
        omega
      MyList.get as ⟨i, bound⟩

  /-- MyList を GetElem のインスタンスにする -/
  instance : GetElem (MyList α) Nat α (fun as i => i < as.length) where
    getElem as i h := as.get ⟨i, h⟩

  -- インデックスアクセスの構文が使えるようになった
  #guard my[1, 2, 3][2] = 3

end MyList

インデックスアクセスの種類

コレクション as に対して、i 番目の要素を取得すると書きましたが、i 番目の要素があるとは限らないという問題があります。これに対処するには様々な方法がありえますが、中でも以下のものは専用の構文が用意されています。

as[i]

as[i] という構文では、インデックス i が範囲内であることの証明を自動で構成します。i が変数になっていて具体的に計算できないときでも、ローカルコンテキスト内に i が範囲内であることの証明があれば動作します。

-- 具体的な数値を渡せば、それが範囲内であることを自動的に証明してくれる
#guard my[1, 2, 3][2] = 3

#eval show IO Unit from do
  let l := my[1, 2, 3]

  -- for 文を回すときに `h : i` とすると
  -- `i` についての情報を取得できる
  for h : i in [0:3] do
    -- インデックスが範囲内であることを証明する
    have : i < l.length := Membership.get_elem_helper h rfl

    IO.println l[i]

as[i]?

as[i]? という構文では、返り値を Option に包みます。範囲外の場合は none を返します。

-- 返り値を Option に包む場合
#guard my[1, 2, 3][2]? = some 3
#guard my[1, 2, 3][3]? = none

as[i]!

as[i]! という構文では、i が範囲外だった時には panic! することにして Option で包まずに直接値を取り出します。

-- 範囲外ならエラーで落とすことにして強引に値を取り出す
#guard my[1, 2, 3][2]! = 3

インデックスアクセスの妥当性を証明する必要がないためユーザは楽ですが、実行時に境界チェックを行うのでその分速度が遅くなります。

as[i]’h

xs[i]'h という構文では、i が範囲内であることの証明 h を明示的に渡して値を取り出します。

-- インデックスが範囲内であることの証明を明示的に渡す
#guard
  let xs := my[1, 2, 3]
  let h := show 2 < xs.length from by decide
  xs[2]'h = 3

HAdd

HAdd (heterogeneous addition)は、+ という二項演算子のための型クラスです。 + 記法が何を意味するかについては制約はありません。 任意の型 α, β, γ : Type に対して足し算 (· + ·) : α → β → γ を定義することができます。

-- 最初は `+` 記号が定義されていないのでエラーになる
#check_failure 1 + (· + 2)

/-- HAdd 型クラスのインスタンスを実装する -/
instance : HAdd Nat (Nat → Nat) (Nat → Nat) where
  hAdd n f := fun m => n + f m

-- 足し算記号が使えるようになった
#check 1 + (· + 2)

足し算 (· + ·) : α → β → γ が一つの型の中で閉じているとき、つまり α = β = γ のときは Add が使用できます。

HAppend

HAppend (heterogeneous append) は、++ という二項演算子のための型クラスです。 任意の型 α, β, γ : Type に対して連結 (· ++ ·) : α → β → γ を定義することができます。

-- 最初は `++` 記号が定義されていないのでエラーになる
#check_failure ([1, 2] ++ (3 : Nat))

/-- HAppend 型クラスのインスタンスを実装する
注意: これは例示のためのインスタンスで、あまり良いインスタンスではない。 -/
instance : HAppend (List Nat) Nat (List Nat) where
  hAppend xs n := xs.map (· + n)

-- 連結記号が使えるようになった
#guard [1, 2] ++ (3 : Nat) = [4, 5]

連結 (· ++ ·) : α → β → γ が一つの型の中で閉じているとき、つまり α = β = γ のときは Append が使用できます。

HMul

HMul (heterogeneous multiplication)は * という二項演算子のための型クラスです。* 記号が何を意味するかについての制約はありません。任意の型 α, β, γ : Type に対して掛け算 (· * ·) : α → β → γ を定義することができます。

-- 最初は `*` 記号が定義されていないのでエラーになる
#check_failure 2 * (· + 1)

/-- `HMul` のインスタンスを定義する -/
instance : HMul Nat (Nat → Nat) (Nat → Nat) where
  hMul xs ys := fun m => xs * ys m

-- 乗算記号が使えるようになった
#check 2 * (· + 1)

掛け算 (· * ·) : α → β → γ が一つの型の中で閉じているとき、つまり α = β = γ のときは Mul が使用できます。

Inhabited

Inhabited は、ある型にデフォルトの項があることを示す型クラスです。

Inhabited のインスタンスである型は、default という項を持ちます。

variable {α : Type} [Inhabited α]

#check (default : α)

Inhabited の注意すべき使われ方として、panic! する関数の返り値として呼ばれるというものがあります。

たとえば a : Array に対して i 番目の要素を取り出す処理を考えます。i 番目の要素が存在するとは限らないので、例外の処理が必要です。一つの方法は、「i 番目の要素がなければエラーにする」というものです。

def get {α : Type} [Inhabited α] (a : Array α) (i : Nat) : α :=
  if h : i < a.size then
    a[i]'h
  else
    panic! "index out of bounds"

何気ない定義のように見えますが、この定義には Inhabited α が必要です。panic! でプログラムを終了させているのですが、このときに α が空でないことが要求されています。

/-
error: failed to synthesize instance of type class
  Inhabited α

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/
def get' {α : Type} (a : Array α) (i : Nat) : α :=
  if h : i < a.size then
    a[i]'h
  else
    panic! "index out of bounds"

これは Lean が定理証明支援系としてもプログラミング言語としても使えるようにするための技術的な制約からくる仕様です。もし値を返さずにプログラムがクラッシュすることが許されたとすると、「空の型は False に等しい」ので、クラッシュするプログラムを False の証明として扱える可能性が生じてしまいます。

LawfulApplicative

LawfulApplicative 型クラスは、Applicative 型クラスのインスタンスが満たすべき法則を明文化したものです。LawfulApplicative クラスのインスタンスになっていることで、「Applicative 型クラスのインスタンスは関数適用と整合性があり、計算の文脈を表している」という意味論が適切であることが保証されます。

定義

LawfulApplicative は、おおむね次のように定義されています。

variable {α β γ : Type}

/-- アプリカティブ則 -/
class LawfulApplicative (f : Type → Type) [Applicative f] : Prop extends LawfulFunctor f where
  /-- `pure` が `seq` の直前にくる場合、その部分はただの `Functor.map` と同じになる。
  つまり、`Applicative` のインスタンスから誘導される `Functor.map` の実装と、
  もともとの `Functor.map` の実装は一致する。-/
  pure_seq (g : α → β) (x : f α) : pure g <*> x = g <$> x

  /-- `pure` の結果に関数を `map` することは、その関数を `pure` の内部で適用することと同じ。 -/
  map_pure (g : α → β) (x : α) : g <$> (pure x : f α) = pure (g x)

  /-- `seq` の後に `pure` を使うことは、`Functor.map` と同じになる。 -/
  seq_pure (g : f (α → β)) (x : α) : g <*> pure x = (fun h => h x) <$> g

  /--
  `seq` は結合的である。

  計算の順序を保ったまま `seq` 呼び出しの入れ子構造を変えても、同値な計算になる。
  これは、`seq` が単に順序付け以上のことはしていないことを意味する。
  -/
  seq_assoc (x : f α) (g : f (α → β)) (h : f (β → γ)) : h <*> (g <*> x) = ((@Function.comp α β γ) <$> h) <*> g <*> x

Applicative 則の帰結

Applicative のインスタンス F は関数適用と整合するという意味論がありますが、LawfulApplicative のインスタンスになっているとそれが正しいことが保証されます。

variable {F : Type → Type} [Applicative F] [LawfulApplicative F]
variable {A B C D : Type}

/-- `pure`を介して関数適用と`<*>`が整合している -/
example {x : A} {f : A → B} : pure f <*> (pure x : F A) = pure (f x) := calc
  _ = f <$> pure x := by rw [pure_seq]
  _ = pure (f x) := by rw [map_pure]

example {x : A} {y : B} {f : A → B → C}
    : pure f <*> (pure x : F A) <*> (pure y : F B) = pure (f x y) := by
  simp only [seq_pure, map_pure]

LawfulFunctor

LawfulFunctor は、Functor 型クラスに関手則を満たすという条件を加えたものです。

関手則とは、関手 F : Type u → Type u が満たしているべきルールで、以下のようなものです。

  1. Functor.map は恒等関数を保存する。つまり id <$> x = x が成り立つ。
  2. Functor.map は関数合成を保存する。つまり (f ∘ g) <$> x = f <$> (g <$> x) が成り立つ。

型が合っているだけでは「Functor.map とは、関手というコンテナに包まれた内部の値に関数を適用するもの」という意味論が成り立たないことがあります。関手則は、この意味論が正当になるための条件を与えます。

LawfulFunctor クラスは、関手則をほぼそのままコードに落とし込んだものとして、おおむね次のように定義されています。

open Function

universe u v

variable {α β γ : Type u}

class LawfulFunctor (f : Type u → Type v) [Functor f] : Prop where
  /-- `Functor.mapConst` が仕様を満たす -/
  map_const : (Functor.mapConst : α → f β → f α) = Functor.map ∘ const β

  /-- 恒等関数を保つ -/
  id_map (x : f α) : id <$> x = x

  /-- 合成を保つ -/
  comp_map (g : α → β) (h : β → γ) (x : f α) : (h ∘ g) <$> x = h <$> g <$> x

関手則の帰結

関手則の帰結を紹介しましょう。

関手は型の同値性を保つ

まず、型 A, B は全単射 f : A → B とその逆射 g : B → A が存在するとき 同値(equivalent) であるといい、これを (· ≃ ·) で表します。つまり A ≃ B であるとは、f : A → Bg : B → A が存在して f ∘ g = id かつ g ∘ f = id が成り立つことを意味します。

関手則が守られているとき、関手 F は合成を保ち、かつ idid に写すので、関手は同値性を保つことになります。

import Mathlib.Logic.Equiv.Defs

variable {A B : Type} {F : Type → Type}

example [Functor F] [LawfulFunctor F] (h : A ≃ B) : F A ≃ F B := by
  obtain ⟨f, g, hf, hg⟩ := h

  -- 関手 `F` による像で同値になる
  refine ⟨Functor.map f, Functor.map g, ?hFf, ?hFg⟩

  -- infoviewを見やすくする
  all_goals
    dsimp [Function.RightInverse] at *
    dsimp [Function.LeftInverse] at *

  case hFf =>
    have gfid : g ∘ f = id := by
      ext x
      simp_all

    intro x
    have : g <$> f <$> x = x := calc
      _ = (g ∘ f) <$> x := by rw [LawfulFunctor.comp_map]
      _ = id <$> x := by rw [gfid]
      _ = x := by rw [LawfulFunctor.id_map]
    assumption

  case hFg =>
    have fgid : f ∘ g = id := by
      ext x
      simp_all

    intro x
    have : f <$> g <$> x = x := calc
      _ = (f ∘ g) <$> x := by rw [LawfulFunctor.comp_map]
      _ = id <$> x := by rw [fgid]
      _ = x := by rw [LawfulFunctor.id_map]
    assumption

関手は Unit との図式を保つ

型と関数がなす圏 Type を考えると、ここでは Unit が終対象になっています。つまり、任意の型 A : Type に対して、A → Unit という関数が一意的に存在します。(関数外延性は仮定します)この多相的な関数を仮に unit : (A : Type) → A → Unit と書くことにします。

ここで、任意の関数 f : A → B に対して、AB のそれぞれから Unit への関数が一意的に存在するので、(unit B) ∘ f = unit A が成り立ちます。

関手則を満たしている関手 F : Type → Type があったとしましょう。このとき F は関数合成を保つので、F によって誘導される関数も同じ等式を満たします。表記を簡単にするために (f <$> ⬝) のことを f* と書くことにすると、(unit B)* ∘ f* = (unit A)* が成り立ちます。

(だから何だという感じがするかもしれませんが、(unit ·)* という多相的な関数によって map の実装が非常に限定されているということです。)

関手の例

いくつか LawfulFunctor クラスのインスタンスを作ってみます。

Id

Id は関手則を満たします。

def MyId (α : Type) := α

def MyId.map {α β : Type} (f : α → β) (x : MyId α) : MyId β := f x

instance : Functor MyId where
  map := MyId.map

instance : LawfulFunctor MyId where
  map_const := by intros; rfl
  id_map := by intros; rfl
  comp_map := by intros; rfl

List

List は関手則を満たします。

/-- 自前で定義したリスト -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)

notation:80 "[]" => MyList.nil
infixr:80 "::" => MyList.cons

/-- リストの中身に関数をそれぞれ適用する -/
def MyList.map {α β : Type} (f : α → β) (xs : MyList α) : MyList β :=
  match xs with
  | [] => []
  | x :: xs => f x :: MyList.map f xs

instance : Functor MyList where
  map := MyList.map

instance : LawfulFunctor MyList where
  map_const := by intros; rfl
  id_map := by
    intro α xs
    dsimp [(· <$> ·)]
    induction xs with grind [MyList.map]
  comp_map := by
    intro α β γ g h xs
    induction xs with
    | nil => rfl
    | cons x xs ih =>
      dsimp [(· <$> ·), MyList.map] at ih ⊢
      rw [ih]

Option

Option は関手則を満たします。

@[aesop unsafe 70% cases]
inductive MyOption (α : Type) where
  | none
  | some (x : α)

def MyOption.map {α β : Type} (f : α → β) (x : MyOption α) : MyOption β :=
  match x with
  | none => none
  | some x => some (f x)

instance : Functor MyOption where
  map := MyOption.map

instance : LawfulFunctor MyOption where
  map_const := by intros; rfl
  id_map := by aesop
  comp_map := by aesop

(A → ·)

fun X => (A → X) という対応 Type → Type は関手則を満たします。

/-- 型から、その型への関数型を返す -/
abbrev Hom (A : Type) (X : Type) := A → X

instance {A : Type} : Functor (Hom A) where
  map f g := f ∘ g

instance {A : Type} : LawfulFunctor (Hom A) where
  map_const := by aesop
  id_map := by aesop
  comp_map := by aesop

Monad

Monad は、圏論におけるモナドという概念からその名がある型クラスで、大雑把に言えば逐次的な計算を表します。この型クラスを実装した型に対しては、do 構文と呼ばれる手続き型プログラミングを一般化したような構文が使用できるようになります。

典型的なインスタンス

Id

Id 関手はモナドです。Id の場合、do 構文は単に手続き型プログラミングを模したものになります。

/-- `n`以下の素数のリストを `Array Bool` の形で返す。
`i` 番目が `true` ならば `i` は素数で、`false` ならば合成数。 -/
def eratosthenesAux (n : Nat) : Array Bool := Id.run do
  let mut isPrime := Array.replicate (n + 1) true

  isPrime := isPrime.set! 0 false
  isPrime := isPrime.set! 1 false

  for p in [2 : n + 1] do
    if not isPrime[p]! then
      continue

    if p ^ 2 > n then
      break

    -- `p` の倍数を消していく
    let mut q := p * p
    while q ≤ n do
      isPrime := isPrime.set! q false
      q := q + p

  return isPrime

/-- エラトステネスの篩 -/
def eratosthenes (n : Nat) : Array Nat :=
  eratosthenesAux n
  |>.zipIdx
  |>.filterMap fun ⟨isPrime, i⟩ =>
    if isPrime then some i else none

#guard eratosthenes 10 = #[2, 3, 5, 7]

#guard
  let actual := eratosthenes 100
  let expected := #[
    2, 3, 5, 7, 11,
    13, 17, 19, 23, 29,
    31, 37, 41, 43, 47,
    53, 59, 61, 67, 71,
    73, 79, 83, 89, 97
  ]
  expected == actual

Option

Option 関手はモナドです。Option の場合、do 構文によって「先行する計算のどれか一つでも none だったら none を返す。全部が some だったときだけ先へ進む」という処理を簡単に書けるようになります。

たとえば、配列の1つめの2つめと3つめの要素をまとめて取り出す関数を do 構文なしで書こうとすると次のようになり、インデックスアクセスがネストしてややこしくなります。

def fstSndThird? (a : Array Nat) : Option (Nat × Nat × Nat) :=
  match a[0]? with
  | none => none
  | some x =>
    match a[1]? with
    | none => none
    | some y =>
      match a[2]? with
      | none => none
      | some z => some (x, y, z)

#guard fstSndThird? #[1] = none
#guard fstSndThird? #[1, 2] = none
#guard fstSndThird? #[1, 2, 3] = some (1, 2, 3)

しかし do 構文を使用すると、ネストを消し去ることができます。

def fstSndThird? (a : Array Nat) : Option (Nat × Nat × Nat) := do
  let x ← a[0]?
  let y ← a[1]?
  let z ← a[2]?
  return (x, y, z)

#guard fstSndThird? #[1] = none
#guard fstSndThird? #[1, 2] = none
#guard fstSndThird? #[1, 2, 3] = some (1, 2, 3)

List

List 関手もモナドにすることができます。List の場合、do 構文を使用することで、リスト内包表記のような操作を簡潔に記述できます。

たとえば、2つのリストの直積を計算する関数は次のように書けます。

/-- `List` をモナドインスタンスにする -/
instance : Monad List where
  pure x := [x]
  bind l f := l.flatMap f
  map f l := l.map f

def cartesianProduct (xs : List Nat) (ys : List Nat) : List (Nat × Nat) := do
  let x ← xs
  let y ← ys
  return (x, y)

#guard cartesianProduct [1, 2] [3, 4] = [(1, 3), (1, 4), (2, 3), (2, 4)]

ただし、この例は Applicative のインスタンスでも書けるのであまり良い例ではありません。後続の計算の中で前の選択に依存するような処理を使っていないためです。

-- `Applicative` のインスタンスを使って同じことが書ける
def cartesianProductAp (xs : List Nat) (ys : List Nat) : List (Nat × Nat) :=
  (fun x y => (x, y)) <$> xs <*> ys

#guard cartesianProductAp [1, 2] [3, 4] = [(1, 3), (1, 4), (2, 3), (2, 4)]

インスタンス自作例

パーサーをモナドとして実装することができます。1

/-- パーサ

* 返り値が `Option` で包まれているのは、パースが成功するとは限らないため
* 返り値の `α × String` は、`α` がパース結果、`String` が残りの入力文字列を表す
-/
def Parser (α : Type) := String → Option (α × String)

/-- `Parser`の`Functor.map`メソッドの実装

* パーサが成功すれば結果に関数を適用する
* 失敗したら失敗を伝搬する
-/
protected def Parser.map {α β : Type} (f : α → β) (p : Parser α) : Parser β :=
  fun input =>
    match p input with
    | none => none
    | some (a, rest) => some (f a, rest)

instance : Functor Parser where
  map := Parser.map

/-- `Parser`の`Applicative.pure`メソッドの実装 -/
protected def Parser.pure {α : Type} (x : α) : Parser α :=
  fun input => some (x, input)

/-- `Parser`の`Applicative.seq`メソッドの実装

関数を返すパーサ`pg`を、引数を返すパーサ`px ()`に適用して、
「その関数を引数に適用した結果を返すパーサ」を返す
-/
protected def Parser.seq {α β : Type} (pg : Parser (α → β)) (px : Unit → Parser α) : Parser β :=
  fun input =>
    match pg input with
    | none => none
    | some (g, out) => (g <$> px ()) out

instance : Applicative Parser where
  pure := Parser.pure
  seq := Parser.seq

instance : Monad Parser where
  bind := fun {_α _β} p f input =>
    match p input with
    | none => none
    | some (v, out) => f v out

これでパーサを組み合わせるのに do 構文が利用できるようになります。

/-- 基礎的なパーサ

入力文字が空の時は失敗し、それ以外の時は最初の文字を消費して返す
-/
def Parser.item : Parser Char := fun input =>
  let chars := input.toList
  match chars with
  | [] => none
  | x :: xs => some (x, String.ofList xs)

/-- 3文字の文字列にマッチするパーサー -/
def Parser.three : Parser String := do
  let x ← item
  let y ← item
  let z ← item
  return String.ofList [x, y, z]

#guard Parser.three "abcdef" = some ("abc", "def")

なお、Lean の標準ライブラリに用意されているパーサもモナドになっています。

open Std Internal in

instance (ι : Type) : Monad (Parsec ι) := by infer_instance

  1. モナドを利用するパーサの詳細については「Monadic Parser Combinators」と「Monadic parsing in Haskell」という論文を参照してください。

Mul

Mul* という二項演算子のための型クラスです。* 記号が何を意味するかについての制約はありません。ここまで HMul と同じですが、HMul は異なるかもしれない型 α, β, γ に対して掛け算 (· * ·) : α → β → γ を定義することができる一方で、Mul は同じ型 α に対して掛け算 (· * ·) : α → α → α を定義することしかできません。

/-- 自前で定義した自然数 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

namespace MyNat
  /- ## MyNat の足し算を定義する -/

  def add (m n : MyNat) : MyNat :=
    match n with
    | zero => m
    | succ n => succ (add m n)

  -- `add` 関数を `+` の実装とする
  instance : Add MyNat where
    add := add

end MyNat

namespace MyNat
  /- ## MyNat の掛け算を定義する -/

  def mul (m n : MyNat) : MyNat :=
    match n with
    | zero => zero
    | succ n => (mul m n) + n

  -- 記法が定義されていないので使えない
  #check_failure MyNat.zero * MyNat.zero

  -- `mul` 関数を `*` の実装とする
  instance : Mul MyNat where
    mul := mul

  -- 掛け算記号が使えるようになった
  #check MyNat.zero * MyNat.zero
end MyNat

舞台裏

Mul 型クラスは、内部的には HMul に展開されています。

-- #check コマンドの出力で記法を使わないようにする
set_option pp.notation false in

/- info: HMul.hMul MyNat.zero MyNat.zero : MyNat -/
#check MyNat.zero * MyNat.zero

Neg

Neg- という前置記法の単項演算子のための型クラスです。- 記法が何を意味するかについては制約はありません。

/-- 自前で定義した自然数型 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

/-- 自前で定義した整数型 -/
inductive MyInt where
  | ofNat (n : MyNat)
  | negSucc (n : MyNat)

namespace MyInt
  /- ## MyInt のマイナス演算を定義する -/

  /-- 自然数に対するマイナス演算 -/
  def negOfNat : MyNat → MyInt
    | .zero => ofNat .zero
    | .succ m => negSucc m

  /-- 整数に対するマイナス演算 -/
  def neg (n : MyInt) : MyInt :=
    match n with
    | ofNat n => negOfNat n
    | negSucc n => ofNat n

  -- 記法が定義されていないので使えない
  #check_failure - MyInt.ofNat MyNat.zero

  -- `neg` 関数を `-` の実装とする
  instance : Neg MyInt where
    neg := neg

  -- マイナス演算記号が使えるようになった
  #check - MyInt.ofNat MyNat.zero

end MyInt

OfNat

OfNat 型クラスは、01 などの数値リテラルを特定の型の値として解釈する方法を指定します。

/-- 偶数全体 -/
inductive Even where
  | zero
  | addTwo (n : Even)

-- まだ `OfNat` のインスタンスがないので、
-- `0` という数値リテラルを `Even` 型の値として解釈することはできない
#check_failure (0 : Even)

/-- `0` という数値リテラルを `Even` の項として解釈する方法を指定 -/
instance : OfNat Even 0 where
  ofNat := Even.zero

-- エラーがなくなった
#check (0 : Even)

01 などの特定の数値リテラルに対して個別に宣言するだけでなく、変数を使って一斉に OfNat のインスタンスを宣言することもできます。

/-- 有理数もどき。
約分したら等しいものは等しいというルールがないので有理数ではない -/
structure Rational where
  num : Int
  den : Nat
  inv : den ≠ 0

/-- 数値リテラル `n` を有理数として解釈する方法を指定 -/
instance (n : Nat) : OfNat Rational n where
  ofNat := { num := n, den := 1, inv := by decide }

-- 数値リテラルを `Rational` の意味で使用できる!
#check (2 : Rational)
#check (42 : Rational)

特定の数値リテラルに対してだけ OfNat を実装しないということもできます。

/-- 正の自然数 -/
inductive Pos where
  | one
  | succ (n : Pos)

/-- 自然数 `n` を `n + 1` に相当する `Pos` の項に写す -/
def Pos.ofNatPlus (n : Nat) : Pos :=
  match n with
  | 0 => Pos.one
  | n + 1 => Pos.succ (Pos.ofNatPlus n)

/-- 数値リテラル `1, 2, 3, ...` に対して `Pos` の項と解釈する方法を指定 -/
instance (n : Nat) : OfNat Pos (n + 1) where
  ofNat := Pos.ofNatPlus n

-- エラーにならない
#check (1 : Pos)
#check (8 : Pos)
#check (42 : Pos)

-- `0` は除外したのでエラーになる
#check_failure (0 : Pos)

応用例

OfNat の応用として、macro コマンドと組み合わせることで数値リテラル n1 + 1 + ⋯ + 1 (n 個の 1 の和) に分解するタクティクを自作することができます。1

theorem unfoldNat (x : Nat) : OfNat.ofNat (x + 2) = OfNat.ofNat (x + 1) + 1 :=
  rfl

theorem unfoldNatZero (x : Nat) : OfNat.ofNat (0 + x) = x :=
  Nat.zero_add x

/-- 自然数を `1 + 1 + ⋯ + 1` に分解する -/
macro "expand_num" : tactic => `(tactic| simp only [unfoldNat, unfoldNatZero])

example (n : Nat) : 3 * n = 2 * n + 1 * n := by
  expand_num

  -- 数値が `1 + 1 + ⋯ + 1` に分解された
  guard_target =ₛ (1 + 1 + 1) * n = (1 + 1) * n + 1 * n

  simp only [Nat.add_mul]

  1. このコード例は、Lean 公式 Zulip の expand_nums tactic トピックにおける Robin Arnez 氏の投稿を参考にしています。

Quote

Lean.Quote は、構文の クォート(quote) を行います。つまり、データ x : α を対応する 構文木 に変換します。構文木への変換という点ではパースに似ていますが、パースはいろんな型からの変換ではなく、コードの文字列からの変換です。

open Lean

/- info: (num "1") -/
#eval
  let x : TSyntax `term := Quote.quote 1
  IO.println x

/- info: `Bool.true._@._internal._hyg.0 -/
#eval
  let x : TSyntax `term := Quote.quote true
  IO.println x

なお export されているので、Lean.Quote.quote の代わりに単に Lean.quote と書くことができます。

section
  variable {α : Type} [Lean.Quote α `term]

  example (x : α) : (Lean.Quote.quote x : Lean.TSyntax `term) = Lean.quote x := by
    rfl
end

使用例

候補を順に試すタクティク

Quoteelab コマンドを組み合わせると、たとえば「存在命題を、候補を順に試すことで示すタクティク」を自作することができます。

open Lean Elab.Tactic Elab.Term in

elab "try_for" n:num : tactic => do
  -- 範囲として与えられた自然数を取得
  let n := n.getNat

  -- `n` までの数を順に試す
  for i in [0 : n+1] do
    let istx := quote i

    -- `evalTactic` を使ってタクティクを呼び出して実行する
    evalTactic <| ← `(tactic| try { exists $istx })

example : ∃ n : Nat, n * 24 = 48 := by
  -- 具体的な数を指定しなくても探す範囲を指定するだけで示すことができる
  try_for 10

example : ∃ n : Nat, (n + 2) / 3 = 5 := by
  -- 12 まで試しても見つからない
  fail_if_success solve
  | try_for 12

  -- 13 まで試すと見つかる
  try_for 13

論理式のための構文を用意する

次のように、Lean のデータとして論理式の AST を定義したとします。

/-- 論理式 -/
inductive PropForm where
  /-- 真 `⊤` -/
  | tr : PropForm
  /-- 偽 `⊥` -/
  | fls : PropForm
  /-- 命題変数 -/
  | var : String → PropForm
  /-- 論理積 `∧` -/
  | conj : PropForm → PropForm → PropForm
  /-- 論理和 `∨` -/
  | disj : PropForm → PropForm → PropForm
  /-- 含意 `→` -/
  | impl : PropForm → PropForm → PropForm
  /-- 否定 `¬` -/
  | neg : PropForm → PropForm
  /-- 同値 `↔` -/
  | biImpl : PropForm → PropForm → PropForm
deriving Repr, DecidableEq, Inhabited

このとき、macro_rules コマンドと Quote を組み合わせることで、論理式のための構文を定義することができます。1

declare_syntax_cat propform

/-- `PropForm` を見やすく定義するための構文 -/
syntax "prop!{" propform "}" : term

syntax:max ident : propform
syntax "⊤" : propform
syntax "⊥" : propform
syntax:35 propform:36 " ∧ " propform:35 : propform
syntax:30 propform:31 " ∨ " propform:30 : propform
syntax:20 propform:21 " → " propform:20 : propform
syntax:20 propform:21 " ↔ " propform:20 : propform
syntax:max "¬ " propform:40 : propform
syntax:max "(" propform ")" : propform

macro_rules
  | `(prop!{$p:ident}) => `(PropForm.var $(Lean.quote p.getId.toString))
  | `(prop!{⊤}) => `(ProfForm.tr)
  | `(prop!{⊥}) => `(ProfForm.fls)
  | `(prop!{¬ $p}) => `(PropForm.neg prop!{$p})
  | `(prop!{$p ∧ $q}) => `(PropForm.conj prop!{$p} prop!{$q})
  | `(prop!{$p ∨ $q}) => `(PropForm.disj prop!{$p} prop!{$q})
  | `(prop!{$p → $q}) => `(PropForm.impl prop!{$p} prop!{$q})
  | `(prop!{$p ↔ $q}) => `(PropForm.biImpl prop!{$p} prop!{$q})
  | `(prop!{($p:propform)}) => `(prop!{$p})

-- 構文が使用できるようになった
#check prop!{p ∧ q → (r ∨ ¬ p) → q}
#check prop!{p ∧ q ∧ r → p}

-- 構文のテスト
#guard
  let actual := prop!{p ∧ q}
  let expected := PropForm.conj (PropForm.var "p") (PropForm.var "q")
  expected == actual

  1. この例は Logic and Mechanized Reasoning を参考にしました。

Repr

Repr は、その型の項をどのように表示するかを指示する型クラスです。

たとえば、以下のように新しく構造体 Point を定義したとき、何も指定しなくても Point の項を #eval で表示することはできますが、実は裏で Repr インスタンスを利用しています。

-- 平面上の点を表す構造体
structure Point (α : Type) : Type where
  x : α
  y : α

-- 原点
def origin : Point Nat := ⟨0, 0⟩

-- `origin` の中身を表示することができる
/- info: { x := 0, y := 0 } -/
#eval origin

-- Repr インスタンスを暗黙的に生成しないように設定
set_option eval.derive.repr false

-- 表示できずにエラーになった!
/-
error: Could not synthesize a `ToExpr`, `Repr`, or `ToString` instance for type
  Point Nat
-/
#eval origin

Repr が満たすべきルール

Repr の出力は Lean のコードとしてパース可能なものに可能な限り近くなければならない、つまり Lean のコードとして実行可能であることが期待されます。このルールは 次のように Repr のドキュメントコメントに書かれています。

The standard way of turning values of some type into Format.

When rendered this Format should be as close as possible to something that can be parsed as the input value.

Repr インスタンスの実装方法

Repr 型クラスの定義は次のようになっています。

class Repr.{u} (α : Type u) where
  /--
  `α` 型の項を、与えられた優先度で `Format` に変換する。
  優先度は、括弧を付けるかどうかの判断に使用される。
  -/
  reprPrec : α → Nat → Std.Format

したがって Repr のインスタンスを実装するには Std.Format の項を構成する必要がありそうですが、実は必ずしもこれを明示的に構成する必要はありません。

deriving を使う

deriving コマンドで Lean に Repr インスタンスを自動生成させることができます。

deriving instance Repr for Point

/- info: { x := 0, y := 0 } -/
#eval origin

あるいは、そもそも型を定義する際に deriving 句を用いて生成しても良いでしょう。

structure Point' (α : Type) : Type where
  x : α
  y : α
deriving Repr

def origin' : Point' Nat := ⟨0, 0⟩

-- 評価できる
#eval origin'

なお Repr の実装が満たすべきルールとして「出力は実行可能な Lean のコードでなければならない」というものがあるので、自分で構文を用意しない限り deriving を使わずに Repr インスタンスを実装する機会はないはずです。

ToString インスタンスから作る

ToString クラスのインスタンスから、Repr のインスタンスを得ることができます。

instance {α : Type} [ToString α] : Repr α where
  reprPrec x _ := toString x

実装すべきメソッド Repr.reprPrec の型は α → Nat → Std.Format なので型が合わないようですが、上記のコードが通るのは String から Format への型強制が存在するためです。

-- `String → Format` という型強制が存在する
#synth Coe String Std.Format

これを利用すると Repr の実装が手軽に得られます。

以下に紹介する例は少し長くて複雑ですが、macro_rules コマンドを使用して見やすい構文を用意した後、Repr の出力がその構文になるように Repr インスタンスを定義する例です。

/-- 2項演算の集合 -/
inductive Op where
  /-- 加法 -/
  | add
  /-- 乗法 -/
  | mul
deriving BEq

namespace Op
  -- ## ToString インスタンスの定義

  protected def toString : Op → String
    | add => "+"
    | mul => "*"

  instance : ToString Op := ⟨Op.toString⟩

end Op

/-- 数式 -/
inductive Expr where
  /-- 数値リテラル -/
  | val (n : Nat)
  /-- 演算子の適用 -/
  | app (op : Op) (left right : Expr)
deriving BEq

namespace Expr
  /- ## Expr の項を定義するための見やすい構文を用意する -/

  /-- `Expr` のための構文カテゴリ -/
  declare_syntax_cat expr

  /-- `Expr` を見やすく定義するための構文 -/
  syntax "expr!{" expr "}" : term

  syntax:max num : expr
  syntax:30 expr:30 " + " expr:31 : expr
  syntax:35 expr:35 " * " expr:36 : expr
  syntax:max "(" expr ")" : expr

  macro_rules
    | `(expr!{$n:num}) => `(Expr.val $n)
    | `(expr!{$l:expr + $r:expr}) => `(Expr.app Op.add expr!{$l} expr!{$r})
    | `(expr!{$l:expr * $r:expr}) => `(Expr.app Op.mul expr!{$l} expr!{$r})
    | `(expr!{($e:expr)}) => `(expr!{$e})

  -- 構文が正しく動作しているかテスト
  #guard
    let expected := Expr.app Op.add (app Op.mul (val 1) (val 2)) (val 3)
    let actual := expr!{1 * 2 + 3}
    expected == actual
end Expr

namespace Expr
  /- ## ToString インスタンスを定義する -/

  protected def toString : Expr → String
    | .val x => toString x
    | .app op l r =>
      brak l ++ " " ++ toString op ++ " " ++ brak r
  where
    /-- 式全体を括弧で囲うことを回避するための補助関数 -/
    brak : Expr → String
    | .val n => toString n
    | e => "(" ++ Expr.toString e ++ ")"

  instance : ToString Expr := ⟨Expr.toString⟩

  -- toString インスタンスのテスト
  #guard toString expr!{1 + 2 * 3} = "1 + (2 * 3)"
  #guard toString expr!{1 + (2 + 3 * 4)} = "1 + (2 + (3 * 4))"
end Expr

namespace Expr
  /- ## Repr インスタンスを定義する -/

  -- `ToString` インスタンスを利用して `Repr` インスタンスを実装する
  instance : Repr Expr where
    reprPrec e _ := "expr!{" ++ toString e ++ "}"

  -- Repr インスタンスのテスト
  /- info: expr!{1 + (2 * 3)} -/
  #eval expr!{1 + (2 * 3)}

end Expr

Setoid

Setoid は、与えられた型の上の同値関係を表します。具体的には、α : Type に対してインスタンス sa : Setoid α は、α 上の二項関係 r : α → α → Propr が同値関係であることの証明の組です。主として Quotient の引数として使用されます。

Setoid は、次のように定義されています。

/--
Setoidは、特定の同値関係(`≈`で表される)を持つ。
これは主に`Quotient`型への入力として使用される。
-/
class Setoid.{u} (α : Sort u) where
  /-- `α` 上の二項関係 -/
  r : α → α → Prop
  /-- `r` は同値関係 -/
  iseqv : Equivalence r

使用例

たとえば、ある型 α 上の関数 f : α → β が与えられていて β 上に同値関係 (· ≈ ·) が定義されているとします。このとき α 上の二項関係 rr a b := f a ≈ f b と定義すると、r は同値関係になります。

証明してみましょう。

section
  variable {α : Type} {β : Type}

  /-- `β` 上の二項関係から誘導される `α` 上の二項関係 -/
  def Rel.contra_map (f : α → β) (r : β → β → Prop) : α → α → Prop :=
    fun a₁ a₂ => r (f a₁) (f a₂)

  /-- `β` 上の同値関係から誘導される `α` 上の同値関係 -/
  def Setoid.contra_map (f : α → β) (sb : Setoid β) : Setoid α where
    r := Rel.contra_map f (· ≈ ·)
    iseqv := by
      constructor <;> dsimp [Rel.contra_map]

      -- 反射律
      case refl =>
        intro x
        apply Setoid.iseqv.refl

      -- 対称律
      case symm =>
        intro x y
        apply Setoid.iseqv.symm

      -- 推移律
      case trans =>
        intro x y z
        apply Setoid.iseqv.trans
end

ToString

ToString は、文字列 String への変換方法を提供する型クラスです。ToString のインスタンスになっている型の項は、ToString.toString 関数で文字列に変換することができます。また、export されているので、単に toString と書いても使えます。

example {α : Type} [ToString α] : ToString.toString (α := α) = toString := by rfl

使用例

/-- 標準ライブラリの `List` を真似て作った自前のリスト -/
inductive MyList (α : Type) where
  | nil : MyList α
  | cons (hd : α) (tl : MyList α) : MyList α

namespace MyList

  variable {α : Type}

  /-- リストをリストらしく `"[a₁, a₂, ..., aₙ]"` という文字列に変換する
  注意: `ToString.toString` と紛らわしいことがあるので `protected` で修飾している
  -/
  protected def toString [ToString α] : MyList α → String
    | nil => "[]"
    | ls@(cons _hd _tail) =>
      "[" ++ helper ls ++ "]"
  where
    /-- 外側の括弧抜きでリストの中身を `,` でつないで表示する -/
    helper : MyList α → String
      | nil => ""
      | cons hd .nil => toString hd
      | cons hd tail => toString hd ++ ", " ++ helper tail

  instance [ToString α] : ToString (MyList α) where
    toString := MyList.toString

  -- `toString` が正しく動作しているかテスト
  #guard (toString <| MyList.cons 1 (MyList.nil)) = "[1]"
  #guard (toString <| MyList.cons 1 (MyList.cons 2 (MyList.nil))) = "[1, 2]"

end MyList

データ型

型(type)は、直観的にはデータを分類するものです。集合が要素を持つように、型は項を持つことができます。

Lean では命題 P : Prop も型ですが、それらを除いた型は Type u の項になります。本書では Type u の項をデータ型と呼んでいます。

Array

Array α は配列を表す型です。特定の型 α : Type u の要素を一直線に並べたものです。 #[a₁, ..., aₖ] という記法で Array α の項を作ることができます。

#check (#[1, 2, 3] : Array Nat)

#check (#["hello", "world"] : Array String)

定義と実行時の性質

Array α は次のように連結リスト List α のラッパーとして定義されているように見えます。

structure Array.{u} (α : Type u) : Type u where
  toList : List α

しかしドキュメントコメントに以下のように書かれている通り、実行時には List とは大きく異なる動的配列(dynamic array)としての振る舞いを見せます。

Array α is the type of dynamic arrays with elements from α. This type has special support in the runtime.

Arrays perform best when unshared. As long as there is never more than one reference to an array, all updates will be performed destructively. This results in performance comparable to mutable arrays in imperative programming languages.

An array has a size and a capacity. The size is the number of elements present in the array, while the capacity is the amount of memory currently allocated for elements. The size is accessible via Array.size, but the capacity is not observable from Lean code. Array.emptyWithCapacity n creates an array which is equal to #[], but internally allocates an array of capacity n. When the size exceeds the capacity, allocation is required to grow the array.

From the point of view of proofs, Array α is just a wrapper around List α.

List のラッパーとしての定義は、証明を行おうとしたときに参照されます。

基本的な操作

インデックスアクセス

配列は GetElem のインスタンスであり、i 番目の要素を取得するために a[i] という記法が使用できます。Array α は実行時には動的配列として振る舞うので、インデックスアクセスは高速に行うことができます。

#guard #[1, 2, 3][0] = 1

#guard #[1, 2, 3][3]? = none

要素の追加

xs : Array α に対して Array.push 関数で末尾に要素を追加できます。

ドキュメントコメントに次のように書かれている通り、この操作は高速に行うことができます。

Adds an element to the end of an array. The resulting array’s size is one greater than the input array. If there are no other references to the array, then it is modified in-place.

This takes amortized O(1) time because Array α is represented by a dynamic array.

#guard #[1, 2, 3].push 4 = #[1, 2, 3, 4]

List と比較した特長

List も同様にインデックスアクセスをサポートしていますが、Array の方がより高速にアクセスすることができます。

なぜかというと、List は連結リストとして実装されており、i : Nat 番目にアクセスしようとすると最初の要素から順に辿っていく必要があるからです。一方で Array は動的配列として実装されているので、i 番目の要素に直接アクセスできます。

open IO

variable {α : Type} {valid : α → Nat → Prop}
variable [GetElem α Nat Nat valid] [GetElem? α Nat Nat valid]

/--
コレクション型に対して、そのインデックスアクセスの実行時間を計測する関数

* `col`: 対象となるコレクション
* `accessPoint`: 計算の中では、同じインデックスに対して何度もアクセスして平均時間を計測する。
  そこでアクセスするインデックス。
-/
@[noinline]
def evalIdxAccess (col : α) (accessPoint : Nat) : IO Nat := do
  -- アクセスを試みる回数
  let access_times := 1000

  -- コンパイラに最適化されて結果が変わることを防ぐために
  -- どうでもいい計算をする
  let mut sum := 0

  let mut sum_time := 0
  for _ in [0:access_times] do
    -- インデックスアクセスにかかる時間を測定する
    let start_time <- monoNanosNow
    let x := col[accessPoint]!
    let end_time <- monoNanosNow
    let time := end_time - start_time

    -- アクセス時間を合計する
    sum := (sum + x) % 2
    sum_time := sum_time + time

  IO.println s!"{sum} is computed..."
  return sum_time / access_times

def main : IO Unit := do
  let size := 10_000_000
  let accessPoint := size / 10
  let listTime ← evalIdxAccess (List.range size) accessPoint
  println s!"List access time: {listTime}ns"

  let arrayTime ← evalIdxAccess (Array.range size) accessPoint
  println s!"Array access time: {arrayTime}ns"

  if listTime < arrayTime then
    throw <| userError "List is faster than Array"

#eval main

使用例

配列の特徴を生かしてプログラムを組んでいる例をいくつか紹介します。

特定の要素だけ末尾に移動させる

arr : Array α に対しては、インデックスアクセスと「指定されたインデックスにある要素の更新」が高速に行えるので、それを生かして「特定の要素だけ末尾に移動させる」操作を効率的に実装することができます。

variable {α : Type} [BEq α]

/-- 配列`arr`の要素`a`が与えられたときに、
他の要素の位置関係を維持したまま、すべての`a`を配列の末尾に移動させる
-/
def Array.move (arr : Array α) (a : α) : Array α := Id.run do
  let mut arr := arr
  let mut write := 0
  -- まず `a` 以外の要素を前詰めで配置
  for x in arr do
    if x != a then
      arr := arr.set! write x
      write := write + 1
  -- 残りの部分を `a` で埋める
  for i in [write:arr.size] do
    arr := arr.set! i a
  return arr

#guard Array.move #[1, 0, 2, 0, 3] 0 == #[1, 2, 3, 0, 0] -- `0` を末尾に移動
#guard Array.move #[1, 2, 3] 0 == #[1, 2, 3] -- `0` がないので変化なし
#guard Array.move #[] 0 == #[] -- 空配列はそのまま
#guard Array.move #[0, 0, 0] 0 == #[0, 0, 0] -- `0` しかないので変化なし

スタックの実装

arr : Array α に対しては「末尾に要素を追加する操作」と「末尾から要素を取り出す操作」が高速に行えるので、スタックの実装に利用することができます。

/-- 括弧が対応しているか判定する -/
def matchParen (c1 c2 : Char) : Bool :=
  match c1, c2 with
  | '(', ')' => true
  | '{', '}' => true
  | '[', ']' => true
  | _, _ => false

/--
文字列 `s` に含まれる括弧が正しく対応しているかどうかを判定する関数。
開き括弧と閉じ括弧が対応しており、正しい順序で閉じられている場合に `true` を返す。
-/
def validParen (s : String) : Bool := Id.run do
  -- 括弧のスタックを空で初期化
  let mut stack : Array Char := #[]

  -- 文字列の各文字に対してループ
  for c in s.toList do
    -- スタックの末尾の要素(最後に追加された開き括弧)を取得
    let last := stack.back?
    match last with
    | none =>
      -- スタックが空なら、文字をスタックに追加(開き括弧のはず)
      stack := stack.push c
    | some last =>
      -- スタックの末尾と現在の文字が対応する括弧なら、スタックから取り除く(ペアが閉じられた)
      if matchParen last c then
        stack := stack.pop
      else
        -- 対応していない場合は、新たにスタックに追加(新しい開き括弧)
        stack := stack.push c

  -- すべての括弧が正しく閉じられていればスタックは空になっている
  return stack.isEmpty

-- テストケース
#guard validParen "()"
#guard validParen "()[]{}"
#guard !validParen "(]"
#guard !validParen "([)]"
#guard validParen "{[]}"
#guard !validParen "{"
#guard !validParen "}"
#guard validParen "([{}])({}){}"

Async

Std.Async.Async は、非同期計算をサポートします。ここで非同期計算とは、複数の計算を「他の計算の結果を待たずに」実行するようなものを指します。

import Std.Async

open Std.Async Async

/-- 疑似的に重い処理を非同期で行う関数 -/
def expensiveOperationAsync (n : Nat) : Async Nat := do
  IO.sleep 100  -- 100 ミリ秒スリープ
  return n

/-- 疑似的に重い処理を同期で行う関数 -/
def expensiveOperationSync (n : Nat) : IO Nat := do
  IO.sleep 100  -- 100 ミリ秒スリープ
  return n

/-- `n` 回、非同期に重い処理を並列実行し、その結果の合計を返す -/
def manyInParallel (n : Nat) : Async Nat := do
  -- `expensiveOperationAsync` を n 個生成
  let tasks := (Array.range n).map expensiveOperationAsync
  -- すべての非同期タスクを並列に実行し、結果を集める
  let results ← concurrentlyAll tasks
  return results.sum

/-- `n` 回、同期に重い処理を順次実行し、その結果の合計を返す -/
def manyInSync (n : Nat) : IO Nat := do
  -- `expensiveOperationSync` を n 個生成
  let tasks := (Array.range n).map expensiveOperationSync
  -- 順次に全て実行して結果を集める
  let results ← tasks.mapM id
  return results.sum

-- 非同期版の方が速い
#eval (manyInParallel 3).block
#eval manyInSync 3

Bool

Bool は真偽値を表す型です。truefalse の2つの値を持ちます。

#check (true : Bool)
#check (false : Bool)

Bool の値を得るためには、たとえば BEq のインスタンスがある型の値を == で比較します。

inductive Foo where
  | bar
  | baz
deriving BEq

example : Bool := Foo.bar == Foo.bar
example : Bool := Foo.bar != Foo.baz

真偽を表すという点で Prop と似ていますが、Bool の項は簡約すれば必ず truefalse になるため計算可能であるという含みがあります。

Char

Char 型は、Unicode 文字を表します。二重引用符 " ではなくてシングルクォート ' で囲んで表されます。

-- Char はシングルクォートで囲む
#check ('a' : Char)
#check ("a" : String)

-- Unicode 文字を含む
#check ('あ' : Char)
#check ('∀' : Char)
#check ('∅' : Char)

符号位置

Char は、以下のように structure として定義されています。

structure Char where
  /-- Unicode スカラー値 -/
  val : UInt32

  /-- `val` が正しく Unicode の code point であること -/
  valid : val.isValidChar

したがって Char.val 関数により 符号位置(code point) を取得することができます。

#guard 'a'.val = 97
#guard '⨅'.val = 10757

これを利用すると、たとえば「アルファベットを与えられた整数 n だけずらして暗号化する関数」を以下のように実装することができます。1

/-- アルファベットのインデックス -/
structure Index where
  /-- 番号。`0`から`25`の数字 -/
  index : Nat
  /-- 小文字かどうか -/
  isLower : Bool
deriving Inhabited, DecidableEq


/-- アルファベットを`Index`に変換する -/
def Char.toIndex (c : Char) : Index :=
  if c.isLower then
    ⟨c.val - 'a'.val |>.toNat, true⟩
  else if c.isUpper then
    ⟨c.val - 'A'.val |>.toNat, false⟩
  else
    panic! s!"toIndex: input is {c}, which is not an alphabet"

#guard 'a'.toIndex = ⟨0, true⟩
#guard 'B'.toIndex = ⟨1, false⟩


/-- インデックスをアルファベットに変換する -/
def Char.ofIndex (i : Index) : Char := Id.run do
  if i.index > 25 then
    panic! s!"ofIndex: index is out of range: {i.index} > 25."

  if i.isLower then
    Char.ofNat ('a'.val.toNat + i.index)
  else
    Char.ofNat ('A'.val.toNat + i.index)

#guard Char.ofIndex ⟨0, true⟩ = 'a'
#guard Char.ofIndex ⟨1, false⟩ = 'B'


/-- アルファベットを`n`だけシフトする -/
def Char.shift (c : Char) (n : Int) : Char :=
  if c.isAlpha then
    let code := (c.toIndex.index + n) % 26 |>.toNat
    let index : Index := ⟨code, c.isLower⟩
    Char.ofIndex index
  else
    c

#guard Char.shift 'a' 3 = 'd'
#guard Char.shift 'z' 3 = 'c'
#guard Char.shift 'B' (-3) = 'Y'


/-- シーザー暗号の実装。文字列に登場する文字をシフトする。 -/
def String.encode (s : String) (n : Int) : String :=
  s.map (Char.shift · n)

#guard "I am a magician.".encode 3 = "L dp d pdjlfldq."
#guard "L dp d pdjlfldq.".encode (-3) = "I am a magician."

  1. この例は、Graham Hutton「プログラミングHaskell第2版」(ラムダノート)第5章を参考にしています。

CommandElab

Lean.Elab.Command.CommandElab は、コマンドの内部実装を表現しています。

CommandElab 型の項は、Syntax → CommandElabM Unit 型の関数です。

import Lean

open Lean Elab Command in

example : CommandElab = (Syntax → CommandElabM Unit) := rfl

[command_elab] 属性を利用することで、CommandElab 型の関数からコマンドを作ることができます。

/-- 挨拶をするコマンド -/
syntax (name := helloCommand) "#hello" : command

open Lean Elab Command in

@[command_elab helloCommand]
def evalHello : CommandElab := fun _stx => do
  let msg := s!"Hello, Lean!"
  logInfo msg

/- info: Hello, Lean! -/
#hello

コマンド作例

以下に、CommandElab 型の関数からコマンドを作る例を示します。

型を確かめるコマンド

以下は、与えられた項と型が一致するかどうかを確かめるコマンドの例です。1

import Lean

open Lean Elab Command Term

-- メタ変数を表示しない
set_option pp.mvars false

/-- 与えられた項の型をチェックするコマンド -/
syntax (name := assertType) "#assert_type " term " : " term : command

@[command_elab assertType]
def evalAssertType : CommandElab := fun stx => do
  match stx with
  | `(command| #assert_type $termStx : $typeStx) =>
    liftTermElabM
      try
        let type ← elabType typeStx
        let _ ← elabTermEnsuringType termStx type
        logInfo "success"
      catch | _ => throwError "failure"
  | _ => throwUnsupportedSyntax

/- info: success -/
#assert_type 5 : Nat

/- info: success -/
#assert_type 42 : ?_

/-
error: Type mismatch
  [1, 2, 3]
has type
  List ?_
but is expected to have type
  Nat
-/
#assert_type [1, 2, 3] : Nat

  1. このコード例は、Metaprogramming in Lean 4 を参考にしました。

DerivingHandler

Lean.Elab.DerivingHandler は、型クラスのインスタンスの自動生成を行うための関数の型で、Array Name → CommandElabM Bool という型と等しいものです。

import Lean

open Lean Elab Command

example : DerivingHandler = (Array Name → CommandElabM Bool) := by
  rfl

deriving ハンドラの登録

deriving コマンドを使用したときに、対応する DerivingHandler 型の関数が呼び出されます。

簡単な例として、任意の型から Unit への自明な関数を提供する ToUnit という型クラスを考えてみましょう。この型クラスが与える変換は自明なので、どんな型に対しても実装方法は「わかりきって」おり自動生成できそうです。

/-- `Unit`への変換方法を与える自明な型クラス -/
class ToUnit (α : Type) where
  toUnit : α → Unit

/-- `Nat`における `ToUnit` のインスタンス -/
instance : ToUnit Nat where
  toUnit _ := ()

/-- `String`における `ToUnit` のインスタンス -/
instance : ToUnit String where
  toUnit _ := ()

#guard ToUnit.toUnit 42 = ()
#guard ToUnit.toUnit "hello" = ()

この型クラスに対して deriving コマンドが使えるようにするには、まず以下のように記述したファイルを作成し、deriving ハンドラを登録します。

import Lean

open Lean Elab Command

/-- `ToUnit` のためのインスタンス自動導出関数 -/
def deriveToUnitInstance (declNames : Array Name) : CommandElabM Bool := do
  for declName in declNames do
    let term := mkCIdent declName
    let cmd ← `(command| instance : ToUnit $term := ⟨fun _ => ()⟩)
    elabCommand cmd
  return true

initialize
  registerDerivingHandler ``ToUnit deriveToUnitInstance

そうすると、このファイルを import することで以下のように使用できるようになります。

-- 最初はインスタンスがない
#check_failure ToUnit.toUnit true

-- `deriving` コマンドを使用
deriving instance ToUnit for Bool, Int, Char

-- インスタンスが生成された!
#guard ToUnit.toUnit true = ()
#guard ToUnit.toUnit (42 : Int) = ()
#guard ToUnit.toUnit 'a' = ()

他の実装例

より実践的な例をご覧になりたい方は、以下の記事を参照してください。

Expr

Lean.Expr は Lean の 抽象構文木(abstract syntax tree) を表すデータ型です。

具象構文木である Syntax から Expr を得る操作のことを エラボレート(elaborate) と呼び、それを行う関数のことを エラボレータ(elaborator) と呼びます。

Syntax と Expr の違い

具象構文木である Syntax との違いを理解するために、具体的な例で比較してみましょう。

import Lean
import Qq


open Qq Lean Parser

/-- コンストラクタを使って定義した `[1]` というリスト -/
def listByCtorExpr : Q(List Nat) := q(List.cons 1 List.nil)

/- info: "List.cons.{0} Nat (OfNat.ofNat.{0} Nat 1 (instOfNatNat 1)) (List.nil.{0} Nat)" -/
#eval toString listByCtorExpr

/-- リストリテラルから定義した `[1]` というリスト -/
def listByListLitExpr : Q(List Nat) := q([1])

/- info: "List.cons.{0} Nat (OfNat.ofNat.{0} Nat 1 (instOfNatNat 1)) (List.nil.{0} Nat)" -/
#eval toString listByListLitExpr


set_option hygiene false in

/- info: "(Term.app `List.cons [(num \"1\") `List.nil])" -/
#eval show MetaM String from do
  -- コンストラクタを使って定義した `[1]` というリストの Syntax
  let stx ← `(term| List.cons 1 List.nil)
  return toString stx

/- info: "(«term[_]» \"[\" [(num \"1\")] \"]\")" -/
#eval show MetaM String from do
  -- リストリテラルから定義した `[1]` というリストの Syntax
  let stx ← `(term| [1])
  return toString stx

Repr の出力を比較すると極めて長くなるので ToString の出力を比較しましたが、このように Syntax は「同じものを意味するが構文が異なるもの」を区別する一方で、Expr は区別しません。

定義

Expr は Lean のソースコードの中で次のように定義されています。

open Lean

inductive Expr where
  /-- 束縛変数 -/
  | bvar (deBruijnIndex : Nat)

  /-- 自由変数 -/
  | fvar (fvarId : FVarId)

  /-- メタ変数 -/
  | mvar (mvarId : MVarId)

  /-- 型の宇宙レベルを表す。 -/
  | sort (u : Level)

  /--
  (宇宙多相的な)定数であり、モジュール内で以前に定義されたか、
  import されたモジュールによって定義されたもの。
  -/
  | const (declName : Name) (us : List Level)

  /-- 関数適用 -/
  | app (fn : Expr) (arg : Expr)

  /-- ラムダ抽象(無名関数) -/
  | lam (binderName : Name) (binderType : Expr) (body : Expr) (binderInfo : BinderInfo)

  /-- 依存関数型 `(a : α) → β` -/
  | forallE (binderName : Name) (binderType : Expr) (body : Expr) (binderInfo : BinderInfo)

  /-- let 式 -/
  | letE (declName : Name) (type : Expr) (value : Expr) (body : Expr) (nonDep : Bool)

  /-- 自然数リテラルと文字列リテラル。 -/
  | lit : Literal → Expr

  /-- メタデータ(注釈) -/
  | mdata (data : MData) (expr : Expr)

  /-- 射影式(projection expression) -/
  | proj (typeName : Name) (idx : Nat) (struct : Expr)

bvar: 束縛変数

Expr.bvar束縛変数(bound variable) を表します。つまり、式の中で letfun などで束縛された変数のことです。deBrujinIndex パラメータは ド・ブラウンインデックス を表します。

たとえば、具体的な式では次のようになります。

open Qq in

/-
info: Lean.Expr.forallE `x (Lean.Expr.const `Nat [])
  ((((Lean.Expr.const `Eq [Lean.Level.zero.succ]).app (Lean.Expr.const `Nat [])).app (Lean.Expr.bvar 0)).app
    (Lean.Expr.bvar 0))
  Lean.BinderInfo.default
-/
#eval q(∀ x : Nat, x = x)

fvar: 自由変数

Expr.fvar自由変数(free variable) を表します。つまり、式の中で束縛されていない変数のことです。fvarId パラメータは自由変数の識別子です。

たとえば、証明において各時点で得られている仮定は自由変数であり Expr.fvar で表されます。

open Lean Elab Tactic in

/-- 現在の証明の状態を表示するタクティク -/
elab "my_trace_state" : tactic => do
  -- 現在のローカルコンテキストを取得する
  let ctx ← getLCtx

  for (decl : LocalDecl) in ctx do
    let fvar := Expr.fvar decl.fvarId
    let type := decl.type
    logInfo m!"{fvar} : {type}"

/-
info: _example : ∀ (n : Nat), n > 5 → ∃ n, n > 5
---
info: n : Nat
---
info: hn : n > 5
-/
example (n : Nat) (hn : n > 5) : ∃ n, n > 5 := by
  my_trace_state
  exists n

mvar: メタ変数

Expr.mvar はメタ変数を表します。メタ変数は、式におけるプレースホルダや穴だと考えることができます。つまり、後で具体的な値で埋めることが期待される変数です。

メタ変数の典型的な例は、証明における「現時点で示すべきゴール」です。

open Lean Elab Tactic in

elab "show_goal" : tactic => do
  -- 現在のゴールを取得する
  let goal ← getMainGoal
  let goalExpr := Expr.mvar goal
  -- ゴールの式を表示する
  logInfo m!"{goalExpr}: {goal}"

set_option pp.mvars false in

/- info: ?_: ⊢ True -/
example : True := by
  show_goal
  trivial

sort: 宇宙レベル

Expr.sort は型の宇宙レベルを表します。

universe u

open Lean Expr Qq

/- info: sort Level.zero -/
#eval q(Prop)

/- info: sort Level.zero -/
#eval q(Sort 0)

/- info: sort Level.zero.succ -/
#eval q(Type 0)

const: 定数

Expr.const は定数や関数を表します。

open Lean Qq Expr

/- info: const `Nat [] -/
#eval q(Nat)

/- info: const `Nat.zero [] -/
#eval q(Nat.zero)

/- info: const `Nat.succ [] -/
#eval q(Nat.succ)

/- info: const `List.map [Level.zero, Level.zero] -/
#eval q(@List.map.{0, 0})

app: 関数適用

Expr.app は関数適用を表します。たとえば fe に対応する Expr がそれぞれ ⟦f⟧⟦e⟧ であるとき、f e に対応する Expr の項は Expr.app ⟦f⟧ ⟦e⟧ です。

open Lean Expr Qq

/- info: (const `Nat.succ []).app (const `Nat.zero []) -/
#eval q(Nat.succ Nat.zero)

lam: ラムダ抽象

Expr.lam はラムダ抽象を表します。つまり、無名関数を表します。

open Lean Expr Qq

/- info: lam `x (const `Nat []) (bvar 0) BinderInfo.default -/
#eval q(fun (x : Nat) => x)

引数について説明しておきます。

  • binderName は束縛変数の名前を表します。下の例では x です。
  • binderType は束縛変数の型を表します。下の例では Nat です。
  • body は関数の返り値を表します。下の例では Expr.bvar 0 で、これは束縛変数 x を参照しています。
  • binderInfo は束縛変数の情報を表します。明示的な引数なのか、暗黙の引数なのかといった情報を持ちます。
open Lean Expr Qq Level

/- info: "fun (x : Nat) => x" -/
#eval show String from
  let expr := Expr.lam
    (binderName := `x)
    (binderType := q(Nat))
    (body := Expr.bvar 0)
    (binderInfo := .default)
  toString expr

/-
info: lam `α (sort zero.succ)
  (lam `x (bvar 0) ((((const `Eq [zero.succ]).app (bvar 1)).app (bvar 0)).app (bvar 0)) BinderInfo.default)
  BinderInfo.implicit
-/
#eval q(fun {α : Type} (x : α) => x = x)

forallE: 依存関数型

Expr.forallE は依存関数型または全称量化を表現します。

open Lean Expr Qq Level

/-
info: forallE `P (sort zero) (forallE Name.anonymous (bvar 0) (bvar 1) BinderInfo.default) BinderInfo.default
-/
#eval q(∀ (P : Prop), P → P)

/-
info: forallE `n (const `Nat []) (((const `Vector [zero]).app (const `Nat [])).app (bvar 0)) BinderInfo.default
-/
#eval q((n : Nat) → Vector Nat n)

letE: let 式

Expr.letE は let 式を表します。let 式は、変数を束縛してその値を後続の式で使用するために使われます。

open Lean Expr Qq

/-
info: letE `x (const `Nat [])
  ((((const `OfNat.ofNat [Level.zero]).app (const `Nat [])).app (lit (Literal.natVal 0))).app
    ((const `instOfNatNat []).app (lit (Literal.natVal 0))))
  (bvar 0) false
-/
#eval q(let x : Nat := 0; x)

lit: 自然数リテラルと文字列リテラル

Expr.lit は自然数リテラルや文字列リテラルを表します。

open Lean Expr Qq

/- info: lit (Literal.strVal "Lean is nice!") -/
#eval q("Lean is nice!")

Fin

Fin n は、「0 以上 n 未満」の自然数全体を表す型です。Fin n にはちょうど n 個の項があります。各項 z : Fin n

  • z.val : Nat と、
  • z.valn 未満であることの証明 z.isLt

の組です。

/-- `isLt` で `z.val` の値が `n` 未満であることの証明を取り出せる -/
example (z : Fin 5) : z.val < 5 := by
  exact z.isLt

/-- `Fin 3` には `0, 1, 2` に対応する3つの項がある -/
example (z : Fin 3) : z.val = 0 ∨ z.val = 1 ∨ z.val = 2 := by
  grind

用途

Lean に有限個しかないことを伝える

Fin 型は、例えば Lean に何かが「有限個しかない」ことを伝えたいときに役に立ちます。

例えば、「自明でない約数を持つ」という述語を考えてみます。 約数を探索すべき範囲が有限であるためこの述語は決定可能ですが、Lean はそれを自動的には認識しません。

/-- 自然数 n は自明でない約数を持つ -/
def Nat.HasProperDivisor (n : Nat) : Prop :=
  ∃ m : Nat, m ∣ n ∧ 1 < m ∧ m < n


-- Decidable インスタンスの導出に失敗する
def instDecidableFail {n : Nat} : Decidable (Nat.HasProperDivisor n) := by
  unfold Nat.HasProperDivisor
  fail_if_success infer_instance
  sorry

∃ m : Nat の部分を Fin で書き換えてやると、探索すべき範囲が有限であることが Lean に伝わるため、決定可能であることが自動的に導出できるようになります。

/-- 自然数 n は自明でない約数を持つ(`Fin`で書き換えたバージョン) -/
def Nat.HasProperDivisorFin (n : Nat) : Prop :=
  ∃ m : Fin n, m.val ∣ n ∧ 1 < m.val

-- 決定可能性を自動的に導出できる
instance (n : Nat) : Decidable (Nat.HasProperDivisorFin n) := by
  unfold Nat.HasProperDivisorFin
  infer_instance

ちょうど n 個の要素を持つ型を作る

Fin n は「ちょうど n 個の要素を持つ型」として標準的なものなので、特定の個数の要素を持つ型を定義したいとき、Fin を使うと便利なことがあります。具体例としては 付録: 嫉妬深い夫たちの川渡りパズル を参照してください。

Float

Float は浮動小数点数を表す型です。

浮動小数点数は、おおざっぱには有限桁の小数のようなものであるといえます。

#eval (0.01 : Float)

#eval (-2.34 : Float)

#eval (-42.0 : Float)

内部実装

浮動小数点数は、IEEE 754 に従い内部的には符号 s ∈ {0, 1} と仮数(significand) c : ℕ と指数 q : ℤ の三つ組で表されています。これは (-1)ˢ × c × 2 ^ q として解釈されます。

-- `64 = 1 * 2⁶` なので、仮数部は 1 で指数は 6 になる
#guard Float.toRatParts' 64.0 = some (1, 6)

-- `6 = 3 * 2¹` なので、仮数部は 3 で指数は 1 になる
#guard Float.toRatParts' 6.0 = some (3, 1)

-- `0.5 = 1 * 2⁻¹` なので、仮数部は 1 で指数は -1 になる
#guard Float.toRatParts' 0.5 = some (1, -1)

誤差

特に、浮動小数点数は2進数として表現されているので、一般に Float で10進数を正確に計算することはできません。 次のように 0.1 : Float を表示させると気が付きませんが、これは表示の際に数値が丸められているためです。

/- info: 0.100000 -/
#eval 0.1

Float を関数 Float.toRat0 で有理数に変換してみると、誤差の存在が明るみになります。

-- `1/10` にはならない!
/- info: 3602879701896397 / 36028797018963968 -/
#eval (0.1 : Float).toRat0

-- 分母の数は2の冪乗になっている
-- これは浮動小数点数が内部で2進数で表現されていることを裏付ける
#guard
  let x := (0.1 : Float).toRat0.den
  2 ^ Nat.log2 x = x

これでは誤差の存在はわかってもその大きさが分かりづらいので、10進数として正確な表現を出力してみましょう。1

/-- 分母が2のベキであるような正の有理数を10進小数として表示する -/
def Rat.pow2ToBase10Pos (x : Rat) : String :=
  -- 整数部分
  let integerPart := toString x.floor

  -- `x` の分母は `2ⁱ` という形をしていると仮定したので、その指数 `i` を求めておく
  let i := Nat.log2 x.den

  -- 小数部分を`x` の分母が `2ⁱ` であることを利用して計算する
  let decimalPart := (x.num % x.den) * 5 ^ i
    |> toString
    |>.leftpad i '0'

  integerPart ++ "." ++ decimalPart

/-- 分母が2のベキであるような有理数を10進小数として表示する -/
def Rat.pow2ToBase10 (x : Rat) : String :=
  if 0 ≤ x then x.pow2ToBase10Pos else "-" ++ (-x).pow2ToBase10Pos

/-- `Float` を丸めを行わずに正確に表示する -/
def Float.toExactDecimal (x : Float) : String := x.toRat0.pow2ToBase10

/- info: "0.1000000000000000055511151231257827021181583404541015625" -/
#eval Float.toExactDecimal 0.1

-- 2進数で表現されているので、0.5 は正確に表現できる
/- info: "0.5" -/
#eval Float.toExactDecimal 0.5

Float では明らかに正しそうに見える等式がしばしば誤差のために偽になるということに注意してください。

-- 等しそうに見えるが、等しくない
#guard 0.1 + 0.2 != 0.3

-- 両辺を `#eval` で評価してみても理由はわからない…

/- info: 0.300000 -/
#eval 0.1 + 0.2

/- info: 0.300000 -/
#eval 0.3

-- 両辺の正確な値を表示させてみると理由がわかる

-- `0.1 + 0.2 : Float` は `0.3 : Rat` より大きい
/- info: "0.3000000000000000444089209850062616169452667236328125" -/
#eval Float.toExactDecimal (0.1 + 0.2)

-- `0.3 : Float` は `0.3 : Rat` より小さい
/- info: "0.299999999999999988897769753748434595763683319091796875" -/
#eval Float.toExactDecimal 0.3

  1. この浮動小数点数を正確に表示させる関数の実装例は、Zulip のトピック “display all of a Float object” における Daniel Weber 氏の投稿を参考にしています。

HashMap

Std.HashMap は、キー(鍵)と値のペアを格納するデータ構造です。

import Lean

open Std in

/- info: Std.HashMap.ofList [(1, "hello"), (2, "world")] -/
#eval ({(1, "hello"), (2, "world")} : HashMap Nat String)

Note

HashMap{ (key, value), ... } という構文で定義することができるのですが、この記法は HashMap 専用のものではなくて型クラスで定義されているものなので、期待されている型が HashMap だとわかっていなければ人間にとっても Lean にとっても解釈に紛れが発生します。そのため、HashMap#eval したときは HashMap.ofList を使った表記が選ばれます。

HashMap 同士の比較

2 つの s t : HashMap があるとき、「st のすべての構成要素が等しい」というのは、s = t ではなくて s ~m t または HashMap.Equiv s t で表現されます。

s = t は「内部実装まで含めて完全に等しい」という意味になってしまうことに注意してください。

open Std HashMap

example : (HashMap.ofList [(1, "a"), (2, "b")]) ~m (HashMap.ofList [(2, "b"), (1, "a")]) := by
  refine Equiv.of_forall_getElem?_eq ?_
  grind

使用例

頻度のカウント

HashMap を使うと、たとえば「文字列中に登場する文字の出現回数を数える」といった処理が効率的にできます。

open Std in

/--
文字列中の各文字の出現回数を数えて `HashMap` にまとめる関数。
-/
def countingChars (s : String) : HashMap Char Nat := Id.run do
  -- 空のハッシュマップを作成(キー:文字、値:出現回数)
  let mut counts : HashMap Char Nat := {}
  -- 文字列をリストに変換して1文字ずつ処理
  for c in s.toList do
    -- 既存のカウント値を取得し(なければ 0)、1 を加算して更新
    counts := counts.insert c (counts.getD c 0 + 1)
  -- 完成したハッシュマップを返す
  return counts

#guard (countingChars "").toList == []  -- 空文字列のカウントは空のハッシュマップ
#guard (countingChars "hello")['h']! == 1
#guard (countingChars "hello")['l']! == 2

ペアの検索

以下は、HashMap を使って「足してゼロになるペア」を配列から効率的に探す例です。

open Std in

/--
`nums` は整数の配列、`target` は目標の和。
`nums` の中から、2つの異なるインデックス `i`, `j` を見つけて、
`nums[i] + nums[j] = target` を満たすものを返す。

見つかった場合は `(i, j)` を `some` で返し、なければ `none` を返す。
-/
def twoSum (nums : Array Int) (target : Int) : Option (Nat × Nat) := Id.run do
  -- すでに見た数とそのインデックスを記録するためのハッシュマップ
  let mut seen : HashMap Int Nat := {}

  -- 配列の要素とそのインデックスを1つずつ取り出して処理する
  for (n, idx) in nums.zipIdx do
    -- 現在の数値に対して、必要な差分(ペアとなるべき数)を計算
    let diff := target - n
    -- 差分がすでに見た値に含まれていれば、それが解の一部
    match seen[diff]? with
    | some seenIdx =>
      -- 差分が見つかったので、そのインデックスと現在のインデックスを返す
      return (seenIdx, idx)
    | none =>
      -- 見つからなければ、現在の値とそのインデックスを記録して次へ
      seen := seen.insert n idx
      continue

  -- 最後まで見つからなければ `none` を返す
  return none

-- 動作確認用のテスト
#guard twoSum #[2, 7, 11, 15] 9 = some (0, 1)
#guard twoSum #[3, 2, 4] 6 = some (1, 2)
#guard twoSum #[3, 3] 6 = some (0, 1)

HashSet

Std.HashSet は、「重複のない集まり」を表すデータ構造です。 { .. } という記法で具体的に HashSet の項を定義することができます。

import Lean

open Std

/- info: Std.HashSet.ofList [1, 2, 3] -/
#eval ({1, 2, 3, 1, 1} : HashSet Nat)

Note

HashSet{ a₁, a₂, ... } という構文で定義することができるのですが、この記法は HashSet 専用のものではなくて型クラスで定義されているものなので、期待されている型が HashSet だとわかっていなければ人間にとっても Lean にとっても解釈に紛れが発生します。そのため、HashSet#eval したときは HashSet.ofList を使った表記が選ばれます。

insert 関数で要素を挿入することができますが、同じ要素を複数回挿入しても1つしか保持されません。

/- info: Std.HashSet.ofList [1] -/
#eval show (HashSet Nat) from Id.run do
  -- 空の `HashSet` を作成
  let mut s : HashSet Nat := {}
  -- `1` を2回挿入
  s := s.insert 1
  s := s.insert 1
  s

HashSet 同士の比較

2 つの s t : HashSet があるとき、「st のすべての構成要素が等しい」というのは、s = t ではなくて s ~m t または HashSet.Equiv s t で表現されます。

s = t は「内部実装まで含めて完全に等しい」という意味になってしまうことに注意してください。

open HashSet

example : (HashSet.ofList [1, 2]) ~m (HashSet.ofList [2, 1]) := by
  refine Equiv.of_forall_mem_iff ?_
  grind

特長

基本的なコレクション型である List と比較すると、HashSet には「要素が存在するか判定するのが高速」という特徴があります。 (要素の挿入や削除も高速ですが、ここでは存在判定に着目します。)

List では要素を順にたどって調べる必要があるのでサイズに対して線形時間かかってしまう一方で、HashSet は(良いハッシュ関数が与えられているという条件下で、平均的に)定数時間で判定することができます。実際に実験して List より高速であることを確かめたのが次のコードです。

import Lean

open Std

/-- HashSetとListで処理を共通化するための型クラス -/
class DS (col : Type) (α : Type) where
  /-- 要素があるか判定する -/
  contains : col → α → Bool

  /-- ランダムな要素を生成する -/
  gen : IO col

/-- 長さ `n` で、中身の値が 0 以上 `bound` 以下であるようなリストをランダム生成する -/
def randList (n : Nat) (bound : Nat) : IO (List Nat) := do
  let mut out := []
  for _ in [0 : n] do
    let x ← IO.rand 0 bound
    out := x :: out
  return out

/-- 実験に使用するリスト・HashSet のサイズ -/
private def sampleSize := 1000_000

/-- 実験に使用するための巨大なリストをランダム生成する -/
private def genSampleList : IO (List Nat) := do
  let size := sampleSize
  let lst ← randList size (size * 1000)
  return lst

/-- 実験に使用するための巨大な HashSet をランダム生成する -/
private def genSampleHashSet : IO (HashSet Nat) := do
  let lst ← genSampleList
  return HashSet.ofList lst

instance : DS (List Nat) Nat where
  contains := fun lst => lst.contains
  gen := genSampleList

instance : DS (HashSet Nat) Nat where
  contains := fun s x => s.contains x
  gen := genSampleHashSet

/-- 試す回数 -/
private def trial_time := 100

/-- `contain`を実行するのにかかる平均時間 -/
@[noinline]
def containAvgTime (type : Type) [inst : DS type Nat] : IO Nat := do
  let col ← inst.gen
  let mut total_time := 0
  let mut useless := false -- 実行時に最適化されないように、無駄な計算をする
  for i in [sampleSize : sampleSize + trial_time] do
    let start_time ← IO.monoNanosNow
    let b := inst.contains col i
    let end_time ← IO.monoNanosNow
    useless := !useless && b -- 無駄な計算をする
    let elapsed := end_time - start_time
    total_time := total_time + elapsed
  return total_time / trial_time

def main : IO Unit := do
  let list_avg_time ← containAvgTime (type := List Nat)
  IO.println s!"Average time for list.contains: {list_avg_time} ns"

  let hash_avg_time ← containAvgTime (type := HashSet Nat)
  IO.println s!"Average time for hashSet.contains: {hash_avg_time} ns"

  if list_avg_time < hash_avg_time then
    throw <| .userError s!"List is faster: {list_avg_time} ns < {hash_avg_time} ns"

#eval main

舞台裏

なぜ HashSet では要素が存在するかの判定が高速なのでしょうか?また HashSet α を構築するには Hashable α のインスタンス(つまり α 上のハッシュ関数)が必要ですが、なぜ必要なのでしょうか?こういった疑問に答えるには、HashSet の内部実装を知る必要があります。

実際の Lean 標準ライブラリにおける実装を紹介することはかないませんが、ここでは Array を使った チェイン法(chaining) と呼ばれる方法による実装コードを紹介します。1(これは例示のためのトイ実装ですが、それほど本質は損なわれていません)

import Lean

open Std

namespace Playground

variable {α : Type} [Hashable α] [BEq α]

/-- チェイン法(chaining)による HashSet の実装 -/
structure HashSet (α : Type) where
  /-- 内部データとしての配列 -/
  data : Array (List α)
  /-- data のサイズ -/
  size : Nat

/-- 空の HashSet。空リストばかり格納されている。 -/
def HashSet.empty (size := 10000) : HashSet α :=
  { size := size, data := Array.replicate size [] }

/-- UInt64 を Nat に自動で変換する -/
instance : Coe UInt64 Nat where
  coe n := n.toNat

/-- HashSet に新しい要素を挿入する -/
def HashSet.insert (s : HashSet α) (x : α) : HashSet α :=
  -- 内部にある配列を取得する
  let data := s.data

  -- 加えようとしている要素のハッシュ値を計算して、
  -- それをインデックスとして内部の配列にアクセスする
  let idx := hash x
  let list : List α := data[(idx : Nat) % data.size]!

  -- 既に x が存在する場合とそうでない場合で場合分けをする
  if list.contains x then
    -- 既に存在する場合はそのままの HashSet を返す
    s
  else
    -- 存在しない場合
    -- そのインデックスにあるリストに要素を追加する
    let newData := data.set! idx (x :: list)

    -- 新しいデータを持つ HashSet を返す
    { size := data.size, data := newData }

/-- HashSet に要素があるか判定する。
ハッシュ関数でインデックスを計算するので高速に判定できる。-/
def HashSet.contains (s : HashSet α) (x : α) : Bool :=
  -- 内部にある配列を取得する
  let data := s.data

  -- 加えようとしている要素のハッシュ値を計算して、
  -- それをインデックスとして内部の配列にアクセスする
  let idx := hash x
  let list : List α := data[(idx : Nat) % data.size]!

  -- そのリストに x が含まれているかどうかを返す
  list.contains x

/-- HashSet から要素を削除する。
ハッシュ関数の値でインデックスがわかるので高速に削除できる。-/
def HashSet.erase (s : HashSet α) (x : α) : HashSet α :=
  -- 内部にある配列を取得する
  let data := s.data

  -- 加えようとしている要素のハッシュ値を計算して、
  -- それをインデックスとして内部の配列にアクセスする
  let idx := hash x
  let list : List α := data[(idx : Nat) % data.size]!

  -- x が存在しない場合はそのままの HashSet を返す
  if !list.contains x then
    s
  else
    -- 存在する場合は、x を除いたリストを作成して新しい HashSet を返す
    let newData := data.set! idx (list.erase x)
    { size := data.size, data := newData }

/-- HashSet をリストに変換する。この操作には、`Ω(|s.size|)` の時間がかかる。 -/
def HashSet.toList (s : HashSet α) : List α := Id.run do
  let mut result : List α := []
  -- 内部の配列をすべて結合してリストにする
  for x in s.data do
    result := x ++ result
  return result

-- contains 関数のテスト
#guard show Bool from Id.run do
  let mut set : HashSet Nat := HashSet.empty
  set := set.insert 1
  set := set.insert 2
  set.contains 1

-- 削除のテスト
#guard show Bool from Id.run do
  let mut set : HashSet Nat := HashSet.empty
  -- 1 を2回挿入
  set := set.insert 1
  set := set.insert 1
  -- 1 を削除する
  set := set.erase 1
  ! set.contains 1

/- info: [2, 1] -/
#eval show (List Nat) from Id.run do
  let mut set : HashSet Nat := HashSet.empty
  -- 1 を2回挿入
  set := set.insert 1
  set := set.insert 1
  set := set.insert 2
  -- HashSet をリストに変換して返す
  set.toList

end Playground

使用例

足して 0 になるペアがあるか判定する

整数からなるリスト l : List Int が与えられたとします。このとき、l の中に足して 0 になるペアが存在するかどうかを判定する問題を考えます。

この問題は HashSet を使うと次のように O(|l|) 時間で解くことができます。要素が存在するかどうかを判定するのが高速という特徴が生かされていることに注目してください。もし List を使っていたら、要素が存在するかどうかの判定に Ω(|l|) 時間かかってしまうので、全体で Ω(|l|²) 時間かかってしまいます。

def findSumZeroPair (l : List Int) : Bool := Id.run do
  -- 今までに見た要素を保持するための HashSet
  let mut seen : HashSet Int := {}

  for x in l do
    -- x の補数を既に見つけていたら true を返す
    if seen.contains (-x) then
      return true

    -- そうでなければ、単に x を HashSet に追加して次へ
    seen := seen.insert x

  return false

文字列の部分文字列を重複を除いて全列挙する

文字列 s := c₀c₁..cₙ に対して、s に含まれる連続する文字列 cₖcₖ₊₁..cₗ を「s の部分文字列」といいます。

与えられた s : String に対して、s のすべての部分文字列を求める関数を考えます。このとき、部分文字列をどう数えるかの流儀が少なくとも2通り考えられます。

  • s から切り出す位置が異なっていても、文字列として同じならば部分文字列としても同じ
  • s から切り出す位置が異なっていれば、部分文字列として異なる

前者の解釈を採用したとするなら、その関数の型は String → HashSet String であるべきです。そうすれば型からより多くの情報が得られるほか、重複を除くための処理が簡潔になるからです。

open String Pos in

/-- ある文字列の部分文字列を重複を除いて全列挙する -/
def allSubstrings (s : String) : HashSet String := Id.run do
  let mut result : HashSet String := {}
  for j in [1 : s.length + 1] do
    for i in [0 : j] do
      let sub := Raw.extract s ⟨i⟩ ⟨j⟩
      result := result.insert sub
  return result.insert ""

#guard (allSubstrings "aaa").toList = ["", "aaa", "a", "aa"]

#guard (allSubstrings "abc").toList = ["", "abc", "bc", "c", "a", "ab", "b"]

  1. チェイン法によるハッシュテーブルの実装については、詳しくは Pat Morin著・堀江、陣内、田中訳「みんなのデータ構造」(ラムダノート)を参考にしてください。

IO

IO は、入出力(input / output)などの外界とのやり取りを含む計算を表すモナドです。ファイル操作・入出力・乱数・時刻の取得などの副作用のある処理を包んで、副作用のない処理と区別します。

IO α の項は、「実行すると α の値を得られるかもしれない計算」を表します。特に、IO α の項は計算によって得られる値そのものではありません。このニュアンスを強調するために、IO α の項のことを IO アクション と呼ぶことがあります。

IO アクションの例

標準入出力

標準出力ストリームに何かを書き込んだり、標準入力ストリームから何かを受け取ったりする処理は IO アクションです。以下は、ユーザによりキーボードから入力された文字を受け取って、挨拶を返す単純なプログラムの例です。

/-- キーボードからユーザの入力を取得する -/
def getUserInput : IO String := do
  -- 入力待ちであることをアピールする
  IO.print "> "
  let stdin ← IO.getStdin
  let input ← stdin.getLine
  -- trim をしないと余計な改行が入る
  return input.trimAscii.copy

/-- 標準入力から入力された名前に対して挨拶をする -/
def main : IO Unit := do
  IO.println "誰に挨拶しますか?"
  let name ← getUserInput
  IO.println s!"Hello, {name}!"

標準出力に書き込んだ後、同じ行を上書きすることによって、ターミナル上でアニメーションを表示することができます。以下は、処理の進捗を表すスピナーを表示する例です。

def spinnerFrames : Array String :=
  #["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

/-- ターミナルに対して、現在のカーソルがある行全体を消せと指示する -/
def IO.clearLine : IO Unit := do
  IO.print "\r\x1b[2K"

def main : IO Unit := do
  -- 標準出力ストリームを取得する
  let stdout ← IO.getStdout

  for i in List.range 80 do
    let frame := spinnerFrames[i % spinnerFrames.size]!
    IO.print s!"\r{frame} 処理中..."

    -- 標準出力への変更をすぐに反映させる
    stdout.flush

    -- ここに「少しずつ進む重い処理」を書く
    IO.sleep 20

  -- 行を消して完了表示
  IO.clearLine
  IO.println "完了"

また、ユーザの入力を扱うことができるということは、対話的(interactive)なプログラムを書くことができるということです。たとえば、以下のようなプログラムを書くことができます。

余談ですが、Lean 4 でもクワイン(quine)を書くことができます。

ファイル操作

以下は、ファイルを読んでその内容を表示するような簡単なコマンドラインツールを実装する例です。

open IO FS System

/-- シンプルな `cat` コマンドの実装。

コマンドライン引数からファイル名を受け取り、そのファイルの内容を表示する。
ファイル名が指定されていない場合や、複数指定された場合にはエラーメッセージを表示する。
-/
def main (args : List String) : IO Unit := do
  match args with
  | [] => println "ファイル名を指定してください。"
  | [fileName] =>
    let filePath : FilePath := fileName
    let fileExists ← filePath.pathExists
    if fileExists then
      let contents ← readFile filePath
      println contents
    else
      println s!"ファイルは存在しません: {fileName}"
  | _ => println "複数のファイル名は指定できません。1つだけ指定してください。"

ランダム性

乱数を扱うような操作は IO アクションです。

/-- 長さ `n` で、中身の値が 0 以上 `bound` 以下であるようなリストをランダム生成する -/
def randList (n : Nat) (bound : Nat) : IO (List Nat) := do
  let mut out := []
  for _ in [0 : n] do
    -- ランダムに 0 以上 bound 以下の自然数を生成する
    let x ← IO.rand 0 bound

    -- 生成した自然数をリストに追加する
    out := x :: out
  return out

#eval randList 5 10

時刻

時刻を扱うような操作も IO アクションです。

/-- フィボナッチ数列 -/
def fibonacci (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci n + fibonacci (n + 1)

/-- `fibonacci` の計算にかかった時間を計測する -/
def computeTime : IO Unit := do
  let start_time ← IO.monoMsNow
  -- コンパイラに最適化されて実行順序が変わらないように、`IO.lazyPure` で包む
  let result ← IO.lazyPure <| fun _ => fibonacci 30
  let end_time ← IO.monoMsNow
  IO.println s!"Result: {result}, Time taken: {end_time - start_time} ms"

#eval computeTime

IO からの脱出

IO アクションは基本的に「副作用のある計算」であり、IO から脱出する安全かつ汎用的な関数はありません。言い換えれば、runIO : IO α → α というような参照透過な関数はありません。

しかし Lean は自由度が高い言語でありまして、この制約も破ることができます。unsafe な機能を使ってよければ、IO から脱出することができます。1

import Lean

open Lean Meta Elab Term

def toExprIO {α : Type} [ToExpr α] (x : IO α) : IO Expr :=
  toExpr <$> x

elab tk:"run_io " t:doSeq : term <= expectedType => withRef t do
  let expectedType := mkApp (mkConst ``IO) expectedType
  let v ← elabTermEnsuringType (← `(do $t)) expectedType
  synthesizeSyntheticMVarsNoPostponing
  let v ← instantiateMVars v
  if (← logUnassignedUsingErrorInfos (← getMVars v)) then
    throwAbortTerm
  let v ← mkAppM ``toExprIO #[v]
  let io ← unsafe evalExpr (IO Expr) (mkApp (mkConst ``IO) (mkConst ``Expr)) v
  let (out, x) ← IO.FS.withIsolatedStreams io.toBaseIO
  unless out.isEmpty do
    logInfoAt tk out
  match x with
  | .ok x => return x
  | .error e => throwErrorAt tk e.toString

-- 使用例。ランダムな数を選んでいるのに、型を `Nat` にすることができる
def number : Nat := run_io IO.rand 0 1000

-- 実行前に既に値が決定されているため、
-- 平気で証明ができる
example : 0 ≤ number ∧ number < 1001 := by
  simp [number.eq_def]

  1. このコードは Lake.DSL.Meta のコードをほぼそのまま引用したものです。

Linter

Lean.Elab.Command.Linter は構文リンター(syntax linter)の本体です。

構文リンターとは何かというと、Lean のリンター(よくない書き方のコードを検出するツール)の分類名です。 構文リンターは基本的には各コマンドのエラボレーション時に自動で実行されます。 構文リンターの種類によっては、set_option コマンドなどで有効化する必要があります。

Lean のリンターには他に、環境リンター(environment linter)があります。

使用例

選択原理を証明の中で使用すると警告してくれるリンターを自作する例を紹介します。1 まず以下のように記述したファイルを作成します。

-- Linter/DetectClassical.lean の内容

import Lean

/--
`detectClassical` リンターは、`Classical.choice` 公理に依存する宣言に対して警告を発する。
デフォルト値は `true`
-/
register_option linter.detectClassical : Bool := {
  defValue := true
  descr := "detectClassicalリンターを有効にする"
}

open Lean Elab Command

/--
ある位置 `pos` 以降にソースコード内で登場するすべての宣言名を収集する
-/
private def getNamesFrom (pos : String.Pos.Raw) : CommandElabM (Array Syntax) := do
  let drs := declRangeExt.toPersistentEnvExtension.getState (asyncMode := .local) (← getEnv)
  let fm ← getFileMap
  let mut nms := #[]
  for (nm, rgs) in drs do
    if pos ≤ fm.ofPosition rgs.range.pos then
      let ofPos1 := fm.ofPosition rgs.selectionRange.pos
      let ofPos2 := fm.ofPosition rgs.selectionRange.endPos
      nms := nms.push (mkIdentFrom (.ofRange ⟨ofPos1, ofPos2⟩) nm)
  return nms

@[inherit_doc linter.detectClassical]
def detectClassicalLinter : Linter where
  run := withSetOptionIn fun stx ↦ do
    -- リンターが有効になっていなければ何もしない
    unless Linter.getLinterValue linter.detectClassical (← Linter.getLinterOptions) do
      return

    -- どこかにエラーがあれば何もしない
    if (← get).messages.hasErrors then
      return

    -- ユーザが定義した名前を取得する
    let names := (← getNamesFrom (stx.getPos?.getD default))
      |>.filter (! ·.getId.isInternal)

    for constStx in names do
      let constName := constStx.getId
      let axioms ← collectAxioms constName

      -- 公理に依存していなければスルーする
      if axioms.isEmpty then
        return

      -- 選択原理に依存していれば警告を出す
      if axioms.contains `Classical.choice then
        Linter.logLint linter.detectClassical constStx
          m!"`{constName}` depends on `Classical.choice`.\nAll axioms: {axioms.toList}"

initialize addLinter detectClassicalLinter

このファイルを読み込むと、次のように使用できます。

import LeanByExample.Type.Linter.DetectClassical

-- 選択原理を使用しているため警告が出る
/-
warning: `prop_iff_neg_self₀` depends on `Classical.choice`.
All axioms: [propext, Classical.choice, Quot.sound]

Note: This linter can be disabled with `set_option linter.detectClassical false`
-/
theorem prop_iff_neg_self₀ (P : Prop) : ¬ (P ↔ ¬ P) := by
  intro h
  by_cases hp : P
  · have : ¬ P := by
      rwa [h] at hp
    contradiction
  · have : ¬ ¬ P := by
      rwa [h] at hp
    contradiction

-- 選択原理に依存しない証明には警告が出ない
theorem prop_iff_neg_self₁ (P : Prop) : ¬ (P ↔ ¬ P) := by
  intro h
  have hnp : ¬ P := by
    intro hp
    have hnp : ¬ P := by
      rwa [h] at hp
    contradiction
  have hp : P := by
    have : ¬ ¬ P := by
      rwa [h] at hnp
    contradiction
  contradiction

  1. この例は Lean 公式の Zulip の restricting axioms というトピックにおける、Damiano Testa さんの投稿を参考にしています。

List

List連結リスト(linked list) を表す型です。

Lean では次のように再帰的に定義されています。

/-- `α` 型の項を集めたリスト -/
inductive List.{u} (α : Type u) where
  /-- 空リスト `[]` はリスト -/
  | nil : List α

  /-- `a : α` と `l : List α` があるとき、`a` を先頭に追加したリストが作れる -/
  | cons (head : α) (tail : List α) : List α

構文と記法

List α の項はカンマ区切りの値を [] で囲むことによって作ることができます。これはリストリテラルと呼ばれる構文です。

#check ([1, 2, 3] : List Nat)

また、List のコンストラクタ List.nil[] で、List.cons:: で表すことができます。

example : [] = @List.nil (α := Unit) := by rfl

-- List.cons x xs は x :: xs と書ける
example {α : Type} (x : α) (xs : List α) : List.cons x xs = x :: xs := by
  rfl

(· :: ·) は「リストの先頭に要素を追加して新しいリストを作る操作」を表していると考えることができます。

-- `::` で先頭に要素を追加する
#guard "hello" :: ["world"] = ["hello", "world"]

連結

リスト同士を順序を保ちながら連結するには、List.append 関数を使います。これは Append 型クラスのインスタンスになっているので、++ という演算子で利用できます。

-- `++` 演算子が利用できる
example {α : Type} (xs ys : List α) : xs ++ ys = List.append xs ys := by
  rfl

-- リストの連結
#guard [1.0, 2.0] ++ [3.0] == [1.0, 2.0, 3.0]

高階関数

「関数を引数に取る関数」や「関数を返す関数」のことを、高階関数(higher-order function) と呼ぶことがあります。List 名前空間には様々な高階関数が定義されています。

map

ListFunctor 型クラスのインスタンスになっているため <$> 演算子が利用できます。<$>List.map で実装されています。

-- `<$>` 演算子が利用できる
#guard (fun x => x * 2) <$> [1, 2, 3] = [2, 4, 6]

example {α β : Type} (f : α → β) (xs : List α) : f <$> xs = List.map f xs := by
  rfl

List.map f は「リストの中身のそれぞれに独立に関数を適用する」操作を表します。具体的には関数 f : α → β があるとき、List.map f は関数 List α → List β であって、リスト xs : List α の各要素に f を適用するようなものです。

section

  variable {α β : Type} {a₁ a₂ a₃ : α}

  example (f : α → β) : [a₁, a₂, a₃].map f = [f a₁, f a₂, f a₃] := by
    rfl

  -- リストの各要素を 2 倍する
  #guard List.map (fun x => x * 2) [1, 2, 3] = [2, 4, 6]

end

filter

List の中の要素から、ある条件を満たすものだけを取り出すには List.filter を使います。

-- `l` 以外の文字を残す
#guard ["h", "e", "l", "l", "o"].filter (· ≠ "l") = ["h", "e", "o"]

-- 偶数を残す
#guard [1, 2, 3, 4, 5].filter (· % 2 = 0) = [2, 4]

foldr

List.foldr は、二項演算でリストの各要素を右結合的に繋げて畳み込む(fold)関数です。

/-- `List.foldr` の例示のための型クラス -/
class Foldr (α β : Type) where
  /-- 右結合的な二項演算 -/
  op : α → β → β

-- 右結合的な演算子として `⋄` を定義する
@[inherit_doc] infixr:70 "⋄" => Foldr.op

section
  variable {α β : Type} [Foldr α β] {a₁ a₂ a₃ : α}

  -- foldr を適用することは、リストの要素を二項演算で繋げることに等しい
  example (init : β) : [a₁, a₂, a₃].foldr (· ⋄ ·) init = a₁ ⋄ a₂ ⋄ a₃ ⋄ init := by
    rfl

end

List.foldr は、多くの再帰関数に共通に現れる再帰のパターンを抽象化したものになっています。たとえば、リストの長さを求める関数 List.length を次のように定義すると、これは List.foldr の具体例になっています。1

namespace Foldr
  variable {α : Type}

  /-- リストの長さを求める -/
  def length (xs : List α) : Nat :=
    match xs with
    | [] => 0
    | _ :: xs => 1 + length xs

  #guard length [1, 2, 3, 4, 5] = 5

  -- length は foldr で表すことができる!
  example (xs : List α) : length xs = xs.foldr (fun _ n => 1 + n) 0 := by
    delta length List.foldr
    rfl

end Foldr

また、リストの順番を逆にする関数 List.reverse を次のように定義すると、これも List.foldr の具体例になっています。

namespace Foldr
  variable {α : Type}

  /-- リストの順番を逆にする関数 -/
  def reverse : List α → List α
    | [] => []
    | x :: xs => reverse xs ++ [x]

  #guard reverse [1, 2, 3, 4, 5] = [5, 4, 3, 2, 1]

  -- reverse は foldr で表すことができる!
  example (xs : List α) : reverse xs = xs.foldr (fun x xs => xs ++ [x]) [] := by
    delta reverse List.foldr
    rfl

end Foldr

foldl

List.foldl は、二項演算でリストの各要素を左結合的に繋げて畳み込む関数です。

/-- `List.foldl` の例示のための型クラス -/
class Foldl (α β : Type) where
  /-- 左結合的な二項演算 -/
  op : α → β → α

-- 左結合的な演算子として `⊗` を定義する
@[inherit_doc] infixl:70 "⊗" => Foldl.op

section
  variable {α β : Type} [Foldl α β] {b₁ b₂ b₃ : β}

  -- foldl を適用することは、リストの要素を二項演算で繋げることに等しい
  example (init : α) : [b₁, b₂, b₃].foldl (· ⊗ ·) init = init ⊗ b₁ ⊗ b₂ ⊗ b₃ := by
    rfl
end

List.foldlList.foldr と同様、多くの再帰関数に共通に現れる再帰のパターンを抽象化したものになっていますが、List.foldr とは異なり末尾再帰関数になります。たとえば、リストの長さを求める関数 List.lengthTR を次のように定義すると、これは List.foldl の具体例になっています。

namespace Foldl
  variable {α : Type}

  /-- リストの長さを求める -/
  def lengthTR (xs : List α) : Nat :=
    aux xs 0
  where
    /-- リストの長さを求めるヘルパ関数 -/
    aux : List α → Nat → Nat
      | [], n => n
      | _ :: xs, n => aux xs (1 + n)

  #guard lengthTR [1, 2, 3, 4, 5] = 5

  -- lengthTR は foldl で表すことができる!
  example (xs : List α) : lengthTR xs = xs.foldl (fun n _ => 1 + n) 0 := by
    delta lengthTR lengthTR.aux List.foldl
    rfl

end Foldl

また、リストの順番を逆にする関数 List.reverseTR を次のように定義すると、これも List.foldl の具体例になっています。

namespace Foldl

  variable {α : Type}

  /-- リストの順番を逆にする関数 -/
  def reverseTR (xs : List α) : List α :=
    aux xs []
  where
    /-- リストの順番を逆にするヘルパ関数 -/
    aux : List α → List α → List α
      | [], acc => acc
      | x :: xs, acc => aux xs (x :: acc)

  #guard reverseTR [1, 2, 3, 4, 5] = [5, 4, 3, 2, 1]

  -- reverseTR は foldl で表すことができる!
  example (xs : List α) : reverseTR xs = xs.foldl (fun xs x => x :: xs) [] := by
    delta reverseTR reverseTR.aux List.foldl
    rfl

end Foldl

モナドインスタンス

Lean では、List は標準では Monad 型クラスのインスタンスになっていません。

#check_failure (inferInstance : Monad List)

しかし、Monad 型クラスのインスタンスにすることは可能です。

instance : Monad List where
  pure x := [x]
  bind l f := l.flatMap f
  map f l := l.map f

List のモナドインスタンスを利用すると、「リスト xs : List α の中から要素 x : α を選んで y : β を構成することをすべての要素 x ∈ xs に対して繰り返し、結果の y を集めてリスト ys : List β を構成する」ということができます。

def List.map' {α β : Type} (xs : List α) (f : α → β) : List β := do
  -- `x ∈ xs` を選ぶ
  let x ← xs

  -- `y := f x` を構成する
  let y := f x

  -- `y : β` を返す。関数全体の返り値は、返される `y` を集めたものになる。
  return y

#guard [1, 2, 3].map' (· + 1) = [2, 3, 4]

#guard [1, 2, 3, 4].map' (· % 3 == 0) = [false, false, true, false]

List のモナドインスタンスを利用すると、リスト内包記法のような処理、つまり「選択肢を選ぶことに依存して得られうる結果を、一つのリストにすべてまとめて返す」系の関数を簡潔に実装できます。

/-- `α` 上の n 項演算全体の型 -/
def Arity (α : Type) : (n : Nat) → Type
  | 0 => α
  | n + 1 => α → Arity α n

/-- 2 項演算 `Bool → Bool → Bool` の例 -/
example : Arity Bool 2 := fun a b => a && b

/-- 真理関数 `p : Arity Bool n` に対して、その真理値表を作成する。-/
def tablen (n : Nat) (p : Arity Bool n) : List (List Bool) :=
  match n with
  | 0 => [[p]]
  | n + 1 => do
    let b ← [true, false]
    let rest ← tablen n (p b)
    return b :: rest

#guard
  let expected := [
    [true, true, true],
    [true, false, false],
    [false, true, false],
    [false, false, false]
  ]
  let actual := tablen 2 (fun a b => a && b)
  expected = actual

#guard
  let result := tablen 3 (fun a b c => a || b || c)

  -- 結果が false になるものだけ集める
  result.filter (fun xs => ! xs.getLast!) = [[false, false, false, false]]

使用例

l : List α に対しては「先頭への要素の追加」と「先頭からの要素の取り出し」が高速に行えるので、スタックの実装に利用することができます。

/-- 括弧が対応しているか判定する -/
def matchParen (c1 c2 : Char) : Bool :=
  match c1, c2 with
  | '(', ')' => true
  | '{', '}' => true
  | '[', ']' => true
  | _, _ => false

/--
文字列 `s` に含まれる括弧が正しく対応しているかどうかを判定する関数。
開き括弧と閉じ括弧が対応しており、正しい順序で閉じられている場合に `true` を返す。
-/
def validParen (s : String) : Bool := Id.run do
  -- 括弧のスタックを空で初期化
  let mut stack : List Char := []

  -- 文字列の各文字に対してループ
  for c in s.toList do
    -- スタックの先頭の要素(最後に追加された開き括弧)を取得
    let last := stack.head?
    match last with
    | none =>
      -- スタックが空なら、文字をスタックに追加(開き括弧のはず)
      stack := c :: stack
    | some last =>
      -- スタックの先頭と現在の文字が対応する括弧なら、スタックから取り除く(ペアが閉じられた)
      if matchParen last c then
        stack := stack.tail
      else
        -- 対応していない場合は、新たにスタックに追加(新しい開き括弧)
        stack := c :: stack

  -- すべての括弧が正しく閉じられていればスタックは空になっている
  return stack.isEmpty

#guard validParen "()"
#guard validParen "()[]{}"
#guard !validParen "(]"
#guard !validParen "([)]"
#guard validParen "{[]}"
#guard !validParen "{"
#guard !validParen "}"
#guard validParen "([{}])({}){}"

  1. ここで使用した例は、Graham Hutton著, 山本和彦訳「Programming Haskell 第2版」(ラムダノート)の7.3章を参考にさせていただきました。

Macro

Lean.Macro 型の項は、マクロの内部実装を表現しています。一般のプログラミング言語においてマクロとは構文を構文に変換することを指す言葉で、必ずしも特定の型や項に対応する概念ではありませんが、Lean の m : Macro は、Syntax → MacroM Syntax という関数型そのものです。

open Lean in

example : Macro = (Syntax → MacroM Syntax) := by rfl

Syntax 型が Lean の構文木をダイレクトに表していたように、Macro 型は Lean のマクロをダイレクトに表しています。

Macro 型とマクロの関係

Macro 型からマクロ

m : Macro を使ってマクロを定義するには、以下のように [macro] 属性を付与します。

open Lean

/-- `zeroLit` という構文の定義 -/
syntax (name := zeroLitStx) "zeroLit" : term

/-- `zeroLit` という構文を展開するマクロ -/
@[macro zeroLitStx]
def expandZeroLit : Macro := fun stx =>
  match stx with
  | `(term| zeroLit) => `(term| 0)
  | _ => Macro.throwUnsupported

-- マクロ展開されるので、0 に等しいという結果になる
#guard zeroLit = 0

マクロから Macro 型

実際にマクロを定義する際は、notation コマンドや macro コマンド、macro_rules コマンドなどを使用するでしょう。こういったコマンドでマクロを定義したとき、それが裏で Macro 型の項を生成していることを確かめることができます。特定のコマンドの実行後に新たに生成された識別子の名前をリストアップすることができる、#whats_new コマンドを使えば可能です。

import Mathlib.Util.WhatsNew

-- `macro_rules` コマンドの `#whats_new` コマンドによる出力の中に、`Macro` 型の項が含まれている
/-- Macro -/
#guard_msgs (substring := true) in
  #whats_new in
    macro_rules
    | `(zeroLit) => `(1)

マクロ展開を確認する方法

マクロがどのように展開されているか確かめるには、次で示すように Macro.expandMacro? 関数が利用できます。また、pp.macroStack オプションを使うという方法もあります。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: notation:50 lhs✝:51 " LXOR " rhs✝:51 => lxor lhs✝ rhs✝ -/
#expand (infix:50 " LXOR " => lxor)

MacroM

MacroM は、マクロ展開のための主要なモナドです。マクロ衛生を実現するために必要な状態を保持しています。

使用例

オセロのゲーム盤

Macro 型の関数および MacroM モナドに包まれた関数を使用して、オセロのゲーム盤を表現する構文を定義することができます。

import Lean

/-- 盤面の1セルの状態 -/
inductive Cell where
  /-- 空のセル -/
  | empty
  /-- 黒のセル -/
  | black
  /-- 白のセル -/
  | white
deriving Inhabited, Repr, BEq

/-- 盤面の1セルを表す構文カテゴリ -/
declare_syntax_cat cell

/-- 盤面の何も置かれていない箇所 -/
syntax "-" : cell
/-- 盤面の黒のセル -/
syntax "●" : cell
/-- 盤面の白のセル -/
syntax "○" : cell

open Lean Macro

/-- cellの構文展開 -/
def expandCell (stx : TSyntax `cell) : MacroM (TSyntax `term) := do
  match stx with
  | `(cell| -) => `(Cell.empty)
  | `(cell| ○) => `(Cell.white)
  | `(cell| ●) => `(Cell.black)
  | _ => throwUnsupported

open Parser TSyntax

syntax row := withPosition((lineEq cell)*)
syntax (name := boardStx) "board" withoutPosition(sepByIndentSemicolon(row)) : term

/-- rowの構文展開 -/
def expandRow (stx : TSyntax `row) : MacroM (TSyntax `term) := do
  match stx with
  | `(row| $cells:cell*) => do
    let cells ← cells.mapM expandCell
    `(term| #[ $[$cells],* ])
  | _ => throwUnsupported

macro_rules
  | `(term| board $rows:row*) => do
    let rows ← (rows : TSyntaxArray `row).mapM expandRow
    `(term| #[ $[$rows],* ])

#guard
  let actual := board
    - - - -
    - ● ○ -
    - ○ ● -
    - - - -
  let expected : Array (Array Cell) := #[
    #[Cell.empty, Cell.empty, Cell.empty, Cell.empty],
    #[Cell.empty, Cell.black, Cell.white, Cell.empty],
    #[Cell.empty, Cell.white, Cell.black, Cell.empty],
    #[Cell.empty, Cell.empty, Cell.empty, Cell.empty]
  ]
  actual == expected

指数表記マクロ

のような指数表記をマクロで定義することもできます。以下の例では、aⁿ のような構文を定義しています。

open Lean Macro Parser

/-- 指数表記 -/
declare_syntax_cat exp
syntax "⁰" : exp
syntax "¹" : exp
syntax "²" : exp
syntax "³" : exp
syntax "⁴" : exp
syntax "⁵" : exp
syntax "⁶" : exp
syntax "⁷" : exp
syntax "⁸" : exp
syntax "⁹" : exp

-- ホワイトスペースなしで`exp`が1つ以上連続したときにマッチ
syntax term (noWs exp)+ : term
syntax term "⁻"(noWs exp)+ : term

-- `Inv`のために用意されている`⁻¹`という構文を上書きする
syntax term "⁻¹"(noWs exp)+ : term

def expToNat (stx : TSyntax `exp) : Nat :=
  match stx with
  | `(exp|⁰) => 0
  | `(exp|¹) => 1
  | `(exp|²) => 2
  | `(exp|³) => 3
  | `(exp|⁴) => 4
  | `(exp|⁵) => 5
  | `(exp|⁶) => 6
  | `(exp|⁷) => 7
  | `(exp|⁸) => 8
  | `(exp|⁹) => 9
  | _ => 0 -- 無効な構文の場合は0を返す

def expToSyntax (exp : Array (TSyntax `exp)) (litPrefix := 0) : MacroM (TSyntax `term) := do
  let digits := exp.map expToNat
  let exp := digits.foldl (fun acc x => 10 * acc + x) litPrefix
  return quote exp

macro_rules
  | `($lhs $[$exp]*) => do
    let stx ← expToSyntax exp
    `($lhs ^ $stx)
  | `($lhs ⁻$[$exp]*) => do
    let stx ← expToSyntax exp
    `($lhs ^ (Int.neg $stx))
  | `($lhs ⁻¹$[$exp]*) => do
    let stx ← expToSyntax exp (litPrefix := 1)
    `($lhs ^ (Int.neg $stx))

#guard (2 : Int)⁰ = 1
#guard 2³ = 8
#guard (2 : Rat)⁻¹⁰ = (1 : Rat) / 1024

ラベル付き木の項表示

ラベル付き木に対する項表示の構文をマクロで定義することができます。

variable {α : Type}

/-- ラベル付き木 -/
inductive Tree (α : Type) where
  | empty
  | node (v : α) (children : List (Tree α))
deriving BEq

/-- 葉(子を持たないノード) -/
def Tree.leaf (v : α) : Tree α := Tree.node v []

open Lean Macro

/-- ラベル付き木のための構文 -/
declare_syntax_cat tree

/-- `φ`が`tree`構文であるならば、`[tree| φ]`はラベル付き木を表す -/
syntax "[tree| " tree "]" : term

/-- 木のラベル。ラベルとしては、数値リテラル・文字列リテラル・文字リテラルを許可する -/
syntax tree_label := num <|> str <|> char

/-- 基底ケース: `[tree| 42]`などは正しい構文 -/
syntax tree_label : tree

/-- 再帰ステップ -/
syntax tree_label " * " "(" sepBy(tree, " + ") ")" : tree

-- 構文のテスト
#check_failure [tree| 42]
#check_failure [tree| 42 * (13 + 22 + 14)]

/-- `tree_label`に属する構文を`term`に変換する -/
def expandTreeLabel (stx : TSyntax `tree_label) : MacroM (TSyntax `term) :=
  match stx with
  | `(tree_label| $num:num) => `(term| $num)
  | `(tree_label| $str:str) => `(term| $str)
  | `(tree_label| $char:char) => `(term| $char)
  | _ => throwUnsupported

/-- 再帰的に`tree`構文を`term`に変換する -/
partial def expandTree (stx : TSyntax `tree) : MacroM (TSyntax `term) := do
  match stx with
  | `(tree| $label:tree_label) =>
    let v ← expandTreeLabel label
    `(term| Tree.leaf $v)
  | `(tree| $label:tree_label * ( $children+* )) =>
    let v ← expandTreeLabel label
    let children ← children.getElems.mapM expandTree
    `(term| Tree.node $v [ $children,* ])
  | _ => throwUnsupported

macro_rules
  | `([tree| $tree_stx:tree]) => expandTree tree_stx

-- マクロ展開のテスト
#guard
  let actual := [tree| 42]
  let expected := Tree.leaf 42
  actual == expected

#guard
  let actual := [tree| "foo" * ("a" + "b" + "c")]
  let expected := Tree.node "foo" [Tree.leaf "a", Tree.leaf "b", Tree.leaf "c"]
  actual == expected

MLList

Warning

MLList は非推奨になりました。代わりに Std.Iterators.IterM または Std.Iter を使用してください。

参考: MLList vs IterM

MLList は、遅延評価のリストです。遅延評価とは、大まかには「値が必要になるまで計算を遅らせること」を意味します。ここで、Lean は純粋関数型言語であり、すべての関数は純粋であるため、評価の順序によって式の値は変わらないことに注意してください。

遅延評価のリストは通常のリストと異なり値が必要になるまで評価されることがないので、「~を満たす値を一つ見つける」というタイプの問題を解くのに向いています。以下は、ゴールドバッハ予想(2より大きいすべての偶数は、2つの素数の和で表せる)を検証する関数を実装する例です。

import Batteries.Data.MLList

/-- 遅延評価のリスト -/
abbrev LazyList := MLList Id

/-- 与えられた自然数が素数かどうか判定する。
素朴なアルゴリズムであり、かなり遅い。 -/
def Nat.isPrime (n : Nat) : Bool := Id.run do
  if n ≤ 1 then
    return false
  for d in [2:n] do
    if n % d = 0 then
      return false
    if d ^ 2 > n then
      break
  return true

-- テスト
#guard
  let actual := (List.range 100).filter Nat.isPrime
  let expected := [
    2, 3, 5, 7, 11,
    13, 17, 19, 23, 29,
    31, 37, 41, 43, 47,
    53, 59, 61, 67, 71,
    73, 79, 83, 89, 97
  ]
  actual == expected

/-- ゴールドバッハ予想を検証する関数。
遅延評価ではないリストを使っているので、
`Nat.isPrime` をすべての`1 .. n`に対して計算するなどしており非効率。
-/
def goldbachEager (n : Nat) (_ : n % 2 = 0 := by decide) : Nat × Nat :=
  let candidates : List (Nat × Nat) := List.range n
    |>.filter Nat.isPrime
    |>.filter (fun x => (n - x).isPrime)
    |>.map (fun x => (x, n - x))
  candidates.head!

/-- ゴールドバッハ予想を検証する関数。
遅延評価のリストを使用しているので、`Nat.isPrime` を必要なときにのみ計算しており高速。-/
def goldbachLazy (n : Nat) (_ : n % 2 = 0 := by decide) : Nat × Nat :=
  let candidates : LazyList (Nat × Nat) := MLList.range -- 無限リスト
    |>.filter (· < n)
    |>.filter Nat.isPrime
    |>.filter (fun x => (n - x).isPrime)
    |>.map (fun x => (x, n - x))
  candidates.head? |>.get!

Nat

Nat は自然数 0, 1, 2, 3, ... を表す型です。「自然数」といったときに 0 を除外する流派も存在しますが、Lean では自然数として 0 を含むことに注意してください。(自然数としてゼロを含まないとしたら、10や120のような数値はどうやって表現するのでしょうか?)

Nat の項は、数値リテラルとして表現することができます。

/- info: 42 : Nat -/
#check 42

Peano の公理

Lean の標準ライブラリにおいて、Natinductive コマンドを使って以下のように定義されています。

inductive Nat where
  /-- ゼロ -/
  | zero : Nat
  /-- 後者関数 -/
  | succ (n : Nat) : Nat

これは Peano の公理に則ったものです。思い出してみると Peano の公理とは、次のようなものでした:

  • 0 は自然数。
  • 後者関数と呼ばれる関数 succ : ℕ → ℕ が存在する。
  • 任意の自然数 n に対して succ n ≠ 0 が成り立つ。
  • succ 関数は単射。つまり2つの異なる自然数 nm に対して succ n ≠ succ m が成り立つ。
  • 帰納法の原理が成り立つ。つまり、任意の述語 P : ℕ → Prop に対して P 0 かつ ∀ n : ℕ, P n → P (succ n) ならば ∀ n : ℕ, P n が成り立つ。

一見すると、上記の Nat を定義するコードは不完全なように見えます。ゼロが自然数であること、後者関数が存在することは明示的に表現されているのでわかりますが、他の条件が表現されているかどうかは一見して明らかではありません。

しかし、実は他の条件も暗黙のうちに表現されています。コンストラクタの像が重ならないこと、コンストラクタが単射であること、帰納法の原理が成り立つことは inductive コマンドで定義した時点で暗黙のうちに保証されているからです。

帰納法の原理の帰結

ところで、Peano の公理の中でも帰納法の原理だけが述語に対する量化を含んでいて複雑ですね。複雑さのために何を意味しているのかわかりづらくなっているので、帰納法の原理から何が導かれるか考えてみます。

たとえば、帰納法の原理から「すべての n : MyNat.zero.succ m のどちらかの形である」ことが導かれます。以下の証明では axiom コマンドで MyNat を再構築することで証明しています。これは、inductive コマンドで定義された型に対しては、cases タクティクでコンストラクタに応じた場合分けが自動的にできてしまうからです。

opaque MyNat : Type

/-- ゼロ -/
axiom MyNat.zero : MyNat

/-- 後者関数 -/
axiom MyNat.succ : MyNat → MyNat

/-- 帰納法の原理 -/
axiom MyNat.induction {P : MyNat → Prop}
  (h0 : P MyNat.zero) (hs : ∀ n, P n → P (MyNat.succ n)) : ∀ n, P n

example (n : MyNat) : n = MyNat.zero ∨ ∃ m, n = MyNat.succ m := by
  -- 述語の定義
  let P : MyNat → Prop := fun n => n = MyNat.zero ∨ ∃ m, n = MyNat.succ m

  -- `∀ n, P n` を示せばよい。
  suffices goal : ∀ n, P n from by
    exact goal n

  have h0 : P MyNat.zero := by
    simp [P]

  have hs : ∀ n, P n → P (MyNat.succ n) := by
    intro n hn
    dsimp [P]
    right
    exists n

  -- 帰納法の原理から従う
  intro n
  exact MyNat.induction h0 hs n

また、「.succ n = n となる n : MyNat は存在しない」ということも帰納法の原理から導かれます。

inductive MyNat where
  | zero : MyNat
  | succ (n : MyNat) : MyNat

example (n : MyNat) : MyNat.succ n ≠ n := by
  intro h
  induction n with
  | zero => injection h
  | succ n ih =>
    exact ih (show n.succ = n from by injection h)

Nat 上の演算

Nat にも四則演算が定義されていますが、少し特殊な定義になっています。

引き算

Nat 上の引き算 m - n は返り値も Nat にならなければならないので、n ≥ m のとき 0 を返すように定義されています。

-- ゼロを下回らないときは整数の引き算と一致する
#guard 32 - 4 = 28

-- ゼロを下回りそうなときはゼロになる
#guard 2 - 32 = 0
#guard 2 + (2 - 4) = 2

example (m n : Nat) (h : n ≥ m) : m - n = 0 := by omega

Nat における引き算は誤解を招きやすいので、注意深く避けた方が良いかもしれません。1

section
  /- ## Nat における引き算が誤解を招くものであるという例 -/

  variable (x y z n : Nat)

  -- 一瞬、Fermat の最終定理が `omega` 一発で示せてしまったかのように見える。
  -- しかし `2 - n < 0` となることはありえないので、この仮定は偽で、
  -- 偽の仮定があるから証明が通っているだけ。
  theorem Fermat (h1 : 2 - n < 0) : x ^ n + y ^ n = z ^ n → (x * y * z = 0) := by
    omega

end

割り算

また Nat 上の割り算 m / n はゼロ除算を許すように定義されていて、m / 0 = 0 が成り立ちます。

-- 割り切れるときの商
#guard 32 / 4 = 8

-- 割り切れないときは余りは切り捨て
#guard 32 / 15 = 2

-- ゼロ除算はゼロとして定義されている
#guard 2 / 0 = 0

-- 任意の数について `n / 0 = 0` が成り立つ
example (n : Nat) : n / 0 = 0 := by simp

  1. このコード例は Lean の公式 Zulip の Fermat Last theorem by omega というトピックで Anton Mellit さんが示したコード例を参考にしています。

Option

Option は、失敗するかもしれない計算を表す型です。

Option は帰納型として定義されています。α : Type u に対して、Option αsome a または none のいずれかの形をした項になっています。none は計算が失敗し、値を返せなかったことを表します。

inductive Option.{u} (α : Type u) where
  /-- 値が返せないことを意味する。 -/
  | none : Option α
  /-- `α` 型の値を単に包んだもの。 -/
  | some (val : α) : Option α

典型的な使用例

たとえば、配列 xs : Array α に対して、インデックス i の要素は存在するとは限りません。xs の長さよりも i が大きければ、返すべき値がないからです。このため、Lean の標準ライブラリには返り値を Option に包んだ関数が用意されています。

-- インデックス 0 の要素は存在するので取得できる
#guard #[1, 2, 3][0]? = some 1

-- インデックス 3 の要素は存在しないので取得できず、none が返ってくる
#guard #[1, 2, 3][3]? = none

配列やリストなどのコレクション型から要素を取り出す類の操作には、多くの場合返り値が Option に包まれた関数が用意されます。より詳しくは GetElem のページを参照してください。

Functor インスタンス

OptionFunctor 型クラスのインスタンスであり、<$> が使用できます。実装上は Option.map が使用されます。

section
  /- ## Functor インスタンスの実装を確かめる例 -/
  variable {α β : Type}

  -- `<$>` は `Option.map` で実装されている
  example (x? : Option α) (f : α → β) : f <$> x? = Option.map f x? := by
    rfl

end

Option.map は、計算が成功すれば関数を適用し、失敗したら失敗を伝搬します。つまり、Option.map f x? は、x?some x ならば some (f x) を返し、x?none ならば none を返します。

section
  variable {α β : Type}

  -- `Option.map f` は some x を some (f x) に写す
  example (x : α) (f : α → β) : Option.map f (some x) = some (f x) := by
    rfl

  -- `Option.map f` は none を none に写す
  example (f : α → β) : Option.map f none = none := by
    rfl

end

Prod

Prod は、データを重ね合わせたものを表現しており、Prod A BA のデータと B のデータの両方を持たされているようなデータの集まりです。A × B と表記されます。

example {A B : Type} : Prod A B = (A × B) := by rfl

#check (Prod.mk 3 "hello" : Nat × String)

(x₁, x₂, .., xₙ) という構文で項を表すことができます。

#guard (3, "hello") = Prod.mk 3 "hello"

カリー化

A × B → CA → B → C の間に自然な全単射が存在し、同値になることが知られています。

example {A B C : Type} : (A × B → C) ≃ (A → B → C) where
  toFun f a b := f (a, b)
  invFun f p := f p.1 p.2
  left_inv := by
    intro f
    funext p
    cases p
    rfl
  right_inv := by
    intro f
    funext a b
    rfl

この同値関係を使って A × B 上の関数をばらして定義域から積を消す操作を カリー化 と呼びます。カリー化を行うと部分適用を行いやすくなるので、Lean では関数は常にカリー化することが推奨されます。

Prop

Prop は、命題全体がなす型宇宙です。

命題とは、直観的には「曖昧さなしに真か偽かが定まっているような文章」のことです。たとえば 1 + 1 = 2 は命題です。1 + 1 = 3 も(偽ではありますが)命題です。

#check (1 + 1 = 2 : Prop)
#check (1 + 1 = 3 : Prop)

カリー・ハワード同型対応

Lean では、各命題 P : Prop は再び型になっています。これは通常の数学では考えないことなので慣れないとわかりにくいのですが、言い換えれば命題 P の項というものを考えることができます。

たとえば、1 + 1 = 2 という命題に対して、その項を構成することができます。

-- `1 + 1 = 2` という型を持つ項を、`rfl` で構成できる
#check (rfl : 1 + 1 = 2)


-- 項に名前を付けた
def one_and_one_eq_two : 1 + 1 = 2 := rfl

実は、いま構成した 1 + 1 = 2 という型の項は 1 + 1 = 2 の証明になっています。実際のところ、命題 P : Prop の証明とは、Lean においては項 h : P そのものです。このことを強調するために、 証明項(proof term) という呼び方をすることもあります。

-- さっき構成した証明項で証明ができる
example : 1 + 1 = 2 := one_and_one_eq_two

このように命題を型として、証明を項として実装できるのは、そもそも直観主義論理と型付きラムダ計算の間に カリー・ハワード同型対応(correspondence) が存在するからです。簡単に言えば、論理と計算(プログラム)は出自は全く異なるものの何故か同じ構造を持っており、同じものであると見なせるということです。

命題論理

Bool には真と偽に対応する truefalse という項がありますが、Prop では真偽は TrueFalse で表されます。

P Q : Prop があるとき、次のようにして新しい命題を得ることができます。

論理積 P ∧ Q

論理積 P ∧ QPQ がともに成り立つと主張します。「P かつ Q」と読みます。PQ がともに真であるときに限り真となり、それ以外のときは偽となります。

#guard True ∧ True
#guard (True ∧ False) = False
#guard (False ∧ True) = False
#guard (False ∧ False) = False

Lean では And という名前の構造体として表現されます。

example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := And.intro hP hQ

論理和 P ∨ Q

論理和 P ∨ QP または Q の少なくとも一つが成り立つという主張です。「P または Q」と読みます。PQ がともに偽であるときに限って偽になり、それ以外のときは真となります。

#guard True ∨ True
#guard True ∨ False
#guard False ∨ True
#guard (False ∨ False) = False

Lean では Or という名前の帰納型として表現されます。

example (P Q : Prop) (hP : P) : P ∨ Q := Or.inl hP

含意 P → Q

含意 P → QP が成り立つならば Q が成り立つという主張です。「P ならば Q」と読みます。P が真であるのに Q が偽であるときだけ P → Q は偽となり、それ以外のときは P → Q は真となります。特に 前提 P が偽のときは P → QQ に関わらず真となります。P を仮定すれば Q が成り立つ、という意味であると解釈しても問題ありません。

#guard (True → True)
#guard (True → False) = False
#guard (False → True)
#guard (False → False)

Lean では含意は関数型 P → Q として表現されます。これは言い換えれば「P の証明項を受け取って Q の証明項を返す関数の型」です。含意専用の型を用意せず、関数型を流用することで含意を表現しているのは、Curry Howard 同型対応を利用しているためです。

example (P Q : Prop) (hP : P) : Q → P := fun _ => hP

否定 ¬ P

否定 ¬ P は、P が成り立たないという主張です。P が偽のとき真になり、P が真のとき偽になります。

#guard (¬ True) = False
#guard (¬ False) = True

Lean では ¬ PP → False として定義されています。

example (P : Prop) : (¬ P) = (P → False) := rfl

同値 P ↔ Q

同値 P ↔ Q は、P → QQ → P がともに成り立つという主張です。読み方は定まっていませんが「PQ は同値である」などと読みます。PQ の真偽が一致するときに真となり、そうでないとき偽となります。

#guard True ↔ True
#guard (True ↔ False) = False
#guard (False ↔ True) = False
#guard False ↔ False

Lean での定義は P → Q ∧ Q → P ではありません。Iff という専用の構造体が用意されています。

example (P Q : Prop) (hP : P) (hQ : Q) : P ↔ Q :=
  Iff.intro (fun _ => hQ) (fun _ => hP)

Bool と Prop の違い

どちらも言明に対応するため、Bool と似ているようですが以下のような目立つ相違点があります:

  1. Prop の項はそれ自身が型であるため、Prop は型宇宙であると言われます。Bool の項は型ではありません。

  2. Prop の項は TrueFalse のどちらであるかを判定するアルゴリズムがあるとは限りません。Bool の項は簡約すれば必ず truefalse になります。

証明無関係

Prop と同様に Type も型宇宙ですが、Prop の宇宙としての振る舞いには Type との大きな差異が2点あります。それが今から説明する証明無関係と非可述性です。

証明にはデータがない

同じ命題 P : Prop の2つの証明項 h1 h2 : P は必ず等しくなります。直観的には、これは「命題の証明はその命題が真であるという以上の情報を持たない」ということです。これを 証明無関係(proof irrelevance) と呼びます。

-- 各命題の証明項はただ一つしかない
theorem my_proof_irrel (P : Prop) (h1 h2 : P) : h1 = h2 := rfl

証明無関係は axiom で導入された公理から従う定理ではなく、Lean の型システムに組み込まれたものであることに注意してください。

/- info: 'proof_irrel' does not depend on any axioms -/
#print axioms proof_irrel

No Large Elimination

証明無関係の重要な帰結のひとつに、「証明から値を取り出すことができるのは、証明の中だけ」というものがあります。この現象は、「Prop は large elimination を許可しない」という言葉で表現されます。1 誤解を恐れずに雑にかみ砕いて言えば、型 T : Sort u が命題宇宙 Prop よりも大きい宇宙に棲んでいる場合(つまり u > 0 の場合)、少数の例外を除き命題 P : Prop からT への関数 P → T を定義することはできないということです。

たとえば次のように、証明の中であれば証明項を casesrcases で分解して値を取り出すことができます。

-- 同じ存在命題の2通りの証明
-- 2乗すると1になる整数を2通り与えた
theorem foo : ∃ x : Int, x ^ 2 = 1 := by exists 1
theorem bar : ∃ x : Int, x ^ 2 = 1 := by exists -1

def Ok.extract (h : ∃ x : Int, x ^ 2 = 1) : True := by
  -- 仮定にある証明項 `h` を分解して
  -- x を取り出すことができる
  rcases h with ⟨_x, _hx⟩

  trivial

しかし、命題の証明という文脈ではなく関数の定義という文脈(つまり返り値の型が命題ではない状況)にすると一転、分解することができなくなります。これは証明無関係の制約によるものです。直観的に言えば、証明には「その命題が成立する」という情報しかないので、そこからデータを生成することはできないということです。

/-
error: Tactic `cases` failed with a nested error:
Tactic `induction` failed: recursor `Exists.casesOn` can only eliminate into `Prop`

h : ∃ x, x ^ 2 = 1
⊢ Int
-/
def Bad.extract (h : ∃ x : Int, x ^ 2 = 1) : Int := by
  -- 仮定で存在が主張されている `x` を取得して、
  -- 返り値として返すために返り値の型を `Int` に変更するとエラーになる
  obtain ⟨x, hx⟩ := h
  exact x

仮に、上記の例がエラーにならなかったとすると、証明無関係を利用して矛盾を示すことができてしまいます。

-- 仮に `x` を何らかの方法で取り出せたとすると、次のような関数が得られるはず
opaque extract (h : ∃ x : Int, x ^ 2 = 1) : Int

-- そして、次のような条件を満たすはずである
axiom extract_foo : extract foo = 1
axiom extract_bar : extract bar = -1

-- このとき、以下のように矛盾が得られる
example : False := by
  -- 証明無関係により `foo` と `bar` は等しい
  have irr : foo = bar := by rfl

  -- extract が満たすべき条件から、`1 = -1` が導けてしまう
  have : 1 = -1 := calc
    _ = extract foo := by rw [extract_foo]
    _ = extract bar := by rw [irr]
    _ = -1 := by rw [extract_bar]

  -- これは矛盾
  contradiction

Singleton Elimination

No Large Elimination のルールの例外として、P : Prop が以下の全ての条件を満たすとき、任意の型への関数を定義することができます。

  • P の帰納型としてのコンストラクタは1つしかない。
  • P のコンストラクタの引数はすべて Prop か添え字(インデックス)である。

たとえば以下のコードは singleton elimination の例になっています。

-- 証明項 `h : α = β` からデータ `p α → p β` を生成する関数
def castFunc {α β : Type} (p : Type → Type) (h : α = β) : p α → p β := fun x =>
  match h with
  | rfl => x

h : α = β は、α = β つまり Eq α β の項ですが、Eq は以下のように定義されている帰納型であり、コンストラクタの引数がインデックスです。したがって、まさに上記の条件を満たしていることになります。

universe u

inductive Eq {α : Sort u} : α → α → Prop where
  | refl (a : α) : Eq a a

非可述性

もう一つの重要な PropType の差異が 非可述性(impredicativity) です。非可述性について簡単に概略を述べるのは難しいので、まず例から入りましょう。

α : Type 上の述語 P : α → Prop があるとき、α で量化された ∀ x : α, P x は再び命題になります。

section
  -- 何かの型
  variable (α : Type)

  -- α 上の述語
  variable (P : α → Prop)

  -- 量化しても再び命題になる
  #check (∀ (x : α), P x : Prop)
end

更に一般的に、α 上の述語 α → Prop に対して量化しても、再び命題になります。

section
  variable (α : Type) (x : α) (P : α → Prop)

  -- 任意の命題 P に対して P x が成り立つ、という命題
  #check (∀ P : α → Prop, P x)
end

上記の挙動は Prop が命題の型であるという直観的解釈と合致しており、自然な挙動だと感じられると思います。しかしながら、「任意の述語 P に対して」と量化して命題を定義することが許されるということは、自分自身に言及するような述語も作ることができるということになります。たとえば、以下の例を考えてみましょう。

-- α を型とする
opaque α : Type

/-- α 上の述語 P に対して、それが簡単なものかそうでないか判定する述語 -/
opaque simple (P : α → Prop) : Prop

/-- `x : α` は「簡単な述語を成り立たせることがない」という述語。
このとき項 `x : α` は「難解である」と呼ぶことにする。 -/
def anti_simple (x : α) : Prop :=
  ∀ P : α → Prop, simple P → ¬ P x

ここで定義した anti_simple という述語は、α 上の述語全体に対する量化を含んでいますが、自分自身が α 上の述語であるため、自分自身に言及していることになります。いわば定義が循環しています。一般に定義されているものそれ自身が含まれるような定義のことを、非可述的(impredicative) であると呼ぶのですが、これはまさに非可述的な定義になっています。

anti_simple について、まさに anti_simple が自分自身に言及しているということを用いた証明の例を以下に示しておきましょう。

-- 難解な項が存在するならば、`anti_simple` 自身は簡単ではない
example (ex : ∃ x, anti_simple x) : ¬ simple anti_simple := by
  -- 難解な項 x を取り出す
  obtain ⟨x, ex⟩ := ex

  -- anti_simple が簡単だと仮定する
  intro (h : simple anti_simple)

  -- anti_simple 自身が α 上の述語であることから、
  -- anti_simple 自身が簡単ではないことがわかる
  have := ex anti_simple h

  -- これは矛盾
  contradiction

以上が非可述性についての直観的な説明です。より形式的には、型宇宙 U に対して、U が非可述的であるとは ∀ a : U, a の型が U 自身になることをいいます。

-- 命題の宇宙 Prop は非可述的
#check (∀ a : Prop, a : Prop)

-- 型宇宙 Type は非可述的ではなく、可述的
#check (∀ a : Type, a : Type 1)

  1. “large elimination” という用語は、The Hitchhiker’s Guide to Logical Verification の 12.2.3 節より拝借しました。

Queue

Std.Queue は、FIFO キュー(先入れ先出しキュー) です。キューとは、「先に追加した要素が先に取り出される」データ構造のことで、たとえばレジ待ちの行列はキューの例になっています。

import Std

open Std

#check Queue

基本操作

Queue の基本操作を紹介します。

空のキュー

empty で空のキューを作成できるほか、isEmpty 関数で空かどうかを判定できます。empty のことを と書くこともできます。

#guard (Queue.empty : Queue Nat).isEmpty

#guard (∅ : Queue Nat).isEmpty

要素の追加

enqueue でキューの末尾に要素を追加できます。toArray で内容を配列として取り出せます。

/- info: #[10, 20] -/
#eval (∅ : Queue Nat)
  |>.enqueue 10
  |>.enqueue 20
  |>.toArray

enqueueAll で、複数の要素をキューに追加できます。このとき順序が逆になることに注意してください。

#guard
  let q := (∅ : Queue Nat).enqueueAll [10, 20]
  q.toArray = #[20, 10]

要素の取り出し

Queue.dequeue? でキューの先頭から要素を取り出せます。キューが空の場合は none を、空でない場合は「取り出した値」と「残りのキュー」のペアを some で包んで返します。

/-- キューから2つの要素を順番に取り出す -/
def popTwo {α : Type} (q : Queue α) : Option (α × α) := do
  let (x, q) ← q.dequeue?
  let (y, _) ← q.dequeue?
  return (x, y)

-- 10 を入れて 20 を入れると、
-- 取り出すときには 10 が出て次に 20 が出てくる
#guard
  let q := ((∅ : Queue Nat).enqueue 10).enqueue 20
  popTwo q = some (10, 20)

-- 空のキューからは取り出せない
#guard (∅ : Queue Nat).dequeue? = none

内部実装

内部的には Std.Queue は2本の List を持つ構造体として実装されています。

structure Queue.{u} (α : Type u) where
  eList : List α := []
  dList : List α := []

Std.Queue の2つのフィールドのそれぞれについて説明します。eList は追加された要素を貯めていくリストで、キューに追加された要素は eList の先頭に追加されます。

/- info: { eList := [6, 5, 4, 3], dList := [1, 2] } -/
#eval
  let q : Queue Nat := { eList := [5, 4, 3], dList := [1, 2] }
  q.enqueue 6

dList は次に取り出す側のリストです。キューから要素を取り出すとき、まず dList の先頭から要素が取り出されます。

/- info: { eList := [5, 4, 3], dList := [2] } -/
#eval
  let q : Queue Nat := { eList := [5, 4, 3], dList := [1, 2] }
  let (_, q') := q.dequeue?.get!
  q'

キューの toArray による出力は (dList ++ eList.reverse).toArray と常に一致します。

example {α : Type} (q : Queue α)
    : (q.dList ++ q.eList.reverse).toArray = q.toArray := by
  simp [Queue.toArray, List.append_toArray, List.reverse_toArray]

使用例

キューというデータ構造の典型的な応用例として、幅優先探索 (BFS) が挙げられます。

以下は Queue を使って二分木のノード値を幅優先順で列挙する例です。

/-- 二分木 -/
inductive Tree (α : Type) where
  | leaf : Tree α
  | node (val : α) (left right : Tree α) : Tree α

/-- `Queue` を使って二分木のノード値を幅優先順 (BFS) で列挙する -/
def Tree.bfsValues {α : Type} (t : Tree α) : Array α := Id.run do
  -- キューを空の状態で初期化
  -- このキューは「これから訪問するべきノード」を管理する
  let mut q : Queue (Tree α) := ∅
  let mut result : Array α := #[]

  -- ルートノードをキューに追加
  q := q.enqueue t

  -- キューが空になるまでループ
  while !q.isEmpty do
    let some (v, q') ← q.dequeue?
      | unreachable!

    match v with
    | .leaf =>
      -- 何も追加せずに次のループへ
      q := q'
      continue
    | .node val left right =>
      result := result.push val

      -- 左の木、右の木の順にキューに追加
      q := q'.enqueue left |>.enqueue right

  return result

/-- テスト用の二分木

```
    1
   / \
  2   3
 / \
4   5
```
-/
def sampleTree : Tree Nat :=
  .node 1 (.node 2 (.node 4 .leaf .leaf) (.node 5 .leaf .leaf)) (.node 3 .leaf .leaf)

-- 幅優先順で列挙すると [1, 2, 3, 4, 5] になる
#guard Tree.bfsValues sampleTree = #[1, 2, 3, 4, 5]

幅優先探索のより非自明な例として、状態遷移を辿って特定の条件を満たす経路が存在するか判定する問題があります。嫉妬深い夫たちの川渡りパズルなどを参照してください。

Quotient

Quotient は、型 α 上の同値関係 r : Setoid α による 商(quotient) を表します。Setoid を受け取って、商型を返します。

商とは

商とは、ある二項関係 r : α → α → Prop によって同じと見なされるものを同一視したものです。たとえば三角形という図形を考えるとき、回転や平行移動させたものを我々は「同じ形」と認識しますが、これは「回転と平行移動で重なり合う」という二項関係に関する商を取っていることになります。

そもそも「2つのリンゴ」や「2つの消しゴム」といった物の集まりに対して「2個の物の集まりである」という共通点を見出すこと自体、商を取る操作です。この場合は、集合に対して「1対1で漏れのない対応が存在する」という二項関係を考えて商を取ったことになります。したがって、自然数という概念を受け入れた時点で、私たちは商を取るというアイデアを受け入れたことになります。

商がいかに受け入れがたく感じられようと、商は基本的で直観に根ざした操作であると言えます。

Quotient に関する基本的な操作

Quotient.mk: 同値類を取る操作

α 上の同値関係 r : α → α → Prop があるとき、r によって同じと見なされる項同士のグループのことをその項の 同値類(equivalence class) と呼びます。たとえば、整数全体 において r x y := 4 ∣ (x - y) と定義すると r は同値関係になりますが、これによる 0 の同値類は 4 で割った余りがゼロになる項の全体、つまり {0, ±4, ±8, ±12, ..} という集合になります。

Leanで項 x : α に対して、同値関係 sr : Setoid α による同値類を取る操作は Quotient.mk で表されます。

variable {α : Type} (sr : Setoid α)

#check (Quotient.mk sr : α → Quotient sr)

元の型 α は、すべての項がどれかの項の同値類に属していて、複数の同値類に属する項はないので、同値類たちの直和として表されます。そこで同値類の全体のことを α 上の同値関係 sr : Setoid α による 商(quotient) と呼びます。Quotient が表すのは、まさにこの商です。同値類を取る操作 Quotient.mk は、商への関数になります。これは恣意的なところがない、構造的に導かれる操作なので、よく 自然な(canonical) 関数であると形容されます。

商の例を挙げると、たとえば時刻がそうです。時刻は 12 で割った余りで同一視する同値関係が入っていて、13 時と 1 時は同じものだと認識されます。また日付は、時刻を時間・分・秒を無視する同値関係で割ることで得られる商です。

Quotient.lift: 関数を商へ持ち上げる操作

α : Type 上の同値関係 r : α → α → Propr による α の商 α/r について考えます。ある型 β から商 α/r への関数を得るには、たとえば自然な関数 α → α/r と関数 β → α を合成すれば良いですが、商からの関数を得るにはどうすればいいでしょうか?

α/r の各要素は同値類であるため、α の要素の集まりです。もしも関数 f : α → β が、同値類 as : α/r のどの要素 a ∈ as に対しても同じ値を返すならば、f の定義域を α/r に持ち上げて α/r → β という関数を得ることができます。

理解しにくいと思うのでもう一度別な表現をすると、たとえば2人の人物に対して、そのひとたちの「年齢の和」を考えることはナンセンスです。これは、年齢の和が生まれた年だけによっては決まらず、「今が何年であるか」という情報にも依存してしまうからです。これが、上記で述べた「同値類のどの要素を選ぶかに依存せず同じ値を返す」という前提が成立しない状況の例になっています。逆に、「年齢の差」を考えることはできて、年齢の差を与える関数が作れます。これは、年齢の差がまさに「生年にしか依存しない」からで、生年が同じという同値類で割った商からの関数に持ち上げることができます。

この操作は Lean では Quotient.lift で実現できます。もし α : Type 上の同値関係 sr : Setoid α と関数 f : α → β が与えられていて h : ∀ x, x ≈ y → f x = f y が成り立つならば、商への持ち上げ Quotient.lift f h : Quotient sr → β が得られます。

variable {α β : Type} (sr : Setoid α)
variable (f : α → β) (h : ∀ x y, x ≈ y → f x = f y)

#check (Quotient.lift f h : Quotient sr → β)

Quotient.lift が持ち上げであると言われるのは、元の f と値が同じになるからです。つまり、f' := Quotient.lift f h としたとき f' (Quotient.mk x) = f x が成り立ちます。

variable {α β : Type} (sr : Setoid α)
variable (f : α → β) (h : ∀ x y, x ≈ y → f x = f y)

example : ∀ x, (Quotient.lift f h) (Quotient.mk sr x) = f x := by
  intro x
  rfl

Quotient.inductionOn: 同値類の代表元を取る

同値類 a : α/r は、r に関して同値な要素の集まりでした。同値類 a に対して、その中から一つ要素を選び出すことを 代表元 を取ると言います。「どれを選んでも r の意味で同じなので、どれかを取ってその同値類の代表とする」というニュアンスです。

これは Lean では Quotient.inductionOn で実現できます。これを使うと、証明の中で「同値類から代表元を取って~」というよくある議論ができます。

variable {α : Type} (sr : Setoid α)

example (a : Quotient sr) : True := by
  induction a using Quotient.inductionOn with
  | h x =>
    -- `x : α` が得られる
    guard_hyp x : α

    trivial

Quotient.sound: 同値なら商へ送って等しい

α の同値関係 sr : Setoid α による商 α/r において、x y : α が同値つまり x ≈ y であるとき、これは商へ送った時には同一視されます。つまり、言い換えれば自然な関数 Quotient.mk sr : α → α/r による像が等しくなっているはずです。

この事実には、Lean では Quotient.sound という名前が付いています。

variable {α : Type} (sr : Setoid α)
variable (x y : α) (h : x ≈ y)

/-- 同値なら商へ送って等しい -/
example : Quotient.mk sr x = Quotient.mk sr y := by
  apply Quotient.sound
  exact h

Quotient.exact: 商に送って等しいなら同値

Quotient.sound とは逆に、商に送って等しいことから同値であることを導く定理には Quotient.exact という名前がついています。

variable {α : Type} (sr : Setoid α)
variable (x y : α)

/-- 商に送って等しいなら同値 -/
example (h : Quotient.mk sr x = Quotient.mk sr y) : x ≈ y := by
  exact Quotient.exact h

使用例

順序なしペア

直積型 A × A 上の二項関係 r : A × A → A × A → Prop を、r (a₁, a₂) (b₁, b₂) := a₁ = b₁ ∧ a₂ = b₂ ∨ a₁ = b₂ ∧ a₂ = b₁ と定義します。これは、順序を無視してペアを同じと見なす同値関係です。

/-- 順序を無視してペアとして同じかどうか判定する同値関係 -/
instance pairRel (A : Type) : Setoid (A × A) where
  r := fun p₁ p₂ => p₁.1 = p₂.1 ∧ p₁.2 = p₂.2 ∨ p₁.1 = p₂.2 ∧ p₁.2 = p₂.1
  iseqv := by constructor <;> grind

この同値関係による商を考えることで、順序なしのペア型を得ることができます。

/-- 順序なしペア -/
def UnorderedPair (A : Type) := Quotient (pairRel A)

実際、(1, 2)(2, 1)UnorderedPair への像は同じペアとして扱われます。

def sample₁ : UnorderedPair Nat := Quotient.mk _ (1, 2)
def sample₂ : UnorderedPair Nat := Quotient.mk _ (2, 1)

example : sample₁ = sample₂ := by
  apply Quotient.sound
  dsimp [(· ≈ ·), pairRel, instHasEquivOfSetoid, Setoid.r]
  grind

自然数の積の商として整数を得る

Lean の標準ライブラリの定義とは異なりますが、Int を自然数の積の商として構成することができます。

Nat × Nat を考えて、最初の要素を「正の部分」、2つめの要素を「負の部分」と考えるのです。つまり (a, b) ↦ a - b という対応を使って整数を構成します。

/-- 自然数を2つペアにしたもの。`(a, b) : PreInt` は `a - b` のつもり -/
abbrev PreInt := Nat × Nat

これは全ての整数を表すことができますが、重複があるので整数そのものになりません。この構成だとたとえば (0, 1)(1, 2) は同じ整数に対応するので、適切に同一視を入れる必要があります。(x₁, y₁) : PreInt(x₂, y₂) : PreInt が同じ整数に対応するのは x₁ - y₁ = x₂ - y₂ のときですが、これは x₁ + y₂ = x₂ + y₁ と書き直すことができます。したがって、PreInt 上の2項関係 rx₁ + y₂ = x₂ + y₁ で定義して r に関する商を取れば、整数を構成できます。

namespace PreInt
  /- ## MyIntのための同値関係の構成 -/

  /-- PreInt 上の二項関係 -/
  def rel (m n : PreInt) : Prop :=
    match m, n with
    | (m₁, m₂), (n₁, n₂) => m₁ + n₂ = m₂ + n₁

  /-- 反射律 -/
  theorem rel.refl : ∀ (m : PreInt), rel m m := by
    intro (m₁, m₂)
    dsimp [rel]
    ac_rfl

  /-- 対称律 -/
  theorem rel.symm : ∀ {m n : PreInt}, rel m n → rel n m := by
    intro (m₁, m₂) (n₁, n₂) h
    dsimp [rel] at *
    omega

  /-- 推移律 -/
  theorem rel.trans : ∀ {l m n : PreInt}, rel l m → rel m n → rel l n := by
    intro (l₁, l₂) (m₁, m₂) (n₁, n₂) hlm hmn
    dsimp [rel] at *
    omega

  /-- `PreInt.rel`は同値関係 -/
  theorem rel.equiv : Equivalence rel :=
    { refl := rel.refl, symm := rel.symm, trans := rel.trans }

  /-- `PreInt` 上の同値関係 -/
  instance srel : Setoid PreInt := ⟨rel, rel.equiv⟩

end PreInt

/-- 整数の定義 -/
def MyInt := Quotient PreInt.srel

MyInt 上にマイナス演算 (- ·) : MyInt → MyInt を定義しましょう。商からの関数は lift で構成することができます。そこでまず PreInt 上で関数を実装し、それを持ち上げる方針を取ります。

def PreInt.neg (m : PreInt) : MyInt := match m with
  | (m₁, m₂) => Quotient.mk _ (m₂, m₁)

/-- 整数上のマイナス演算 -/
def MyInt.neg : MyInt → MyInt := Quotient.lift PreInt.neg <| by
  intro (m₁, m₂) (n₁, n₂) h
  dsimp [PreInt.neg]; apply Quotient.sound
  dsimp [(· ≈ ·), PreInt.srel, PreInt.rel, instHasEquivOfSetoid, Setoid.r] at *
  omega

instance : Neg MyInt := ⟨MyInt.neg⟩

次に MyInt 上に足し算 (· + ·) : MyInt → MyInt → MyInt を定義しましょう。商からの関数なのでやはり Quotient.lift を使いたくなるのですが、引数が二つあるので Quotient.lift₂ を使う方が良いです。

def PreInt.add (m n : PreInt) : MyInt :=
  match m, n with
  | (m₁, m₂), (n₁, n₂) => Quotient.mk _ (m₁ + n₁, m₂ + n₂)

/-- 整数上の足し算 -/
def MyInt.add : MyInt → MyInt → MyInt := Quotient.lift₂ PreInt.add <| by
  intro (m₁, m₂) (n₁, n₂) (m₁', m₂') (n₁', n₂') h₁ h₂
  dsimp [PreInt.add]; apply Quotient.sound
  dsimp [(· ≈ ·), PreInt.srel, PreInt.rel, instHasEquivOfSetoid, Setoid.r] at *
  omega

instance : Add MyInt := ⟨MyInt.add⟩

StateM

StateM は、状態を変更するような計算を表現するモナドです。 StateM σ α で、「型 σ を持つ状態のデータを読み書きしながら α 型の項を返す計算」を表します。

たとえば、フィボナッチ数列の n 番目の値を計算しながら、計算過程で生成された中間結果を記録していくような関数は次のように書けます。

import Lean

open Std

/-- フィボナッチ数列の計算過程ログ -/
structure FibLog where
  /-- `n`番目のフィボナッチ数列の値を記録する辞書。
  デフォルト値として`0`番目と`1`番目の値を持たせてある。
  -/
  dict : HashMap Nat Nat := HashMap.ofList [(0, 0), (1, 1)]
deriving Repr

/-- 今までの計算結果を記録しながら、フィボナッチ数列の`n`番目の値を計算する -/
def StateM.fibonacci (n : Nat) : StateM FibLog Nat := do
  match n with
  | 0 => return 0
  | 1 => return 1
  | n + 2 => do
    -- 今までの計算結果を取得しようと試みる
    let dict := (← get).dict

    match dict[n + 2]? with
    | some y =>
      -- すでに計算済みならばそれを返す
      return y
    | none =>
      -- まだ計算していなければ再帰的に計算する
      let x1 ← fibonacci (n + 1)
      let x2 ← fibonacci n
      let y := x1 + x2

      -- 計算結果をログに記録する
      modify fun log => { log with dict := log.dict.insert (n + 2) y }

      return y

/-- `n`番目のフィボナッチ数列を計算する -/
def StateM.nthFib (n : Nat) : Nat :=
  -- 空の状態(つまりデフォルト値で埋められた状態)から出発して、
  -- 計算を実行して値を取り出す
  (StateM.fibonacci n).run' {}

/-- `0`から`n`番目までのフィボナッチ数列を計算する -/
def StateM.seqFib (n : Nat) : List Nat :=
  -- 空の状態から出発して、計算を実行した後、
  -- その時点で状態として保持している値を取り出す
  let (_val, log) := (StateM.fibonacci n).run {}
  Prod.snd <$> log.dict.toList

#guard StateM.nthFib 22 = 17711
#guard StateM.seqFib 10 = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

String

String は文字列を表す型です。次のように、文字を表す型 Char のリストに変換したり、その逆を行ったりすることができます。

-- `Char` のリストから `String` を構築する
#guard String.ofList ['H', 'e', 'l', 'l', 'o'] = "Hello"

-- `String` を `Char` のリストに変換する
#guard "Hello".toList = ['H', 'e', 'l', 'l', 'o']

文字列結合

String.append を使って文字列を結合することができます。この関数は Append 型クラスのインスタンスになっているので、++ という記号で利用できます。

#guard String.append "Hello, " "world!" = "Hello, world!"
#guard "Hello, " ++ "world!" = "Hello, world!"

文字列の長さ

文字列の長さは String.length 関数で取得することができます。

#guard "Hello".length = 5

#check List.length

String.length を素朴に実装すると、

  1. 文字列を List Char に変換する
  2. List.length を使って長さを求める

という手順になるかと思います。n 個の要素を持つ xs : List α に対して長さを求めようとすると、先頭から順に要素をたぐっていくので n に比例する時間がかかります。したがって String.length は長い文字列に対しては遅くなりそうなものですが、コンパイラによって実装がオーバーライドされているため、実際には n が大きくても高速に実行できます。

このあたりの背景は、次に示すようにドキュメントコメントに書かれています。

A string is a sequence of Unicode scalar values.

At runtime, strings are represented by dynamic arrays of bytes using the UTF-8 encoding. Both the size in bytes (String.utf8ByteSize) and in characters (String.length) are cached and take constant time. Many operations on strings perform in-place modifications when the reference to the string is unique.

文字列補間

String 型の変数の「評価した後の値」を文字列に埋め込むことができます。これを 文字列補間(string interpolation) と呼びます。Lean では、これは s! という構文で行うことができます。

def greet := "Hello"

/- info: "Hello, world!" -/
#eval s!"{greet}, world!"

Subtype

Subtype は、大雑把に言えば型 A : Type の部分集合を表します。すなわちある述語 p : A → Prop があったとき、Subtype pA の項であって p という性質を満たすようなものの全体を表します。

-- 正の数を表す subtype
#check Subtype (fun n => n > 0)

{x : T // p x} という専用の構文が用意されていて、これで Subtype を表すことができます。

-- 正の数を表す subtype
#check { n : Nat // n > 0 }

実行時の性質

ASubtype は実行時には A の項であるかのように振る舞います。言い換えれば、{ x : A // p x} の項は計算の実行時には A の項と同一視されます。これは、Subtype でラップしてもメモリアドレスが変わらないことから確認できます。

unsafe def checkSubtype : IO Unit := do
  let x : Nat := 42
  let pos : { n : Nat // n > 0} := ⟨x, by omega⟩
  if ptrAddrUnsafe x != ptrAddrUnsafe pos then
    throw <| .userError "メモリアドレスが異なります。"

#eval checkSubtype

用途

Subtype を使うと、ある型 AU という型の一部を切り取ったものだということをコードで表現することができます。たとえば、自然数の型 Nat に対して、正の数だけを抜き出して正の整数の型 Pos を定義することを考えてみます。

このとき、Subtype を使わずに Pos を帰納型として以下のように定義することもできるのですが、こうすると PosNat は実装上まったく無関係ということになってしまいます。

inductive Pos where
  | one
  | succ (n : Pos)

その結果、Nat に対するコンパイラ上の演算の最適化の恩恵を受けることができなくなり、たとえば足し算が非常に遅くなります。

namespace Inductive
  /- ## 別の帰納型として正の整数を定義する -/

  inductive Pos where
    | one
    | succ (n : Pos)

  def Pos.ofNat (n : Nat) : Pos :=
    match n with
    | 0 => Pos.one
    | 1 => Pos.one
    | n + 2 => Pos.succ (Pos.ofNat n)

  instance (n : Nat) : OfNat Pos (n + 1) where
    ofNat := Pos.ofNat n

  def Pos.add (m n : Pos) : Pos :=
    match n with
    | Pos.one => Pos.succ m
    | Pos.succ n' => Pos.succ (Pos.add m n')

  instance : Add Pos where
    add := Pos.add

end Inductive

namespace Subtype
  /- ## Subtype として正の整数を定義する -/

  def Pos := { n : Nat // n > 0 }

  def Pos.ofNat (n : Nat) : Pos :=
    ⟨n + 1, Nat.succ_pos n⟩

  instance (n : Nat) : OfNat Pos (n + 1) where
    ofNat := Pos.ofNat n

  def Pos.add (m n : Pos) : Pos :=
    ⟨m.val + n.val, by
      have mp := m.property
      have np := n.property
      omega
    ⟩

  instance : Add Pos where
    add := Pos.add

end Subtype

-- 帰納型として定義すると5倍以上も遅くなってしまう
#reduce (500 : Subtype.Pos) + (500 : Subtype.Pos)
#reduce (500 : Inductive.Pos) + (500 : Inductive.Pos)

Sum

Sum は、データの選択肢を束ねたものを表現しており、Sum A BAB のどちらかの値を取るような型です。A ⊕ B と表記されます。

-- `A ⊕ B` と表記される
example {A B : Type} : Sum A B = (A ⊕ B) := by rfl

#check (Sum.inl 42 : Nat ⊕ String)
#check (Sum.inr "hello world" : Nat ⊕ String)

関数 f : A ⊕ B → C は、f₁ : A → Cf₂ : B → C の組に対応するという性質があります。

example {A B C : Type} : (A ⊕ B → C) ≃ (A → C) × (B → C) where
  toFun f := (f ∘ Sum.inl, f ∘ Sum.inr)
  invFun f p := Prod.casesOn f fun f₁ f₂ =>
    match p with
    | Sum.inl a => f₁ a
    | Sum.inr b => f₂ b
  left_inv := by grind
  right_inv := by
    dsimp [Function.RightInverse, Function.LeftInverse]
    intros
    rfl

Syntax

Lean.Syntax は、Lean の具象構文木(concrete syntax tree)を表すデータ型です。

具象構文木とは何かを理解するには、私たちがエディタに #eval "Hello" などと入力したとき、Lean がこれを実行する過程で何をしているかを考えるとわかりやすいでしょう。入力された #eval "Hello" は最初は単なる文字列ですが、Lean はまずこれを Lean の文法に照らして合法的なコードであるか解析します。合法ならば次のステップに進むことができますし、そうでなければ「こんなコマンドは知らない」というエラーを表示します。この解析結果を保存する中間的データが具象構文木(Syntax)として表現されます。

なお、ただの文字列を解析して構文木を得ることを構文解析または パース(parse) と呼びます。

定義

Syntax 型は以下のように帰納型として定義されています。

open Lean

/-- 構文木 -/
inductive Syntax where
  /-- パース時のエラー。エラーが起こったときにそれ以降のパースが
  すべて失敗するということを避けるために用意されている。-/
  | missing : Syntax

  /-- 構文木のノード -/
  | node (info : SourceInfo) (kind : SyntaxNodeKind) (args : Array Syntax) : Syntax

  /-- アトム(`atom`)は、構文木の葉の部分を構成する。
  `⊕` や `+` などの演算子、`(` や `)` などの括弧がこれにあたる。-/
  | atom (info : SourceInfo) (val : String) : Syntax

  /-- 識別子(`ident`)は、構文木の葉の部分を構成する。たとえば以下は識別子である。
  * `Nat.add` などの関数名
  * 式 `1 + x` における変数名 `x`
  -/
  | ident (info : SourceInfo) (rawVal : Substring.Raw)
    (val : Name) (preresolved : List Syntax.Preresolved) : Syntax

パースと構文木

実際に、s : String を解析して Syntax の項を得ることができます。

open Lean Parser

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- `true` は識別子としてパースされている。
/-
info: Lean.Syntax.ident
  (Lean.SourceInfo.original "".toRawSubstring { byteIdx := 0 } "".toRawSubstring { byteIdx := 4 })
  "true".toRawSubstring
  `true
  []
-/
#eval parse `term "true"

-- `0` は構文木のノードになっていて、
-- 子としてアトムが一つある。
/-
info: Lean.Syntax.node
  (Lean.SourceInfo.none)
  `num
  #[Lean.Syntax.atom
      (Lean.SourceInfo.original "".toRawSubstring { byteIdx := 0 } "".toRawSubstring { byteIdx := 1 })
      "0"]
-/
#eval parse `term "0"

見ての通りすぐに複雑怪奇になってしまうので、以降は表示を簡略化しましょう。SyntaxToString のインスタンスを実装しており、これは SourceInfo などを含まないシンプルな表現をしてくれるので、それを利用します。

-- 文字列として表示するとかなり簡略化される
/- info: `true -/
run_meta IO.println (← parse `term "true")

/- info: (num "0") -/
run_meta IO.println (← parse `term "0")

/- info: (term!_ "!" `false) -/
run_meta IO.println (← parse `term "! false")

-- 識別子はアトムと違って `Name` を受け取るのでバッククォートがつく
/- info: `Nat.zero -/
run_meta IO.println (← parse `term "Nat.zero")

もちろん項(term)以外の構文カテゴリについてもパースを行うことができます。あと少しだけ例を見ましょう。

-- コマンドをパースする例
/- info: (Command.eval "#eval" (str "\"hello\"")) -/
run_meta IO.println (← parse `command "#eval \"hello\"")

-- タクティクをパースする例
-- 木構造が現れている
/-
info: (Tactic.«tactic_<;>_» (Tactic.constructor "constructor") "<;>" (Tactic.intro "intro" [`h]))
-/
run_meta IO.println (← parse `tactic "constructor <;> intro h")

Tactic

Lean.Elab.Tactic.Tactic 型の項は、タクティクの内部実装を表現しています。タクティクとは証明の状態を操作する関数であり、Tactic 型は Syntax → TacticM Unit という関数型そのものです。

import Lean

open Lean Elab Tactic in

example : Tactic = (Syntax → TacticM Unit) := by rfl

Tactic 型からタクティクを作る

Tactic 型の項からはタクティクを定義することができます。

tada で証明終了をお祝いするタクティク

done タクティクの派生として、ゴールがなくなったら 🎉 でお祝いするタクティクを作ることができます。

import Lean

syntax (name := tada) "tada" : tactic

open Lean Elab Tactic Term

@[tactic tada]
def evalTada : Tactic := fun _stx => do
  -- 現在の未解決のゴールを取得する
  let goals ← getUnsolvedGoals

  -- 未解決のゴールがある場合
  unless goals.isEmpty do
    reportUnsolvedGoals goals
    throwAbortCommand

  logInfo "🎉 おめでとうございます!証明完了です!"

/- info: 🎉 おめでとうございます!証明完了です! -/
example : True := by
  trivial
  tada

trivial タクティクの制限版

trivial タクティクの機能を制限し、True というゴールを閉じる機能だけを持つタクティクを構成することができます。1

import Qq
import Batteries

/-- True というゴールを閉じる機能だけを持つタクティク -/
syntax (name := myTrivial) "my_trivial" : tactic

open Lean Elab Tactic Qq

@[tactic myTrivial]
def evalMyTrivial : Tactic := fun _stx => do
  -- 現在のゴールを取得する
  let goal : MVarId ← getMainGoal
  try
    -- ゴールが `True.intro` で閉じられるか試す
    goal.assignIfDefEq q(True.intro)
  catch _error =>
    -- 失敗した場合はゴールの型を取得してエラーメッセージを表示する
    let goalType ← goal.getType
    throwError "my_trivialタクティクが失敗しました。ゴールの型は`{goalType}`であって`True`ではありません。"

example : True := by
  my_trivial

/- error: my_trivialタクティクが失敗しました。ゴールの型は`False`であって`True`ではありません。 -/
example : False := by
  my_trivial

assumption タクティク

assumption タクティクのように、ゴールの証明が既に仮定にあるときにゴールを閉じるタクティクは次のように Tactic 型の関数によって実装できます。

import Batteries

syntax (name := myAssumption) "my_assumption" : tactic

open Lean Elab Tactic

@[tactic myAssumption]
def evalMyAssumption : Tactic := fun _stx => withMainContext do
  -- 現在のゴールとローカルコンテキストを取得する
  let goal ← getMainGoal
  let ctx ← getLCtx

  for (decl : LocalDecl) in ctx do
    -- ローカル宣言の種類がデフォルトでない場合はスキップする
    if decl.kind != .default then
      continue
    try
      -- ゴールにローカル宣言の変数を代入する
      goal.assignIfDefEq decl.toExpr
      -- 成功したら終了
      return
    catch _ =>
      -- 失敗しても無視して次の候補に進む
      pure ()
  throwError "my_assumptionタクティクが失敗しました。"

example {P : Prop} (hP : P) : P := by
  my_assumption

/- error: my_assumptionタクティクが失敗しました。 -/
example {P Q : Prop} (hP : P) : Q := by
  my_assumption

And 専用 constructor

constructor タクティクの機能を制限し、And 型のゴールを分割する機能だけを持つタクティクを構成する例を示します。2

import Qq

/-- ゴールが`P ∧ Q`という形をしていたら、分解してそれぞれ別ゴールにする -/
syntax (name := andConstructor) "and_constructor" : tactic

open Lean Elab Tactic Qq Meta

/-- ゴールが `P ∧ Q` の形をしているかチェックして、
`P ∧ Q` の形をしていたら `P` と `Q` をそれぞれ返す -/
def extracetAndGoals : TacticM (Q(Prop) × Q(Prop)) := do
  have tgt : Q(Prop) := ← getMainTarget -- 右辺でQqを使用していないのでhaveを使う
  match tgt with
  | ~q($p ∧ $q) => return (p, q)
  | _ => throwError "ゴールが `P ∧ Q` の形ではありません。"

@[tactic andConstructor]
def evalAndConstructor : Tactic := fun _stx => withMainContext do
  -- ゴールを取得する
  let goal ← getMainGoal
  have (p, q) := ← extracetAndGoals

  -- 新しいメタ変数(ゴール)を作成する
  have left : Q($p) := ← mkFreshExprSyntheticOpaqueMVar p (tag := `left)
  have right : Q($q) := ← mkFreshExprSyntheticOpaqueMVar q (tag := `right)

  -- ゴールを`?_ ∧ ?_`の形にはめる
  goal.assign q(And.intro $left $right)

  -- アクティブなゴールのリストは自動的には更新されないので、
  -- 2つのゴールを作ったことを宣言する
  replaceMainGoal [left.mvarId!, right.mvarId!]

example : True ∧ True := by
  and_constructor
  · trivial
  · trivial

Iff 専用 constructor

constructor タクティクの機能を制限し、P ↔ Q という形のゴールを分解する機能だけを持つタクティクを構成する例を示します。3

import Qq

/-- ゴールが`P ↔ Q`という形をしていたら`P → Q`と`Q → P`という二つのゴールに分解する -/
syntax (name := iffConstructor) "iff_constructor" : tactic

open Lean Elab Tactic Qq Meta

/-- ゴールが `P ↔ Q` の形をしているかチェックして、
`P ↔ Q` の形をしていたら `P` と `Q` をそれぞれ返す -/
def extractIffGoals : TacticM (Q(Prop) × Q(Prop)) := do
  have tgt : Q(Prop) := ← getMainTarget -- 右辺でQqを使用していないのでhaveを使う
  match tgt with
  | ~q($p ↔ $q) => return (p, q)
  | _ => throwError "ゴールが `P ↔ Q` の形ではありません。"

@[tactic iffConstructor]
def evalIffConstructor : Tactic := fun _stx => withMainContext do
  -- ゴールを取得する
  let goal ← getMainGoal
  have (p, q) := ← extractIffGoals

  -- 新しいメタ変数(ゴール)を作成する
  have mp : Q($p → $q) := ← mkFreshExprSyntheticOpaqueMVar q($p → $q) (tag := `mp)
  have mpr : Q($q → $p) := ← mkFreshExprSyntheticOpaqueMVar q($q → $p) (tag := `mpr)

  -- ゴールを`?_ ↔ ?_`の形にはめる
  goal.assign q(Iff.intro $mp $mpr)

  -- アクティブなゴールのリストは自動的には更新されないので、
  -- 2つのゴールを作ったことを宣言する
  replaceMainGoal [mp.mvarId!, mpr.mvarId!]

example : True ↔ True := by
  iff_constructor
  · simp
  · simp

A₁ ∧ A₂ ∧ ... ∧ Aₙ という形の前提から ⊢ Aᵢ を示すタクティク

h : A₁ ∧ A₂ ∧ ... ∧ Aₙ という形の前提から ⊢ Aᵢ を示すタクティクを実装する例を示します。これは引数を持つタクティクの例であるとともに、再帰的な挙動をするタクティクの例でもあります。4

import Qq
import Batteries

/-- `A₁ ∧ A₂ ∧ ... ∧ Aₙ` という形の前提から `⊢ Aᵢ` を示すタクティク -/
syntax (name := destructAnd) "destruct_and" ident : tactic

open Lean Elab Tactic Qq Meta

/-- 証明項 `hp : Q` が `A₁ ∧ A₂ ∧ ... ∧ Aₙ` の形の命題の証明であるかチェックして、
再帰的に分解して現在のゴールと一致する証明が得られるかを確認し、
もし一致すればゴールを閉じて`true`を返す。一致しなければ`false`を返す。 -/
partial def destructAndExpr (P : Q(Prop)) (hp : Q($P)) : TacticM Bool := withMainContext do
  -- 今証明を構成しようとしている命題を取得
  have target : Q(Prop) := ← getMainTarget

  -- `P` が `target` と一致しているなら、示すべきゴールの証明が得られたことになる。
  if (← isDefEq P target) then
    let goal ← getMainGoal
    goal.assignIfDefEq hp
    return true

  match P with
  | ~q($Q ∧ $R) =>
    let hq : Q($Q) := q(And.left $hp)
    let success ← destructAndExpr Q hq -- 再帰的にチェック
    -- 成功していたら早期リターン
    if success then
      return true

    let hr : Q($R) := q(And.right $hp)
    destructAndExpr R hr -- 再帰的にチェック
  | _ => return false

@[tactic destructAnd]
def evalDestructAnd : Tactic := fun stx => withMainContext do
  match stx with
  | `(tactic| destruct_and $h) =>
    let h ← getFVarFromUserName h.getId
    let success ← destructAndExpr (← inferType h) h
    if !success then
      failure
  | _ => throwUnsupportedSyntax

example (a b c d : Prop) (h : a ∧ b ∧ c ∧ d) : a := by
  destruct_and h

example (a b c d : Prop) (h : a ∧ b ∧ c ∧ d) : b := by
  destruct_and h

example (a b c d : Prop) (h : a ∧ b ∧ c ∧ d) : c := by
  destruct_and h

example (a b c d : Prop) (h : a ∧ b ∧ c ∧ d) : d := by
  destruct_and h

example (a b c : Prop) (h : a ∧ b ∧ c) : a ∧ b := by
  constructor <;> destruct_and h

A₁ ∧ ⋯ ∧ Aₙ という形の前提を分解して新しい仮定として追加するタクティク

再帰的な挙動をするタクティクの例として、さらに A₁ ∧ A₂ ∧ ... ∧ Aₙ という形の前提を分解して新しい仮定として追加するタクティクを実装する例を示します。5

import Qq
import Batteries

/-- 仮定にある `A₁ ∧ A₂ ∧ ... ∧ Aₙ` を分解する -/
syntax (name := casesAnd) "cases_and" : tactic

open Lean Elab Tactic Meta Qq Parser Term

/-- 命題`P`とその証明項`hp`を受け取り、`P = Q₁ ∧ Q₂ ∧ ... ∧ Qₙ` という形だった場合には
各`Qᵢ`とその証明項`hqᵢ`のリストを返す。その形でなければ単に`[(P, hp)]`を返す。-/
partial def casesAndAux (P : Q(Prop)) (hp : Q($P)) : TacticM (List ((P : Q(Prop)) × Q($P))) := do
  if (← inferType hp) != P then
    throwError "型の不一致エラー: {hp} は {P} の証明ではありません"

  if let ~q($Q₁ ∧ $Q₂) := P then
    let hq₁ : Q($Q₁) := q(And.left $hp)
    let hq₂ : Q($Q₂) := q(And.right $hp)
    return (← casesAndAux Q₁ hq₁) ++ (← casesAndAux Q₂ hq₂)
  else
    return [⟨P, hp⟩]

@[tactic casesAnd]
def evalCasesAnd : Tactic := fun _stx => withMainContext do
  -- 現在のローカルコンテキストを取得する
  let ctx ← getLCtx

  for (decl : LocalDecl) in ctx do
    -- ローカル宣言の種類がデフォルトでない場合はスキップする
    if decl.kind != .default then
      continue

    -- `hp : P` (ただし `P : Prop`)というタイプのローカル宣言に絞り込む
    -- `A : Prop` のようなローカル宣言は除外する
    let declType := decl.type
    let declTypeType ← inferType declType
    if ! declTypeType.isProp then
      continue

    -- 宣言し直す
    have P : Q(Prop) := declType
    have hp : Q($P) := decl.toExpr
    trace[debug] m!"対象となるローカル宣言を見つけました: {hp} : {P}"

    let proofList ← casesAndAux P hp
    for (⟨Q, hq⟩, idx) in proofList.zipIdx do
      let hypName := decl.userName.appendAfter s!"_{idx}"
      trace[debug] m!"新しい仮定を追加: {Q}"
      evalTactic <| ← `(tactic| have $(mkIdent hypName) : $(← Q.toSyntax) := $(← hq.toSyntax))

-- デバッグ出力をOFFにする
set_option trace.debug false

/-
trace: a b c d : Prop
h : a ∧ (b ∧ c) ∧ d
h_0 : a
h_1 : b
h_2 : c
h_3 : d
⊢ b ∧ d
-/
example (a b c d : Prop) (h : a ∧ (b ∧ c) ∧ d) : b ∧ d := by
  cases_and
  trace_state -- 現在の infoview の状態を表示

  constructor <;> assumption

exact? タクティク

ゴールを直接閉じることができる定理を探すタクティクとして exact? タクティクがあります。これに相当する(しかしかなり原始的で低性能な)ものを自前で実装する例を示します。6

import Lean

-- `my_exact?` というタクティックの構文を定義する(構文として `my_exact?` を認識させる)
syntax (name := myExact?) "my_exact?" : tactic

open Lean Elab Tactic in

-- `my_exact?` タクティックの実装を定義する
@[tactic myExact?]
def evalMyExact? : Tactic := fun _stx => do
  -- 現在の環境(定理などが格納されている)を取得
  let env ← getEnv

  -- 環境中の定数を取得し、以下の条件でフィルターする:
  -- 1. unsafe な定数ではない
  -- 2. 種類が axiom か thm(定理)のもの
  let theorems : List Name := SMap.toList (Environment.constants env)
    |>.filter (fun (_name, info) => ! ConstantInfo.isUnsafe info)
    |>.filterMap (fun (name, _info) => do
        let kind ← getOriginalConstKind? env name
        match kind with
        | .axiom | .thm => name
        | _ => none
      )

  -- 条件を満たす定理に対して順に試す
  for name in theorems do
    try
      -- 名前を構文ノードに変換
      let nameStx := mkIdent name

      -- `apply name <;> assumption` を構文的に展開・実行する
      evalTactic <| ← `(tactic| apply $nameStx <;> assumption)

      -- 成功したらログを出力し、タクティックの実行を終了する
      logInfo m!"Applied {name} successfully."
      return

    catch _ =>
      -- 失敗しても続行(次の定理を試す)
      continue

  -- どの定理も適用できなかった場合はタクティックとして失敗を返す
  failure

set_option maxHeartbeats 500000 in

-- 使用例
example (x y : Nat) (h : x = y) : y = x := by
  my_exact?

  1. このコード例を書くにあたり lean-tactic-programming-guide を参考にしました。

  2. このコード例を書くにあたり lean-tactic-programming-guide を参考にしました。

  3. このコード例を書くにあたり Metaprogramming in Lean 4 を参考にしました。

  4. このコード例を書くにあたり The Hitchhiker’s Guide to Logical Verification を参考にしました。

  5. このコード例を書くにあたり The Hitchhiker’s Guide to Logical Verification の演習問題を参考にしました。

  6. このコード例を書くにあたり The Hitchhiker’s Guide to Logical Verification を参考にしました。

Type

Type は、型がなす型宇宙です。ここで型宇宙とは、項が再び型であるような型のことをいいます。

たとえば NatInt, BoolString などが Type の項になっています。

#check (Nat : Type)
#check (Int : Type)
#check (Bool : Type)
#check (String : Type)

可算無限個の宇宙

では Type 自身の型はどうなっているのでしょうか?Type は明らかに型なので Type : Type となっているのでしょうか。実際に試してみると、Type の型は Type 1 です。

#check (Type : Type 1)

そして Type 1 の型は Type 2 であり、また Type 2 の型は Type 3 であり…と無限に続いていきます。

#check (Type 1 : Type 2)
#check (Type 2 : Type 3)

universe u
#check (Type u : Type (u+1))

Type は実は Type 0 の略記になっています。

example : Type = Type 0 := rfl

また、PropType u という2つの宇宙の系列をまとめて書き表すために Sort u という書き方があります。 Prop = Sort 0 で、以降順に宇宙レベルが上がっていきます。

example : Sort 0 = Prop := rfl
example : Sort 1 = Type 0 := rfl
example : Sort 2 = Type 1 := rfl
example : Sort (u + 1) = Type u := rfl

なぜ Type : Type ではないのか

何故このような仕様になっているのでしょうか。可算無限個の宇宙を用意するよりも Type : Type とした方が簡単ではないでしょうか?実は、Type : Type となるような型理論を採用すると矛盾が生じてしまいます。

これは ジラールのパラドックス(Girard’s paradox) と呼ばれる有名な結果です。しかしジラールのパラドックスを直接説明しようとすると準備が多く必要になるので、ここではジラールのオリジナルの議論を追うことはせず、代わりに濃度による簡潔な議論を紹介します。

以下証明を説明します。仮に TypeType の項だったとしましょう。このとき α := Type とすることにより、ある Type の項 α を選べば、全射 f : α → Type を作れることがわかります。したがって、任意の型 α : Type に対して関数 f : α → Type が全射になることはありえないことを示せばよいことになります。

これはカントールの定理の単射バージョンに帰着して示すことができます。

open Function

theorem not_surjective_Type {α : Type} (f : α → Type) : ¬ Surjective f := by
  -- f が全射だと仮定する
  intro h

  -- f から依存ペア型を構成する
  let T : Type := (a : α) × f a

  -- f は全射なので、Set T の逆像の要素が存在する
  let ⟨x, hx⟩ := h (Set T)

  -- 関数 `g : Set T → T` を構成できる
  let g : Set T → T := fun s ↦ ⟨x, cast hx.symm s⟩

  -- このとき、g は単射になる
  have hg : Injective g := by
    intro s t h
    grind

  -- これはカントールの定理に反する
  exact cantor_injective g hg

Vector

Vector は、長さが固定された配列です。標準ライブラリにおいて、次のように定義されています。

/-- `Vector α n` はサイズが `n` であるような `Array α` -/
structure Vector.{u} (α : Type u) (n : Nat) where
  /-- 地の配列 -/
  toArray : Array α
  /-- 配列のサイズ -/
  size_toArray : toArray.size = n
deriving Repr, DecidableEq

Vector の要素を定義するには、#v[a₁, a₂, .. aₙ] のように書きます。

#check #v[1, 2, 3]

用途

典型的な使用場面は、配列に対してその配列の長さが一定であることを保証したい場合です。

たとえば、配列の順番を逆にする関数を定義したいとします。「配列の要素を順に交換していくことで逆順にする」というアルゴリズムで実装してみます。

/-- 配列を逆順にする関数。
インデックスアクセスの妥当性証明を `swapIfInBounds` を使うことで回避している。 -/
def Array.myReverse₁ {α : Type} (arr : Array α) : Array α := Id.run do
  let mut array := arr
  let size := array.size
  for i in [0 : size / 2] do
    array := array.swapIfInBounds i (size - 1 - i)
  return array

#guard Array.myReverse₁ #[1, 2, 3, 4, 5] = #[5, 4, 3, 2, 1]
#guard Array.myReverse₁ (#[] : Array Nat) = #[]

ここで Array.swapIfInBounds ではなくて Array.swap を使用すると、インデックスアクセスの妥当性の証明が必要になります。しかし、let mut で宣言した配列の長さは変わってしまう可能性があるので、この証明は困難です。

def Array.myReverse₂ {α : Type} (arr : Array α) : Array α := Id.run do
  let mut array := arr

  -- `array` は可変な配列なので、
  -- `size = array.size` が常に成り立つとは限らない!
  let size := array.size

  for h : i in [0 : size / 2] do
    -- これは証明できる
    have : i < size := by
      dsimp [(· ∈ ·)] at h
      grind

    have : i < array.size := by
      -- `size = array.size` が成り立つことを Lean に伝える手段が難しくて
      -- 証明が回らない
      dsimp [(· ∈ ·)] at h
      fail_if_success grind
      sorry

    -- 同様に証明できない
    have : size - 1 - i < array.size := by
      dsimp [(· ∈ ·)] at h
      fail_if_success grind
      sorry

    array := array.swap i (size - 1 - i)
  return array

そこで Array の代わりに Vector を使用すると、配列の長さの情報が型レベルで固定されるので、Lean に「for によるループの間、配列のサイズが変わらない」ことを伝えることができ、インデックスアクセスの妥当性が容易に証明できるようになります。

def Array.myReverse₃ {α : Type} (arr : Array α) : Array α := Id.run do
  -- 配列ではなくて可変なベクトルとして保持する
  -- 可変変数だろうと型は変えられないので、長さが変わらないことを Lean に伝えられる
  let mut vec := arr.toVector
  let size := vec.size
  for h : i in [0 : size / 2] do
    -- 証明が通るようになった!
    have : i < vec.size := by
      dsimp [(· ∈ ·)] at h
      grind
    have : size - 1 - i < vec.size := by grind
    vec := vec.swap i (size - 1 - i)

  -- ベクトルを配列に戻して返す
  return vec.toArray

do 構文

while

while は、「ある条件が成り立っている間、同じ処理を繰り返す」ための構文です。while P do B という構文で用いて、条件 P が成り立つ間、命令 B を繰り返し実行します。

/-- 1から5までの数字をリストに詰めて返す -/
def packOneToFive : List Nat := Id.run do
  let mut result := []
  let mut n := 1
  while n ≤ 5 do
    result := result ++ [n]
    n := n + 1
  return result

#guard packOneToFive = [1, 2, 3, 4, 5]

while ループと停止性

while ループを使うと、停止するとは限らない計算が(停止性の証明を与えてもいないのに)書けてしまいます。たとえば以下は、述語 P : Nat → Bool を成り立たせる最小の自然数を探してくる関数の例です。

/-- 述語 `P : Nat → Bool` を成り立たせる最小の自然数を返す。
`P` が成り立つ要素が存在する保証はないので停止するとは限らない。 -/
def searchMinExample (P : Nat → Bool) : Nat := Id.run do
  let mut n := 0
  while !P n do
    n := n + 1
  return n

#guard searchMinExample (fun n => n > 5) = 6

これは暗黙的に partial で修飾された関数を利用していることを想像させますが、実際には違います。while ループで書かれた関数について何かを証明することは可能です。

/-- 明らかに停止するし明らかに`0`しか返さない関数 -/
def trivialWhile : Nat := Id.run do
  let mut m := 0
  while false do
    m := m + 1
  return m

-- `while false` の本体は実行されないので、結果は初期値のままになる
example : trivialWhile = 0 := by
  unfold trivialWhile
  simp only [ForIn.forIn]
  rw [Lean.Loop.forIn_eq_of_monadTail]
  simp

タクティク

タクティクは、Lean において証明を対話的に行ったり自動化したりすることを可能にするものです。

なお本書は全部のタクティクを網羅していません。Mathlib の全タクティクのリストが必要であれば Mathlib4 Help を参照してください。

よく使うタクティク早見表

Lean で利用可能なタクティクは多岐にわたるので、ここによく使うタクティクの早見表を載せておきます。

関数と等式系

タクティク用途
rwA = B という等式を使って書き換えを行いたいとき
rflA = B という等式を定義に展開することで示したいとき
congrf a = f ba = b に帰着して示したいとき
calc等式を連鎖させて a = b を示したいとき

命題論理系

タクティク用途
exact仮定 h : A からゴール ⊢ A を示したいとき
assumption仮定の中にゴールと同じものがあるとき
introA → B を示したいとき
applyh : A → B という仮定や命題を使いたいとき
constructorA ∧ BA ↔ B を示したいとき
obtainh : A ∧ B という仮定や命題を分解したいとき
rwh : A ↔ B という仮定や命題を使って書き換えを行いたいとき
left⊢ A ∨ B というゴールを ⊢ A に帰着したいとき
right⊢ A ∨ B というゴールを ⊢ B に帰着したいとき
cases仮定 h : A ∨ B をもとに場合分けしたいとき
by_cases命題 A について A ∨ ¬ A という場合分けを行いたいとき
exfalsoゴールを矛盾を示すことに帰着したいとき
contradictionA¬ A が両方成り立つことからゴールを閉じたいとき
have証明の途中で補題を立てたいとき
sufficesゴールを十分条件に帰着したいとき

述語論理系

タクティク用途
intro∀ x, P x を示したいとき
exists∃ x, P x を示したいとき
obtainh : ∃ x, P x という仮定や命題を分解したいとき

帰納型と帰納法

タクティク用途
induction帰納法を行いたいとき
fun_induction関数定義に基づいて帰納法を行いたいとき
cases帰納型の値をコンストラクタに基づいて場合分けしたいとき
fun_cases関数定義に基づいて場合分けしたいとき
ext外延性を利用したいとき
unfold名前を定義に展開したいとき

ライブラリ検索と自動証明

タクティク用途
exact?ゴールを閉じることのできる補題を検索したいとき
rw?ゴールを書き換えることのできる補題を検索したいとき
decide計算によって命題の真偽を判定したいとき
simp等式書き換えによる単純化を自動で行いたいとき
grind書き換え・前方推論・後方推論・場合分けを自動で行いたいとき
try?とりあえず証明したいとき

<;>

<;> は、直前のタクティクによって生成されたすべてのサブゴールに対して後続のタクティクを適用することを意味するタクティク結合子です。1

variable (P Q : Prop)

example (hP : P) (hQ : Q) : P ∧ Q := by
  constructor <;> assumption

all_goals との違い

<;>all_goals とよく似た挙動をします。 しかし、<;>all_goals は完全に同じではありません。 <;> が「直前のタクティクによって生成された全てのサブゴール」に対して後続のタクティクを実行するのに対して、all_goals は「すべての未解決のゴール」に対して後続のタクティクまたはタクティクブロックを実行します。

実際に以下のような例ではその違いが現れます。

variable (P Q R : Prop)

/-- <;> を使用したとき -/
example (hP : P) (hQ : Q) (hR : R) : (P ∧ Q) ∧ R := by
  constructor

  constructor <;> try assumption

  -- まだ示すべきことが残っている
  show R
  assumption

/-- all_goals を使用したとき -/
example (hP : P) (hQ : Q) (hR : R) : (P ∧ Q) ∧ R := by
  constructor

  constructor
  all_goals
    try assumption

  -- 証明完了
  done

ゴールが途中で閉じられたとき

tac1 <;> tac2 の実行において、tac1 の実行でゴールが閉じられた場合、tac2 は実行されずスキップされます。これは tac1; tac2 とは異なる挙動であることに注意してください。

example (hP : P) : P := by
  -- `assumption`の時点でゴールは閉じているが、エラーにならない
  -- `simp`の実行は単にスキップされる
  assumption <;> simp

example (hP : P) : P := by
  -- `tac1; tac2` では、`tac2` を実行しようとする
  -- そのため `tac2` が実行できないとエラーになることがある
  fail_if_success
    assumption; simp

  assumption

  1. <;> の正式な呼び名はわかりません。linter.unnecessarySeqFocus というリンタが存在するので、ここでは seqFocus と仮に呼んでいます。

ac_rfl

ac_rfl は、結合的(associative)かつ可換(commutative)な演算に対して、結合性と可換性だけから示せる等式を示すタクティクです。

/-- 3次元格子点がなす空間 -/
@[ext]
structure Point : Type where
  x : Int
  y : Int
  z : Int

namespace Point

  /-- `Point` 上の足し算 -/
  def add (a b : Point) : Point :=
    ⟨a.x + b.x, a.y + b.y, a.z + b.z⟩

  /-- `Point` 上の足し算を `+` で表せるようにする -/
  instance : Add Point where
    add := add

  @[simp]
  theorem x_add (a b : Point) : (a + b).x = a.x + b.x := rfl

  @[simp]
  theorem y_add (a b : Point) : (a + b).y = a.y + b.y := rfl

  @[simp]
  theorem z_add (a b : Point) : (a + b).z = a.z + b.z := rfl

  /-- `Point` 上の足し算は可換 -/
  theorem add_comm (a b : Point) : a + b = b + a := by
    ext
    all_goals
      simp
      apply Int.add_comm

  /-- `Point` 上の足し算は結合的 -/
  theorem add_assoc (a b c : Point) : a + b + c = a + (b + c) := by
    ext
    all_goals
      simp
      apply Int.add_assoc

  -- `ac_rfl` から使えるように、`Std.Commutative` のインスタンスにする
  instance : Std.Commutative (α := Point) (· + ·) where
    comm := Point.add_comm

  -- `ac_rfl` から使えるように、`Std.Associative` のインスタンスにする
  instance : Std.Associative (α := Point) (· + ·) where
    assoc := Point.add_assoc

  -- 可換性と結合性から示せることなら示せる
  example (a b c: Point) : (a + b) + c + (a + b) = a + a + b + b + c := by
    ac_rfl

end Point

ac_rfl は、上記の構造体 Point の例のように、自分で定義した演算が可換で結合的であることを後から簡単に利用できるようにしておきたいときに役立ちます。ここで @[simp] タグを付けるのは、可換性や結合法則は項の単純化ではないため上手くいかないということに注意してください。

交換法則と ac_rfl

ac_rfl は、可換性と結合性の両方がインスタンスとして登録されていないと使えないことがあります。以下は、可換性だけが登録されているときに、可換性だけで示せそうな命題が示せないという例です。

@[ext]
structure Color : Type where
  r : Nat
  g : Nat
  b : Nat

namespace Color

  def add (a b : Color) : Color :=
    ⟨a.r + b.r, a.g + b.g, a.b + b.b⟩

  instance : Add Color where
    add := add

  /-- `add` は可換 -/
  protected theorem add_comm (a b : Color) : a + b = b + a := by
    ext <;> apply Nat.add_comm

  /-- `add_comm` を `Std.Commutative` に登録する。 -/
  local instance : Std.Commutative (α := Color) (· + ·) where
    comm := Color.add_comm

  example (a b : Color) : a + b = b + a := by
    -- ac_rfl を使うことができない
    fail_if_success ac_rfl

    ext <;> apply Nat.add_comm

end Color

結合法則と ac_rfl

上記のように、ac_rfl は可換性だけで示せることを可換性だけで示せないのですが、結合法則だけで示せることは結合法則だけで示すことができます。

namespace Color

  /-- `Color` の足し算は結合的 -/
  theorem add_assoc (a b c : Color) : a + b + c = a + (b + c) := by
    ext <;> apply Nat.add_assoc

  -- エラーになっているので、
  -- Commutative のインスタンスはないことが確認できる
  /-
  error: failed to synthesize
    Std.Commutative fun x1 x2 => x1 + x2

  Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.
  -/
  #synth Std.Commutative (α := Color) (· + ·)

  /-- `add_comm` を `Std.Associative` に登録する。 -/
  local instance : Std.Associative (α := Color) (· + ·) where
    assoc := Color.add_assoc

  -- 結合法則だけで示せることは示すことができる
  example {a b c : Color} : (a + b) + (c + (b + a)) = a + b + c + b + a := by
    ac_rfl

end Color

grind と ac_rfl

なお、grind タクティクでも ac_rfl と同様のことができます。grind タクティクは ac_rfl の完全上位互換かもしれません。

/-- 自然数をイメージした何か -/
opaque MyNat : Type

variable [Add MyNat]

/-- 加法の結合法則 -/
axiom MyNat.add_assoc (a b c : MyNat) : (a + b) + c = a + (b + c)

/-- 結合法則が成り立つことを型クラス経由で登録する -/
instance : @Std.Associative MyNat (· + ·) :=
  ⟨MyNat.add_assoc⟩

example (a b c d : MyNat) : (a + b) + (c + d) = a + (b + c) + d := by
  -- grind で証明することができる
  grind only


/-- 加法の交換法則 -/
axiom MyNat.add_comm (a b : MyNat) : a + b = b + a

/-- 交換法則が成り立つことを型クラス経由で登録する -/
instance : @Std.Commutative MyNat (· + ·) :=
  ⟨MyNat.add_comm⟩

example (a b c : MyNat) : a + b + c = c + b + a := by
  -- grind で証明することができる
  grind only

aesop

aesop は汎用的かつ強力な自動証明のためのタクティクです。

Automated Extensible Search for Obvious Proofs (自明な証明の拡張可能な自動探索)からその名があります。様々なタクティクやルールを使用しながら最良優先探索を行い、証明を自動で終わらせようとします。

import Aesop -- `aesop` を使用するため
import Mathlib.Tactic.Says

-- 以下 `X` `Y` `Z`を集合とする
variable {X Y Z : Type}

open Function

-- 合成 `g ∘ f` が単射なら、`f` も単射
example {f : X → Y} {g : Y → Z} (hgfinj : Injective (g ∘ f)) : Injective f := by
  rw [Injective]
  show ∀ ⦃a₁ a₂ : X⦄, f a₁ = f a₂ → a₁ = a₂

  -- `simp_all` では示せない
  fail_if_success simp_all

  -- 示すべきことがまだまだあるように見えるが、一発で証明が終わる
  aesop

aesop?

aesop が成功するとき、aesop? に置き換えるとゴールを達成するのにどんなタクティクを使用したか教えてくれます。

example {f : X → Y} {g : Y → Z} (hgfinj : Injective (g ∘ f)) : Injective f := by
  rw [Injective]
  aesop? says
    intro a₁ a₂ a
    apply hgfinj
    simp_all only [comp_apply]

上記の例により、とくに aesop が実行の過程で simp_all タクティクや intro タクティク等を使用することがわかります。 特に、aesopsimp_all の強化版であるということができます。 実際には aesopsimp_all とは異なり、単純化だけでなく「試行錯誤しながらよい証明を探索する」ということができます。

カスタマイズ

aesop はユーザがカスタマイズ可能です。補題やタクティクを [aesop] 属性で登録することで、証明可能な命題を増やすことができます。

/-- 自然数 n が正の数であることを表す帰納的述語 -/
inductive Pos : Nat → Prop where
  | succ n : Pos (n + 1)

example : Pos 1 := by
  -- ルールが登録されていないので、`aesop` で示すことはできない
  fail_if_success aesop

  -- 手動でコンストラクタを `apply` することで証明できる
  apply Pos.succ

-- `Pos` 関連のルールを `aesop` に憶えさせる
attribute [aesop safe constructors] Pos

-- `aesop` で証明できるようになった!
example : Pos 1 := by aesop

カスタマイズ方法の詳細を知りたい方はaesopのリポジトリをご参照ください。また、内部のロジックの詳細については論文 Aesop: White-Box Best-First Proof Search for Lean で説明されています。

all_goals

all_goals は、後に続くタクティクをすべてのゴールに対して適用します。

variable (P Q : Prop)

example (hP : P) (hQ : Q) : P ∧ Q := by
  -- `P` と `Q` をそれぞれ示せばよい
  constructor

  -- どちらも仮定から従うので、
  -- 両方に `assumption` を適用する
  all_goals assumption

all_goals には、タクティクブロックを渡すこともできます。

example {R : Prop} (hnP : ¬ P) : (P → R) ∧ (P → Q) := by
  constructor
  all_goals
    intro h
    contradiction

タクティク結合子 <;> によってもほぼ同じことができます。

example (hP : P) (hQ : Q) : P ∧ Q := by
  constructor <;> assumption

apply .. at

apply は通常ゴールに対して適用しますが、at を付けてローカルコンテキストの命題などに対して使用するという使い方ができます。

実際にはこの構文は apply と似ているだけで、別のタクティクです。この at がついている構文は通常の apply とは異なり、後方推論ではなく前方推論になります。

import Mathlib.Tactic.ApplyAt

variable (P Q : Prop)

example (h : P → Q) (hP : P) : Q := by
  -- `hP` に `h` を適用してしまう
  apply h at hP

  -- `hP` が書き換わる
  guard_hyp hP : Q

  assumption

便利な記法以上のものではなく、他のタクティクを利用しても同じことができます。

example (h : P → Q) (hP : P) : Q := by
  -- `apply at` は `replace` と同じように動作する
  replace hP := h hP

  -- `hP` が書き換わる
  guard_hyp hP : Q

  assumption

apply_assumption

apply_assumption は、ゴールが ⊢ head であるときに、... → ∀ _, ... → head という形の命題をローカルコンテキストから探し、それを用いてゴールを書き換えます。

variable (P Q R : Prop)

example (hPQ : P → Q) : ¬ Q → ¬ P := by
  intro hQn hP

  -- 矛盾を示したい
  show False

  -- 自動で `hQn` を適用
  apply_assumption
  show Q

  -- 自動で `hPQ` を適用
  apply_assumption
  show P

  -- 自動で `hP` を適用
  apply_assumption

  -- 証明終わり
  done

タクティクを繰り返すことを指示するタクティク repeat と組み合わせると、「ローカルコンテキストにある仮定を適切に選んで apply, exact することを繰り返し、ゴールを閉じる」ことができます。

example (hPQ : P → Q) (hQR : Q → R) (hQ : P) : R := by
  repeat apply_assumption

apply

apply は含意 をゴールに適用するタクティクです。ゴールが ⊢ Q で、ローカルコンテキストに h : P → Q があるときに、apply h を実行するとゴールが ⊢ P に書き換わります。

variable (P Q : Prop)

/-- `P → Q` かつ `P` ならば `Q` -/
example (h : P → Q) (hP : P) : Q := by
  apply h

  -- ゴールが `P` に変わっている
  show P

  exact hP

「十分条件でゴールを置き換える」タクティクであると言えますが、十分条件がローカルコンテキストに存在しない場合は suffices の使用も検討してください。

特殊な用途

仮説から否定を消去する

Lean では否定 ¬ PP → False として実装されているため、ゴールが ⊢ False であるときに hn : ¬P に対して apply hn とするとゴールを ⊢ P に書き換えることができます。

-- P の否定は、「P を仮定すると矛盾する」ということに等しい
example : (¬ P) = (P → False) := by rfl

example (hn : ¬ P) (hP : P) : False := by
  apply hn
  show P
  exact hP

exact の強力版として

exact の代わりに apply を使うこともできます。

example (hP : P) : P := by
  apply hP

example (h : P → Q) (hP : P) : Q := by
  apply h hP

また仮定に全称命題の証明 h : ∀ a, P a があってゴールが P a であるとき、exact h は失敗しますが apply h であれば成功します。これは「exact では通りそうで通らないが apply では通る例」であると言えるかもしれません。

variable {α : Type}

example (a : α) (P : α → Prop) (h : ∀ a, P a) : P a := by
  -- `exact h` は失敗する
  fail_if_success exact h

  apply h

舞台裏

一般に、apply は関数適用を逆算するタクティクです。手元に関数 f : A → B があって型 B の型を作りたいとき、A の項を構成すれば f に適用することで B の項が得られる…といった推論を行います。

-- 関数をタクティクを使用して構成する例
def apply {α β : Type} (f : α → β) (a : α) : β := by
  apply f
  exact a

apply?

apply? は、カレントゴールを applyrefine で変形することができないか、ライブラリから検索して提案してくれるタクティクです。 複数の候補が提案されたときは、どれを選ぶとゴールが何に変わるのか表示されるので、その中から好ましいものを選ぶと良いでしょう。

import Mathlib.Algebra.Algebra.Basic -- 群を使うのに必要
import Mathlib.Tactic.Says -- `says` を使うのに必要

set_option says.verify true

variable (G H : Type)

/-- 群順同型が積を保つという定理 -/
example [Group G] [Group H] (f : G →* H) (a b : G) :
    f (a * b) = f a * f b := by
  -- `exact MonoidHom.map_mul f a b` を提案してくれる
  apply? says exact MonoidHom.map_mul f a b

補足

apply? はあくまで証明を書くときに補助として使うものです。 sorry と同じように、清書した証明に残してはいけません。 sorry と同じと言いましたが、実際 apply?sorryAx を裏で使用します。

theorem T (x y : Nat) (_: x ≤ y) : 8 ^ x ≤ 16 ^ y := by
  apply?

  -- `apply?` しただけで `done` が通り、示せているように見える
  done

/- info: 'T' depends on axioms: [propext, sorryAx] -/
#print axioms T

assumption

assumption は、現在のゴール ⊢ P がローカルコンテキストにあるとき、ゴールを閉じます。

variable (P Q : Prop)

example (hP: P) (_: Q) : P := by
  assumption

assumption による証明は、どの仮定を使うか明示すれば exact で書き直すことができます。assumption を使用することにより、仮定の名前の変更に対してロバストになるほか、意図がわかりやすくなるというメリットがあります。

example (hP: P) (_: Q) : P := by
  exact hP

補足

なお、シングル山括弧 ‹› を使って ‹P› と書くと、「命題 P の証明を by assumption で埋めてください」と Lean に指示したことになります。

example (P Q R : Prop) (hp : P) (hq : Q) (h : P → Q → R) : R := by
  exact h ‹P› ‹Q›

by_cases

by_cases は排中律を使って場合分けをするタクティクです。by_cases h : P とすると、P が成り立つときと成り立たないときのゴールがそれぞれ生成されます。

example (P: Prop) : ¬¬P → P := by
  intro hnnP

  -- `P` が成り立つかどうかで場合分けする
  by_cases hP : P

  case pos =>
    -- `P` が成り立つとき
    assumption

  case neg =>
    -- `¬ P` が成り立つとき
    contradiction

by_contra

by_contra は、背理法を使いたいときに役立つタクティクです。

ゴールが ⊢ P であるときに by_contra h を実行すると、h : ¬ P がローカルコンテキストに追加されて、同時にゴールが ⊢ False になります。

import Mathlib.Tactic.ByContra

variable (P Q : Prop)

example (h: ¬Q → ¬P) : P → Q := by
  -- `P` であると仮定する
  intro hP

  -- `¬Q` であると仮定して矛盾を導きたい
  by_contra hnQ
  show False

  -- `¬ Q → ¬ P` と `¬Q` から `¬P` が導かれる
  have := h hnQ

  -- これは仮定に矛盾
  contradiction

calc

calc は計算モードに入るためのタクティクです。推移律が成り立つような二項関係をつなげて、一つの証明項を構成します。

-- `calc` そのものは `import` なしで使える
import Mathlib.Tactic -- 大雑把に import する

example (a b : ℝ) : 2 * a * b ≤ a ^ 2 + b ^ 2 := by
  -- `a ^ 2 - 2 * a * b + b ^ 2 ≥ 0` を示せばよい
  suffices hyp : a ^ 2 - 2 * a * b + b ^ 2 ≥ 0 from by
    linarith

  -- 少しずつ式変形して示していく
  have : a ^ 2 - 2 * a * b + b ^ 2 ≥ 0 := calc
    _ = (a - b) ^ 2 := by ring
    _ ≥ 0 := by positivity
  assumption

<calc で繋げることもできます。

/-- 掛け算 `ℝ × ℝ → ℝ` の原点における連続性 -/
example : ∀ x y ε : ℝ, 0 < ε → ε ≤ 1 → |x| < ε → |y| < ε → |x * y| < ε := by
  intro x y ε epos ele1 xlt ylt

  have : |x * y| < ε := calc
    _ = |x| * |y| := abs_mul x y
    _ < ε * ε := by gcongr
    _ ≤ 1 * ε := by gcongr
    _ = ε := by simp
  assumption

カスタマイズ

自前で定義した二項関係も、Trans 型クラスのインスタンスにすれば calc で推移律を連鎖させることができます。(ただし、細かいですが、推移律を連鎖させることが必要なければ Trans 型クラスのインスタンスでなくても calc は使えます)

/-- 絶対値が同じであることを表す二項関係 -/
def same_abs (x y : Int) : Prop := x = y ∨ x = - y

-- same_abs のための中置記法の2項演算子を用意する
infix:50 " ≡ " => same_abs

/-- `same_abs` の反射性 -/
@[refl] theorem same_abs_refl (x : Int) : x ≡ x := by
  simp [same_abs]

-- メタ変数の番号を表示しないようにする
set_option pp.mvars false

-- `calc` が推移律と関係なければ使えるが...
example (x : Int) : x ≡ x := calc
  _ ≡ x := by rfl

-- `calc` で推移律を連鎖させようとすると
-- `Trans` 型クラスのインスタンスではないというエラーになってしまう
/-
error: invalid 'calc' step, failed to synthesize `Trans` instance
  Trans same_abs same_abs ?_

Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.
-/
example {x y z : Int} (hxy : x ≡ y) (h : y = z) : x ≡ z := calc
  x ≡ y := hxy
  _ ≡ z := by rw [h]

/-- same_abs の推移律 -/
theorem same_abs_trans {x y z : Int} (hxy : x ≡ y) (hyz : y ≡ z) : x ≡ z := by
  dsimp [same_abs]

  -- hxy と hyz について場合分けをする
  rcases hxy with hxy | hxy <;> rcases hyz with hyz | hyz

  -- omega でカタをつける
  all_goals omega

/-- same_abs を「推移的な二項関係」として登録する -/
instance : Trans same_abs same_abs same_abs where
  trans := same_abs_trans

-- same_abs についても calc が使えるようになった!
example {x y z : Int}(hxy : x ≡ y)(h : y = z) : x ≡ z := calc
  x ≡ y := hxy
  _ ≡ z := by rw [h]

証明が1行で終わらないとき

calc を使用しているとき、証明を見やすく保つためには各行の証明は1行で完結させた方が良いのですが、そうもいかない場合があります。そのような場合、その行の証明項をメタ変数で置き換えると、証明を後回しにすることができます。

example {x y z : Int} (hxy : x = y) (h : y = z) : x = z := by
  have : x = z := calc
    x = y := ?lem -- この行の証明を後回しにすることができる
    _ = z := by rw [h]
  assumption

  -- 後回しにした証明を埋める
  case lem =>
    rw [hxy]

cases

cases は場合分けを行うことができるタクティクです。

たとえば、ローカルコンテキストに h : P ∨ Q があるときに cases h とすると、仮定に P を付け加えたゴールと、仮定に Q を付け加えたゴールをそれぞれ生成します。

よく似たタクティクに rcases があります。

example (P Q R : Prop) : P ∨ Q → (P → R) → (Q → R) → R := by
  -- `h: P ∨ Q`
  intro h hPR hQR

  -- `inl` と `inr` の2つのゴールを生成する
  cases h with

  -- `P` が成り立つ場合
  | inl hP =>
    exact hPR hP

  -- `Q` が成り立つ場合
  | inr hQ =>
    exact hQR hQ

=> を省略することもできます。

example (P Q R : Prop) : P ∨ Q → (P → R) → (Q → R) → R := by
  intro h hPR hQR

  cases h with | inl hP | inr hQ

  · exact hPR hP
  · exact hQR hQ

case タクティク

cases .. with 構文を使わずに、case タクティクを使って次のように書くこともできます。

example (P Q R : Prop) : P ∨ Q → (P → R) → (Q → R) → R := by
  intro h hPR hQR

  cases h

  case inl hP =>
    apply hPR
    assumption

  case inr hQ =>
    apply hQR
    assumption

舞台裏

帰納型の分解

cases は、実際には論理和に限らず帰納型をコンストラクタに分解することができるタクティクです。

-- 帰納型として定義した例示のための型
inductive Sample where
  | foo (x y : Nat)
  | bar (z : String)

example (s : Sample) : True := by
  -- cases で場合分けを実行できる
  cases s

  case foo x y =>
    trivial

  case bar z =>
    trivial

論理和を分解することができるのも、Or が次のように帰納型として定義されているからです。

inductive Or (a b : Prop) : Prop where
  | inl (h : a)
  | inr (h : b)

帰納的述語の分解

特に、帰納的述語も cases タクティクで分解することができます。 仮定がどのコンストラクタから来たものなのかに応じて場合分けをすることができます。

たとえば、以下のように偶数であることを表す帰納的述語を定義したとします。 このとき Even (n + 2) という仮定があれば Even n を結論出来ますが、これは cases タクティクで行うことができます。

/-- 偶数であることを表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | cons {n : Nat} (ih : Even n) : Even (n + 2)

example (n : Nat) (h : Even (n + 2)) : Even n := by
  -- h に対して場合分けを行う。
  -- `Even (n + 2)` という仮定はどのコンストラクタから来たか?で場合分けできる。
  -- `cases` タクティクはある程度賢いので、
  -- `zero` のケースはありえないと判断してスキップできる。
  cases h with
  | cons ih =>
    exact ih

cases’

cases'cases に似ていますが Lean3 の cases に近い挙動をします。証明を構造化するため、 cases' は使用しないことをお勧めします。

import Mathlib.Tactic.Cases -- `cases'` を使用するために必要

variable {P Q R : Prop}

example : P ∨ Q → (P → R) → (Q → R) → R := by
  intro h hPR hQR

  -- 場合分けをする
  cases' h with hP hQ
  · apply hPR hP
  · apply hQR hQ

choose

h : ∀ x, ∃ y, P(x, y) が成り立っているときに、choose f hf using h は関数 f : X → Yf が満たす性質 hf : ∀ x, P(x, f x) のペアを作ります。

import Mathlib.Tactic.Choose

section
  -- X,Y は型で P : X → Y → Prop は述語
  variable (X Y : Type) (P : X → Y → Prop)

  theorem choice (h : ∀ x, ∃ y, P x y) : ∃ f : X → Y, ∀ x, P x (f x) := by
    -- 関数 `f : X → Y` を構成する
    choose f hf using h

    exact ⟨f, hf⟩
end

舞台裏

choose は裏で選択原理 Classical.choice を使用しています。

/- info: 'choice' depends on axioms: [Classical.choice] -/
#print axioms choice

choose が自動で示してくれることを選択原理 Classical.choice を使って手動で示すこともできます。例えば以下のようになります。

section
  /- ## choose タクティクを使わずに同等のことをする例 -/

  variable (X Y : Type) (P : X → Y → Prop)

  example (h : ∀ x, ∃ y, P x y) : ∃ f : X → Y, ∀ x, P x (f x) := by
    -- `f` を作る
    let f' : (x : X) → {y // P x y} := by
      intro x
      have hne_st : Nonempty {y // P x y} := by
        let ⟨y, py⟩ := h x
        exact ⟨⟨y, py⟩⟩
      exact Classical.choice hne_st

    let f : X → Y := fun x ↦ (f' x).val

    -- 上記で作った関数が条件を満たすことを示す
    have h₁ : ∀ x, P x (f x) := by
      intro x
      exact (f' x).property

    exists f

end

clear

Lean で複雑な証明を書いていると、ローカルコンテキストに今まで導入した定義や示した補題が溜まってきて見づらくなることがあります。そうしたとき、clear タクティクを使うと指定した項を削除したり、指定した項以外を削除したりといったことができます。

import Mathlib.Tactic.ClearExcept -- `clear` タクティクで「~以外」を指定できるようにする

-- 未使用の変数に対する警告をオフにする
set_option linter.unusedVariables false

variable (P Q R : Prop)

example (hP : P) (hQ : Q) (hR : R) : R := by
  -- `hP` と `hR` 以外をローカルコンテキストから削除する
  clear * - hR hP

  -- `hP` も削除する
  clear hP

  exact hR

ローカルコンテキストが複雑で見づらいときに、clear で関係があるもの以外を削除して見やすくするといった使い方ができます。

congr

f₁ = f₂ かつ a₁ = a₂ ならば f₁ a₁ = f₂ a₂」という事実は、等号が合同(congruence)関係であると解釈できますが、congr はこれを使ってゴールを分解するタクティクです。

congr は、⊢ f as = f bs という形のゴールがあったときに、ゴールを ⊢ as = bs に変えます。再帰的に適用されるので、⊢ g (f as) = g (f bs) という形のゴールでも ⊢ as = bs というゴールになります。

variable (X : Type) (x : Int) (f : Int → Int)

example (h : x = 0) : f (2 + x) = f 2 := by
  congr
  show 2 + x = 2

  simp [h]

congr は等号以外の関係は扱えません。等号以外の関係の合同性も扱えるタクティクに gcongr があります。

variable (a b : Int)

example (h : a = b) : a + 1 = b + 1 := by
  -- 等号の場合はOK
  congr

example (h : a < b) : a + 1 < b + 1 := by
  -- 不等号の場合エラーにはならないが何も起こらない
  congr
  show a + 1 < b + 1

  exact Int.add_lt_add_right h 1

再帰の深さの調節

congr が適用される再帰の深さを引数として渡すことができます。これは、主に単に congr とするだけだと「行き過ぎ」になるときに調整する目的で使用されます。

example (g : Int → X) (h : x = 0) (hf : ∀ x, f x = f (- x)) :
    g (f (2 + x)) = g (f (- 2)) := by

  -- congr の再帰がアグレッシブすぎて上手くいかないことがある
  try
    congr

    -- 分解しすぎた
    show 2 + x = -2

    -- これでは示すことができない
    fail

  -- 再帰の深さを数値として指定できる
  congr 1

  -- ちょうどよい分解になった
  show f (2 + x) = f (-2)

  simp only [h]
  exact hf _

constructor

constructor はゴールを分割するためのタクティクです。

たとえばゴールが ⊢ P ∧ Q であるとき、constructor を実行すると、ゴールが2つのサブゴール ⊢ P⊢ Q に分割されます。

variable (P Q : Prop)

example (hP: P) (hQ: Q) : P ∧ Q := by
  -- goal が `left` と `right` に分割される
  constructor

  case left =>
    -- `P` を示す
    show P
    exact hP

  case right =>
    -- `Q` を示す
    show Q
    exact hQ

またゴールが同値 P ↔ Q であるとき、constructor を実行するとゴールが2つのサブゴール ⊢ P → Q⊢ Q → P に分割されます。

example (x : Nat) : x = 0 ↔ x + 1 = 1 := by
  constructor
  case mp =>
    -- `x = 0 → x + 1 = 1` を示す
    show x = 0 → x + 1 = 1
    intro hx
    rw [hx]
  case mpr =>
    -- `x + 1 = 1 → x = 0` を示す
    show x + 1 = 1 → x = 0
    simp_all

補足

このように、constructor は論理積 や同値 を「示す」ために使われます。逆にこういった命題が仮定にあって「使用したい」場合は obtainhave などが使用できます。

example (h: P ∧ Q) : P := by
  obtain ⟨hp, _hq⟩ := h
  exact hp

example (h : P ↔ Q) (hP : P) : Q := by
  have ⟨pq, _qp⟩ := h
  apply pq
  assumption

また論理積や同値性は構造体なので、次の関数も利用できます。

-- 論理積から構成要素を取り出す関数
#check (And.left : P ∧ Q → P)
#check (And.right : P ∧ Q → Q)

-- 同値から構成要素を取り出す関数
#check (Iff.mp : (P ↔ Q) → P → Q)
#check (Iff.mpr : (P ↔ Q) → Q → P)

舞台裏

constructor は一般に、論理積や同値に限らずゴールにある任意の構造体を分解することができます。

structure Sample where
  name : String
  index : Nat

def getSample (index : Nat) (name : String) : Sample := by
  -- constructor タクティクでゴールの構造体をフィールドに分解する
  constructor

  · exact name
  · exact index

contradiction

矛盾からはどんな命題でも証明することができます。これを爆発律(principle of explosion)と呼びますが、contradiction は、この爆発律を使ってゴールを閉じるタクティクです。

ローカルコンテキストに P¬ P が同時にあるなど、矛盾した状況にあるときにゴールを閉じます。

variable (P Q : Prop)

-- `False`
example (h : False) : P := by contradiction

-- 明らかに偽な等式
example (h : 2 + 2 = 3) : P := by contradiction

-- 明らかに偽な等式
example (x : Nat) (h : x ≠ x) : P := by contradiction

-- 矛盾する仮定
example (hP : P) (hnP : ¬ P) : Q := by contradiction

爆発律を利用するタクティクには他に exfalso もありますが、あちらはゴールを False に書き換えるだけで、ゴールを閉じるところまでは行いません。

補足

以下の例では、contradiction がいかにも通りそうに見えるのですが、通りません。順序の扱いは contradiction の仕事ではないからです。contradiction にはあまり強力な前処理は備わっていないので、注意が必要です。

variable (n m : ℕ)

example (hl : n ≤ m) (hg : m < n) : False := by
  -- 明らかに矛盾に見えるが、 通らない
  fail_if_success contradiction

  -- 通らない理由は、 `n ≤ m` が `¬ m < n` を意味することを
  -- `contradiction` は知らないから。

  -- 次のようにして教えてあげると…
  have := hl.not_gt

  -- `contradiction` が通るようになる
  contradiction

contrapose

contrapose は対偶を取るタクティクです。

ローカルコンテキストに h : P という仮定があるときに contrapose h を実行すると、ゴールの否定がローカルコンテキストに追加されて、同時にゴールが ⊢ ¬ P に変わります。

import Mathlib.Tactic

variable {f : Int → Int} {a b : Int}

/-- 自前で定義した単調性 -/
def MyMonotone (f : Int → Int) : Prop :=
  ∀ ⦃a₁ a₂⦄, a₁ ≤ a₂ → f a₁ ≤ f a₂

example (h : MyMonotone f) (h' : f a < f b) : a < b := by
  -- 対偶をとる
  contrapose h'

  -- ゴールと仮定が否定になって入れ替わる
  show ¬f a < f b

  simp_all only [not_lt]
  apply h
  assumption

contrapose!

contrapose! は、対偶をとった後に簡略化を実行します。

example (h : MyMonotone f) (h' : f a < f b) : a < b := by
  -- 対偶をとる
  contrapose! h'

  -- 不等号の否定を逆向きの不等号に簡略化してくれる
  show f b ≤ f a

  apply h
  assumption

conv

conv は、変換モード(conversion mode)に入るためのタクティクです。ゴールや仮定の中の特定の部分式だけを変形するために使います。

/-- 何らかの自然数 -/
opaque x : Nat

axiom hx : x = x + 1

example (a : Nat) : x + a = x + 1 + a := by
  -- rw では解決できない
  -- なぜかといえば、`rw` はゴール全体にある `x` を書き換えてしまうので
  -- 右辺の `x` も `x + 1` に書き換えてしまうから
  fail_if_success solve |
    rw [hx]

  conv =>
    -- 等式の左辺 `x + a` に移動する
    lhs

    -- 左辺の `x` だけを `x + 1` に書き換える
    rw [hx]

適宜 Mathlib の conv ガイドTheorem Proving in Lean4 なども参照してください。

式の中を移動する

conv => と書くと変換モードに入ります。変換モードでは、ゴールは通常の ではなく | の形で表示され、現在注目している式を表します。

変換モードの中でのみ使える、式の中を移動するためのタクティクが用意されています。

  • lhs は二項演算の左辺に移動します。
  • rhs は二項演算の右辺に移動します。
  • congr は現在注目している式の引数へ移動します。
  • rfl は何もしません。式をそのままにします。
example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv =>
    -- 等式の左辺 `a * (b * c)` に移動する
    lhs

    -- `a` と `b * c` の2つに分ける
    congr

    -- `a` は書き換えない
    · rfl

    -- `b * c` だけを `c * b` に書き換える
    · rw [Nat.mul_comm]

パターンで場所を指定する

手で lhscongr を使って移動する代わりに、conv in パターン => ... と書くことで書き換えたい部分式を直接指定できます。 プレースホルダ _ も使えます。

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv in b * c =>
    rw [Nat.mul_comm b c]

example (a b c : Nat) : a * (b * c) = a * (c * b) := by
  conv in _ * c =>
    rw [Nat.mul_comm b c]

同じ形の部分式が複数あるときは、occs で何番目の出現を書き換えるか指定できます。

example (a b : Nat) :
    (a + b) + (a + b) = (a + b) + (b + a) := by
  conv in (occs := 2) a + b =>
    rw [Nat.add_comm a b]

convert

ローカルコンテキストに現在のゴールに近いけれども等しくはない h があるとき、exact h としても失敗します。しかし convert h は成功する可能性があり、成功した場合は h とゴールの差分を新たなゴールとします。

import Mathlib.Tactic.Convert

variable (a b c: Nat)

example (f : Nat → Nat) (h : f (a + b) = 0) (hc: a + b = c) : f (c) = 0 := by
  -- `h` はゴールと等しくないので失敗する
  fail_if_success exact [h]

  -- `h` とゴールの差分を新たなゴールにする
  convert h

  -- ゴールが `⊢ c = a + b` に変わっている
  show c = a + b

  rw [hc]

decide

decide は、決定可能な命題を示すタクティクです。

命題 P : Prop が決定可能であるとは、型クラス Decidable のインスタンスであることを意味します。PDecidable のインスタンスであるとき、decide 関数を適用することにより decide P : Bool が得られるので、これを使って証明したり反証したりできます。

-- 決定可能な命題を決定する関数 decide が存在する
#check (decide : (P : Prop) → [Decidable P] → Bool)

つまり一言で言えば、decide とは「計算すればわかる」ことを証明または反証するためのタクティクです。

-- 計算すればわかるので示せる
example : 1 + 1 = 2 := by decide

-- 等式以外でも、決定可能なら示せる
example : 5 * 7 ≤ 21 + 19 := by decide

-- 整除関係も決定可能なので示せる
example : 11 ∣ 121 := by decide

-- 証明しようとしたことが間違っていたら教えてくれる
/-
error: Tactic `decide` proved that the proposition
  15 ∣ 21
is false
-/
example : 15 ∣ 21 := by decide

カスタマイズ

Decidable 型クラスのインスタンスに登録すれば、自前で用意した述語を decide に示させることができます。

/-- 奇数であること -/
def Odd (n : Int) : Prop := ∃ t : Int, n = 2 * t + 1

example : Odd (7 : Int) := by
  -- 自前で定義したばかりなので decide で示せない
  fail_if_success decide

  -- 手動で示す
  exists 3

/-- 奇数であることが決定可能であること -/
instance (n : Int) : Decidable (Odd n) := by
  -- n % 2 の計算に帰着させる
  refine decidable_of_iff (n % 2 = 1) ?_
  dsimp [Odd]
  constructor <;> intro h
  · exists (n / 2)
    omega
  · obtain ⟨t, th⟩ := h
    rw [th]
    omega

-- decide で証明できる
-- 具体的に 7 = 2 * k + 1 となる k を求める必要がなくなって嬉しい
theorem odd_seven : Odd (7 : Int) := by
  decide

よくあるエラー

decide は、整礎再帰 を使って定義された関数に対してはそのまま使用することができません。native_decide タクティクを使えば一応証明は可能です。

/-- 文字列を指定した長さになるまで特定の文字で埋める関数 -/
def String.padWith (s : String) (c : Char) (n : Nat) : String :=
  if n ≤ s.length then
    s
  else
    (c.toString ++ s).padWith c n
termination_by n - s.length

#guard String.padWith "abc" 'x' 5 = "xxabc"

example : String.padWith "abc" 'x' 5 = "xxabc" := by
  -- decide は整礎再帰の関数には使えない
  fail_if_success decide

  -- 一応証明できる
  native_decide

done

done は、証明終了の合図です。証明すべきゴールが残っていない時に成功し、それ以外の時にはエラーになります。QED のようなものです。証明がサブゴールに分かれている場合、サブゴールごとに判定を行います。

example (P Q : Prop) (h : P → Q) : ¬ P ∨ Q := by
  -- `P` が成り立つかどうかで場合分けを行う
  by_cases hP : P

  -- `P` が成り立つ場合
  case pos =>
    -- `P → Q` より `Q` が成り立つ
    have := h hP

    -- したがって結論が従う
    exact Or.inr this

    -- `P` が成り立つ場合の証明終わり。
    done

  -- `P` が成り立たない場合
  case neg =>
    -- `¬ P` が成り立つので、`¬ P ∨ Q` も成り立つ
    exact Or.inl hP

    -- `P` が成り立たない場合の証明終わり。
    done

舞台裏

getUnsolvedGoals という関数で、現在の残りのゴールを取得することができます。これを利用すると、done タクティクと同様のはたらきをするタクティクを自作することができます。ここでは elab コマンドを使った実装を紹介します。

open Lean.Elab Tactic Term in

elab "my_done" : tactic => do
  -- 未解決のゴールを List として取得する
  let gs ← getUnsolvedGoals

  -- ゴールが残っている場合はエラーにする
  unless gs.isEmpty do
    reportUnsolvedGoals gs
    throwAbortTactic

example : 1 = 1 := by
  rfl
  my_done

/-
error: unsolved goals
⊢ 2 = 2
-/
example : 1 = 1 ∧ 2 = 2 := by
  refine ⟨rfl, ?_⟩
  my_done

これを使うと、派生タクティクを自作することもできます。ゴールを閉じたときに 🎉 でお祝いしてくれるタクティクを自作してみましょう。

open Lean Elab Tactic Term in

elab "tada" : tactic => do
  let gs ← getUnsolvedGoals

  unless gs.isEmpty do
    reportUnsolvedGoals gs
    throwAbortTactic

  -- ゴールが残っていない場合はお祝いメッセージを表示する
  logInfo "Goals accomplished 🎉"

/- info: Goals accomplished 🎉 -/
example : 1 = 1 := by
  rfl
  tada

dsimp

dsimp は、定義的に等しい(definitionally equal) 変形だけを行うという制約付きの simp で、一言でいえば「名前を定義に展開する」タクティクです。

dsimp [e₁, e₂, ..., eᵢ] という構文でゴールに登場する名前 e₁, ..., eᵢ を定義に展開します。

section

  /-- `n < m`を真似て構成した自前の述語 -/
  def Nat.mylt (n m : Nat) := (n + 1) ≤ m

  /-- `n < m`と書いたら標準の`Nat.lt`の代わりに上記の`Nat.mylt`を使用する -/
  local instance : LT Nat where
    lt := Nat.mylt

  example : 1 < 2 := by
    -- `<`という記号、およびその実装である`Nat.mylt`を展開する
    dsimp [(· < ·), Nat.mylt]

    -- ゴールが展開されて変形された
    guard_target =ₛ 2 ≤ 2

    omega

end

舞台裏

「定義的に等しい(definitionally equal)ような変形だけを行う」というのは、rfl で示せるような命題だけを使用するという意味です。rfl で示せないような簡約は dsimp ではできません。

/-- 自前で定義した自然数 -/
inductive MyNat where
  | zero : MyNat
  | succ : MyNat → MyNat

instance : Zero MyNat where
  zero := MyNat.zero

/-- MyNat の足し算 -/
def MyNat.add (n m : MyNat) : MyNat :=
  match m with
  | .zero => n
  | .succ m => MyNat.succ (MyNat.add n m)

/-- MyNat.add を足し算記号で書けるようにする -/
infix:65 " + " => MyNat.add

/-- ゼロを左から足しても変わらない。-/
theorem MyNat.zero_add {n : MyNat} : (0 : MyNat) + n = n := by
  induction n with
  | zero => rfl
  | succ n ih =>
    dsimp [MyNat.add]
    rw [ih]

example (n : MyNat) : n + (0 : MyNat) = n := by
  -- rfl で証明ができる
  rfl

example (n : MyNat) : n + (0 : MyNat) = n := by
  -- dsimp でも証明ができる
  dsimp [MyNat.add]

example (n : MyNat) : (0 : MyNat) + n = n := by
  -- rfl では証明ができない
  fail_if_success rfl

  -- dsimp でも証明ができない
  fail_if_success dsimp [MyNat.add]

  rw [MyNat.zero_add]

カスタマイズ

dsimp で自動的に示せる命題を増やすには、[defeq] 属性と [simp] 属性を付与します。 ただし、[defeq] 属性は [simp] 属性の前に付与しなければなりません。

theorem MyNat.add_zero (n : MyNat) : n + (0 : MyNat) = n := by
  rfl

example (n : MyNat) : n + (0 : MyNat) = n := by
  -- 最初は `dsimp` で証明ができない
  fail_if_success dsimp

  rfl

section
  -- `simp`, `defeq` の順に属性を与える
  attribute [local simp, defeq] MyNat.add_zero

  example (n : MyNat) : n + (0 : MyNat) = n := by
    -- まだ `dsimp` では証明ができない
    fail_if_success dsimp

    -- `simp` では証明ができる
    simp
end

-- `defeq` 属性を先に付与する
attribute [defeq, simp] MyNat.add_zero

example (n : MyNat) : n + (0 : MyNat) = n := by
  -- `dsimp` で証明ができるようになった!
  dsimp

unfold と比べた長所

同じく名前を定義に展開するタクティクとして unfold があります。たいていの場合両者は同じように使うことができますが、unfold は次のような意外な挙動をすることがあります。

-- α の部分集合を表す型
def Set (α : Type) := α → Prop

-- 部分集合の共通部分を取る操作
def Set.inter {α : Type} (s t : Set α) : Set α := fun x => s x ∧ t x

-- ∩ という記法を使えるようにする
instance (α : Type) : Inter (Set α) where
  inter := Set.inter

variable {α : Type} (s u : Set α)

example: True ∨ (s ∩ u = u ∩ s) := by
  -- ∩ 記号を展開する
  dsimp [Inter.inter]

  -- Set.inter で書き直される
  guard_target =ₛ True ∨ (s.inter u = u.inter s)

  left; trivial

example : True ∨ (s ∩ u = u ∩ s) := by
  -- ∩ 記号を展開する
  unfold Inter.inter

  -- 展開結果にインスタンスが入ってしまう
  show True ∨ (instInterSet α).1 s u = (instInterSet α).1 u s

  -- 再びインスタンスの展開を試みると
  unfold instInterSet

  -- 振り出しに戻ってしまう!
  show True ∨ (s ∩ u = u ∩ s)

  left; trivial

また、dsimp は識別子(ident)ではないものに対しても簡約を行うことができますが、unfold は識別子でなければ簡約が行えません。これも dsimp の長所といえます。

example : True ∨ (s ∩ u = u ∩ s) := by
  -- dsimp はラムダ式に対する簡約ができる
  dsimp [(· ∩ ·)]

  -- ゴールが展開される
  guard_target =ₛ True ∨ (s.inter u = u.inter s)

  left; trivial

open Lean Parser

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- 識別子を渡したときはパースできる
#eval parse `tactic "unfold Inter.inter"

-- 識別子でないものを渡すとパースできない
/- error: <input>:1:7: expected identifier -/
#eval parse `tactic "unfold (· ∩ ·)"

unfold と比べた短所

ただし、dsimp が失敗して unfold が成功するケースも存在します。 たとえば、dsimp[irreducible] 属性が付与された定義を展開することができませんが unfold は展開することができます。

@[irreducible]
def foo : Nat := 0

example : foo = 0 := by
  -- dsimp は失敗する
  fail_if_success dsimp [foo]
  -- unfold は成功する
  unfold foo
  rfl

exact

ゴールが P で、ローカルコンテキストに hP : P があるときに、exact hP はゴールを閉じます。hP がゴールの証明になっていないときには、失敗してエラーになります。

example {P Q : Prop}(hP : P)(hQ : Q) : P := by
  -- `hQ : Q` は `P` の証明ではないのでもちろん失敗する
  fail_if_success exact hQ

  exact hP

exact は与えられた証明項をそのまま証明として使うタクティクなので、by exact だけで証明が終わるときには、by exact を消しても証明が通ります。

section
  /- ## by exact の省略ができるケース -/

  variable {P Q : Prop}

  example (hP : P) (hQ : Q) : P ∧ Q := by
    -- exact を使う証明
    exact And.intro hP hQ

  example (hP : P) (hQ : Q) : P ∧ Q :=
    -- by exact を使わない証明
    And.intro hP hQ
end

なお And構造体なので無名コンストラクタ記法を用いて次のように書くこともできます。

example {P Q : Prop} (hP : P) (hQ : Q) : P ∧ Q := ⟨hP, hQ⟩

assumption との関連

exact は常にどの命題を使うか明示する必要がありますが、「ゴールを exact で閉じることができるような命題をローカルコンテキストから自動で探す」 assumption というタクティクもあります。

exact?

exact? は、カレントゴールを exact で閉じることができないか、import されているファイル群から検索して提案してくれるタクティクです。閉じることができなければ、エラーになります。

import Mathlib.Algebra.Order.Floor.Defs -- `Nat.floor` を使うために必要
import Mathlib.Data.Rat.Floor -- `ℚ` の性質を使うために必要
import Mathlib.Tactic.Says -- `says` を使うために必要


-- `exact?` はライブラリ検索を行う
example (x : Nat) : x < x + 1 := by
  exact? says
    exact lt_add_one x

-- ローカルコンテキストにある仮定を自動で使ってゴールを導いてくれる
example {P Q R : Prop} (hPQ : P → Q) (hQR : Q → R) (hQ : P) : R := by
  exact? says
    exact (hQR ∘ hPQ) hQ

exact? using h とするとローカルコンテキストにある仮定 h を使用してほしいと明示的に指定することができます。

example (n m : Nat) (h1 : n ≠ 0) (_h2 : n > 0) : n * m / n = m := by
  -- `h1` を指定すると `h2` は使わない
  exact? using h1 says
    exact Eq.symm (Nat.eq_div_of_mul_eq_right h1 rfl)

example (n m : Nat) (_h1 : n ≠ 0) (h2 : n > 0) : n * m / n = m := by
  -- `h2` を指定すると `h1` は使わない
  exact? using h2 says
    exact Nat.mul_div_right m h2

ローカルにある定理の検索

exact? は現在の環境にある定理・定数などを読み取るので、ローカルにある定理も検索してくれます。

inductive MyNat where
  | zero
  | succ (n : MyNat)

namespace MyNat

  def add (m n : MyNat) : MyNat :=
    match n with
    | zero => m
    | succ n => succ (add m n)

  -- `+`記号と`0`が使えるようにする
  instance : Add MyNat := ⟨MyNat.add⟩
  instance : Zero MyNat := ⟨MyNat.zero⟩

  theorem add_zero (n : MyNat) : 0 + n = n := by
    induction n
    case zero => rfl
    case succ n ih =>
      rw [show 0 + n.succ = succ (0 + n) from by rfl]
      rw [ih]

  example (n : MyNat) : 0 + n = n := by
    -- 自前で示した定理をちゃんと見つけてくれる
    exact? says
      exact MyNat.add_zero n
end MyNat

exact? を使用する際のコツ

これは exact? に限らず、Lean でライブラリ検索を行うとき常に意識した方が良いことですが、 強すぎる仮定を使用していたり表現が具体的過ぎたりすると上手くいかないことがあります。 適切な抽象化を心掛けてください。

variable (n : ℕ) (a : ℚ)

example (h : ↑ n < a) : n ≤ Nat.floor a := by
  -- ここで単に `exact?` しても通らない
  fail_if_success exact?

  -- 仮定の `<` が強すぎる。
  -- 結論を成り立たせるには `≤` で十分。
  suffices ↑ n ≤ a from by
    -- そうすると `exact?` が通るようになる
    exact? says
      exact Nat.le_floor this

  -- 後は `<` から `≤` を示せばよいだけ
  show ↑n ≤ a
  exact LT.lt.le h

exfalso

exfalso はゴールを矛盾を意味する False に換えます。

「矛盾からはどんな命題でも示せる」という命題 False → P のことを爆発律(principle of explosion)といいますが、これはラテン語で ex falso と呼ばれるようで、これが名前の由来のようです。

仮定の中に矛盾があるとき、ゴールが具体的に何であるかは関係がないので、exfalso を使ってゴールを False に変えた方が分かりやすいでしょう。

example {P Q : Prop} (h : P) (hn : ¬ P) : P ∧ Q := by
  -- 仮定の中に矛盾があるので、ゴールが何であるかは関係ない
  -- なので、ゴールを `False` に変える
  exfalso
  show False

  exact hn h

ローカルコンテキスト内に P¬ P があって矛盾することを指摘したい場合は、contradiction を使うとより簡潔です。また、一般に問題が命題論理に帰着されている状況では tauto または itauto が利用できます。

舞台裏

爆発律はどこから来たか

矛盾つまり False の項からはどんな命題でも示せるというのは最初は奇妙に感じるかもしれません。実際に False の構成を真似て自分で帰納型を定義することによって、同様のことを再現できます。

universe u

-- まずコンストラクタを持たない帰納型を定義する
-- これで False を模倣したことになる
inductive MyFalse : Prop

-- 帰納型を定義すると、再帰子(recursor)が自動生成される
-- 再帰子は数学的帰納法の原理に相当する
-- MyFalse の再帰子は以下のようになる
-- 再帰子によって、MyFalse からの関数を定義することができる
#check (@MyFalse.rec : (motive : MyFalse → Sort u) → (t : MyFalse) → motive t)

-- motive の返り値の型の Sort u には Prop も含まれる
-- したがって `fun _ => P` という関数を再帰子に渡すことができて任意の命題の証明ができる
theorem MyFalse.elim {P : Prop} (h : MyFalse) : P := @MyFalse.rec (fun _ => P) h

/-- `MyFalse` についての爆発律。`MyFalse` からはなんでも証明できる -/
example {Q : Prop} (h : MyFalse) : Q := by
  apply MyFalse.elim
  exact h

exfalso の定義

実際、exfalsorefine False.elim ?_ に展開されるマクロです。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: refine False.elim✝ ?_ -/
#expand (exfalso)

exists

exists タクティクは、「~を満たす x が存在する」という命題を示すために、証拠になる x を具体的に示します。

ゴールが ⊢ ∃ x, P x のとき、x : X がローカルコンテキストにあれば、exists x によりゴールが ⊢ P x に変わります。同時に、P x が自明な場合は証明が終了します。

example : ∃ x : Nat, 3 * x + 1 = 7 := by
  exists 2

上位互換にあたるタクティクに use タクティクがあります。

舞台裏

なお Lean での存在量化の定義は次のようになっています。

inductive Exists.{u} {α : Sort u} (p : α → Prop) : Prop where
  /-- `a : α` と `h : p a` から `∃ x : α, p x` の証明を得る -/
  | intro (w : α) (h : p w) : Exists p

したがって Exists は単一のコンストラクタを持つ帰納型なので、上記の existsexact無名コンストラクタで次のように書き直すことができます。

example : ∃ x : Nat, 3 * x + 1 = 7 := by
  exact ⟨2, show 3 * 2 + 1 = 7 from by rfl⟩

一般に exists e₁, e₂, .., eₙrefine ⟨e₁, e₂, .., eₙ, ?_⟩; try trivial の糖衣構文です。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: (refine ⟨1, 2, 3, ?_⟩; try trivial) -/
#expand (exists 1, 2, 3)

ext

ext は、外延性(extensionality)を使うタクティクです。外延性とは、「同じものから作られているものは同じである」という主張のことです。たとえば、「2つの関数 f g : A → B があるとき ∀ a : A, f a = g a ならば f = g」というのは外延性の一種で、関数外延性(functional extensionality) と呼ばれます。

example {A B : Type} (f g : A → B) (h : ∀ a : A, f a = g a) : f = g := by
  -- `x : A` を取って外延性を使用する
  ext x

  -- ゴールが `f x = g x` に変わる
  guard_target =ₛ f x = g x

  apply h

カスタマイズ

特定の型の外延性を登録し、ext タクティクで使用できるようにするには、外延性を述べた命題に [ext] 属性を付与します。

/-- `α` を全体集合とする集合の全体。別の言い方をすれば、`α` のベキ集合。
`α` の部分集合と、`α` 上の述語を同一視している。-/
def Set (α : Type u) := α → Prop

namespace Set

variable {α : Type u}

/-- `x : α` が `s : Set α` の要素であるという命題。-/
def Mem (s : Set α) (x : α) : Prop := s x

/-- 集合らしく `s x` を `x ∈ s` と書けるようにする -/
instance : Membership α (Set α) := ⟨Mem⟩

/-- 集合の外延性。同じ要素からなる集合は等しい -/
theorem ext {a b : Set α} (h : ∀ x, x ∈ a ↔ x ∈ b) : a = b := by
  -- 関数外延性を使う
  apply funext

  intro x
  rw [show a x ↔ b x from h x]

example {a b : Set α} (h : ∀ x, x ∈ a ↔ x ∈ b) : a = b := by
  -- 最初は使えない
  fail_if_success ext x
  sorry

-- `[ext]` 属性を付与
attribute [ext] Set.ext

example {a b : Set α} (h : ∀ x, x ∈ a ↔ x ∈ b) : a = b := by
  -- ext タクティクが使えるようになった!
  ext x
  apply h

end Set

構造体に対する [ext] 属性

[ext] 属性は構造体に対しても与えることができます。このとき、その構造体に対して自動的に .ext.ext_iff の2つの定理が生成されます。

variable {α : Type}

structure Point (α : Type) where
  x : α
  y : α

-- 最初は存在しない
#check_failure Point.ext
#check_failure Point.ext_iff

-- `Point` に `[ext]` 属性を与える
attribute [ext] Point

-- 自動生成された定理
-- 各フィールドの値が等しければ、2つの `Point` は等しいという主張
#check (Point.ext : ∀{x y : Point α}, x.x = y.x → x.y = y.y → x = y)

-- 自動生成された定理その2
-- 2つの `Point` の点が等しいことは、各フィールドの値が等しいことと同値
#check (Point.ext_iff : ∀{x y : Point α}, x = y ↔ x.x = y.x ∧ x.y = y.y)

これにより、構造体に対して ext タクティクが使用できるようになります。

structure Foo where
  x : Nat
  y : Nat

example (p q : Foo) (hx : p.x = q.x) (hy : p.y = q.y) : p = q := by
  -- 最初は ext タクティクが使えない
  fail_if_success ext
  sorry

-- `Foo` に `[ext]` 属性を与える
attribute [ext] Foo

example (p q : Foo) (hx : p.x = q.x) (hy : p.y = q.y) : p = q := by
  -- ext タクティクが使えるようになった!
  ext
  · exact hx
  · exact hy

field_simp

field_simp は、体1における等式を示す際の「分母を払う」操作に対応するタクティクです。

import Mathlib.Tactic

example (n m : Rat) : n * m = ((n + m) ^ 2 - n ^ 2 - m ^ 2 ) / 2 := by
  -- 分母を払う
  field_simp

  -- Rat は可換環なので、 示すべきことが言える
  ring

分母がゼロでないことが判らない場合は動作しません。分母がゼロでないことを証明してローカルコンテキストに加えると動作することがあります。

example {x y z : Rat} (hy : y = z ^ 2 + 1) : (x + 2 * y) / y = x / y + 2 := by
  -- 最初 `field_simp` は動作しない
  fail_if_success solve
  | field_simp

  -- 分母がゼロでないことを示す
  have ypos : y ≠ 0 := by
    rw [hy]
    positivity

  -- 動作するようになった
  field_simp

制約

field_simp という名前の通り、割り算が体の割り算でなければ動作しないことがあります。たとえば以下のコードは、自然数 Nat における割り算なので field_simp では扱うことができないという例です。

example (n m : Nat) : n * m = ((n + m) ^ 2 - n ^ 2 - m ^ 2 ) / 2 := by
  -- field_simp は動作しない
  fail_if_success field_simp

  -- 分母を払う
  suffices goal : 2 * (n * m) = (n + m) ^ 2 - n ^ 2 - m ^ 2 from by
    simp [← goal]

  -- `(n + m) ^ 2` を展開する
  rw [show (n + m) ^ 2 = n ^ 2 + 2 * n * m + m ^ 2 from by ring]

  -- 打消し合う項を消して簡単にすれば、示すべきことが言える
  simp [add_assoc, mul_assoc]

また field_simp の扱うのは等式のみで、順序関係は扱いません。体の定義に順序関係は含まれていないので、当然かもしれません。

variable (x y : Rat)

example : x * y ≤ (x ^ 2 + y ^ 2) / 2 := by
  -- 何も起こらない
  fail_if_success solve
  | field_simp

  suffices x ^ 2 - 2 * x * y + y ^ 2 ≥ 0 from by
    linarith

  calc
    x ^ 2 - 2 * x * y + y ^ 2 = (x - y) ^ 2 := by ring
    _ ≥ 0 := by apply sq_nonneg

example (h : x = y) : x * y = (x ^ 2 + y ^ 2) / 2 := by
  -- 等式ならば扱える
  field_simp

  -- ゴールの分母を払うことができた
  show x * y * 2 = x ^ 2 + y ^ 2

  rw [h]
  ring

  1. 体とは、四則演算が定義されていて、0 でない要素で割り算ができるものを指します。

fin_cases

fin_cases は有限通りの場合分けを行うタクティクです。 何度も cases をしないと全通りに場合分けできない場合でも、一発で全てのケースを生成することができます。

h : x ∈ [a₁, ..., aₙ] といった形の仮定に対して fin_cases h とすると、代入 x = a₁, ..., x = aₙ を施した n 個のゴールが生成されます。

import Mathlib.Tactic

example {n : ℕ} (h : n ∈ [2, 4, 42]) : 2 ∣ n := by
  -- n に 2, 4, 42 を順に代入した 3 つのゴールが生成される
  fin_cases h

  -- あとはそれぞれのゴールに対して具体的に計算して証明する
  next =>
    show 2 ∣ 2; decide

  next =>
    show 2 ∣ 4; decide

  next =>
    show 2 ∣ 42; decide

fin_cases を使わない場合、以下のように cases を繰り返し用いて一つずつケースを取り出すことになります。

example {n : ℕ} (h : n ∈ [2, 4, 42]) : 2 ∣ n := by
  cases h
  case head =>
    show 2 ∣ 2; decide
  case tail h =>
    -- n ∈ [4, 42] であるケース
    change n ∈ [4, 42] at h
    cases h
    case head =>
      show 2 ∣ 4; decide
    case tail h =>
      -- n ∈ [42] であるケース
      change n ∈ [42] at h
      cases h
      case head =>
        show 2 ∣ 42; decide
      case tail h =>
        -- n ∈ [] であるケース
        change n ∈ [] at h
        contradiction

fin_casesList α のほかに、Finset αMultiset α に対して適用可能です。

example {n : ℕ} (h : n ∈ ({2, 4, 42} : Finset ℕ)) : 2 ∣ n := by
  fin_cases h
  all_goals decide

example {n : ℕ} (h : n ∈ ({2, 4, 42} : Multiset ℕ)) : 2 ∣ n := by
  fin_cases h
  all_goals decide

また、型 α が「有限な型」である(インスタンス Fintype α が実装されている)場合、fin_cases xx : α のとりうる値に関する場合分けを行います。

-- `Fin n` は 0 から n-1 までの整数からなる型で、val : ℕ と isLt : val < n の 2 つのフィールドを持つ
example (n : Fin 10) (h : n.val ∣ 6) : n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 6 := by
  -- n.val = 0, ..., n.val = 9 の 10 通りに場合分けする
  fin_cases n

  -- h : n.val ∣ 6 が成り立たないケースは contradiction で示される
  any_goals contradiction

  -- 残りのケースについて、n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 6 が成り立つ
  all_goals decide

fun_cases

fun_cases は、関数定義に応じて場合分けを行うためのタクティクです。

たとえば以下の例では、swapHead はリストに対して「空か、シングルトンか、そのほか」で場合分けをして定義されています。一方でリストは再帰的なデータ型としては「空か、そのほか」で定義されているため、cases タクティクで普通に場合分けをしようとすると場合分けの分岐が合いません。fun_cases を使うと、関数定義で使用された場合分けを再利用することができます。

variable {α : Type}

/-- リストの先頭2要素を入れ替える -/
@[simp]
def swapHead (xs : List α) : List α :=
  match xs with
  | [] => []
  | [x] => [x]
  | x :: y :: zs => y :: x :: zs

example (l : List α) : swapHead (swapHead l) = l := by
  fun_cases swapHead l <;> simp

-- `fun_cases` を使用しなかった場合、場合分けの手間が増える
example (l : List α) : swapHead (swapHead l) = l := by
  cases l with
  | nil => simp
  | cons x xs =>
    cases xs with
    | nil => simp
    | cons y ys => simp

舞台裏

Lean で関数 f を定義すると f.fun_cases という名前の補助的な定理が自動生成されます。fun_cases タクティクは、この補助定理を使用して場合分けを行っています。cases タクティクでも、using で明示的に指定すれば fun_cases と同様のことができます。

example (l : List α) : swapHead (swapHead l) = l := by
  cases l using swapHead.fun_cases <;> simp

関数内で宣言した補題の再利用

以上のように「関数定義に使用した場合分けを再利用できる」のが fun_cases タクティクの主な利点ですが、他にも「関数定義の中で宣言した補題を再利用できる」という利点があります。

variable {α : Type} [LE α] [DecidableEq α] [DecidableLE α]

instance : Min α := minOfLe

/-- リストの最小値を先頭に持ってくる -/
@[simp, grind]
def minFirst (xs : List α) : List α :=
  match h : xs with
  | [] => []
  | x :: xs =>
    -- `x :: xs` の最小値を `μ` とする
    let μ := List.min (x :: xs) (h := by simp)

    -- `x :: xs` から `μ` を削除したリストを `rest` とする
    let rest := List.erase (x :: xs) μ

    -- `μ` は(最小値なので当然)`x :: xs` に含まれる
    have : μ ∈ x :: xs := by
      exact List.minOn?_mem rfl

    -- 関数定義の中で補題を示しておくと、`fun_cases` で再利用できる
    have : (μ :: rest).length = (x :: xs).length := by
      grind

    μ :: rest

@[grind =]
theorem minFirst_length (xs : List α) :
    (minFirst xs).length = xs.length := by
  -- 普通にinductionすると失敗する
  fail_if_success
    induction xs with grind only [minFirst]

  -- `fun_cases` を使うと成功する
  fun_cases minFirst xs with grind only [minFirst]

fun_induction

fun_induction は、特定の再帰関数用の帰納法ができるようにします。

たとえば、自然数について帰納法を行うと n = 0 の場合と n = n' + 1 の場合に場合分けをすることになります。しかし、関数 f について何かを示そうとしているとき、f が自然数の再帰的構造に沿って定義されているとは限りません。そのような場合に fun_induction を使うと、場合分けの枝が一致しない問題と格闘しないで済みます。

/-- フィボナッチ数列の通常の定義をそのまま Lean の関数として書いたもの -/
@[simp]
def fibonacci (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci n + fibonacci (n + 1)

/-- フィボナッチ数列の線形時間の実装 -/
def fib (n : Nat) : Nat :=
  (loop n).1
where
  loop (x : Nat) : Nat × Nat :=
    match x with
    | 0 => (0, 1)
    | n + 1 =>
      let p := loop n
      (p.2, p.1 + p.2)

@[simp]
theorem fib_zero : fib 0 = 0 := by rfl

@[simp]
theorem fib_one : fib 1 = 1 := by rfl

/-- `fib` が `fibonacci` と同じ漸化式を満たす -/
@[simp]
theorem fib_add (n : Nat) : fib n + fib (n + 1) = fib (n + 2) := by rfl

/-- `fibonacci` と `fib` は同じ結果を返す -/
example (n : Nat) : fibonacci n = fib n := by
  fun_induction fibonacci n with
  | case1 => rfl
  | case2 => simp
  | case3 n ih1 ih2 =>
    simp [ih1, ih2]

induction タクティクと同様に、with の後にタクティクを続けると、すべての枝に対してそのタクティクを適用します。

example (n : Nat) : fibonacci n = fib n := by
  fun_induction fibonacci n with simp_all

引数の省略

fun_induction f x₁ ... xₙ と書く代わりに、引数 x₁ ... xₙ を省略して fun_induction f と書くこともできます。引数が省略されると、Lean はゴールの中から f の適用箇所を探して引数を自動的に特定しようとします。ゴール中で f が一意に決まる引数に適用されていれば(すなわち、どの引数について帰納法を回すかが一意に決まれば)、引数を省略することができます。たとえば、先ほどの例では n を省略しても動作します。

-- n を省略しても動作する
example (n : Nat) : fibonacci n = fib n := by
  fun_induction fibonacci with simp_all

一方、ゴール中に f複数の異なる引数で 呼ばれているとき、どの引数について帰納法を回すかを特定できなくなるため、引数を省略するとエラーになります。次の例では、ゴール中に myLast?l ++ rlr という 3 つの引数を与えられてそれぞれ現れており、引数を省略するとエラーになります。

variable {α : Type}

/-- リストの最後の要素を返す関数 -/
@[grind]
def myLast? (l : List α) : Option α :=
  match l with
  | [] => none
  | [a] => some a
  | _ :: rest => myLast? rest

@[grind =, simp]
theorem myLast?_append_singleton (l : List α) (a : α) :
    myLast? (l ++ [a]) = some a := by
  induction l with grind

-- ゴール中に myLast? が複数の異なる引数で現れるため、引数を省略するとエラーになる
example (l r : List α) :
    myLast? (l ++ r) = if r = [] then myLast? l else myLast? r := by
  -- 引数を省略するとエラー
  fail_if_success fun_induction myLast?

  -- 帰納法に使う引数 r を明示することで成功する
  fun_induction myLast? r generalizing l with
  | case1 => simp
  | case2 => simp
  | case3 b rest h ih =>
    replace ih := ih (l ++ [b])
    grind

舞台裏

再帰関数 foo を定義すると、裏で Lean が帰納原理(induction principle) foo.inductfoo.induct_unfolding を生成します。

/-- フィボナッチ数列 -/
def fibonacci (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci (n + 1) + fibonacci n

#check fibonacci.induct

#check fibonacci.induct_unfolding

帰納原理が生成されるのは再帰的な関数のみです。再帰的でない関数には生成されません。

def swapHead (l : List Nat) : List Nat :=
  match l with
  | [] => []
  | [x] => [x]
  | x :: y :: xs => y :: x :: xs

-- 帰納原理が生成されていない
#check_failure swapHead.induct

fun_induction タクティクは、この自動生成された foo.induct_unfolding を利用して帰納法を行っています。

帰納的述語への応用

帰納原理が自動生成されるのは再帰関数に対してだけで、帰納的述語に対しては生成されません。しかし、帰納的述語を再帰関数として書き直すことができるのであれば、その再帰関数に対して生成された *.induct 定理を使って帰納法を行うことができます。

これにより、(再帰関数として書き直せるような)帰納的述語に対しても、帰納法の枝が上手くハマらない問題を解決することができます。

/-- 回文を表す帰納的述語 -/
@[grind]
inductive Palindrome {α : Type} : List α → Prop
  /-- 空リストは回文 -/
  | nil : Palindrome []
  /-- 要素が一つだけのリストは回文 -/
  | single (a : α) : Palindrome [a]
  /-- 回文の両端に同じ要素を追加しても回文 -/
  | sandwich {a : α} {as : List α} (ih : Palindrome as) : Palindrome ([a] ++ as ++ [a])

variable {α : Type}


-- 普通に帰納法を使おうとすると、場合分けの枝がうまくはまらない
example (as : List α) (h : as.reverse = as) : Palindrome as := by
  induction as with
  | nil => grind
  | cons a as ih =>
    /-
    ih : as.reverse = as → Palindrome as
    h : (a :: as).reverse = a :: as
    ⊢ Palindrome (a :: as)
    -/

    -- 簡単には証明できない
    fail_if_success grind
    sorry

-- `α`に対して`(· = ·)`が決定可能という仮定がないため、
-- 古典論理を使用する
open scoped Classical in

/-- 回文判定を行う再帰関数。
`Palindrome` の定義になるべく忠実に書き直したもの -/
def PalindromeRec (as : List α) : Prop :=
  match as with
  | [] => True
  | [a] => True
  | a₁ :: a₂ :: as =>
    let xs := (a₂ :: as).dropLast
    let x := (a₂ :: as).getLast (by simp)

    -- 後に証明に使うときの利便性のために補題を示しておく
    have : [a₁] ++ xs ++ [x] = a₁ :: a₂ :: as := by
      grind [List.dropLast_concat_getLast]

    if a₁ = x then
      PalindromeRec xs
    else
      false
termination_by as.length

-- あっさり証明できる!
example (as : List α) (h : as.reverse = as) : Palindrome as := by
  induction as using PalindromeRec.induct with grind

fun_prop

fun_prop は、連続性や可測性など、関数に関する性質を示すタクティクです。

import Mathlib.Tactic
import Mathlib.MeasureTheory.Constructions.BorelSpace.Basic

variable {u v : ℝ → ℝ} (hu : Continuous u) (hv : Continuous v)

/-- 連続関数の積は連続関数 -/
example : Continuous (fun x ↦ u x * v x) := by
  fun_prop

/-- 有理関数が可測関数であることを示す -/
example : Measurable fun x : ℝ => (x * x - 1) / x + (x - x * x) := by
  fun_prop

funext

関数 fg が等しいことを示す際に、引数 x をとって f x = g x を示そうとすることがありますが、funext はそれを行うタクティクです。

def f := fun (x : Nat) ↦ x + x

def g := fun (x : Nat) ↦ 2 * x

example : f = g := by
  -- 引数 `x` を取る
  funext x

  -- `f` と `g` を定義に展開する
  dsimp [f, g]

  -- `x + x` と `2 * x` が等しいことを証明する
  show x + x = 2 * x
  grind

なお funextext で置き換えることができます。

example : f = g := by
  -- `ext` で書き換えることができる
  ext x

  dsimp [f, g]

  grind

gcongr

gcongr は合同関係(congruence)を扱うタクティクです。

n 変数関数 fn+1 個の2項関係 , ~₁, … ∼ₙ に対して、x₁ ~₁ x₁' → ... xₙ ~ₙ xₙ' → f x₁ ... xₙ ∼ f x₁' ... xₙ' が成り立つという形の補題を使って、ゴールを書き換えます。

同様に合同性を扱うタクティクに congr がありますが、あちらは等号を扱います。

import Mathlib.Tactic.GCongr -- `gcongr` を使うため
import Mathlib.Analysis.SpecialFunctions.Log.Basic -- `log` を使うため

open Real

variable (a b : ℝ) (x y : ℝ)

example (h : a ≤ b) : log (1 + exp a) ≤ log (1 + exp b) := by
  gcongr

example (h1 : x ≤ y) (h2 : a ≤ b) (h3 : 0 ≤ a) (h4 : 0 ≤ y)
    : x * a ≤ y * b := by
  gcongr

gcongr はデフォルトでは分解できなくなるまで分解するので、「行き過ぎ」になることがあります。gcongr に分解パターンを直接指定することで、行き過ぎを防ぐことができます。

example {c d : ℝ} (h : a + c + 1 ≤ b + d + 1) :
    x ^ 2 * (a + c) + 5 ≤ x ^ 2 * (b + d) + 5 := by
  -- 単に `gcongr` とすると
  try
    gcongr

    -- 分解が行き過ぎてしまう
    · show a ≤ b

      -- これは証明できない
      fail

  -- 引数でパターンを指定できる
  gcongr x ^ 2 * ?_ + 5

  -- 望ましい分解になった
  show a + c ≤ b + d

  linarith

補題の登録

さらに [gcongr] 属性を付与することにより、 gcongr で呼び出して使える補題を増やすことができます。

variable {U : Type*}
variable (A B C : Set U)

/-- 独自に定義した二項関係。中身は `⊆` と同じ。-/
def mysubset (A B : Set U) : Prop := ∀ x, x ∈ A → x ∈ B

/-- `mysubset` を二項関係らしく書けるようにしたもの。-/
infix:50 " ⊆ₘ " => mysubset

example : B ∩ C ⊆ₘ (A ∪ B) ∩ C := by
  -- gcongr が使えない
  fail_if_success gcongr

  intro x hx
  aesop

-- `@[gcongr]` で `gcongr` が使える補題を増やす
@[gcongr]
lemma inter_subset_inter_left (h : A ⊆ B) : A ∩ C ⊆ₘ B ∩ C := by
  intro x hx
  aesop

example : B ∩ C ⊆ₘ (A ∪ B) ∩ C := by
  -- gcongr が使えるようになった
  gcongr

  -- ゴールが変わった
  show B ⊆ A ∪ B
  intro x hx
  aesop

generalize

generalize は、示したい命題を一般化するために使用されます。

variable {n m : Nat}

example : (n + m) ^ 2 + n * (n + m) = n * (n + m) + (n + m) ^ 2 := by
  -- 式の代わりに `x` に置き換えて一般化する
  generalize n * (n + m) = x

  -- `y` に置き換えて一般化する
  generalize (n + m) ^ 2 = y

  -- `y + x = x + y` を示せばよい
  show y + x = x + y

  -- 足し算の可換性から従う
  simp [Nat.add_comm]

単に generalize e = x とすると、非可逆的に置換されますが、generalize h : e = x とすると置換に使用した命題に後からアクセスできるようになります。

example : m ^ 2 + 1 ≥ m ^ 2 := by
  -- 一般化する
  generalize h : m ^ 2 = x

  -- ローカルコンテキストに命題 `h` が追加される
  guard_hyp h : m ^ 2 = x

  simp

grind

grind は、汎用的かつ強力な証明自動化タクティクです。1

非常に強力であり、時に驚くほどギャップのある証明を自動で完了させることができます。2

/-- 階乗関数 -/
@[grind]
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

@[inherit_doc factorial]
notation:max n "!" => factorial n

/-- 階乗関数の値は1以上 -/
@[simp]
theorem one_le_factorial (n : Nat) : 1 ≤ n ! := by
  fun_induction factorial <;> grind

-- `n !` を見かけたら `one_le_factorial` を利用するよう `grind` に指示
grind_pattern one_le_factorial => n !

/-- Pascal の三角形 -/
def pascal (a b : Nat) : Nat :=
  match a, b with
  | _, 0 => 1
  | 0, _ + 1 => 1
  | a + 1, b + 1 => pascal (a + 1) b + pascal a (b + 1)

/-- Pascal の三角形の性質 -/
theorem pascal_le_factorial (a b : Nat) : pascal a b ≤ (a + b)! := by
  -- 1行で証明できてしまう!
  fun_induction pascal with grind

-- `grind` を使用しない証明の例
example (a b : Nat) : pascal a b ≤ (a + b)! := by
  fun_induction pascal with
  | case1 => simp
  | case2 => simp
  | case3 a b iha ihb =>
    dsimp
    calc
      _ = pascal (a + 1) b + pascal a (b + 1) := by rfl
      _ ≤ (a + 1 + b)! + (a + (b + 1))! := by omega
      _ = (a + b + 1)! + (a + b + 1)! := by congr 1; ac_rfl
      _ = 2 * (a + b + 1)! := by omega
      _ ≤ (a + b + 2) * (a + b + 1)! := ?step
      _ = (a + b + 2)! := by rfl
      _ = (a + 1 + (b + 1))! := by congr 1; ac_rfl

    case step =>
      generalize h : (a + b + 1)! = k
      suffices 2 ≤ (a + b + 2) from by
        exact Nat.mul_le_mul_right k this
      omega

grind が複雑な証明を自動で完了させる様子をもっと見たい方は、付録: 選択ソート なども参考にしてください。

動作原理の概要

grind タクティクの動作原理を理解するには、仮想的な黒板を思い浮かべると良いでしょう。 新しい等式や不等式を見つけるたびに、grind はその事実を黒板に書き込んでいきます。 grind は「最初に結論の否定を仮定して矛盾を示す」ことでゴールを示すように設計されているため、最初に黒板に書き込むのは仮定と示したい結論の否定です。

set_option trace.grind.assert true in

/-
trace: [grind.assert] P
[grind.assert] ¬P
-/
example (P : Prop) (h : P) : P := by
  grind

そうやって黒板に事実を書き込みながら、grind は同値類(equivalence class、互いに等しいもの同士のグループのこと)を管理していて、等しいと分かった同値類をマージしていきます。そして最終的に True のグループと False のグループをマージする(つまり、矛盾を示す)ことでゴールを閉じます。

set_option trace.grind.eqc true in

/-
trace: [grind.eqc] P = True
[grind.eqc] P = False
-/
example (P : Prop) (h : P) : P := by
  grind

以上の「仮想的な黒板による同値類の管理」が中核的な動作原理で、さらに grind には以下のようなエンジンが組み込まれています。

  • 合同閉包(congruence closure)
  • E-マッチング(E-matching)
  • 制約伝播(Constraint Propagation)
  • サテライトソルバー(特定の代数系に対するソルバー群)

なお上記で説明したように、grind は「最初に結論を否定して矛盾を示すことでゴールを閉じる」という原理で動くため、必要がない場合であっても選択原理を使用します。

theorem easy_theorem (P : Prop) (h : P) : P := by
  grind

/- info: 'easy_theorem' depends on axioms: [propext, Classical.choice, Quot.sound] -/
#print axioms easy_theorem

合同閉包(congruence closure)

grind には、合同閉包(congruence closure)アルゴリズムが使用されています。3

合同閉包は、等式とその否定のグループが充足可能かどうかを決定するアルゴリズムです。既知の等式から以下のルールによって新たな等式が出てこなくなるまで等式を導出し、矛盾があれば充足不能と判断します。(ただし、無限ループを避けるために合同性ルールの適用は制限します)

  • 等式の反射律: a = a
  • 等式の対称律: a = b ならば b = a
  • 等式の推移律: a = b かつ b = c ならば a = c
  • 合同性: a = b ならば f a = f b
variable {α : Type} (a₁ a₂ : α)

set_option trace.grind.debug.congr true in

/-
trace: [grind.debug.congr] f a₂ = f a₁
[grind.debug.congr] f (f a₂) = f (f a₁)
-/
example (f : α → α) (h : a₁ = a₂) : f (f a₁) = f (f a₂) := by
  grind

-- 複雑な例も扱うことができる
example (f : α → α) (x : α)
  (h1 : f (f (f x)) = x) (h2 : f (f (f (f (f x)))) = x) :
    f x = x := by
  grind

E-マッチング

ある定理を適用して新たに分かったことを grind が黒板に書き込むことを「定理をインスタンス化する」と呼びます。grind は、定理を効率的にインスタンス化するために E-マッチング(E‑matching)と呼ばれる手法を使用します。なおE-マッチングとは、SMT ソルバなどで使われる、等式を考慮したパターンマッチの手法のことです。

grind_pattern

grind_pattern コマンドを使うと、特定の定理をいつインスタンス化するかを grind タクティクに指示することができます。

grind_pattern (定理名) => (パターン) という構文で使用し、「ローカルコンテキストに宣言されたパターンが見つかった時に定理をインスタンス化してください」という指示をしたことになります。

/-- 二項関係`R`がリストの隣接要素に対して成立するという述語。
つまり `[x₁, x₂, ..., xₙ].IsChain R` は `R x₁ x₂ ∧ R x₂ x₃ ∧ ... ∧ R xₙ₋₁ xₙ` を表す。-/
inductive List.IsChain {α : Type} (R : α → α → Prop) : List α → Prop
  | nil : IsChain R []
  | single (a : α) : IsChain R [a]
  | cons_cons {a b : α} {l : List α} (hab : R a b) (ih : IsChain R (b :: l)) :
    IsChain R (a :: b :: l)

namespace List

variable {α : Type} {R : α → α → Prop}

-- `grind`を使用しない証明の例
example (a b : α) (h : R a b) : [a, b].IsChain R := by
  apply IsChain.cons_cons h
  apply IsChain.single

-- `grind_pattern`を使って定理のインスタンス化を`grind`に指示する。
-- ローカルコンテキストに`=>`の右側のパターンを見かけたらインスタンス化させる。
grind_pattern IsChain.cons_cons => IsChain R (a :: b :: l)
grind_pattern IsChain.single => IsChain R [a]

example (a b : α) (h : R a b) : [a, b].IsChain R := by
  -- 一撃で片づけることができるようになった
  grind

-- どの定理がインスタンス化されたかのログを出す
set_option trace.grind.ematch.instance true in

/-
trace: [grind.ematch.instance] IsChain.cons_cons: R a b → IsChain R [b] → IsChain R [a, b]
[grind.ematch.instance] IsChain.single: IsChain R [b]
-/
example (a b : α) (h : R a b) : [a, b].IsChain R := by
  grind

end List

複数のパターンを指定することもできます。その場合は、「指定されたパターンのすべてが見つかった時に定理をインスタンス化してください」という指示になります。

/-- 何らかの関数 -/
opaque f : Nat → Nat

/-- `f` は単調増加関数 -/
axiom f_monotone {a b : Nat} (h : a ≤ b) : f a ≤ f b

section

  -- 多くのパターンを指定してインスタンス化ルールを設定
  local grind_pattern f_monotone => f a, f b, a ≤ b

  example (a : Nat) (h1 : a ≤ 1) (h2 : f 2 = 10) : f a ≤ 10 := by
    -- 最初は示すことができない
    fail_if_success grind

    -- `a ≤ 2` であることを明示的に教えると通るようになる。
    -- `f_monotone` のインスタンス化パターンが満たされるからだと考えられる。
    have : a ≤ 2 := by grind
    grind

end

-- インスタンス化の条件を緩めると、`grind` 一発で通るようになる
grind_pattern f_monotone => f a, f b

example (a : Nat) (h1 : a ≤ 1) (h2 : f 2 = 10) : f a ≤ 10 := by
  grind

[grind =]

等式を主張する定理に [grind =] 属性を付与すると、結論の等式の左辺がパターンとして登録され、「左辺を見かけたらその定理をインスタンス化して、黒板に新たな事実を書き留める」という挙動をするようになります。

def f : Nat → Nat := fun x => x - 1

def g : Nat → Nat := fun x => x + 1

theorem fg (x : Nat) : f (g x) = x := by
  dsimp [f, g]

-- `f (g x)` を見かけたら `f (g x) = x` という事実を
-- 黒板に書き込んで覚えておくよう指示
attribute [grind =] fg

set_option trace.grind.assert true in
set_option trace.grind.debug.congr true in
set_option trace.grind.ematch.instance true in

/-
trace: [grind.assert] f a = b
[grind.assert] a = g c
[grind.assert] ¬b = c
[grind.ematch.instance] fg: f (g c) = c
[grind.assert] f (g c) = c
[grind.debug.congr] f (g c) = f a
-/
example (a b c : Nat) (h1 : f a = b) (h2 : a = g c) : b = c := by
  grind

なお、結論が等式になっていないような命題に付与するとエラーになります。

theorem bar (n m : Nat) (h : n ≤ m) (hm : m = 1) : n ≤ 1 := by
  grind

/-
error: invalid E-matching equality theorem, conclusion must be an equality
  n ≤ 1
-/
attribute [grind =] bar

[grind _=_]

等式を主張する定理に [grind _=_] 属性を付与すると、結論の等式の左辺と両辺がともにパターンとして登録され、「左辺か右辺のパターンを見かけたら定理を黒板に書き込む」という挙動をするようになります。

/-- 自前で定義した可換モノイド -/
class CommMonoid (M : Type) [One M] [Mul M] where
  /-- 掛け算は結合的 -/
  mul_assoc : ∀ a b c : M, (a * (b * c)) = ((a * b) * c)
  /-- 単位元を左から掛けても変わらない -/
  one_mul : ∀ a : M, (1 * a) = a
  /-- 単位元を右から掛けても変わらない -/
  mul_one : ∀ a : M, (a * 1) = a
  /-- 掛け算は可換 -/
  mul_comm : ∀ a b : M, (a * b) = (b * a)

namespace CommMonoid

variable {M : Type} [One M] [Mul M] [inst : CommMonoid M]

@[grind _=_]
theorem mul_assoc' (a b c : M) : a * (b * c) = (a * b) * c := by
  apply mul_assoc

@[grind =]
theorem mul_comm' (a b : M) : a * b = b * a := by
  apply mul_comm

example (a b c : M) : a * b * (c * c * a) = a * a * b * c * c := by
  grind

end CommMonoid

なお、結論が等式になっていないような定理に付与するとエラーになります。

/-
error: invalid E-matching equality theorem, conclusion must be an equality
  n ≤ 1
-/
@[grind _=_]
theorem bar₂ (n m : Nat) (h : n ≤ m) (hm : m = 1) : n ≤ 1 := by
  grind

[grind →]

定理に [grind →] 属性を付与すると、定理の前提の命題が満たされたときに定理がインスタンス化されるようになります。

/-- 自然数上の広義の順序関係を再定義する -/
protected inductive Nat.myle (n : Nat) : Nat → Prop
  /-- `∀ n, n ≤ n` -/
  | refl : Nat.myle n n
  /-- `n ≤ m`ならば`n ≤ m + 1` -/
  | step {m : Nat} : Nat.myle n m → Nat.myle n (m + 1)

/-- `Nat.myle`のための記号。標準の`≤`と被らないようにする -/
infix:50 " ≤? " => Nat.myle

attribute [grind →] Nat.myle.step

variable {m n a b k : Nat}

/-- 推移律 -/
@[grind →]
theorem Nat.myle_trans (hnm : n ≤? m) (hmk : m ≤? k) : n ≤? k := by
  induction hmk with grind

example (h1 : a ≤? b) (h2 : b ≤? k) (h3 : k ≤? m) : a ≤? m := by
  grind

なお [grind →] は定理の前提となる命題からパターンを作るので、Prop 値の前提を持たない定理は登録できません。

theorem Nat.add_le (n m : Nat) : n ≤ n + m := by
  omega

/-
error: invalid `grind` forward theorem,
theorem `Nat.add_le` does not have propositional hypotheses
-/
attribute [grind ->] Nat.add_le

[grind =>]

[grind =>] は、[grind →] と同様に定理の前提が見つかった時にインスタンス化するように指示をするのですが、[grind ->] とは異なり、前提だけでなく必要なら結論も見てパターンを作ります。

/-- 何らかの述語 -/
opaque P : Nat → Prop

/-- 何らかの二項関係 -/
opaque R : Nat → Nat → Prop

axiom R_of_P (x y : Nat) (h : P x) : R x y

-- `[grind ->]` 属性は登録できない。
-- これは、前提の `P x` だけからは引数の `y` が特定できないため
/-
error:
`@[grind →] theorem R_of_P` failed to find patterns in the antecedents of the theorem,
consider using different options or the `grind_pattern` command
-/
attribute [grind →] R_of_P

-- `[grind =>]` 属性は付与できる
-- これは、結論の `R a b` も手掛かりにしてインスタンス化できるため
attribute [grind =>] R_of_P

example (a b : Nat) (hP : P a) : R a b := by
  -- 成功する
  grind

また、[grind =>] 属性は、[grind ->] とは異なり定理の前提が命題であることを要求しません。

/-- 群 -/
class Group (G : Type) extends One G, Mul G, Inv G where
  /-- 単位元を右から掛けても変わらない -/
  mul_one (g : G) : g * 1 = g
  /-- 単位元を左から掛けても変わらない -/
  one_mul (g : G) : 1 * g = g
  /-- 元とその逆元を掛けると単位元になる -/
  mul_inv_cancel (g : G) : g * g⁻¹ = 1
  /-- 逆元と元を掛けると単位元になる -/
  inv_mul_cancel (g : G) : g⁻¹ * g = 1
  /-- 掛け算は結合的である -/
  mul_assoc (g₁ g₂ g₃ : G) : (g₁ * g₂) * g₃ = g₁ * (g₂ * g₃)

attribute [grind =>]
  Group.mul_one Group.one_mul
  Group.mul_inv_cancel Group.inv_mul_cancel Group.mul_assoc

namespace Group

variable {G : Type} [Group G]

@[grind ->]
theorem mul_right_inv {g h : G} (hy : g * h = 1) : h = g⁻¹ := calc
  _ = 1 * h := by grind
  _ = g⁻¹ := by grind

@[grind ->]
theorem mul_left_inv {g h : G} (hy : h * g = 1) : h = g⁻¹ := by
  grind

theorem inv_inv (g : G) : g⁻¹⁻¹ = g := by
  grind

end Group

[grind intro]

帰納的述語に [grind intro] 属性を付与すると、コンストラクタの適用を自動で行うようになります。

/-- 偶数であることを表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | step {n : Nat} : Even n → Even (n + 2)

example : Even 0 := by
  -- 最初は証明できない
  fail_if_success grind

  apply Even.zero

example {m : Nat} (h : Even m) : Even (m + 2) := by
  -- 最初は証明できない
  fail_if_success grind
  apply Even.step h

attribute [grind intro] Even

example {m : Nat} (h : Even m) : Even (m + 2) := by
  -- 証明できるようになった
  grind

外延性定理

[grind ext] 属性を付与すると、外延性定理を grind に使わせることができます。 以下は、構造体に対して [grind ext] 属性を付与する例です。

/-- 2次元の点を表す構造体 -/
@[ext]
structure Point where
  x : Int
  y : Int

-- `@[ext]` タグを付けているので外延性定理が自動生成される
#check Point.ext

example (p : Point) (a : Int) : a = p.x → p = ⟨a, p.y⟩ := by
  -- 最初は grind だけでは証明できない
  fail_if_success grind

  rcases p with ⟨p_x, p_y⟩
  simp_all

-- 属性を付与すると...
attribute [grind ext] Point

example (p : Point) (a : Int) : a = p.x → p = ⟨a, p.y⟩ := by
  -- grind で証明できるようになった!
  grind

構造体ではなく、外延性定理そのものに [grind ext] 属性を与えることもできます。

/-- 値を一つ持つラッパー型 -/
structure Box where
  val : Nat

@[ext, grind ext]
theorem Box.ext {a b : Box} (h : a.val = b.val) : a = b := by
  rcases a with ⟨a_val⟩
  rcases b with ⟨b_val⟩
  simp at *
  assumption

example (a b : Box) (h : a.val = b.val) : a = b := by
  grind

関数外延性には最初から標準ライブラリにおいて [grind ext] が付与されているため、grind は関数等式の形のゴールを扱うことができます。

-- 外延性を使用したときにログを出させる
set_option trace.grind.ext true in

/- trace: [grind.ext] funext: ∀ {h : ∀ (x : Nat), f x = g x}, False -/
example (f g : Nat → Nat) (h : ∀ x, f x = g x) : f = g := by
  grind

ガイド付き場合分け(guided case analysis)

grindmatch 式や if 式を場合分けして分解することができます。

def oneOrTwoIf (n : Nat) : Nat :=
  if n = 0 then 1 else 2

example (n : Nat) : oneOrTwoIf n > 0 := by
  dsimp [oneOrTwoIf]
  grind

def oneOrTwoMatch (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | _ => 2

-- どういう場合分けを行ったかトレースを出す
set_option trace.grind.split true in

/-
trace: [grind.split] match n with
    | 0 => 1
    | x => 2, generation: 0
-/
example (n : Nat) : oneOrTwoMatch n > 0 := by
  dsimp [oneOrTwoMatch]
  grind

[grind cases] 属性が付与されている帰納的命題に対しても、場合分けを行います。

example : ¬ Even 1 := by
  -- まだ示せない
  fail_if_success grind

  -- grind なしで証明する
  intro h
  cases h

-- 属性を付与する
attribute [grind cases] Even

example : ¬ Even 1 := by
  -- grind で示せるようになった
  grind

制約伝播(Constraint Propagation)

grind タクティクが黒板に新たな事実を書き込み、True または False の同値類が更新されたとき、grind は多数の前方推論を行い、新たな事実を導出していきます。これを制約伝播(Constraint Propagation)と呼びます。

制約伝播で使用される導出ルールには様々な種類のものがあります。

ブール演算

grindATrue であれば A ∨ BTrue である、などの基本的な導出を行います。

example {P Q : Prop} (h : P) : P ∨ Q := by
  grind

example {P Q R : Prop} (h : P ∧ Q ∧ R) : P ∧ Q := by
  grind

example (a : Bool) : (a && !a) = false := by
  grind

サテライトソルバー

ac ソルバー

結合的(associative)もしくは結合的かつ可換(commutative)な二項演算を持つ代数系に対して、grind は推論を行うことができます。この機能は ac と呼ばれるソルバーによって提供されています。

/-- 自然数をイメージした何か -/
opaque MyNat : Type

variable [Add MyNat]

/-- 加法の結合法則 -/
axiom MyNat.add_assoc (a b c : MyNat) : (a + b) + c = a + (b + c)

/-- 結合法則が成り立つことを型クラス経由で登録する -/
instance : @Std.Associative MyNat (· + ·) :=
  ⟨MyNat.add_assoc⟩

example (a b c d : MyNat) : (a + b) + (c + d) = a + (b + c) + d := by
  -- ac ソルバーを無効にすると証明できない
  fail_if_success grind -ac

  -- grind で証明することができる
  grind only


/-- 加法の交換法則 -/
axiom MyNat.add_comm (a b : MyNat) : a + b = b + a

/-- 交換法則が成り立つことを型クラス経由で登録する -/
instance : @Std.Commutative MyNat (· + ·) :=
  ⟨MyNat.add_comm⟩

example (a b c : MyNat) : a + b + c = c + b + a := by
  -- ac ソルバーを無効にすると証明できない
  fail_if_success grind -ac

  -- grind で証明することができる
  grind only

前提選択(premise selection)

grind には premise selection の仕組みが組み込まれており、grind +suggestions とすることで利用することができます。

import Lean.LibrarySuggestions.Default

/--
Lean リポジトリの `tests/elab/library_suggestions_persistent.lean` で使われている例。
`Dyadic.roundDown_le` などの補題は `grind` だけでは見つけられないが、
`grind +suggestions` は premise selection によって関連する補題を見つける。
-/
example {x : Dyadic} {prec : Int} : x.roundDown prec ≤ x := by
  fail_if_success grind -- grind だけでは証明できない
  grind +suggestions

example {x : Dyadic} {prec : Int} : x.roundDown prec ≤ x := by
  grind only [Dyadic.roundDown_le]

ただし premise selection とは、一般に自動定理証明において「証明したいゴールに関係のある定理・補題を探し出す」ことを指す言葉です。grind +suggestions は premise selection を行い、選び出した定理・補題を grind の引数に与えて実行します。


  1. このページの記述は全体的に The Lean Language Reference の The grind tactic という章 を参考にしています。

  2. この例は Zulip の Grind is impressive というトピックにおける Sorrachai Yingchareonthawornchai さんの投稿を元にしたものです。

  3. Marijn J. H. Heule “Logic and Mechanized Reasoning” を参考にしました。

guard_hyp

guard_hyp は、ローカルコンテキストにある命題を確認するタクティクです。指定した仮定が存在すれば成功し、そうでなければ失敗します。

通常の証明で使うことはあまりないかもしれません。本書では、ローカルコンテキストの変化を説明するために使用することがあります。

variable (P : Prop)

example (hP : P) : P := by
  -- 現在ローカルコンテキストにある命題を確認できる
  guard_hyp hP : P

  exact hP

have

have は、証明の途中でわかったことを補題としてローカルコンテキストに追加するタクティクです。

have h : P := ...P という命題の証明を構成し、その証明に h という名前を付けることができます。

/-- 3重否定の簡略化 -/
example (P : Prop) : ¬¬¬ P → ¬ P := by
  intro hn3p hp

  -- ここで`¬¬ P`が成り立つ。
  have hn2p : ¬¬ P := by
    -- なぜなら、`¬ P`であると仮定したとき
    intro hnp
    -- 仮定の`P`と矛盾するから
    contradiction

  -- これで`¬¬¬ P`と`¬¬ P`が得られたが、これは矛盾である
  contradiction

have で示した補題には必ず名前がつきます。名前を省略して have : P := ... とすると、自動的に this という名前になります。無名の補題が欲しい場合、代わりに show .. from 構文を検討してみてください。

example (P : Prop) : ¬¬¬ P → ¬ P := by
  intro hn3p hp

  -- 名前をつけないと…
  have : ¬¬ P := by
    intro hnp
    contradiction

  -- `this : ¬¬ P`という仮定が得られている
  guard_hyp this : ¬¬ P

  contradiction

また have で同じ名前を2回宣言すると、古い方はアクセス不能になってしまいます。ローカルコンテキストの補題の置き換えを行いたいときは、代わりに replace を使用してください。

無名コンストラクタ

P の証明 hp : PQ の証明 hq : Q があるとき、P ∧ Q の証明は And.intro hp hq で構成できます。ここで And.intro は構造体 And 型のコンストラクタです。

これを、コンストラクタ名を明示せずにシンプルに ⟨hp, hq⟩ と書くことができます。これは無名コンストラクタと呼ばれるものです。

section
  variable (P Q : Prop)

  theorem hpq (hp : P) (hq : Q) : P ∧ Q := ⟨hp, hq⟩
  theorem hpq' (hp : P) (hq : Q) : P ∧ Q := And.intro hp hq
end

無名コンストラクタを利用することで、記述を簡略化できます。

論理積 ∧

次のように、P ∧ Q という命題から PQ を取り出すことができます。

example (P Q : Prop) (hPQ : P ∧ Q) : P := by
  -- `P ∧ Q` という仮定を分解する
  -- `hQ : Q` は不要なのでアンダースコアに置き換える
  have ⟨ hP, _ ⟩ := hPQ

  assumption

存在 ∃

次のように、∃ x : X, P x という命題から、条件を満たす x を取り出すことができます。x : Xhx : P x がローカルコンテキストに追加されます。

-- `x` が偶数のとき `3 * x` も偶数
example (x : Nat) (hx : ∃ y, x = 2 * y) : ∃ z, 3 * x = 2 * z := by
  -- `hx` で存在が主張されている `y` と、
  -- `x = 2 * y` という命題を得る
  have ⟨y, hy⟩ := hx
  exists 3 * y
  rw [hy]
  omega

hint

hint は複数のタクティクを試し、上手くいったものを報告してくれるタクティクです。

import Mathlib.Tactic -- `hint` は検索を伴うので、おおざっぱに import している

/-
info: Try these:
  [apply] 🎉️ simp_all only [forall_const]
  [apply] norm_num
  Remaining subgoals:
  ⊢ Q
-/
example (P Q : Prop) (p : P) (h : P → Q) : Q := by
  hint

/-
info: Try these:
  [apply] 🎉️ simp_all only [and_self]
  [apply] norm_num
  Remaining subgoals:
  ⊢ Q ∧ P ∧ R
-/
example (P Q R : Prop) (x : P ∧ Q ∧ R ∧ R) : Q ∧ P ∧ R := by
  hint

登録されているタクティク

hint に登録されているタクティクのリストは、Mathlib.Tactic.Hint.getHints 関数を介して確認することができます。

open Lean Mathlib.Tactic in

/-- `hint` タクティクに登録されているタクティクの完全なリストを出力する -/
def getRegisteredTactics : CoreM Unit := do
  let hintTactics := (← Hint.getHints).map (fun (_n, tac) => tac.raw)
  for tactic in hintTactics do
    let .node _ _ arr := tactic
      | panic! "error: unexpected syntax term"
    IO.println arr[0]!

/-
info: "compute_degree"
"noncomm_ring"
"finiteness"
"linarith"
"field"
"field_simp"
"positivity"
"bound"
"add_group"
"group"
"ring"
"abel"
"norm_num"
"aesop"
"fun_prop"
"omega"
"grind"
"tauto"
"exact?"
"simp_all?"
"decide"
"intro"
"split"
"trivial"
"gcongr"
-/
#eval getRegisteredTactics

タクティクの新規登録

登録されているタクティクに tac を追加するには、register_hint tac を実行します。

register_hint 1000 nlinarith

/-
info: Try these:
  [apply] 🎉️ nlinarith
-/
example (a b : Nat) (h : a ≤ b) : (a + b) ^ 2 ≤ 4 * b ^ 2 := by
  hint

induction

induction は、帰納法のためのタクティクです。自然数 Nat や連結リスト List など、帰納的に定義されたものに対して何か証明しようとしているとき、帰納法を使うことが自然な選択です。

典型的な例は、自然数に対する数学的帰納法です。前提として、ある述語 P : Nat → Prop に対して ∀ n, P n を示そうとしているとします。このとき、以下を示せば十分であるというのが、数学的帰納法の主張です。

  • P 0 が成り立つ。
  • ∀ n, P n → P (n + 1) が成り立つ。
/-- `0` から `n` までの和を計算する関数 -/
def sum (n : Nat) : Rat :=
  match n with
  | 0 => 0
  | n + 1 => (n + 1) + sum n

example (n : Nat) : sum n = n * (n + 1) / 2 := by
  -- `n` についての帰納法で示す
  induction n with

  -- `n = 0` の場合
  | zero => grind [= sum]

  -- `0` から `n` までの自然数で成り立つと仮定する
  | succ n ih =>
    -- 帰納法の仮定が手に入る
    guard_hyp ih : sum n = n * (n + 1) / 2

    -- `sum` の定義を展開し、帰納法の仮定を適用する
    simp [sum, ih]

    -- 後は可換環の性質から示せる
    grind

なお、=> は省略することができます。

example (n : Nat) : sum n = n * (n + 1) / 2 := by
  induction n with | zero | succ n ih
  · grind [= sum]
  · simp [sum, ih]
    grind

また induction .. with の直後にタクティクを書くと、そのタクティクをすべてのゴールに対して適用します。

example (n : Nat) : sum n = n * (n + 1) / 2 := by
  induction n with grind [= sum]

inductive コマンドとの関係

実際には帰納法は自然数の専売特許ではありません。inductive コマンドで定義されたものであれば、帰納法を使うことができます。

/-- 標準ライブラリの定義を真似て構成した順序関係 -/
inductive Nat.myle (n : Nat) : Nat → Prop where
  /-- 常に `n ≤ n` が成り立つ -/
  | refl : myle n n

  /-- `n ≤ m` ならば `n ≤ m + 1` が成り立つ -/
  | step {m : Nat} : myle n m → myle n (m + 1)

@[inherit_doc] infix:50 " ≤ₘ " => Nat.myle

-- 順序関係について帰納法を回して証明をする例
example {m n k : Nat} (h₁ : m ≤ₘ n) (h₂ : n ≤ₘ k) : m ≤ₘ k := by
  induction h₂ with
  | refl => assumption
  | @step l h₂ ih =>
    apply Nat.myle.step (by assumption)

generalizing 構文

時として、帰納法の仮定が弱すぎることがあります。

/-- 階乗関数 -/
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => factorial n * (n + 1)

/-- 階乗関数の末尾再帰バージョン -/
@[grind]
def factorialTR (n : Nat) : Nat :=
  aux n 1
where
  @[grind] aux (n acc : Nat) :=
    match n with
    | 0 => acc
    | n + 1 => aux n (acc * (n + 1))

example (n acc : Nat) : factorialTR.aux n acc = acc * factorialTR.aux n 1 := by
  induction n with
  | zero => simp [factorialTR.aux]
  | succ n ih =>
    dsimp [factorialTR.aux]

    -- 得られている帰納法の仮定では `.aux` の2つめの引数は `acc` だが
    guard_hyp ih : factorialTR.aux n acc = acc * factorialTR.aux n 1

    -- これは示すべきことに合致しないので使えない。
    guard_target = factorialTR.aux n (acc * (n + 1)) = acc * factorialTR.aux n (1 * (n + 1))

    sorry

generalizing 構文で帰納法の仮定の中の特定の変数を一般化することができて、そうすると証明が通るようになることがあります。

example (n acc : Nat) : factorialTR.aux n acc = acc * factorialTR.aux n 1 := by
  induction n generalizing acc with
  | zero => simp [factorialTR.aux]
  | succ n ih =>
    dsimp [factorialTR.aux]

    -- 帰納法の仮定が強くなっている!!
    guard_hyp ih : ∀ (acc : Nat), factorialTR.aux n acc = acc * factorialTR.aux n 1

    grind

ただし注意点として、induction .. generalizing 構文を実行するとき、帰納法を行う変数が一般化される変数に依存していてはいけないというルールがあります。

/-- 偶数を表す帰納的述語 -/
inductive Even : Nat → Prop where
  | zero : Even 0
  | succ : {n : Nat} → Even n → Even (n + 2)

/- error: Variable `m` cannot be generalized because the induction target depends on it -/
example {n m : Nat} (h : Even (n + m)) (hm : Even m) : Even n := by
  induction hm generalizing m

完全帰納法

時には、より強い帰納法が必要なこともあります。 強い帰納法とは、 たとえば以下のような形式で表されるような帰納法のことです。

  • ∀ n, (∀ k < n, P (k)) → P (n) を示す。
  • したがって ∀ n, P (n) である。

これは超限帰納法の特別な場合で、完全帰納法や累積帰納法とも呼ばれます。 自然数の場合は、Nat.strongRecOnusing キーワードに渡せば使うことができます。

/-- 素数であるという述語 -/
@[simp]
def IsPrime (n : Nat) := 1 < n ∧ ∀ k, 1 < k → k < n → ¬ k ∣ n

@[grind ->]
theorem IsPrime_pos (n : Nat) (h : IsPrime n) : 1 < n := by
  simp_all

/-- 1より大きい任意の自然数は素因数を持つ -/
theorem exists_prime_factor (n : Nat) (hgt : 1 < n) :
    ∃ k, IsPrime k ∧ k ∣ n := by
  induction n using Nat.strongRecOn with
  | ind n ih =>
    -- nが素数であるかどうかによって場合分けをする。
    by_cases hprime : IsPrime n
    case pos =>
      -- nが素数であるときは明らか。
      grind [Nat.dvd_refl]

    -- 以下、nは素数でないとする。
    -- nは素数ではないのでnより真に小さい約数を持つ。
    have ⟨k, _, _, _⟩ : ∃ k, 1 < k ∧ k < n ∧ k ∣ n := by
      simp_all

    -- 帰納的に、k には素因数が存在するとしてよい。
    have := ih k ‹k < n›

    -- k ∣ n なので、k に素因数があるなら n にも存在する。
    grind [Nat.dvd_trans]

再帰的定理

Lean では、実は帰納法を使用するのに必ずしも induction は必要ありません。場合分けの中で示されたケースを帰納法の仮定として使うことができます。これは recursive theorem(再帰的定理) と呼ばれることがあります。1

theorem sum_exp (n : Nat) : sum n = n * (n + 1) / 2 := by
  match n with

  -- `n = 0` の場合
  | 0 => grind [= sum]

  -- `0` から `n` までの自然数で成り立つと仮定する
  | n + 1 =>
    -- 仮定から、`n` について成り立つ
    have ih := sum_exp n

    -- 仮定を適用して展開する
    simp [sum, ih]

    -- 後は可換環の性質から示せる
    grind

have で宣言された命題の証明の中では、この方法は使用できません。

theorem sample : True := by
  have h : ∀ n, sum n = n * (n + 1) / 2 := by
    intro n
    match n with
    | 0 => grind [= sum]
    | n + 1 =>
      -- h 自身を参照することができない
      fail_if_success have ih := h n
      sorry
  trivial

再帰的定理のテクニックと termination_by を組み合わせるとかなり複雑な帰納法も実行できますが、引数の中に帰納法で使用する変数が入っていなければいけません。induction タクティクを使うと、引数の中に入っていない変数に対しても帰納法を回すことができるので、それは induction タクティクの強みであると言えるでしょう。

/-- 空でない自然数の部分集合は最小元を持つ。 -/
theorem Nat.exists_min_of_exists (P : Nat → Prop) (h : ∃ n, P n) :
    ∃ m, P m ∧ ∀ n, P n → m ≤ n := by
  -- 存在が主張されている `n ∈ P` の大きさに対する帰納法によって証明する。
  obtain ⟨n, hn⟩ := h
  induction n using Nat.strongRecOn with
  | ind n ih =>
    -- n が最小元のときは明らか。
    -- n は最小元ではないとして良い。
    by_cases hsmall : ∃ k, k < n ∧ P k
    case neg => grind

    case pos =>
      -- n は最小元ではないので、n より小さい P の要素 k が存在する。
      obtain ⟨k, hk⟩ := hsmall

      -- k に対して帰納法の仮定から、最小元 m が存在する。
      obtain ⟨m, hm⟩ := ih k (show k < n from by grind) (show P k from by grind)

      -- この m が求めるものであった。
      exists m

よくあるエラー

induction タクティクを使ったときに、index in target's type is not a variable というエラーが出ることがあります。

/-- 偶数であることを表す帰納的述語 -/
inductive MyEven : Nat → Prop where
  | zero : MyEven 0
  | succ : {n : Nat} → MyEven n → MyEven (n + 2)

/-
error: Invalid target: Index in target's type is not a variable (consider using the `cases` tactic instead)
  0
-/
example (h : MyEven 0) : True := by
  induction h

これは型族の添え字が変数ではないから起こることです。その証拠に、変数にするとエラーにならなくなります。

example (n m : Nat) (h : MyEven (n + m)) : True := by
  generalize n + m = x at h
  induction h
  · trivial
  · trivial

  1. lean公式ブログの Functional induction についての記事 で recursive theorem という言葉が使われています。

induction’

induction'induction タクティクの構文が異なるバージョンです。

import Mathlib.Tactic -- 大雑把に import する

/-- `0` から `n` までの和を計算する。
多項式関数として表現する都合で、返り値は `Rat` にしてある。-/
def sum (n : Nat) : Rat :=
  match n with
  | 0 => 0
  | n + 1 => (n + 1) + sum n

example (n : Nat) : sum n = n * (n + 1) / 2 := by
  -- `n` についての帰納法で示す
  induction' n with n ih

  -- `n = 0` の場合
  case zero =>
    simp_all [sum]

  -- `0` から `n` までの自然数で成り立つと仮定する
  case succ =>
    -- `sum` の定義を展開し、帰納法の仮定を適用する
    simp [sum, ih]

    -- 後は可換環の性質から示せる
    ring

完全帰納法

時には、より強い帰納法が必要なこともあります。 強い帰納法とは、 たとえば以下のような形式で表されるような帰納法のことです。

  • ∀ n, (∀ k < n, P (k)) → P (n) を示す。
  • したがって ∀ n, P (n) である。

これは超限帰納法の特別な場合で、完全帰納法や累積帰納法とも呼ばれます。

/-- フィボナッチ数列の通常の定義をそのまま Lean の関数として書いたもの -/
def fibonacci : Nat → Nat
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci n + fibonacci (n + 1)

/-- フィボナッチ数列の線形時間の実装 -/
def fib (n : Nat) : Nat :=
  (loop n).1
where
  loop : Nat → Nat × Nat
    | 0 => (0, 1)
    | n + 1 =>
      let p := loop n
      (p.2, p.1 + p.2)

/-- `fib` が `fibonacci` と同じ漸化式を満たすことを証明する -/
@[simp]
theorem fib_add (n : Nat) : fib n + fib (n + 1) = fib (n + 2) := by rfl

/-- `fibonacci` と `fib` は同じ結果を返す -/
example : fibonacci = fib := by
  -- 関数が等しいことを示すので、引数 `n` が与えられたとする
  ext n

  -- `n` についての強い帰納法で示す
  induction' n using Nat.strong_induction_on with n ih
  match n with
  | 0 => rfl
  | 1 => rfl
  | n + 2 => simp_all [fibonacci]

intro

intro は数学で慣習的に行われる

  • P → Q を示すときに最初に P を仮定する
  • ∀ x ∈ A, P(x) を示すときに最初に x ∈ A が与えられたと仮定する

といった導入(introduction)を行います。

具体的には、intro は次のような挙動をします。

  • ゴールが ⊢ P → Q という形であるときに P をローカルコンテキストに追加して、ゴールを ⊢ Q に変える。
  • ゴールが ⊢ ∀ x, P x という形であるときに x をローカルコンテキストに追加してゴールを ⊢ P x に変える。
variable (P Q R : Prop)

example (hPQ: P → Q) (hQR: Q → R) : P → R := by
  -- 示したいことが `P → R` なので、`P` だと仮定する
  intro hP

  -- 仮定 `hPQ : P → Q` と `hP : P` から `Q` が導かれる
  have hQ : Q := hPQ hP

  -- 仮定 `hQR : Q → R` と `hQ : Q` から `R` が導かれる
  exact hQR hQ

特定の形の命題に対しての使用法

A ∧ B → C

前提が論理積の形をしていた場合、無名コンストラクタで仮定を分解することができます。

example {S : Prop} (hPR : P → R) (hQR : Q → S) : P ∧ Q → R ∧ S := by
  -- `P ∧ Q` だと仮定する
  intro ⟨hP, hQ⟩

  constructor
  . exact hPR hP
  . exact hQR hQ

A ∨ B → C

前提が論理和の形をしていた場合、次のように分解することができます。

example (hPR : P → R) (hQR : Q → R) : P ∨ Q → R := by
  intro

  -- `P` が成り立つとする
  | Or.inl hP =>
    exact hPR hP

  -- `Q` が成り立つとする
  | Or.inr hQ =>
    exact hQR hQ

rcases を使って分解することも一般的です。

example (hPR : P → R) (hQR : Q → R) : P ∨ Q → R := by
  intro h
  rcases h with hP | hQ

  -- `P` が成り立つとする
  case inl =>
    exact hPR hP

  -- `Q` が成り立つとする
  case inr =>
    exact hQR hQ

また、rintro を使うと上記の introrcases の組み合わせを同時に行うことができます。

example (hPR : P → R) (hQR : Q → R) : P ∨ Q → R := by
  rintro (hP | hQ)

  -- `P` が成り立つとする
  case inl =>
    exact hPR hP

  -- `Q` が成り立つとする
  case inr =>
    exact hQR hQ

∀ x, P x

intro∀ x, P x という形のゴールにも使用できます。

example (P Q : Nat → Prop) (h : ∀ n, P n ↔ Q n) : ∀ y, P (y + 1) → Q (y + 1) := by
  -- 任意の `y` について示すので、`intro` で `y` を導入する
  -- そして `P (y + 1) → Q(y + 1)` を示したいので、`P (y + 1)` を仮定する
  intro y hyP

  -- `Q (y + 1)` を示せば良い
  show Q (y + 1)

  -- 同値を使ってゴールを書き換える
  rw [← h]

  -- 仮定 `P (y + 1)` より従う
  assumption

否定 ¬ について

Lean では否定 ¬ PP → False として定義されているので、ゴールが ¬ P のときに intro すると P が仮定に追加されて、ゴールが False に変わります。

False は矛盾を導けば証明できます。

example (h: P → Q) : ¬Q → ¬P := by
  -- 示したいことが `¬Q → ¬P` なので、`¬Q` だと仮定する
  -- そうするとゴールが `¬P` になるので、
  -- さらに `intro` を行って仮定 `hP : P` を導入する
  intro hnQ hP

  -- 矛盾を示したい
  show False

  -- `hP : P` と `h : P → Q` から `Q` が導かれる
  have hQ : Q := h hP

  -- `hQ : Q` と `hnQ : ¬Q` から矛盾が導かれる
  contradiction

関数の構成

より一般的には、intro は関数の構成に使うことができます。 intro が全称命題 ∀ x, P x や含意 P → Q を統一的に扱うことができるのも、これらが Lean 内部で関数として扱われているからです。

variable {α : Type}

/-- `intro` による恒等関数の構成 -/
def f : α → α := by
  intro x
  exact x

example (x : α) : f x = id x := by dsimp [f]

itauto

itauto は、直観主義論理(intuitionistic logic)の範囲内でトートロジー(tautology)を証明します。「Contraction-free sequent calculi for intuitionistic logic」という1992年の論文に基づいて実装されています。

命題論理を扱うタクティクには他にも tauto がありますが、あちらは選択原理 Classical.choice を勝手に使用することがあります。なお選択原理は Lean が標準で用意している公理のひとつで、排中律 P ∨ ¬ P や二重否定の除去 ¬¬ P → P を示すのに必要です。

import Mathlib.Tactic.ITauto
import Mathlib.Tactic.Tauto

-- 命題とその否定は同値ではない
-- itauto で示したバージョン
theorem not_iff_not₀ (p : Prop) : ¬ (p ↔ ¬ p) := by itauto

-- tauto で示したバージョン
theorem not_iff_not₁ (p : Prop) : ¬ (p ↔ ¬ p) := by tauto

-- 選択原理に依存していない
/- info: 'not_iff_not₀' does not depend on any axioms -/
#print axioms not_iff_not₀

-- 勝手に選択原理を使用している!
/- info: 'not_iff_not₁' depends on axioms: [propext, Classical.choice, Quot.sound] -/
#print axioms not_iff_not₁

tauto と同様に、扱えるのは命題論理のトートロジーだけです。述語論理は扱えないことがあります。

/-- 排中律の二重否定 -/
example (P : Prop) : ¬¬ (P ∨ ¬ P) := by
  -- `itauto` で示せる
  itauto

example : ∀ (P : Prop), ¬¬ (P ∨ ¬ P) := by
  -- 量化されただけで `itauto` で示せなくなる
  fail_if_success itauto

  intro P hP

  -- 量化がなくなると扱えるようになる
  itauto

left

ゴールが ⊢ P ∨ Q であるとき、left はゴールを ⊢ P に変えます。類似のタクティクに right があります。

variable (P Q : Prop)

example (hP : P) : P ∨ Q := by
  left

  -- ゴールが変わる
  guard_target =ₛ P

  assumption

left, right を使わない方法

以下に示すように、Or.inla から a ∨ b を得る関数です。また Or.inrb から a ∨ b を得る関数です。これを使うことで leftright を使わずに証明できます。

#check (Or.inl : P → P ∨ Q)

#check (Or.inr : Q → P ∨ Q)

example (hP: P) : P ∨ Q := by
  apply Or.inl
  exact hP

linarith

linarith は線形算術(linear arithmetic)を行うタクティクです。Fourier-Motzkin elimination を用いて、線形な(不)等式系から矛盾を導こうとします。一般に、ゴールが False でないときにはゴールの否定を仮定に加えることで、ゴールを閉じようとします。

import Mathlib.Tactic.Linarith -- `linarith` のために必要

example (x y : ℚ) (h1: x = 2 * y) (h2 : - x + 2 * y = 1) : False := by
  linarith

example (x y z : ℚ) (h1 : 2 * x < 3 * y) (h2 : -4 * x + 2 * z < 0) :
    12 * y - 4 * z ≥ 0 := by
  linarith

linarith はローカルコンテキストにある命題を読むので、linarith が通らないとき、追加で補題を示すことで解決することがあります。

example (x : ℚ) : id x ≤ x := by
  -- `linarith` で示すことはできない
  fail_if_success linarith

  have : id x = x := rfl

  -- `id x = x` だと教えてあげると `linarith` で示せる
  linarith

また、使ってほしい補題を直接渡すこともできます。

example (x y : ℚ) (h : x ≤ y) (pos : 0 ≤ x) : x + x ^ 2 ≤ y + y ^ 2 := by
  -- `linarith` では示せない
  fail_if_success linarith

  -- `linarith` 単独で扱えない部分、つまり `x ^ 2 ≤ y ≤ 2` を示すための
  -- 補題を引数で渡してやると通る
  linarith [pow_le_pow_left₀ pos h 2]

舞台裏

linarith は一般に、[CommRing R], [LinearOrder R], [IsStrictOrderedRing R] のインスタンスであれば使用することができます。ここで linear order とは全順序のことです。

variable {R : Type} [CommRing R] [LinearOrder R] [IsStrictOrderedRing R]

variable (x y z : R)

-- `R` 上の不等式だが `linarith` で証明できる
example (h1 : 2 * x < 3 * y) (h2 : -4 * x + 2 * z < 0) :
    12 * y - 4 * z ≥ 0 := by
  linarith

linarith と他のタクティクの使い分け

1 < 2 のような簡単な数値のみの不等式の場合、norm_numsimp でも証明ができます。 同じ命題を示すのに複数のタクティクがあるわけですが、タクティク実行にかかる時間に違いがあります。

実行環境により正確な実行時間は異なりますが、linarith は比較的重いタクティクです。

#time example : 1 < 2 := by simp

#time example : 1 < 2 := by norm_num

#time example : 1 < 2 := by linarith

mvcgen

mvcgen は、モナディック(monadic)なプログラムを含むゴールを 検証条件(verification condition) に分解して処理するようなタクティクです。ただしここでいう検証条件とは、do 構文の背景にあるモナドを参照しない部分ゴールのことを指します。1

do 構文を使って手続き的に定義された関数に対して証明を行うことを支援するフレームワークが Lean には組み込まれているのですが、mvcgen はその一部です。

シンプルな使用例として、リストの和を for ループを使って計算する関数が、標準ライブラリに用意されている関数と等しいことを mvcgen を使って証明する例を紹介します。このとき、ループを回しても変わらずに成り立ち続ける 不変条件(invariant) を指定することに注意してください。

import Lean

-- mvcgen はまだ使用しないでというwarningを消す
set_option mvcgen.warning false

variable {α : Type} [Zero α] [Add α]
variable [@Std.Associative α (· + ·)] [@Std.LawfulIdentity α (· + ·) 0]

/-- for文を使って命令的に実装された、和を計算する関数 -/
@[grind]
def sumDo (l : List α) : α := Id.run do
  let mut out := 0
  for i in l do
    out := out + i
  return out

-- 証明の中で必要になる補題
@[grind =, simp]
theorem List.sum_append_singleton {l : List α} {x : α} :
    (l ++ [x]).sum = l.sum + x := by
  induction l with simp_all <;> grind

-- `scoped` で用意された表記法を有効にする
open Std.Do

/-- 手続き的に実装した和の計算と、標準ライブラリに用意されている`List.sum`関数が等しい -/
theorem sumImp_eq_sumFunc (l : List α) : sumDo l = List.sum l := by
  -- モナディックに実装されている(`Id.run do ...`)部分にフォーカスする
  generalize h : sumDo l = s
  apply Id.of_wp_run_eq h

  -- 検証条件に分解する
  mvcgen

  -- ループ全体を通して成り立たせたい不変条件を指定する
  -- * `out` は `let mut` で導入した変数の現在値を表す
  -- * `cursor` は `List.Cursor` で,リストを接頭辞 `cursor.prefix` と接尾辞 `cursor.suffix` に
  --   分割して表すデータ構造。どこまでループが進んだかを追跡する。
  --   つまり進捗(ループの到達位置)を記録する。
  -- 不変条件は「`out` が接頭辞の総和を保持している」こと。
  -- 記法 `⌜p⌝` は,命題 `p : Prop` をアサーション言語に埋め込む。
  case inv1 => exact ⇓⟨cursor, out⟩ => ⌜cursor.prefix.sum = out⌝

  -- `mleave` はある決まった `simp` 補題に対する `simp only [...] at *` の糖衣構文。
  all_goals mleave

  -- 各反復で不変条件が保たれることを示す
  case vc1.step pref cur suff hyp b ih =>
    -- 与えられているリストは
    -- * `pref`: for 文で既に処理された部分
    -- * `cur`: 今回処理する要素
    -- * `suff`: まだ処理されていない残りの部分
    -- に分割されているという仮定がある。
    guard_hyp hyp :ₛ l = pref ++ cur :: suff

    -- そして今まで処理された部分に関しては不変条件が成り立っているという帰納法の仮定がある
    guard_hyp ih :ₛ pref.sum = b

    -- このとき、現在の要素 `cur` を処理した後でも不変条件が成り立つことを示せばよい
    guard_target =ₛ (pref ++ [cur]).sum = b + cur

    -- 証明そのものは `grind` で終わる
    grind

  -- ループ開始時に不変条件が成り立つことを示す
  case vc2.pre =>
    -- 空リストの和が 0 であることを示せばよい
    guard_target =ₛ ([] : List α).sum = 0

    grind

  -- ループ終了時の不変条件から目標の性質が従うことを証明する
  case vc3.post.success result hr =>
    -- ループ終了時には「処理された部分」は全体なので、
    -- 不変条件から次が成りたつ
    guard_hyp hr :ₛ l.sum = result

    -- したがって目標も成り立つ
    grind

構文

mvcgen タクティクを使用するとき、まず不変条件を指定し、そのあとに all_goals mleave を実行するというパターンがよく見られます。そこで、定型文を減らすために以下のように mvcgen invariants という構文が用意されています。

import Std.Tactic.Do

set_option mvcgen.warning false

open Std.Do

/-- 素朴に実装されたべき乗関数 -/
def naiveExpo (x n : Nat) : Nat := Id.run do
  let mut result := 1
  for _ in [:n] do
    result := result * x
  return result

theorem naiveExpo_correct (x n : Nat) : naiveExpo x n = x ^ n := by
  generalize h : naiveExpo x n = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  -- 不変条件を指定する
  -- ここでは`cursor`がループの進捗を表していて、
  -- `cursor.prefix.length`はこれまでにループが回った回数を表す
  -- `result`はループ内で更新される変数の値を表す
  · ⇓⟨cursor, result⟩ => ⌜result = x ^ cursor.prefix.length⌝
  with simp_all <;> grind -- すべてのゴールに対して `mleave` に加えて `simp_all <;> grind` を適用してみる

機能

早期リターン

for 文の途中で return 文があるような場合でも、mvcgen は対応できます。その場合は不変条件の書き方が変わって、「早期終了した場合」と「早期終了せず継続する場合」の2つを考慮する必要が生じます。

import Lean

open Std.Do

set_option mvcgen.warning false

variable {α : Type} [Hashable α] [DecidableEq α]

/-- リストに重複がないか判定する。
true なら重複がない。false なら重複がある。-/
def nodupDo (l : List α) : Bool := Id.run do
  let mut seen : Std.HashSet α := ∅
  for x in l do
    if x ∈ seen then
      return false
    seen := seen.insert x
  return true

/-- `nodupDo` の結果は標準ライブラリに用意されている `Nodup` と一致する -/
theorem nodup_correct (l : List α) : nodupDo l ↔ l.Nodup := by
  generalize h : nodupDo l = r
  apply Id.of_wp_run_eq h
  mvcgen invariants
  · Invariant.withEarlyReturnNewDo
      -- forループの1ステップが平常終了したとき。
      -- プログラム中の可変変数`seen`は、現在までに見た要素の集合`cursor.prefix`に等しく、
      -- かつ`cursor.prefix`は重複がない。
      (onContinue := fun cursor seen => ⌜(∀ x, x ∈ seen ↔ x ∈ cursor.prefix) ∧ cursor.prefix.Nodup⌝)
      -- 早期リターンしたとき。
      -- 返り値は`false`であり、かつ`l`に重複がある
      (onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
  with grind

break 文

for ループを途中で抜ける構文としては return 文のほかに break 文もあります。break 文に対しては retrun 文のように不変条件を指定する方法がそれ専用に用意されていはいないため、不変条件に工夫が必要になります。具体的には、通常通りループが終了した場合でも break で抜けた場合でも成り立つような不変条件を考える必要があります。

以下は、takeWhile 関数を早期リターンと break のそれぞれを使って実装し、それぞれの不変条件を比較する例です。2

import Lean

open Std.Do

variable {α : Type}

/-- `takeWhile` を命令型スタイルで実装したもの。
`return` 文を使うバージョン。
(ただし、`result ++ [x]` の部分が非効率的で、あまり高速ではない)
-/
def takeWhileReturn (p : α → Bool) (l : List α) : List α := Id.run do
  let mut result := []
  for x in l do
    if ! p x then
      return result
    result := result ++ [x]
  return result

set_option mvcgen.warning false

@[grind =, simp]
theorem takeWhile_simplify_false (P : α → Bool) (pref suff : List α) (cur : α) (h : P cur = false) :
    List.takeWhile P (pref ++ cur :: suff) = List.takeWhile P pref := by
  fun_induction List.takeWhile with simp_all

@[grind =>]
theorem takeWhile_eq_self_iff_all (P : α → Bool) (l : List α) :
    List.takeWhile P l = l ↔ l.all P := by
  fun_induction List.takeWhile with simp_all

theorem takeWhileReturn_spec (p : α → Bool) (l : List α) :
    takeWhileReturn p l = l.takeWhile p := by
  generalize h : takeWhileReturn p l = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  · Invariant.withEarlyReturnNewDo
    (onContinue := fun cursor result =>
      ⌜result = cursor.prefix ∧ result = cursor.prefix.takeWhile p⌝)
    (onReturn := fun ret result => ⌜ret = result ∧ result = l.takeWhile p⌝)
  with grind

/-- `takeWhile` を命令型スタイルで実装したもの。
`break` 文を使うバージョン。-/
def takeWhileBreak (p : α → Bool) (l : List α) : List α := Id.run do
  let mut result := []
  for x in l do
    if ! p x then
      break
    result := result ++ [x]
  return result

theorem takeWhileBreak_spec (p : α → Bool) (l : List α) :
    takeWhileBreak p l = l.takeWhile p := by
  generalize h : takeWhileBreak p l = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  · ⇓⟨cursor, result⟩ =>
    -- `return` を使うものと比較すると、不変条件に `cursor.suffix = []` が追加されている。
    -- これは `break` でループを抜けた場合にも成り立つようにするための工夫
    ⌜(cursor.suffix = [] ∨ result = cursor.prefix) ∧ result = cursor.prefix.takeWhile p⌝
  with grind

continue 文

continue 文がある場合も break 文の場合と同様で、continue で抜けた場合でも通常終了した場合でも常に成り立つような不変条件を考えて指定します。

import Lean

variable {α : Type}

/-- `continue` 文を使って実装した `Array.filter` -/
def Array.filterDo (p : α → Bool) (l : Array α) : Array α := Id.run do
  let mut res : Array α := #[]
  for x in l do
    if ! p x then
      continue
    res := res.push x
  return res

-- 証明のための補題を用意する
attribute [grind =] Array.toList_inj
attribute [grind _=_] Array.toList_filter

open Std.Do

set_option mvcgen.warning false

theorem Array.filterDo_spec (p : α → Bool) (l : Array α) :
    l.filterDo p = l.filter p := by
  generalize h : l.filterDo p = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  · ⇓⟨cursor, res⟩ => ⌜res.toList = cursor.prefix.filter p⌝
  with grind

複数の可変変数

for ループの中で複数の可変変数が let mut で導入されている場合には、以下のように不変条件の書き方を変えることで対応できます。3

import Lean

open Std.Do

/-- フィボナッチ数列の仕様 -/
@[grind]
def fibSpec (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | k + 2 => fibSpec (k + 1) + fibSpec k

/-- 手続き的に実装された、フィボナッチ数列の実装 -/
def fibImpl (n : Nat) : Nat := Id.run do
  if n = 0 then
    return 0
  let mut a := 0
  let mut b := 1
  for _i in [1:n] do
    let a' := a
    a := b
    b := a' + b
  return b

-- `mvcgen` はまだ安定していないという警告を消す
set_option mvcgen.warning false

theorem fibImpl_eq_fibSpec (n : Nat) : fibImpl n = fibSpec n := by
  generalize h : fibImpl n = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  -- 不変条件の指定。
  -- `a` と `b` はループ内で更新される可変変数。
  -- `let mut` で定義された順番に拘束される。
  -- `cursor.pos` はループの進捗を表していて、いままでにループが回った回数を表す。
  · ⇓⟨cursor, a, b⟩ => ⌜a = fibSpec cursor.pos ∧ b = fibSpec (cursor.pos + 1)⌝
  with grind

二重の for ループ

for ループがネストしていても mvcgen は対応できます。その場合は、外側のループと内側のループそれぞれについて不変条件を指定する必要があります。また、内側のループの不変条件において、外側のループの進捗状況に言及することができます。

import Lean

open Std.Do

-- `α` は加法的な可換モノイドであると仮定する
variable {α : Type} [Add α] [Zero α]
variable [@Std.Associative α (· + ·)] [@Std.LawfulIdentity α (· + ·) 0]
variable [@Std.Commutative α (· + ·)]

/-- 二重リストに対する和を計算する、関数型スタイルで定義された関数 -/
@[grind]
def doubleSum (l : List (List α)) : α :=
  l.foldr (fun xs acc => acc + xs.sum) 0

/-- 二重リストに対する和を計算する、命令型スタイルで定義された関数 -/
def doubleSumDo (l : List (List α)) : α := Id.run do
  let mut result := 0
  for sublist in l do
    for x in sublist do
      result := result + x
  return result

set_option mvcgen.warning false

@[grind =, simp]
theorem List.sum_append_singleton {l : List α} {x : α} :
    (l ++ [x]).sum = l.sum + x := by
  induction l with simp_all <;> grind

/-- `doubleSum` は `append` を和に変換する -/
@[grind =]
theorem doubleSum_append {l1 l2 : List (List α)} :
    doubleSum (l1 ++ l2) = doubleSum l1 + doubleSum l2 := by
  induction l1 with grind

theorem doubleSum_spec (l : List (List α)) : doubleSumDo l = doubleSum l := by
  generalize h : doubleSumDo l = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  -- 外側のループについての不変条件。
  -- `cursor.prefix` はこれまでに外側の`for`ループで見てきた部分を指している
  · ⇓⟨cursor, result⟩ => ⌜result = doubleSum cursor.prefix⌝

  -- 内側のループについての不変条件。
  · ⇓⟨cursor, result⟩ => by
    expose_names -- すべての死んだ変数に名前を付ける

    -- `pref` は外側のループで今まで見てきた部分を表していて、
    -- `l = pref ++ (cur :: suff)` が成り立つ。
    guard_hyp h_1 :ₛ l = pref ++ cur :: suff

    -- `cursor` は内側のループの進捗を表している。
    exact ⌜result = doubleSum pref + (cursor.prefix).sum⌝
  with grind

while ループ

mvcgen タクティクは while ループにも対応しています。for ループと異なり while ループは停止することが明らかではないため、停止することを保証するために「ループごとに減少していくもの」を不変量とともに指定します。

import Lean

open Std.Do

set_option mvcgen.warning false

/-- 繰り返し自乗法で自然数の指数計算を行う -/
def binaryExpo (root n : Nat) : Nat := Id.run do
  let mut x := root
  let mut y := 1
  let mut e := n
  while 0 < e do
    if e % 2 = 1 then
      y := x * y
      e := e - 1
    else
      x := x * x
      e := e / 2
  return y

#guard binaryExpo 2 10 = 1024
#guard binaryExpo 23 0 = 1

private theorem pow_zero_for_grind {n a : Nat} (h : a = 0) : n ^ a = 1 := by
  simp_all

grind_pattern pow_zero_for_grind => n ^ a where
  guard a = 0

@[grind =]
private theorem pow_mul_self_halve_of_even (x e : Nat) (he : e % 2 = 0) :
    (x * x) ^ (e / 2) = x ^ e := calc
  _ = (x ^ 2) ^ (e / 2) := by grind
  _ = x ^ (2 * (e / 2)) := by grind [Nat.pow_mul]
  _ = x ^ e := by grind

@[grind! ·]
private theorem mul_pow_sub_one_of_odd (x e : Nat) (he : 0 < e) :
    x * x ^ (e - 1) = x ^ e := calc
  _ = x ^ (1 + (e - 1)) := by grind
  _ = x ^ e := by grind

theorem binaryExpo_spec (root n : Nat) : binaryExpo root n = root ^ n := by
  generalize h : binaryExpo root n = r
  apply Id.of_wp_run_eq h
  mvcgen invariants
  · -- while ループが停止することを保証するために
    -- e がループごとに減少していくと指定する
    fun ⟨x, y, e⟩ => ⟨e⟩
  · -- 不変条件を指定する。
    -- for ループとは異なり各ループの開始ごとに「次も続けるのか」の判定が来るので、
    -- どちらであるかに応じて2つの不変条件を記述する必要がある。
    post⟨ fun
    | .inl (x, y, e) => ⌜y * x ^ e = root ^ n⌝
    | .inr (_x, y, _e) => ⌜y = root ^ n⌝ ⟩
  with grind

使用例

農民の掛け算

農民の掛け算は、2で割る操作と2倍する操作と足し算だけで掛け算を行うアルゴリズムです。mvcgen を使って、このアルゴリズムが正しいことを証明する例を紹介します。4

import Lean

set_option mvcgen.warning false

open Std.Do

/-- 農民の掛け算。2で割る操作と2倍する操作と足し算だけで掛け算を行う -/
def peasantMul (x y : Nat) : Nat := Id.run do
  let mut curX := x
  let mut curY := y
  let mut prod := 0
  for _ in [0:x] do
    if curX % 2 = 1 then
      prod := prod + curY
    curX := curX / 2
    curY := curY * 2
    if curX = 0 then
      break
  return prod

theorem Nat.div_pow_add (x a b c : Nat) : x / c ^ (a + b) = (x / c ^ a) / c ^ b := by
  grind [Nat.div_div_eq_div_mul, Nat.pow_add]

-- 左辺をみかけたらインスタンス化する
-- ただし a, b, c が 0 や 1 になっているときは(無駄なので)インスタンス化しない
grind_pattern Nat.div_pow_add => x / c ^ (a + b) where
  a =/= 0
  b =/= 0
  c =/= 0
  c =/= 1

-- a / b を見かけたらインスタンス化する
grind_pattern Nat.div_add_mod' => a / b where
  not_value a
  is_value b
  b =/= 0
  b =/= 1

@[grind =]
theorem Nat.div_pow_self_two_eq_zero (n : Nat) : n / 2 ^ n = 0 := by
  have : n < 2 ^ n := Nat.lt_two_pow_self
  simp_all

example (x y : Nat) : peasantMul x y = x * y := by
  generalize h : peasantMul x y = s
  apply Id.of_wp_run_eq h

  mvcgen invariants
  · ⇓⟨cursor, curX, curY, prod⟩ =>
    ⌜curX = x / 2 ^ cursor.pos ∧ curX * curY + prod = x * y⌝
  with (simp at * <;> grind)

繰り返し自乗法による指数計算

繰り返し自乗法(binary exponentiation)は、x ^ n を計算する際に指数部を2冪の和に分解することで計算を高速化するアルゴリズムのことです。mvcgen を使って、このアルゴリズムが正しいことを証明する例を紹介します。5

import Lean

/-- 繰り返し自乗法で自然数の指数計算を行う -/
def binaryExpo (root n : Nat) : Nat := Id.run do
  let mut x := root
  let mut y := 1
  let mut e := n
  for _ in [0:n] do
    if e % 2 = 1 then
      y := x * y
      e := e - 1
    else
      x := x * x
      e := e / 2
    if e = 0 then
      break
  return y

open Std.Do

set_option mvcgen.warning false

@[grind =]
theorem Nat.Grind.pow_zero {n a : Nat} (h : a = 0) : n ^ a = 1 := by
  simp_all

@[grind =]
theorem Nat.pow_mul_self_halve_of_even (x e : Nat) (he : e % 2 = 0) :
    (x * x) ^ (e / 2) = x ^ e := calc
  _ = (x ^ 2) ^ (e / 2) := by grind
  _ = x ^ (2 * (e / 2)) := by grind [Nat.pow_mul]
  _ = x ^ e := by grind

@[grind! ·]
theorem Nat.mul_pow_sub_one_of_odd (x e : Nat) (he : e % 2 = 1) :
    x * x ^ (e - 1) = x ^ e := calc
  _ = x ^ (1 + (e - 1)) := by grind
  _ = x ^ e := by congr; grind

theorem binaryExpo_spec (root n : Nat) :
    binaryExpo root n = root ^ n := by
  generalize h : binaryExpo root n = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  -- 不変条件の指定。
  -- ローカル可変変数は定義順に拘束される。
  · ⇓⟨cursor, x, y, e⟩ => ⌜y * x ^ e = root ^ n ∧ e + cursor.pos ≤ n⌝
  with grind

先頭から足して和が0未満になる時点があるか判定する

整数のリストに対して、前から順に足していったときに和が0未満になる瞬間があるかチェックする関数について仕様を証明する例を紹介します。6

import Std.Tactic.Do

/-- `operations : List Int` を先頭から順に足していったときに、
どこかの時点で合計値が 0 未満になることがあるか判定する -/
def belowZero (operations : List Int) : Bool := Id.run do
  let mut balance := 0
  for op in operations do
    balance := balance + op
    if balance < 0 then
      return true
  return false

namespace List

variable {α : Type}

/-- リスト `l` の先頭からある部分までを取り出せば述語 `P : List α → Prop` が成り立つ -/
def HasPrefix (P : List α → Prop) (l : List α) : Prop := ∃ n, P (l.take n)

/-- `HasPrefix` の定義を言い換えるだけの定義 -/
theorem hasPrefix_iff {P : List α → Prop} {l : List α} :
    l.HasPrefix P ↔ ∃ n, P (l.take n) := by
  simp [HasPrefix]

@[simp, grind =]
theorem hasPrefix_nil {P : List α → Prop} : [].HasPrefix P ↔ P [] := by
  simp [hasPrefix_iff]

@[simp, grind =>]
theorem hasPrefix_of_nil {P : List α → Prop} {l : List α} (h : P []) : l.HasPrefix P := by
  exists 0

@[simp, grind =>]
theorem hasPrefix_of_all {P : List α → Prop} {l : List α} (h : P l) : l.HasPrefix P := by
  exists l.length
  simpa

@[grind =]
theorem hasPrefix_cons {P : List α → Prop} {a : α} {l : List α} :
    (a :: l).HasPrefix P ↔ P [] ∨ l.HasPrefix (fun l' => P (a :: l')) := by
  constructor
  · intro ⟨n, hn⟩
    by_cases nzero : n = 0
    · simp_all
    · right
      exists n - 1
      suffices a :: take (n - 1) l = take n (a :: l) from by
        simp_all
      grind [take_cons]
  · rintro (h | ⟨⟨n, hn⟩⟩)
    · grind
    · exists n + 1

@[grind =]
theorem hasPrefix_append {P : List α → Prop} {l l' : List α} :
    (l ++ l').HasPrefix P ↔ l.HasPrefix P ∨ l'.HasPrefix (fun l'' => P (l ++ l'')) := by
  induction l generalizing P with grind [= HasPrefix]

@[grind =]
theorem sum_append_singleton {α : Type} {l : List α} {x : α}
    [Add α] [Zero α] [@Std.Associative α (· + ·)] [@Std.LawfulIdentity α (· + ·) 0] :
    (l ++ [x]).sum = l.sum + x := by
  induction l with simp_all <;> grind

end List

open Std.Do

set_option mvcgen.warning false

theorem belowZero_iff {l : List Int} : belowZero l ↔ l.HasPrefix (fun l => l.sum < 0) := by
  generalize h : belowZero l = res
  apply Id.of_wp_run_eq h
  mvcgen invariants
  -- 早期終了がある場合の不変条件
  · Invariant.withEarlyReturnNewDo
    -- 早期終了しなかった場合、現在の接頭辞の和が `balance` に等しく、
    -- かつ「今までループで見てきた部分」は「和が0未満になる接頭辞」を持たない
    (onContinue := fun cursor (balance : Int) =>
      ⌜balance = cursor.prefix.sum ∧ ¬ cursor.prefix.HasPrefix (fun l => l.sum < 0)⌝)

    -- 早期終了した場合、返り値の`ret`は`true`であり、かつ和が0未満になる接頭辞がある
    (onReturn := fun ret (balance : Int) => ⌜ret = true ∧ l.HasPrefix (fun l => l.sum < 0)⌝)
  with grind

足してゼロになるペアがあるか判定する

整数のリストに対して、足してゼロになるペアが存在するか判定する関数について仕様を証明する例を紹介します。7

import Std.Data.HashSet.Lemmas
import Std.Tactic.Do

open Std Do

variable {α : Type}

/-- リストに、与えられた条件を満たすペアが存在する -/
def List.Any₂ (P : α → α → Prop) (l : List α) : Prop := ¬ l.Pairwise (fun x y => ¬P x y)

@[simp, grind ·]
theorem List.not_any₂_nil {P : α → α → Prop} : ¬List.Any₂ P [] := by
  simp [List.Any₂]

@[simp, grind =]
theorem List.any₂_cons {P : α → α → Prop} {x : α} {xs : List α} :
    List.Any₂ P (x :: xs) ↔ (∃ y ∈ xs, P x y) ∨ List.Any₂ P xs := by
  grind [List.Any₂, pairwise_cons]

@[simp, grind =]
theorem List.any₂_append {P : α → α → Prop} {xs ys : List α} :
    List.Any₂ P (xs ++ ys) ↔ List.Any₂ P xs ∨ List.Any₂ P ys ∨ (∃ x ∈ xs, ∃ y ∈ ys, P x y) := by
  grind [List.Any₂]

/-- 与えられた整数のリストの中に、足してゼロになるペアがあるか判定する -/
def pairsSumToZero (l : List Int) : Bool := Id.run do
  let mut seen : HashSet Int := ∅
  for x in l do
    if -x ∈ seen then
      return true
    seen := seen.insert x
  return false

set_option mvcgen.warning false

theorem pairsSumToZero_spec (l : List Int) :
    pairsSumToZero l = true ↔ l.Any₂ (fun a b => a + b = 0) := by
  generalize h : pairsSumToZero l = r
  apply Id.of_wp_run_eq h

  mvcgen invariants
  · Invariant.withEarlyReturnNewDo
    (onContinue := fun cursor seen =>
      ⌜(∀ x, x ∈ seen ↔ x ∈ cursor.prefix) ∧ ¬cursor.prefix.Any₂ (fun a b => a + b = 0)⌝)
    (onReturn := fun r b => ⌜r = true ∧ l.Any₂ (fun a b => a + b = 0)⌝)
  with simp_all <;> grind

ICan’tBelieveItCanSort

P.Y.Fung さんによる「Is this the simplest (and most surprising) sorting algorithm ever?」という論文で、非常に単純かつ動作が理解しにくいソートアルゴリズムが提案されました。ここでは、その論文での呼称にならってそのアルゴリズムを ICan’tBelieveItCanSort と呼ぶことにします。mvcgen を使って、このアルゴリズムが配列を正しくソートすることを証明することができます。8

import Std.Tactic.Do
import Batteries.Data.Array

open Std

variable {α : Type}
variable [LT α] [DecidableLT α]

def ICan'tBelieveItCanSort (arr : Array α) := Id.run do
  let n := arr.size
  let mut vec := arr.toVector
  for hi : i in [0:n] do
    for hj : j in [0:n] do
      if vec[i] < vec[j] then
        vec := vec.swap i j
  return vec.toArray

open Std.Do

set_option mvcgen.warning false

theorem ICan'tBelieveItCanSort_perm (arr : Array α) : Array.Perm (ICan'tBelieveItCanSort arr) arr := by
  generalize h : ICan'tBelieveItCanSort arr = x
  apply Id.of_wp_run_eq h
  mvcgen invariants
  · ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
  · ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
  with grind [Array.Perm.trans, Array.Perm.symm, Array.swap_perm]

@[grind =]
theorem Vector.toArray_extract_size {α : Type} {n : Nat} (v : Vector α n) :
    v.toArray.extract 0 n = v.toArray := by
  grind

@[grind! =>]
theorem List.Cursor.pos_le_length' (n : Nat) (xs : [0:n].toList.Cursor) : xs.pos ≤ n := by
  have : xs.prefix.length + xs.suffix.length = n := by
    simp [← List.length_append, xs.property]
  grind only

@[grind <=]
theorem Array.sorted_take_swap {α : Type} [LE α] [IsPreorder α] {arr : Array α}
  (s t : Nat) (hs : s < arr.size) (ht : t < arr.size)
  (h : Array.Pairwise (· ≤ ·) (arr.take s))
  (le1 : arr[s] ≤ arr[t])
  (le2 : ∀ (i : Nat) (_ : i < t), arr[i] ≤ arr[s])
    : Array.Pairwise (· ≤ ·) ((arr.swap s t).take s) := by
  simp [Array.pairwise_iff_getElem] at h ⊢
  grind

@[grind <=]
theorem Array.pairwise_take_succ {α : Type} {R : α → α → Prop} {arr : Array α}
  (k : Nat) (hk : k < arr.size)
  (h : Array.Pairwise R (arr.take k))
  (le : ∀ (i : Nat) (_ : i < arr.size), R arr[i] arr[k])
    : Array.Pairwise R (arr.take (k + 1)) := by
  simp [Array.pairwise_iff_getElem] at h ⊢
  grind

variable [LE α] [IsLinearOrder α] [LawfulOrderLT α]

theorem ICan'tBelieveItCanSort_sorted (arr : Array α) : ICan'tBelieveItCanSort arr |>.Pairwise (· ≤ ·) := by
  generalize h : ICan'tBelieveItCanSort arr = x
  apply Id.of_wp_run_eq h
  mvcgen invariants
  | inv1 => ⇓⟨cursor, vec⟩ =>
    -- 外側のforループの不変条件。
    -- 外側ループが要素`i ∈ [0:n]`を処理する反復の開始時に、
    -- `vec[0...i]`はソート済みである。
    let i := cursor.pos
    ⌜vec.take i |>.toArray.Pairwise (· ≤ ·)⌝
  | inv2 i _ _ _ _ => ⇓⟨cursor, vec⟩ =>
    -- 内側のforループの不変条件。
    -- 外側ループが要素`i ∈ [0:n]`を処理する反復の途中で、
    -- 内側ループが要素`j ∈ [0:n]`を処理する反復の開始時に、以下が成立。
    -- * `vec[0...i]`はソート済み
    -- * `vec[0...j]`のすべての要素は`vec[i]`以下
    let j := cursor.pos
    ⌜(vec.take i |>.toArray.Pairwise (· ≤ ·)) ∧
      ∀ k (_ : k < j), vec[k]'(by grind) ≤ vec[i]'(by grind)⌝
  with (simp at *; grind)

  1. このページの内容およびコード例は、公式のドキュメントである Verifying imperative programs using mvcgen を参考にしています。

  2. このコード例は Lean の公式 Zulip の new monadic program verification framework というトピックにおける Sebastian Graf さんの投稿を参考にしました。

  3. このコード例は、Lean のリポジトリの doLogicTests.lean の内容を参考にしました。

  4. このコード例は Lean の公式 Zulip の new monadic program verification framework というトピックにおける pandaman さんの投稿を参考にしました。

  5. このコード例は Lean の公式 Zulip の new monadic program verification framework というトピックにおける Aaron Liu さんの投稿を参考にしました。

  6. このコード例は human-eval-leanHumanEval3.lean のコードを参考にしています。

  7. このコード例は human-eval-leanHumanEval43.lean のコードを参考にしています。

  8. このコード例は Lean の公式 Zulip の new members > mvcgen doesn’t produce any invariant goals というトピックにおける Anthony Wang さんの投稿を参考にしています。

native_decide

native_decide は、式を評価したときに判ることを示すことができます。

たとえば、Lean では再帰関数 f を定義したらそれが停止することの証明を求められますが、それを sorry で回避したとしましょう。このとき f の具体的な値を評価することは #eval! を使えば可能ですが、その値をとることを rfl で証明することはできなくなります。

しかし、native_decide を使うと証明が可能です。

/-- Euclide のアルゴリズム -/
def gcd (m n : Nat) : Nat :=
  if m = 0 then
    n
  else
    gcd (n % m) m

  -- 停止性を証明しない
  decreasing_by sorry

-- 値を評価することはできる
#eval! gcd 42998431 120019

-- `rfl` では証明できない
-- これは停止性を証明していないため
#check_failure (by rfl : gcd 42998431 120019 = 1)

-- `native_decide` ならば証明できる
#check (by native_decide : gcd 42998431 120019 = 1)

補足すると、native_decide を使用するときにはコンパイラを信頼することになります。具体的には(定理ごとに個別の)追加の公理が使用されます。

theorem native_thm : Nat.gcd 42998431 120019 = 1 := by native_decide

/- info: 'native_thm' depends on axioms: [native_thm._native.native_decide.ax_1_1] -/
#print axioms native_thm

注意

native_decide を使うことは安全ではなく、native_decide を使うと簡単に False を示す(つまり矛盾を導く)ことができてしまいます。つまり、native_decide を使った証明は正式な証明ではありません。しかし、native_decide を使用した不正な証明は #print axioms コマンドを確認することで見破ることができます。

def one := 1

-- 間違った実装をわざと提供する
@[implemented_by one] def zero := 0

theorem zero_ne_eq_one : False := by
  have : zero ≠ one := by decide

  -- native_decide は implemented_by を真に受けるので、
  -- 実際には間違いだが示せてしまう
  have : zero = one := by native_decide

  contradiction

/- info: 'zero_ne_eq_one' depends on axioms: [zero_ne_eq_one._native.native_decide.ax_1_1] -/
#print axioms zero_ne_eq_one

nlinarith

nlinarith は 非線形(non-linear)な式も扱えるように linarith にいくつか前処理を追加したものです。

import Mathlib.Tactic.Linarith -- `linarith` や `nlinarith` を使うため

variable (a b : ℕ)

example (h : a ≤ b) : a ^ 2 ≤ b ^ 2 := by
  -- `linarith` では示すことができない
  fail_if_success linarith

  nlinarith

norm_cast

norm_cast は、型キャスト(ある型からある型への変換)を簡約するタクティクです。

詳細については論文 「Simplifying Casts and Coercions」 などを参照してください。

import Mathlib.Tactic

-- `x, m, n` は自然数とする
variable (x m n : ℕ)

example (left : (x : ℝ) < ↑m + ↑n) (right : ↑m + ↑n < (x : ℝ) + 1) : False := by
  -- `linarith` では示すことができない
  fail_if_success linarith

  -- `omega` でも示すことができない
  fail_if_success omega

  -- 仮定の `left` と `right` は実数上の不等式だが、
  -- 自然数上の不等式と解釈できるはずである。
  -- `norm_cast` はそれを実行してくれる。
  norm_cast at left right

  -- 仮定が自然数における不等式に変わった!
  guard_hyp left : x < m + n
  guard_hyp right : m + n < x + 1

  -- 後は `omega` で示せる
  omega

[norm_cast] 属性によるカスタマイズ

命題に [norm_cast] 属性を付与することにより、norm_cast タクティクでできることを増やすことができます。

/-- 自然数のペア -/
def IntBase := Nat × Nat

/-- 自然数のペアが「整数として等しい」という同値関係 -/
def IntBase.equiv : IntBase → IntBase → Prop :=
  fun (a₁, b₁) (a₂, b₂) => a₁ + b₂ = b₁ + a₂

/-- `IntBase` と同値関係 `IntBase.equiv` をペアにする -/
instance IntBase.sequiv : Setoid IntBase where
  r := IntBase.equiv
  iseqv := by
    constructor
    case refl =>
      intro ⟨x, y⟩
      dsimp [IntBase.equiv]
      ac_rfl
    case symm =>
      intro ⟨x, y⟩ ⟨x', y'⟩ h
      dsimp [IntBase.equiv] at *
      omega
    case trans =>
      intro ⟨x, y⟩ ⟨x', y'⟩ ⟨x'', y''⟩ hxy hyz
      dsimp [IntBase.equiv] at *
      omega

/-- 自前で定義した整数 -/
abbrev MyInt := Quotient IntBase.sequiv

/-- 自然数を `myInt` と解釈する関数 -/
def MyInt.ofNat (n : Nat) : MyInt := ⟦(n, 0)⟧

@[grind inj]
theorem MyInt.ofNat_inj : Function.Injective MyInt.ofNat := by
  intro n m h
  dsimp [MyInt.ofNat] at h
  have h' : IntBase.equiv (n, 0) (m, 0) := by
    apply Quotient.exact h
  suffices IntBase.equiv (n, 0) (m, 0) from by
    grind [= IntBase.equiv]
  assumption

/-- 型強制を定義 -/
instance : Coe Nat MyInt where
  coe := MyInt.ofNat

/-- 型キャストの簡約を行う補題。`ℕ` の項が `myInt` として等しいなら、元から等しい。 -/
theorem MyInt_eq {x y : ℕ} : (x : MyInt) = (y : MyInt) ↔ x = y := by
  grind

-- `[norm_cast]` 属性の制約として、
-- 登録する補題の中には型強制が含まれていなくてはいけない
-- たとえば `↑` など
/-
error: Invalid `norm_cast` lemma: At least one coe function must appear in the left-hand side
  MyInt.ofNat x = MyInt.ofNat y

Note: coe functions are registered using the `[coe]` attribute
-/
attribute [norm_cast] MyInt_eq

-- `[coe]` 属性を付与し、`MyInt.ofNat` を型キャストを行う関数として認識させる
attribute [coe] MyInt.ofNat

example {x y z : ℕ} (h : (x : MyInt) = (y : MyInt)) : x + z = y + z := by
  norm_cast at h

  -- `norm_cast` の効果が出ていない
  guard_hyp h : (x : MyInt) = (y : MyInt)
  simp [MyInt_eq] at h
  omega

-- `norm_cast` に使ってもらえるように登録する
attribute [norm_cast] MyInt_eq

example {x y z : Nat} (h : (x : MyInt) = (y : MyInt)) : x + z = y + z := by
  norm_cast at h

  -- `norm_cast` で簡約できるようになった!
  guard_hyp h : x = y

  omega

norm_num

norm_num は、数値リテラルを含む式を正規化するタクティクです。変数を含まない等式・不等式・整除関係等を示したいときに使います。

import Mathlib.Tactic.NormNum

example : 37 * 23 + 19 = 870 := by
  norm_num

example : ¬ (15 : Int) ^ 2 < 100 := by
  norm_num

example : (2 : ℚ) / 3 + 1 / 6 = 5 / 6 := by
  norm_num

norm_num は仮定に対しても使うことができます。 たとえば norm_num at h とすると、仮定 h の中の数値式を正規化します。

example {n : Nat} (h : 3 ∣ n + 4) : 3 ∣ n + 1 := by
  -- 最初は、h とゴールは違う式なので証明できない
  fail_if_success assumption

  -- `n + 4` は `n + 1 + 3` と同じなので、3 で割った余りは変わらない
  norm_num at h

  assumption

norm_numnorm_num [f] のように補題や定義を渡して使うこともできます。 渡した補題で式を展開してから、数値計算を正規化します。

def taxIncluded (price : Nat) (taxRate : Nat) : Nat :=
  price + price * taxRate / 100

example : taxIncluded 1200 10 = 1320 := by
  -- 最初は norm_num だけでは証明が終わらない
  fail_if_success solve |
    norm_num

  norm_num [taxIncluded]

nth_rw

rw はマッチした項をすべて置き換えてしまいます。特定の項だけを書き換えたいとき、nth_rw が使用できます。対象の式中に現れる順番を1始まりで指定することで、項を指定します。

import Mathlib.Tactic.NthRewrite
import Mathlib.Algebra.Group.Basic -- 群の定義を import する

-- `G` は群
variable (G : Type) [Group G]


example (a b : G) : a * b⁻¹ = 1 ↔ a = b := by

  -- `one_mul: 1 * b = b` を使って `b` を `1 * b` に書き換えたい
  try
    -- 仮に普通に `rw` しようとすると…
    rw [← one_mul b]

    -- 左側にある `b` まで一緒に置き換わってしまった!
    show a * (1 * b)⁻¹ = 1 ↔ a = 1 * b

    -- これは失敗
    fail

  -- `b` は2回出現するが、2番目だけ置き換える
  nth_rw 2 [← one_mul b]

  -- `mul_inv_eq_iff_eq_mul: a * b⁻¹ = c ↔ a = c * b` を使う
  exact mul_inv_eq_iff_eq_mul

obtain

obtain は、分解して何かを得るときに使います。たとえばローカルコンテキストの h : P ∧ Q から hP : PhQ : Q を取り出したり、h : ∃ e : X, P e から e : XhP : P e を取り出したりすることができます。

基本的には obtain <パターン> : 型または命題 := 証明項 という構文で使用します。型は省略することができます。

-- 論理積の分解
example (P Q : Prop) (h : P ∧ Q) : True := by
  obtain ⟨hP, hQ⟩ : P ∧ Q := h

  -- 分解することができた
  guard_hyp hP : P
  guard_hyp hQ : Q

  trivial

-- P は X 上の述語
variable (X : Type) (P : X → Prop)

-- 存在命題の分解
example (h : ∃ e : X, P e) : True := by
  obtain ⟨e, hP⟩ := h

  -- 分解することができた
  guard_hyp e : X
  guard_hyp hP : P e

  trivial

obtain に与えることのできるパターンは rcases に与えられるパターンと同様で、一般の帰納型を分解することができます。

inductive Sample where
  | foo (x y : Nat)
  | bar (z : String)

example (s : Sample) : True := by
  obtain ⟨x, y⟩ | ⟨z⟩ := s

  case foo =>
    -- `x`, `y` が取り出せている
    guard_hyp x : Nat
    guard_hyp y : Nat

    trivial
  case bar =>
    -- `z` が取り出せている
    guard_hyp z : String

    trivial

omega

omega タクティクは、「The omega test: a fast and practical integer programming algorithm for dependence analysis」に基づいて実装されたタクティクで、整数や自然数の線形制約を扱う能力を持ちます。

似たタクティクに linarith がありますが、linarith が有理数や実数を扱うのに長けているのに対して、omega は自然数や整数を扱うのに長けています。

import Mathlib.Tactic.Linarith
import Mathlib.Tactic.Ring

example (n m : Nat) : n * m = ((n + m) ^ 2 - n ^ 2 - m ^ 2 ) / 2 := by
  -- `(n + m) ^ 2` を展開する
  ring_nf

  -- `n * m` の部分は、一般の `x` に置き換えて証明してもよい
  generalize n * m = x

  -- つまり、以下を示せばよい
  show x = (x * 2 + n ^ 2 + m ^ 2 - n ^ 2 - m ^ 2) / 2

  -- これは `linarith` では示せない
  fail_if_success linarith

  -- `omega` では示せる
  omega

Lean では自然数同士の引き算は整数同士の引き算とは異なる結果になって厄介なのですが、omega はこの問題を上手く処理します。たとえば、以下は linarith では示すことができない線形な命題です。

section
  variable (a b : Nat)

  example (h : (a - b : Int) ≤ 0) : (a - b = 0) := by
    -- `linarith` では示すことができない
    fail_if_success linarith

    -- `omega` では示すことができる
    omega

  example (h : a > 0) : (a - 1) + 1 = a := by
    fail_if_success linarith
    omega

  example (h : a / 2 < b) : a < 2 * b := by
    fail_if_success linarith
    omega

  example : (a - b) - b = a - 2 * b := by
    fail_if_success linarith
    omega
end

omega は整数や自然数の整除関係を扱うこともできます。

example {a b c : ℤ} : 3 ∣ (100 * c + 10 * b + a) ↔ 3 ∣ (c + b + a) := by omega

example {a b c : ℕ} : 3 ∣ (100 * c + 10 * b + a) ↔ 3 ∣ (c + b + a) := by omega

補題を渡す構文

omega に明示的に補題を渡す構文を自作することができます。

open Lean Elab.Tactic in

/-- `omega [lem₁, lem₂, .. , lemₙ]` のように、カンマ区切りで補題を渡す構文 -/
elab "omega" "[" s:term,* "]" : tactic => do
  for term in s.getElems do
    evalTactic <| ← `(tactic| have := $term)
  evalTactic <| ← `(tactic| omega)

/-- テスト用の構造体 -/
structure Test where
  val : Nat
  property : 30 ≤ val

example (x y : Test) : 2 ≤ x.val + y.val := by
  -- 通常の omega では示すことができない
  fail_if_success omega

  -- 補題を渡す構文を使えば示せる
  omega [x.property, y.property]

order

order タクティクは、順序関係を扱うための専用のタクティクです。Preorder クラスや PartialOrder, LinearOrder クラスのインスタンスに対して使用することができます。

import Mathlib.Tactic.Order

section
  /- ## Preorder に対して使用する例 -/

  variable {α : Type} [Preorder α] (a b c d : α)

  example (h1 : a ≤ b) (h2 : b ≤ c) : a ≤ c := by
    order

  example (h1 : a ≤ b) (h2 : b ≤ c) (h3 : c ≤ d) : a ≤ d := by
    order

  example (h1 : a ≤ b) (h2 : ¬ (a < b)) : b ≤ a := by
    order
end

section
  /- ## PartialOrder に対して使用する例 -/

  variable {α : Type} [PartialOrder α] (a b c d : α)

  example (h1 : a ≤ b) (h2 : ¬ (a < b)) : a = b := by
    order

  example : ¬ (a < a) := by
    order
end

section
  /- ## LinearOrder に対して使用する例 -/

  variable {α : Type} [LinearOrder α] (a b c d : α)

  example (h1 : ¬ (a < b)) (h2 : ¬ (b < a)) : a = b := by
    order
end

plausible

plausible は、証明しようとしているゴールが間違っていないかランダムに例を生成してチェックし、反例を見つけるとエラーで警告するタクティクです。

import Plausible


variable (a b : Nat)

/- error: Found a counter-example! -/
example (h : 0 ≤ a + b) : 1 ≤ a := by
  plausible (config := { quiet := true })
  sorry

100 個のテストケースでテストして反例が見つからなかった場合、ギブアップして sorry と同様にはたらきます。

/- warning: Gave up after failing to generate values that fulfill the preconditions 100 times. -/
example (a : Nat) : a ≠ a → a ≤ 1 := by
  plausible

同様の機能を持つコマンドとして #test コマンドがあります。

引数

引数として、オプションを渡すことができます。

numInst

numInst を設定すると、ギブアップするまでに行うテストの回数を指定することができます。

/- warning: Gave up after failing to generate values that fulfill the preconditions 10 times. -/
example (a : Nat) : a ≠ a → a ≤ 1 := by
  plausible (config := { numInst := 10 })

カスタマイズ

組み込みではない、自作の型に対して plausible は準備なしには使うことができません。

open Plausible

/-- 自前で `Nat` を模倣して定義した型 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)
deriving Repr


example : ∀ (a b : MyNat), a = b := by
  -- `plausible` は最初使うことができない
  fail_if_success plausible

  sorry

ここで定義した MyNat のような自作の型に対して plausible を使用するためには、いくつかのステップがあります。

1. Shrinkable クラスのインスタンスにする

plausible が反例を見つけたときに、「より小さな反例を見つける」ことができるようにするために、Shrinkable 型クラスのインスタンスを実装する必要があります。

/-- 引数よりも小さい `MyNat` の項のリストを返す -/
def MyNat.shrink : MyNat → List MyNat
  | zero => []
  | succ n => n :: n.shrink

instance : Shrinkable MyNat where
  shrink := MyNat.shrink

2. Arbitrary クラスのインスタンスにする

plausible はテストするための要素をランダムに生成します。その方法を指定するのが Arbitrary 型クラスです。Arbitrary 型クラスは deriving ハンドラを持っているので、自動的にインスタンスを生成することができます。

deriving instance Arbitrary for MyNat

Arbitrary 型クラスのインスタンスであることは、#sample コマンドで確認できます。

#sample MyNat

3. 決定可能にする

plausible でテストが行えるようにするためには、テスト対象の主張が決定可能(Decidable 型クラスのインスタンス)である必要があります。

-- `MyNat` 同士の比較を決定可能にする
deriving instance DecidableEq for MyNat

以上の準備で plausible が使えるようになります。

/- error: Found a counter-example! -/
example : ∀ (a b : MyNat), a = b := by
  -- `plausible` が使えるようになった!
  plausible (config := { quiet := true})

positivity

positivity は、0 ≤ x0 < xx ≠ 0 という形のゴールを証明するためのタクティクです。

import Mathlib.Tactic

variable {a b : ℤ}

example (ha : 3 < a) : 0 ≤ a ^ 3 + a := by
  -- linarith では示せない
  fail_if_success linarith

  -- omega でも示せない
  fail_if_success omega

  positivity

example (ha : 1 < a) : 0 < |3 + a| := by
  -- linarith では示せない
  fail_if_success linarith

  -- omega でも示せない
  fail_if_success omega

  positivity

ゴールを構成する式がすべて数値的な下界を持つならば、positivity は再帰的に適用されます。

example : 0 ≤ max (-3) (b ^ 2) := by positivity

example : 0 < max (-3) ((1 + a) ^ 2 + 3) := by positivity

example : 0 < |2 + a| + |3 + b| + |1 + a ^ 2| := by positivity

push_cast

push_cast タクティクは、ゴールや仮定に含まれる型強制を「内側へ押し込む」はたらきをします。

import Mathlib.Tactic

example (m n : ℕ) (h : m ≥ n) : n + ((m - n) : ℕ) = (m : ℤ) := by
  -- 示すべき命題には `m - n : ℕ` を整数 `ℤ` に型キャストしたものが含まれている。
  guard_target =ₛ ↑n + ↑(m - n) = (m : ℤ)

  -- この状態では整数と自然数の演算が混ざっていてややこしいので、
  -- `ring` だけで示すことはできない。
  fail_if_success solve
  | ring

  -- 自然数の引き算は整数での引き算とは異なる定義がされているが、
  -- この場合は `h : m ≥ n` という仮定があるので、`m - n : ℤ` と一致する。
  -- この変換を `push_cast` タクティックで行うことができる。
  push_cast [h]

  -- 型キャストが「内側に押し込まれ」て、整数の話になった。
  show ↑n + (↑m - ↑n) = (m : ℤ)

  -- `ring` で示せるようになった!
  ring

norm_castzify などの型キャスト系のタクティクと併用されることもあります。

example (m n : ℕ) (h : n ≥ m) : (n + m) * (n - m) = n * n - m * m := by
  -- 整数にキャストする
  zify

  -- 自然数の引き算が含まれているので、`ring` では示せない
  fail_if_success solve
  | ring

  -- 仮定 `h : n ≥ m` を使って、`n - m` と `n * n - m * m` を整数にキャストする
  push_cast [h, show n * n ≥ m * m from by bound]

  -- `ring` で示せるようになった!
  ring

push

push タクティクは、演算子などを式の内側に押し込む(push)ためのタクティクです。

否定の押し込み

典型的な push の使用例は、ドモルガン則を使って否定(negation)を式の中に押し込むことです。 デフォルトの設定だと、次のように変形します。

  • ¬ (P ∧ Q)P → ¬ Q に変形。
  • ¬ ∀ x, P x∃ x, ¬ P x に変形。
import Mathlib.Tactic

example (P Q : Prop) (h : P → Q) : ¬ (P ∧ ¬ Q) := by
  -- ドモルガン則を適用して、`¬` を内側に押し込む
  push Not

  -- デフォルトの設定だと `P → Q` に変形される
  show P → Q

  exact h

以下の例は、「酔っぱらいのパラドクス」として有名な命題です。

-- `People` という空ではない集合がある
variable {People : Type} [Inhabited People]

-- 人が飲んでいるかどうかを表す述語を考える
variable (isDrinking : People → Prop)

/-- ある `x` という人が存在して、以下が成り立つ:
「`x` が飲んでいるのであれば、すべての人が飲んでいる」 -/
example : ∃ (x : People), isDrinking x → ∀ (y : People), isDrinking y := by
  -- 「すべての人が飲んでいる」かどうかで場合分けをする
  by_cases h : ∀ (y : People), isDrinking y

  case pos =>
    -- すべての人が飲んでいると仮定したので、
    -- 任意の誰かを `x` として取ればよい
    exists default
    intro _
    assumption

  case neg =>
    -- 「すべての人が飲んでいる」が偽の場合。
    -- このとき、飲んでいない人が存在する。
    push Not at h

    -- このとき、飲んでいない人を `x` として取れば前件が偽になるので条件を満たす
    replace ⟨x, h⟩ := h
    exists x
    simp_all

distrib オプション

option で +distrib を渡すと、¬ (p ∧ q)¬ p ∨ ¬ q に変形します。

example (P Q : Prop) (h : P → Q) : ¬ (P ∧ ¬ Q) := by
  -- ドモルガン則を適用して、`¬` を内側に押し込む
  push +distrib Not

  -- goal が論理和の形になる
  show ¬ P ∨ Q

  -- 場合分けで示す
  by_cases hP : P
  · right
    exact h hP
  · left
    assumption

他の使用例

以下は、push タクティクを使って所属関係 _ ∈ _ を式の内側に押し込む例です。 x ∈ A ∪ Bx ∈ A ∨ x ∈ B に変形することができます。

open Set in

example {α : Type} (x y : α) (s : Set α) :
    x ∈ ({y} : Set α) ∪ sᶜ ↔ x = y ∨ ¬ x ∈ s := by
  push _ ∈ _

  -- ∈ が内側に押し込まれる
  guard_target =ₛ x = y ∨ x ∉ s ↔ x = y ∨ x ∉ s

  simp

qify

qify は自然数や整数に関する命題を有理数に関する命題にキャストします。

import Mathlib.Tactic.GCongr
import Mathlib.Tactic.Linarith
import Mathlib.Tactic.Qify -- `qify` を使うために必要

example (x : Nat) (h : x ≥ 1) : 2 * x ≥ 2 := by
  -- 自然数から有理数にキャストする
  qify at h ⊢

  -- 次の状態になる
  guard_hyp h : 1 ≤ (x : ℚ)

  have : (2 : ℚ) ≤ 2 * x := calc
    _ = 2 * 1 := by simp
    _ ≤ 2 * (x : ℚ) := by gcongr
  assumption

用途

自然数や整数は体ではないので、体を想定したタクティクが使えないことがあります。 qify で有理数にキャストすることで、そのようなタクティクが使えるようになり、証明が楽になることがあります。

example (n m : Nat) (x : Rat) (hx : x > 0) (h : n * m = x) : m > 0 := by
  -- 最初は `nlinarith` が使えない
  fail_if_success nlinarith

  -- 有理数にキャストする
  qify

  -- `nlinarith` が使えるようになった!
  nlinarith

rcases

rcasescases をパターンに従って再帰的(recursive)に適用します。cases の上位互換という立ち位置です。

variable (P Q R : Prop)

example : P ∨ Q → (P → R) → (Q → R) → R := by
  intro h hPR hQR

  -- 場合分けをする
  rcases h with hP | hQ
  · apply hPR hP
  · apply hQR hQ

-- 論理積 ∧ に対しても使える
example : P ∧ Q → Q ∧ P := by
  -- `h: P ∧ Q` と仮定する
  intro h

  -- `h: P ∧ Q` を `hP: P` と `hQ: Q` に分解する
  rcases h with ⟨hP, hQ⟩

  -- `Q ∧ P` を証明する
  exact ⟨hQ, hP⟩

rcases は一般には ⟨x₁, x₂, ...⟩ | ⟨y₁, y₂, ...⟩ | ... という記法で帰納型の分解が可能です。

inductive Sample where
  | foo (x y : Nat)
  | bar (z : String)

example (s : Sample) : True := by
  rcases s with ⟨x, y⟩ | ⟨z⟩

  case foo =>
    -- `x`, `y` が取り出せている
    guard_hyp x : Nat
    guard_hyp y : Nat

    trivial

  case bar =>
    -- `z` が取り出せている
    guard_hyp z : String

    trivial

rfl パターン

rcases のパターンには rfl も使えます。等式の仮定 h : a = b に対して rcases h with rfl と書くと、ba に書き変わります。つまり、rw タクティクで書き換える手間が省けます。

example {a b c : Nat} (h : a = b) : a + c = b + c := by
  rcases h with rfl

  -- `b` が `a` に書き変わる
  guard_target =ₛ a + c = a + c

  rfl

example {a b : Nat} (h : a = b ∧ b = 3) : a = 3 := by
  rcases h with ⟨rfl, hb⟩

  -- `b` が `a` に書き変わる
  guard_target =ₛ a = 3

  exact hb

refine

refineexact タクティクと同様に機能しますが、メタ変数(? から始まる変数で、プレースホルダとして機能する)を受け入れて新しいゴールを生成するという違いがあります。

example {P Q : Prop} (hP : P) (hQ : Q) : P ∧ Q := by
  -- 穴埋め形式で証明を作ることができる
  refine ⟨?_, hQ⟩

  -- ゴールが `⊢ P` になる
  show P

  exact hP

用途

refine はかなり一般的なタクティクであり、様々な場面で使うことができます。

constructor の一般化として

refineconstructor の代わりに使うこともできます。実際 refineconstructor よりも柔軟で、⊢ P ∧ Q ∧ R のような形のゴールは constructor よりも簡潔に分割できます。

example {P Q R : Prop} (hP : P) (hQ : Q) (hR : R) : P ∧ Q ∧ R := by
  -- ゴールを3つに分割する
  refine ⟨?_, ?_, ?_⟩

  · exact hP
  · exact hQ
  · exact hR

constructor を使った場合、一度に2つのゴールに分割することしかできません。

example {P Q R : Prop} (hP : P) (hQ : Q) (hR : R) : P ∧ Q ∧ R := by
  constructor
  · exact hP
  · constructor
    · exact hQ
    · exact hR

apply の一般化として

h : P → Q という命題があって、ゴールが ⊢ Q であるとき refine h ?_apply h と同様に機能するので、refineapply を代用することができます。

example {P Q : Prop} (hPQ : P → Q) (hP : P) : Q := by
  refine hPQ ?_

  -- ゴールが `⊢ P` になる
  show P

  refine hP

自明なケースを示す

ゴールが ⊢ P ∧ Q であり、P が成り立つことが自明であった場合、いちいち ⊢ P⊢ Q の2つのサブゴールを作って示すのは面倒に思えます。こうしたとき、refine を使うとサブゴールを生成せずにゴールを単に ⊢ Q に変えることができます。

example {P Q : Prop} (hP : P) (hQ : ¬ P ∨ Q) : P ∧ Q := by
  -- `P` が成り立つのは自明なので、`Q` だけ示せばよい
  refine ⟨by assumption, ?_⟩

  -- ゴールが `⊢ Q` になる
  guard_target =ₛ Q

  simp_all

場合分けの他の枝で示したことを再利用する

メタ変数を使って、場合分けの他の枝で示したことを再利用することができます。

example {P Q : Prop} (h1 : P → Q) (h2 : P → Q → False) (hP : P) : False := by
  refine h2 ?a ?_ -- `?a` は `P` の証明を要求するメタ変数
  · exact hP
  · exact h1 ?a -- `?a` を `P` の証明の代替として使うことができる

example {P : Prop} (h : P → P → False) (hP : P) : False := by
  -- 本来2つの場合分けがあるが、証明が全く同じなので1つの証明で済ませることができる
  refine h ?a ?a

  · exact hP

rel

rel は、一般化された合同性を用いてゴールを分解し、命題を代入することで示すタクティクです。ゴールが関係(relation)について述べているときに使用できます。

典型的には不等式を代入して適用し、不等式を示します。

import Mathlib.Tactic

example {a b c d : Nat} (h1 : a ≤ b) (h2 : c ≤ d) : a + c ≤ b + d := by
  rel [h1, h2]

下記で示すように、ゴールが関係式でないときにはエラーになります。

/- error: rel failed, goal not a relation -/
example (x : Nat) : Nat := by rel [x]

なお、基本的に rel よりも gcongr の方が強いタクティクです。gcongrrel とは異なり、ローカルコンテキストから必要な命題を自動的に読み込むことができます。

example {a b c d : Nat} (h1 : a ≤ b) (h2 : c ≤ d) : a + c ≤ b + d := by
  -- 引数を与えないと通らない
  fail_if_success rel []

  -- `gcongr` は必要な仮定をローカルコンテキストから取得する
  gcongr

rename_i

rename_i は、 がついてアクセス不能になった変数に名前を付けることができます。

一般に、rename_i x₁ x₂ ... xₐ により、最初の a 個のアクセス不能変数に名前を付けることができます。

example {n : Nat} (h0 : n ≠ 0) : n ≥ 1 := by
  cases n

  case zero =>
    contradiction

  case succ _ =>
    -- `n✝` が infoview に表示される
    -- これは `succ` の引数に名前をつけなかったため

    -- 名前をつける
    rename_i n
    show Nat.succ n ≥ 1

    simp

必要な変数をアクセス不能にしてしまうこと自体を避けるべきであるため、rename_i を使うべき場面は多くありませんが rename_i の使用によりコードが簡単になることがあります。

variable {m : Nat}

/-- 自然数が0でも1でも2でもなければ3以上。
`case` をネストさせて示した場合。-/
example (h0 : m ≠ 0) (h1 : m ≠ 1) (h2 : m ≠ 2) : 3 ≤ m := by
  cases m with
  | zero => contradiction
  | succ m =>
    cases m with
    | zero => contradiction
    | succ m =>
      cases m with
      | zero => contradiction
      | succ m =>
        show 3 ≤ m + 3
        simp

/-- 自然数が0でも1でも2でもなければ3以上。
`rename_i` を使って示した場合。-/
example (h0 : m ≠ 0) (h1 : m ≠ 1) (h2 : m ≠ 2) : 3 ≤ m := by
  -- 仮定から `m = 0, 1, 2` のときは考えなくていい
  repeat
    cases m
    contradiction
    rename_i m

  -- 自然数 `m` に対して `3 ≤ m + 3` を示せばよい
  clear h0 h1 h2
  show 3 ≤ m + 3

  -- これは明らか
  simp

/-- 補足。おそらく最も簡潔な証明。 -/
example (h0 : m ≠ 0) (h1 : m ≠ 1) (h2 : m ≠ 2) : 3 ≤ m := by
  let .succ m := m
  let .succ m := m
  let .succ m := m

  show 3 ≤ m + 3
  simp

repeat

repeat は、指定したタクティクを失敗するまで繰り返します。

variable (P Q R S T : Prop)

example : (P → Q) → (Q → R) → (R → S) → (S → T) → P → T := by
  repeat intro
  repeat apply_assumption

replace

replacehave と同じく補題を入手するためのタクティクですが、have とは異なりローカルコンテキストにすでにある命題を置き換えることができます。

have を使った場合、ローカルコンテキストにすでに h : P がある状態で、再び h という名前で別の命題を示すと、古い方の h はアクセス不能になって が付いた状態になってしまいます。

replace であれば、古い方が新しい方に置き換えられ、 の付いた命題は出現しません。

import Mathlib.Tactic -- 大雑把に import する

/-- `5 * n` が偶数なら、`n` も偶数 -/
example : ∀ (n : ℤ), Even (5 * n) → Even n := by
  intro n hn

  -- `Even (5 * n)` という仮定を分解
  obtain ⟨k, hk⟩ := hn

  -- 以下がローカルコンテキストに追加される
  guard_hyp hk : 5 * n = k + k

  -- `k + k` という形が使いづらいので、`2 * k` に置き換える
  replace hk : 5 * n = 2 * k := by
    rw [hk]
    ring

  -- `hk` の内容が変化している
  guard_hyp hk : 5 * n = 2 * k

  -- 計算をする
  have : n = 2 * (k - 2 * n) := calc
    _ = 5 * n - 4 * n := by ring
    _ = 2 * k - 4 * n := by rw [hk]
    _ = 2 * (k - 2 * n) := by ring

  exists k - 2 * n
  nth_rw 1 [this]
  ring

revert

revert は、intro の逆の操作をするタクティクです。ゴールが ⊢ P x であるときに revert x を実行すると、ゴールが ∀ x, P に変わります。

example (x : Nat) : x = 1 := by
  revert x

  -- ゴールが `∀ x, x = 1` に変わる
  guard_target =ₛ ∀ x, x = 1

  sorry

また、ゴールが ⊢ P であって仮定 hq : Q があるときに revert hq を実行すると、ゴールが ⊢ Q → P に変わります。

example {P Q : Prop} (hq : Q) (h : Q → P) : P := by
  revert hq

  -- ゴールが `Q → P` に変わる
  guard_target =ₛ Q → P

  assumption

用途

revert には一見使い道がないようですが、識別子をマクロ内で導入するのを避けたいときに役に立ちます。

たとえば次のように自然数の商として整数を定義したとしましょう。

def PreInt := Nat × Nat

def PreInt.r (m n : PreInt) : Prop :=
  match m, n with
  | (m₁, m₂), (n₁, n₂) => m₁ + n₂ = m₂ + n₁

namespace PreInt
  /- ## PreInt.rは同値関係である -/

  /-- 反射律 -/
  theorem r.refl : ∀ (m : PreInt), r m m := by
    intro (m₁, m₂)
    dsimp [r]
    ac_rfl

  /-- 対称律 -/
  theorem r.symm : ∀ {m n : PreInt}, r m n → r n m := by
    intro (m₁, m₂) (n₁, n₂) h
    dsimp [r] at *
    omega

  /-- 推移律 -/
  theorem r.trans : ∀ {l m n : PreInt}, r l m → r m n → r l n := by
    intro (l₁, l₂) (m₁, m₂) (n₁, n₂) hlm hmn
    dsimp [r] at *
    omega

  /-- `PreInt.r`は同値関係 -/
  theorem r.equiv : Equivalence r :=
    { refl := r.refl, symm := r.symm, trans := r.trans }

end PreInt

/-- `PreInt`上の同値関係 -/
instance PreInt.sr : Setoid PreInt := ⟨r, r.equiv⟩

/-- 整数。`Nat × Nat`を同値関係で割ることで構成している。 -/
@[reducible] def MyInt := Quotient PreInt.sr

namespace MyInt
  /- ## MyInt を数値リテラルで表せるようにする -/

  /-- 同値類を表す記法 -/
  notation:arg (priority := low) "⟦" a "⟧" => Quotient.mk _ a

  def ofNat (n : Nat) : MyInt := ⟦(n, 0)⟧

  instance {n : Nat} : OfNat MyInt n where
    ofNat := MyInt.ofNat n

end MyInt

このとき、整数上の足し算を次のように構成することができます。

def PreInt.add (m n : PreInt) : MyInt :=
  match m, n with
  | (m₁, m₂), (n₁, n₂) => ⟦(m₁ + n₁, m₂ + n₂)⟧

/-- 整数の足し算 -/
def MyInt.add : MyInt → MyInt → MyInt := Quotient.lift₂ PreInt.add <| by
  intro (m₁, m₂) (n₁, n₂) (m'₁, m'₂) (n'₁, n'₂) rm rn
  dsimp [PreInt.add]
  apply Quotient.sound
  dsimp [(· ≈ ·), Setoid.r, PreInt.r, instHasEquivOfSetoid] at *
  omega

instance : Add MyInt where
  add := MyInt.add

ここで m + 0 = m0 + m = m の証明は次のように書けます。

example (m : MyInt) : m + 0 = m := by
  refine Quotient.inductionOn m ?_
  intro (a₁, a₂)
  apply Quot.sound
  dsimp [(· ≈ ·), Setoid.r, PreInt.r] at *
  omega

example (m : MyInt) : 0 + m = m := by
  refine Quotient.inductionOn m ?_
  intro (a₁, a₂)
  apply Quot.sound
  dsimp [(· ≈ ·), Setoid.r, PreInt.r] at *
  omega

2つの証明はまったく同じです!そこで証明を共通化すべく macro コマンドでマクロを定義してみると、素朴には上手くいきません。

section
  local macro "unfold_int" : tactic => `(tactic| focus
    refine Quotient.inductionOn m ?_
    intro (a₁, a₂)
    apply Quot.sound
    dsimp [(· ≈ ·), Setoid.r, PreInt.r] at *
    omega
  )

  example (m : MyInt) : m + 0 = m := by
    -- 証明が通らない!
    fail_if_success unfold_int
    sorry
end

なぜ上手くいかないかというと、マクロ内で参照しているmという識別子がLeanのマクロ衛生機構に引っかかって使用不可になってしまうからです。実際、hygiene オプションでマクロ衛生を無効にすると通るようになります。

section
  set_option hygiene false

  local macro "unfold_int" : tactic => `(tactic| focus
    refine Quotient.inductionOn m ?_
    intro (a₁, a₂)
    apply Quot.sound
    dsimp [(· ≈ ·), Setoid.r, PreInt.r] at *
    omega
  )

  example (m : MyInt) : m + 0 = m := by
    -- 証明が通るようになる
    unfold_int
end

しかしこの解決策は本質的な解決ではありません。仮にマクロ衛生機構に引っかからなかったとしても、証明すべき命題の変数が m ではなかったらどうするのでしょうか?このままではこのマクロは使い物になりません。

この問題を解決する方法の一つが、revert を使って自由変数を消してしまうことです。

section
  local macro "unfold_int" : tactic => `(tactic| focus
    intro m
    refine Quotient.inductionOn m ?_
    intro (a₁, a₂)
    apply Quot.sound
    dsimp [(· ≈ ·), Setoid.r, PreInt.r] at *
    omega
  )

  example (m : MyInt) : m + 0 = m := by
    revert m
    unfold_int

  example (m : MyInt) : 0 + m = m := by
    revert m
    unfold_int
end

rfl

rfl は、定義的に等しい(definitionally equal) もの同士が等しいことを示すタクティクです。

/-- 自前で定義した自然数 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

namespace MyNat
  /- ## 足し算の定義 -/

  /-- MyNat の足し算 -/
  def add : MyNat → MyNat → MyNat
    | a, .zero => a
    | a, .succ b => .succ (add a b)

  /-- MyNat.add を `+` で書けるようにする -/
  instance : Add MyNat where
    add := MyNat.add

end MyNat

/-- 1 に相当する項 -/
def MyNat.one : MyNat := .succ zero

/-- 2 に相当する項 -/
def MyNat.two : MyNat := .succ one

/-- 1 + 1 = 2 に相当する命題 -/
example : MyNat.one + MyNat.one = MyNat.two := by
  rfl

-- 定義的に等しい(definitionally equal)場合でないと証明できない
/-
error: Tactic `rfl` failed: The left-hand side
  MyNat.one
is not definitionally equal to the right-hand side
  MyNat.two

⊢ MyNat.one = MyNat.two
-/
example : MyNat.one = MyNat.two := by
  rfl

#reduce コマンドとの関係

ここで定義的に等しい(definitionally equal)というのは、おおむね #reduce コマンドに与えた結果の式が等しいというのと同じことです。

section
  /- ## rfl で等しいと示せるもの同士の #reduce の出力が等しいことの確認 -/

  -- フィールド記法を無効にする
  set_option pp.fieldNotation false

  /- info: MyNat.succ (MyNat.succ MyNat.zero) -/
  #reduce MyNat.one + MyNat.one

  /- info: MyNat.succ (MyNat.succ MyNat.zero) -/
  #reduce MyNat.two
end

ただし、rfl で示せる等式と、#reduce コマンドでの両辺の簡約結果が等しい等式とは完全には一致しません。以下のように、両辺の #reduce 結果は異なるものの rfl で示せる等式も存在します。

section
  variable {α : Type} {β : α → Type} (f : (x : α) → β x)

  /- info: f -/
  #reduce f

  /- info: fun x => f x -/
  #reduce (fun x => f x)

  example : f = (fun x => f x) := by
    rfl

end

カスタマイズ

rfl は、実は等式だけでなく一般の反射的(reflexive)な関係 R に対して関係式 R a b を示すために使用することができます。ここで二項関係 R : α → α → Prop が反射的であるとは、∀ a, R a a が成り立つことをいいます。

関係 R が反射的であることを rfl に利用させるには、R の反射性を示した定理に [refl] 属性を付与します。

-- `MyEq` という二項関係を定義する
inductive MyEq.{u} {α : Type u} : α → α → Prop where
  | refl (a : α) : MyEq a a

example (n : Nat) : MyEq n n := by
  -- `rfl` で示すことはできない。
  fail_if_success rfl

  apply MyEq.refl

-- `MyEq` が反射的であることを登録する
attribute [refl] MyEq.refl

-- `rfl` で示せるようになった!
example (n : Nat) : MyEq n n := by rfl

right

ゴールが ⊢ P ∨ Q であるとき、right はゴールを ⊢ Q に変えます。類似のタクティクに left があります。

variable (P Q : Prop)

example (hQ : Q) : P ∨ Q := by
  right

  -- ゴールが変わる
  guard_target =ₛ Q

  assumption

left, right を使わない方法

以下に示すように、Or.inla から a ∨ b を得る関数です。また Or.inrb から a ∨ b を得る関数です。これを使うことで leftright を使わずに証明できます。

#check (Or.inl : P → P ∨ Q)

#check (Or.inr : Q → P ∨ Q)

example (hP: P) : P ∨ Q := by
  apply Or.inl
  exact hP

ring_nf

ring_nf は、ring タクティクの変種で、等式を証明する代わりに式を標準形に変形します。

ring_nf は部分項の変形を行うことができます。

import Mathlib.Tactic.Ring

example {x y : Rat} (F : Rat → Rat) : F (x + y) + F (y + x) = 2 * F (x + y) := by
  ring_nf

example {x y : Int} (h : x + y - x = -2) : y = -2 := by
  ring_nf at h

  assumption

ring

ring は、可換環(commutative ring)上の等式を示すタクティクです。「Proving Equalities in a Commutative Ring Done Right in Coq」 という論文に基づいて実装されています。

ただし環とは、加法 + と乗法 * とマイナスを取る演算 - が定義されていて、それぞれが分配法則や結合法則などのいくつかの法則を満たしているものをいいます。その中でも乗法が可換、つまり a * b = b * a が成り立つような環を可換環と呼びます。可換環の典型的な例は、整数全体 や有理数全体 や、多項式環 ℚ[X] などです。

import Mathlib.Tactic.Ring -- `ring` のために必要

example (x y : ℤ) : (x - y) ^ 2 = x ^ 2 - 2 * x * y + y ^ 2 := by
  ring

example (x : ℤ) : x ^ 3 - 1 = (x ^ 2 + x + 1) * (x - 1) := by
  ring

また、可換な半環(semiring)上の等式も示すことができます。

ただし半環とは、加法 + と乗法 * が定義されていて、分配法則や結合法則などの法則が満たされているものをいいます。半環の典型的な例は、自然数全体 などです。

example (n m : Nat) : (n + m) ^ 2 = n ^ 2 + 2 * n * m + m ^ 2 := by
  ring

example (n m : Nat) : (n + m) ^ 3 = n ^ 3 + 3 * n * m * (n + m) + m ^ 3 := by
  ring

ring はローカルコンテキストの仮定は読まず、(半)環の公理だけを使います。

example (x y z : ℤ) (hz : z = x - y) : x * z = x ^ 2 - x * y := by
  -- `ring` はローカルコンテキストの仮定を読まないので、証明できない
  fail_if_success solve
  | ring

  -- `rw` などで、環の公理だけを使って示せる形にすれば証明できるようになる
  rw [hz]
  ring

ring_nf

ring が扱える対象には制約があり、たとえば自然数 は可換環にならないので、自然数の引き算に関する等式を証明しようとしても上手くいきません。ring_nf タクティクを提案されますが、ring_nf に変更すれば成功するとは限りません。

/-
info: Try this:
  [apply] ring_nf

  The `ring` tactic failed to close the goal. Use `ring_nf` to obtain a normal form.
-/
example {n : Nat} : n - n + n = n := by
  -- `ring_nf` を提案される
  ring

example {n : Nat} : n - n + n = n := by
  -- 提案通りに `ring_nf` を使っても証明できない
  fail_if_success solve
  | ring_nf

  simp

実際、ring タクティクは失敗すると常に ring_nf を提案するようなマクロとして定義されています。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

-- マクロ展開の中に `try_this ring_nf` が含まれる
/-
info: first
| ring1
-/
#expand (ring)

カスタマイズ

新たに型 R : Type に対して ring タクティクが利用できるようにするためには、RCommSemiring または CommRing のインスタンスにします。

/-- 組み込みの自然数のラッパー -/
@[ext] structure MyNat : Type where
  val : Nat

namespace MyNat
  /- ## MyNat に掛け算と足し算を定義する -/

  /-- `MyNat` に掛け算を定義 -/
  instance : Mul MyNat where
    mul x y := ⟨x.val * y.val⟩

  /-- `MyNat` に足し算を定義 -/
  instance : Add MyNat where
    add x y := ⟨x.val + y.val⟩

end MyNat

example (m n : MyNat) : n * (n + m) = n * n + n * m := by
  -- `ring` は `MyNat` に対しては使えない
  fail_if_success solve
  | ring

  sorry

namespace MyNat
  /- ## MyNat が半環であることを証明するための準備 -/

  /-- `MyNat` として等しいことと、`val` を取って自然数として等しいことは同値 -/
  @[simp]
  theorem translate (m n : MyNat) : m = n ↔ m.val = n.val := by
    constructor <;> intro h
    · rw [h]
    · ext
      assumption

  /-- `MyNat` の和を自然数の和に翻訳する -/
  @[simp] theorem add_val (m n : MyNat) : (m + n).val = m.val + n.val := by rfl

  /-- `MyNat` の積を自然数の積に翻訳する -/
  @[simp] theorem mul_val (m n : MyNat) : (m * n).val = m.val * n.val := by rfl

  /-- `MyNat` についての等式を自然数に翻訳して示す -/
  macro "translate" : tactic => `(tactic| with_reducible
    intros
    simp
    try ring
  )
end MyNat

namespace MyNat
  /- ## MyNat を AddCommMonoid のインスタンスにする -/

  instance : Zero MyNat where
    zero := ⟨0⟩

  /-- `MyNat` のゼロを自然数のゼロに翻訳する -/
  @[simp] theorem zero_val : (0 : MyNat).val = 0 := by rfl

  instance : AddCommMonoid MyNat where
    zero_add := by translate
    add_zero := by translate
    add_assoc := by translate
    add_comm := by translate
    nsmul := nsmulRec

end MyNat

namespace MyNat
  /- ## MyNat を CommSemiring のインスタンスにする -/

  instance : One MyNat where
    one := ⟨1⟩

  /-- `MyNat` の1を自然数の1に翻訳する -/
  @[simp] theorem one_val : (1 : MyNat).val = 1 := by rfl

  /-- `MyNat` は可換な半環(semiring)である -/
  instance : CommSemiring MyNat where
    left_distrib := by translate
    right_distrib := by translate
    zero_mul := by translate
    mul_zero := by translate
    one_mul := by translate
    mul_one := by translate
    mul_assoc := by translate
    mul_comm := by translate
end MyNat

example (m n : MyNat) : n * (n + m) = n * n + n * m := by
  -- `ring` が使えるようになった!
  ring

rw

rw は rewrite(書き換え)を行うタクティクです。等式や同値関係をもとに書き換えを行います。

hab : a = bhPQ : P ↔ Q がローカルコンテキストにあるとき、rw は以下のような動作をします。

  • rw [hab] でゴールの中の a をすべて b に置き換える。
  • rw [hPQ] でゴールの中の P をすべて Q に置き換える。

複数の仮定 h1, h2, ... について続けて置き換えを行いたいときは、rw [h1, h2, ...] のようにします。

variable {a b c d e f : Nat}

-- 等式による置き換えの例
example (ha : a + 0 = a) (hb : b * b = 0) (hc : c + c = 0)
    : a + 0 + b * b = a + (c + c) := by
  -- `a + 0` を `a` に置き換える
  rw [ha]

  -- 複数ルールについて書き換え
  rw [hb, hc]

-- 同値関係に基づいて書き換えを行う例
example (P Q : Prop) (h : P ↔ P ∧ Q) : P → Q := by
  intro (hP : P)
  rw [h] at hP

  -- `P ↔ P ∧ Q` で書き換えを行ったので、
  -- `P` が `P ∧ Q` に置き換わった
  guard_hyp hP : P ∧ Q

  exact hP.right

利用可能な構文

右辺を左辺に書き換える

順番は重要で、ba に置き換えたいときなどは rw [← hab] のように をつけます。

example (h : a = b) (hb : a + 3 = 0) : b + 3 = 0 := by
  -- `rw [h]` だと `a` を `b` に置き換えるという意味になり、失敗する
  fail_if_success rw [h]

  -- `←` をつけて逆向きにすれば通る
  rw [← h]

  assumption

書き換え場所の指定

rwat 構文 を受け入れます。ゴールではなく、ローカルコンテキストにある h : P を書き換えたいときには at をつけて rw [hPQ] at h とします。すべての箇所で置き換えたいときは rw [hPQ] at * とします。

また、ゴールとローカルコンテキストの仮定 h に対して同時に書き換えたいときは 記号を使って rw [hPQ] at h ⊢ のようにします。

example (h : a + 0 = a) (_h1 : b + (a + 0) = b + a) (_h2 : a + (a + 0) = a)
    : a + 0 = 0 + a := by
  -- ローカルコンテキストとゴールのすべてに対して書き換えを行う
  rw [h] at *

  simp

example (h : a * b = c * d) (h' : e = f + 0) : a * (b * e + 0) = c * (d * f) := by
  -- ゴールとローカルコンテキストの両方に対して書き換えを行う
  rw [Nat.add_zero] at h' ⊢

  rw [h']

  -- 結合法則を使う
  rw [← Nat.mul_assoc, h]

  -- 結合法則を使う
  ac_rfl

rw の制約

変数束縛

rw は、変数束縛の下では使うことができません。代わりに simp タクティクなどを使用してください。

example (f g : Nat → Nat) (h : ∀ a, f (a + 0) = g a) : f = g := by
  ext x

  -- rw は失敗する
  fail_if_success rw [Nat.add_zero] at h

  -- simp は成功する
  simp only [Nat.add_zero] at h

  exact h x

nth_rw

rw は何も設定しないとマッチした項をすべて置き換えてしまいます。

example {P Q : Prop} (h : P ↔ Q) (hq : Q) : P ∧ P ∧ P ∧ P := by
  -- 単に `rw` するとすべて置き換わる
  rw [h]

  guard_target =ₛ Q ∧ Q ∧ Q ∧ Q

  simp [hq]

このとき config を適切に渡すことで、指定した n 番目の出現だけを書き換えることができます。

example {P Q : Prop} (h : P ↔ Q) (hq : Q) : P ∧ P ∧ P ∧ P := by
  rw (config := {occs := .pos [2]}) [h]

  -- 2番目の出現だけが置き換わる
  guard_target =ₛ P ∧ Q ∧ P ∧ P

  simp [h, hq]

config を渡さなくても、nth_rw が使用できればそれによって同じことができます。

ローカル変数の展開

rw はローカル変数の展開を行うことができません。代わりに dsimp タクティクなどを使用してください。

/-- 5未満の自然数が存在する -/
example : ∃ x : Nat, x < 5 := by
  let n := 2
  have h : n < 5 := by
    -- rw はローカル変数の展開は行わない
    fail_if_success rw [n]

    -- dsimp で展開できる
    dsimp [n]

    -- あとは 2 < 5 を示せばよいだけ
    guard_target =ₛ 2 < 5
    decide

  exact ⟨n, h⟩

一般の同値関係の書き換えはできない

rw で書き換えることができるのは等式と論理的な同値関係だけです。一般の同値関係による書き換えはできません。

def add_equiv (c₁ c₂ : Nat) : Prop :=
  ∀ n : Nat, c₁ + n = c₂ + n

/-- `Nat`上の同値関係 -/
instance add_setoid : Setoid Nat where
  r := add_equiv
  iseqv := by
    constructor
    · grind [add_equiv]
    · grind [add_equiv]
    · intro x y z h₁ h₂ n
      have := h₁ n
      have := h₂ n
      rw [h₁, h₂]

theorem add_equiv_trans {c₁ c₂ c₃ : Nat} (h₁ : c₁ ≈ c₂) (h₂ : c₂ ≈ c₃) : (c₁ ≈ c₃) := by
  -- `rw`が成功しない
  fail_if_success rw [h₁]
  sorry

rewrite

rewrite というタクティクもあります。rw とよく似ていて、違いは rw が書き換え後に自動的に rfl を実行するのに対して、rewrite は行わないということです。rewrite はユーザにとっては rw の下位互換なので、あまり使うことはないかもしれません。

example (h : a = b) : a = b := by
  -- `rw` を使用した場合は一発で証明が終わる
  rw [h]

example (h : a = b) : a = b := by
  rewrite [h]

  -- ゴールを `b = b` にするところまでしかやってくれない
  show b = b

  rfl

says

exact?apply? は証明を書いている過程で使用することを想定したタクティクです。Try this: という提案をクリックして採用したら、exact?apply? は提案内容で上書きされて、最終的な証明には残りません。

では、証明のある部分が apply? などにより提案された内容であることを明示したい場合はどうしたら良いでしょうか?says タクティクはまさにその問題を解決するタクティクです。提案タクティクを残しつつ、実際には実行されないようにします。

より詳しく書くと、検索タクティク X があり、その提案内容が Try this: Y だったとき、X says とすると saysTry this: Y の代わりに Try this: X says Y という提案を infoview 上で出します。 それをクリックすると、X says の内容が X says Y で置換されます。 そして、X says Y が実行されるときには X は飛ばされます。

import Mathlib.Tactic

-- `says` のチェックを有効にする
set_option says.verify true

variable (P Q R S : Prop)

-- `exact?` に対して使用する例
example (hPQ : P → Q) (hQR : Q → R) (hRS : R → S) (hP : P) : S := by
  -- `exact?` は実行されない
  exact? says
    exact (hRS ∘ hQR) (hPQ hP)

simpaesop のような証明自動化系のタクティクに対して、動作を軽量化しながらも証明の読みやすさを保つという目的でも使用できます。たとえば aesop? says ... と書かれていたら、その後のブロックでどんな複雑なことが書かれていようと、単に aesop の発見した証明内容を丁寧に書いているだけだとわかるわけです。

-- 以下 `X` `Y` `Z`を集合とする
variable {X Y Z : Type}

open Function

/-- 合成 `g ∘ f` が単射なら、`f` も単射 -/
example {f : X → Y} {g : Y → Z} (hgfinj : Injective (g ∘ f)) : Injective f := by
  rw [Injective]
  aesop? says
    intro a₁ a₂ a
    apply hgfinj
    simp_all only [comp_apply]

オプション

says.no_verify_in_CI : Bool : true にすると、CI 環境で X says YY の部分が実際に提案されている内容と一致するかのチェックが走らなくなります。

-- CI 環境でのチェックを無効にする
set_option says.no_verify_in_CI true

says.verify : Bool : true にすると、X says YY のところに、実際には提案されていないタクティクを入れたときにエラーになります。

-- チェックを無効にする
set_option says.verify false

example (h : P → Q) (p : P) : Q := by
  -- 提案されない内容を渡してもエラーにならない
  exact? says
    try contradiction
    exact h p

-- チェックを有効にする
set_option says.verify true

-- チェックを有効にするとエラーになる
/-
error: Tactic `exact?` produced `exact Nat.add_eq_left.mpr rfl`,
but was expecting it to produce `  try contradiction
  exact Nat.add_eq_left.mpr rfl`!

You can reproduce this error locally using `set_option says.verify true`.
-/
example (n : Nat) : n + 0 = n := by
  exact? says
    try contradiction
    exact Nat.add_eq_left.mpr rfl

set

set は、ローカル変数を導入するためのタクティクです。

Lean ではローカル変数の定義に let をよく使いますが、let だと「ゴールやローカルコンテキストにある命題を導入した定義に基づいて書き換えてくれない」という不満があります。たとえば let y := f x としたとき、既存の f x を使用した部分が y に書き変わってはくれませんし、y = f x という命題の証明にアクセスでないので書き換えることもままなりません。

set タクティクはこうした不満に対応します。

import Mathlib.Tactic.Set -- `set` のために必要

variable (X : Type) (f : Nat → Nat)

example (x : Nat) (h : f x = x) : f (f x) = f x := by
  -- `let` を使用した場合
  try
    let y := f x

    -- ゴールは `⊢ f (f x) = f x` のままで、
    -- 導入した `y` を用いて書き換えてくれない
    show f (f x) = f x

    fail

  set y := f x with yh

  -- ゴールが書き換わる
  show f y = y

  -- 仮定も書き変わる
  guard_hyp h : y = x

  -- `y = f x` であるという事実に名前も付いている
  guard_hyp yh : y = f x

  rw [h] at *
  apply yh.symm

show_term

show_term は、タクティクで作られた項を明示的に表示します。

タクティクは、表面的には「証明を行うためのツール」として振る舞いますが、実際には「示すべき命題に対して、その命題を型に持つような証明項を生成するプログラム」です。裏で生成されている項は普段ユーザからは隠されていますが、show_term はそれを見えるようにします。

import Lean

example (n : Nat) : n + 0 = n := by
  rfl

-- `show_term` で `rfl` が生成している具体的な項を表示
/-
info: Try this:
  [apply] Eq.refl (n + 0)
-/
example (n : Nat) : n + 0 = n := show_term by
  rfl

by? 構文

byby? に変えることでも、show_term を呼び出すことができます。

/-
info: Try this:
  [apply] Eq.refl (n + 0)
-/
example (n : Nat) : n + 0 = n := by?
  rfl

実際、by?show_term に展開されるマクロです。

section
  open Lean

  /-- `#expand` の入力に渡すための構文カテゴリ -/
  syntax macro_stx := command <|> tactic <|> term

  /-- マクロを展開するコマンド -/
  elab "#expand " "(" stx:macro_stx ")" : command => do
    let t : Syntax :=
      match stx.raw with
      | .node _ _ #[t] => t
      | _ => stx.raw
    match ← Elab.liftMacroM <| Macro.expandMacro? t with
    | none => logInfo m!"Not a macro"
    | some t => logInfo m!"{t}"
end

/- info: show_term by rfl -/
#expand (by? rfl)

show

show はこれから示すことを宣言するタクティクです。

show は、ゴールが特定の命題に等しいかどうかチェックします。show P と書くと、ゴールの中に ⊢ P が存在しないときにエラーになり、存在すればそれをメインのゴールにします。たとえば、証明中にこれから示すべきことを明示し、コードを読みやすくする目的で使うことができます。

variable (P Q : Prop)

example (hP : P) (hQ : Q) : P ∧ Q := by
  constructor
  · show P
    exact hP
  · show Q
    exact hQ

ゴールを定義上等しい命題に変形するために使用することもできます。

def factorial : Nat → Nat
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

example : factorial 3 = 6 := by
  -- 定義上等しいのでゴールを変形できる
  show 3 * factorial 2 = 6

  -- 定義上等しいのでゴールを変形できる
  show 3 * (2 * factorial 1) = 6

  rfl

show タクティクとよく似た構文を持つものに、show .. from 構文があります。

simp_all

simp_all タクティクは、simp タクティクの派生で、仮定とゴールに対してこれ以上適用できなくなるまで simp を適用します。

simp [*] at * との違い

simp [*] at * と似ていますが、simp_all は単純化された仮定を再び単純化に使うという、再帰的な挙動をします。

example (P : Nat → Bool)
    (h1 : P (if 0 + 0 = 0 then 1 else 2))
    (h2 : P (if P 1 then 0 else 1) ) : P 0 := by
  -- `simp [*] at *` では示せない
  fail_if_success solve
  | simp [*] at *

  -- 複数回 `simp [*] at *` を繰り返す必要がある
  simp [*] at *
  simp [*] at *
  simp [*] at *

example (P : Nat → Bool)
    (h1 : P (if 0 + 0 = 0 then 1 else 2))
    (h2 : P (if P 1 then 0 else 1) ) : P 0 := by
  -- 一発で終わる。
  -- h1 を単純化した後で、h2 を「単純化後の h1」を使って単純化し、
  -- さらにゴールを単純化するという挙動をする。
  simp_all

注意点

なお、これは simp [*] at * と同じですが simp_all はローカルコンテキストにある命題を使って単純化を行おうとするため、ローカルコンテキストにある命題によってはエラーになることがあります。1

example (_h : 1 + 1 = 2) : True := by
  have : 1 = 1 + 1 - 1 := by simp

  -- `simp_all` では示せない
  -- 仮定にある `1` を `1 + 1 - 1` に置き換えて無限ループになっているようだ
  fail_if_success simp_all

example (_h : 1 + 1 = 2) : True := by
  have : 1 = 1 + 1 - 1 := by simp

  -- `simp [*] at *` でも示せない
  fail_if_success simp [*] at *

example (_h : 1 + 1 = 2) : True := by
  -- 左辺と右辺を逆にしてみると
  have : 1 + 1 - 1 = 1 := by simp

  -- `simp_all` で示せるようになる
  simp_all

  1. ここで挙げているコード例は、Lean の公式 Zulip の aesop with a “bad simp hypothesis” in the context というスレッドで Frédéric Dupuis さんが挙げたコード例を参考にしています。

simp

simp は、ターゲットを等式や同値性に基づいて自動で単純化(simplify)するタクティクです。

基本的には、A = B という形の補題を登録しておくと、AB に自動で単純化します。左辺を右辺に書き換え、右辺を左辺に戻すことはないため、右辺は左辺よりも「単純」であることが求められます。等価性に基づいて書き換えるので rw タクティクと似ていますが、rw と異なり明示的に書き換えルールを引数として与えなくても自動で補ってくれます。

[simp] 属性を付けることにより単純化に使ってほしい命題を登録することができます。

/-- 標準の`Nat`を真似て自作した型 -/
inductive MyNat where
  | zero
  | succ (n : MyNat)

/-- `MyNat`上の足し算 -/
def MyNat.add (m n : MyNat) : MyNat :=
  match n with
  | .zero => m
  | .succ n => succ (add m n)

instance : Add MyNat where
  add := MyNat.add

instance : Zero MyNat where
  zero := MyNat.zero

@[simp]
theorem MyNat.add_zero (n : MyNat) : n + 0 = n := by
  rfl

@[simp]
theorem MyNat.zero_add (n : MyNat) : 0 + n = n := by
  induction n with
  | zero => rfl
  | succ n ih =>
    rw [show 0 + n.succ = (0 + n).succ from by rfl]
    rw [ih]

example (n : MyNat) : (0 + n + 0) + 0 = n := by
  -- 単に`simp`と書くだけで自動的に登録した補題によって書き換えが行われる
  simp

同値性を扱う

等式 A = B の形をした補題だけでなく、同値性 A ↔ B の形をした補題も simp 補題として登録し、単純化に使用することができます。等式と同様に、AB に単純化するのに使用されます。

/-- 何かの命題 -/
opaque Foo : Prop

@[simp]
axiom foo_iff_true : Foo ↔ True

example : Foo := by
  -- `Foo` を `True` に単純化できる
  simp

simp の基本的な構文

[h₁, h₂, …] で引数を渡す

既知の h : P という命題を使って単純化させたいときは、明示的に simp [h] と指定することで可能です。複数個指定することもできて、その場合はカンマ区切りで simp [h₁, h₂, …] とします。

/-- 0 っぽい何か -/
opaque zero : Nat

/-- 1っぽい何か -/
opaque one : Nat

/-- `zero` に右から1を足すと `one` に等しい -/
axiom add_zero_one_eq_one : zero + 1 = one

example : zero + 1 = one := by
  -- 単に`simp`としても何も起こらない
  -- これは`simp`補題として登録していないから
  fail_if_success simp

  -- 明示的に引数として書き換えルールを与えれば証明が通る
  simp [add_zero_one_eq_one]

/-- `zero` 同士を足すと `zero` に等しい -/
axiom add_zero_zero_eq_zero : zero + zero = zero

example : (zero + zero) + 1 = one := by
  -- 複数の補題を指定することもできる
  simp [add_zero_zero_eq_zero, add_zero_one_eq_one]

simp only

simp only [h₁, h₂, …] と書くと、登録された補題は無視して、引数として与えた補題だけを使って単純化を行います。

/-- 0 っぽい何か -/
opaque zero : Nat

/-- 1っぽい何か -/
opaque one : Nat

/-- `zero` に右から1を足すと `one` に等しい -/
@[simp] axiom add_zero_one_eq_one : zero + 1 = one

/-- `zero` 同士を足すと `zero` に等しい -/
@[simp] axiom add_zero_zero_eq_zero : zero + zero = zero

example : (zero + zero) + 1 = one := by
  -- 1つだけ使用して単純化を行ってみる
  -- このとき `add_zero_one_eq_one` は登録済みだが無視される
  simp only [add_zero_zero_eq_zero]

  -- まだゴールが残っている
  guard_target =ₛ zero + 1 = one

  simp

at 構文

simpat 構文 を受け入れます。simp は何も指定しなければゴールを単純化しますが、ローカルコンテキストにある h : P を単純化させたければ simp at h と指定することで可能です。ゴールと h の両方を単純化したいときは simp at h ⊢ とします。

example {n m : Nat} (h : n + 0 + 0 = m) : n = m + (0 * n) := by
  simp only [Nat.add_zero, Nat.zero_mul] at h ⊢
  assumption

ローカルコンテキストとゴールをまとめて全部単純化したい場合は simp at * とします。

等式・同値性以外の補題を扱う

等式や同値性以外の補題を登録した場合、等式・同値性への変換が自動的に行われたうえで登録されます。

たとえば下記の例のように、1 ≠ 0 という命題を simp 補題として登録した場合、(1 = 0) = False という定理が自動生成されそれに基づいて単純化が行われるようになります。

import Mathlib.Util.WhatsNew

/-- 0と1は等しくない -/
theorem Nat.one_neq_zero : 1 ≠ 0 := by
  simp

-- `[simp]`属性を追加したことによって、`(1 = 0) = False`という命題が追加されている
/-
info: theorem Nat.one_neq_zero._simp_1 : (1 = 0) = False :=
eq_false Nat.one_neq_zero

-- Lean.Meta.simpExtension extension: 1 new entries
-/
#whats_new in attribute [simp] Nat.one_neq_zero

-- `simp`による書き換えの過程を表示する
set_option trace.Meta.Tactic.simp.rewrite true

/-
trace: [Meta.Tactic.simp.rewrite] Nat.succ_ne_self:1000:
      1 = 0
    ==>
      False
-/
example (h : 1 = 0) : False := by
  simp at h

一般に等式の形をしているとは限らない命題 P : Prop に対して P という命題を simp 補題として登録すると P = True という定理が、¬ P という命題を登録すると P = False という定理がそれぞれ自動生成されて単純化に使用されます。

import Mathlib.Util.WhatsNew

/-- 偶数を表す帰納的述語 -/
inductive MyEven : Nat → Prop where
  | zero : MyEven 0
  | step (n : Nat) : MyEven n → MyEven (n + 2)

theorem MyEven_two : MyEven 2 := by
  apply MyEven.step
  apply MyEven.zero

-- `MyEven 2 = True` という書き換えルールが自動生成されている
/-
info: theorem MyEven_two._simp_1 : MyEven 2 = True :=
eq_true MyEven_two

-- Lean.Meta.simpExtension extension: 1 new entries
-/
#whats_new in attribute [simp] MyEven_two

-- `simp`による書き換えの過程を表示する
set_option trace.Meta.Tactic.simp.rewrite true

/-
trace: [Meta.Tactic.simp.rewrite] MyEven_two:1000:
      MyEven 2
    ==>
      True
-/
example : MyEven 2 := by
  simp

simp 補題のループ

なお、[simp] 属性を付与した命題は「左辺を右辺に」単純化するルールとして登録されます。 左辺と右辺を間違えて登録すると、無限ループになって simp の動作が破壊されることがあります。[simp] 属性は慎重に登録してください。

section
  -- 良くない simp 補題を検知するリンターを無効にする
  set_option warning.simp.varHead false

  -- 何もしていなければ simp で通る
  example (n m : Nat) : (n + 0) * m = n * m := by simp

  -- 良くない simp 補題の例
  -- 「左辺を右辺に」単純化するため、かえって複雑になってしまう
  -- なお local を付けているのは、この simp 補題登録の影響をセクション内に限定するため
  @[local simp]
  theorem bad_add_zero (n : Nat) : n = n + 0 := by rw [Nat.add_zero]

  -- 今まで通った証明が通らなくなる
  /-
  error: Tactic `simp` failed with a nested error:
  maximum recursion depth has been reached
  use `set_option maxRecDepth <num>` to increase limit
  use `set_option diagnostics true` to get diagnostic information
  -/
  example (n m : Nat) : (n + 0) * m = n * m := by simp

end

なお linter.loopingSimpArgs オプションを有効にすると、simp 引数のループを検出して警告を出すようになります。

-- ループを引き起こす `simp` の引数に対して警告を出す
set_option linter.loopingSimpArgs true in

/-
warning: Possibly looping simp theorem: `bad_add_zero`

Hint: You can disable a simp theorem from the default simp set by passing `- theoremName` to `simp`.
-/
example (n m : Nat) : (n + 0) * m = n * m := by
  simp [bad_add_zero]

discharger について

simp 補題を適用するときに、補題が要求する前提条件を埋める仕組みのことを discharger と呼びます。simp のデフォルトの discharger はあまり強力ではありません。

以下の例では、0 ≤ 1 という前提条件を自動では示すことができずに simp が失敗します。1

theorem Nat.max_eq_left' {a b : Nat} (h : b ≤ a) : max a b = a := by
  grind

-- dischargeの過程を表示する
set_option trace.Meta.Tactic.simp.discharge true in

/-
trace: [Meta.Tactic.simp.discharge] ✅️ Nat.max_eq_left' discharge ❌️
      0 ≤ 1
-/
example : max 1 0 = 1 := by
  fail_if_success
    simp only [Nat.max_eq_left']

  grind

これは decide タクティクで示すことができるため、(disch := ...) という構文で decide タクティクを discharger に指定すれば証明が通るようになります。

example : 0 ≤ 1 := by
  -- decide で証明できる
  decide

example : max 1 0 = 1 := by
  simp (disch := decide) only [Nat.max_eq_left']

条件付き書き換えはできない

simp は「A = B という補題に基づいて AB に書き換える」ということはできるのですが、「C → A = B という補題に基づいて C が成り立つときに AB に書き換える」ということはできません。

discharger として assumption タクティクを指定すれば多少は証明が通るようになります。

/-- 整数をイメージした何か -/
opaque MyInt : Type

variable [LE MyInt] [Zero MyInt]

/-- `x ≤ 0` という前提の下では `0 ≤ x` と `x = 0` は同値 -/
@[simp]
axiom MyInt.le_zero_implies {x : MyInt} (le : x ≤ 0) : 0 ≤ x ↔ x = 0

example {x : MyInt} (le : x ≤ 0) (ge : 0 ≤ x) : x = 0 := by
  -- `simp` は「前提条件を満たしたときの書き換え」ができない
  fail_if_success simp at ge

  -- 手動で引数を `by assumption` で与えればできる
  rw [MyInt.le_zero_implies (by assumption)] at ge
  assumption


example {x : MyInt} (le : x ≤ 0) (ge : 0 ≤ x) : x = 0 := by
  -- discharger として `assumption` を与えれば、
  -- ローカルコンテキストにある前提条件は拾ってくれるようになる
  simp (disch := assumption) at ge
  assumption

arith オプション

simp の設定で arith を有効にすると、算術的な単純化もできるようになります。

example (x y : Nat) : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  -- `simp` だけでは証明が終わらない
  fail_if_success solve
  | simp

  -- 適当に証明する
  grind

example {x y : Nat} : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  -- config を与えれば一発で終わる
  simp +arith

関連タクティク

simpa

simpa は、simp を実行した後 assumption を実行するという一連の流れを一つのタクティクにしたものです。simpa at h 構文は存在せず、simpa using h と書くことに注意してください。

example (P : Prop) (h : P) : True → P := by
  simpa

example {n m : Nat} (h : n + 0 + 0 = m) : n = m := by
  simpa using h

simp?

simp は自動的に証明を行ってくれますが、何が行われたのか知りたいときもあります。simp? は単純化に何が使われたのかを明示し、simp only を用いて書き直すことができるようにしてくれます。

/-
info: Try this:
  [apply] simp only [forall_const, imp_self, or_true]
-/
example (P : Prop) : (True → P) ∨ (P → P) := by
  simp?

simp_all

simp_all はローカルコンテキストとゴールをこれ以上単純化できなくなるまですべて単純化します。

example {P Q : Prop} (hP : P) (hQ : Q) : P ∧ (Q ∧ (P → Q)) := by
  -- simp at * は失敗する
  fail_if_success simp at *

  simp_all

  1. このコード例は https://leanprover-community.github.io/extras/simp.html における記述を参考にしています。

sorry

証明の細部を埋める前にコンパイルが通るようにしたいとき、証明で埋めるべき箇所に sorry と書くとコンパイルが通るようになります。ただし、sorry を使用しているという旨の警告が出ます。

-- Fermat の最終定理
def FermatLastTheorem :=
  ∀ x y z n : Nat, n > 2 ∧ x * y * z ≠ 0 → x ^ n + y ^ n ≠ z ^ n

/- warning: declaration uses `sorry` -/
theorem flt : FermatLastTheorem :=
  sorry

警告を消すオプション

sorry を使用すると通常は警告が出ますが、問題がない場合は warn.sorry オプションをオフにすることで警告を消すことができます。

set_option warn.sorry false in

-- 警告が出ない
example : True := by
  sorry

補足: sorry 使用の痕跡は隠すことができる

基本的に、sorry タクティクを使用すれば sorryAx という公理が使用されて、#print axioms コマンドの出力に現れるようになります。

/- info: 'flt' depends on axioms: [sorryAx] -/
#print axioms flt

しかし、[csimp] 属性を経由することで sorryAx を隠してしまうことができます。

def one := 1
def two := 2


@[csimp] theorem one_eq_two : one = two := by
  sorry

theorem false_theorem : 1 = 2 := by
  rw [show 1 = one from rfl]
  native_decide

/- info: 'false_theorem' depends on axioms: [false_theorem._native.native_decide.ax_1_1] -/
#print axioms false_theorem

split

split は、仮定やゴールにある if ... then ... elsematch ... with ... 式を扱うのに有用なタクティクです。

if/match 式を扱う必要が生じるのは、典型的には Lean で定義したアルゴリズムや関数に関して、何か性質を証明しようとしたときです。

ゴールが ⊢ Q (if P then a else b) であったときに、split を使用すると次のように2つのサブゴールが生成されます。

  • 1つはローカルコンテキストに † : P が追加され、ゴールが ⊢ Q (a) になったもの。
  • 1つはローカルコンテキストに † : ¬ P が追加され、ゴールが ⊢ Q (b) になったもの。

split によって追加される仮定は名前がついているとは限りません。名前がついていなかった場合、case などで名前を付けることができます。

splitat 構文 に対応しており、仮定に対して用いる場合は split at h のように利用します。

-- if 式を使って関数を定義する
def myabs (x : Int) : Int :=
  if x ≥ 0 then x else - x

example (x : Int) : myabs (2 * x) = 2 * myabs x := by
  -- `myabs` の定義を展開する
  dsimp [myabs]

  -- ゴールの中に if 式があって複雑
  show (if 2 * x ≥ 0 then 2 * x else -(2 * x)) = 2 * if x ≥ 0 then x else -x

  -- `split` タクティクでケース分割する
  split

  case isTrue h =>
    -- `2 * x ≥ 0` の場合
    guard_hyp h: 2 * x ≥ 0

    -- 左辺にあった if 式が消えた
    show 2 * x = 2 * if x ≥ 0 then x else -x

    replace h : x ≥ 0 := by linarith [h]

    -- `simp` で if を消すことができる
    simp [h]

  case isFalse h =>
    -- `2 * x < 0` の場合
    guard_hyp h: ¬2 * x ≥ 0

    -- 左辺にあった if 式が消えた
    show -(2 * x) = 2 * if x ≥ 0 then x else -x

    -- if 式を消すための補題を準備する
    have hx : ¬ x ≥ 0 := by linarith [h]

    -- `simp` で単純化
    simp [hx]

if 式だけでなく match 式に対しても使うことができます。

-- match式を使って関数を定義する
def mysgn (x : Int) :=
  match x with
  | Int.negSucc _ => -1
  | Int.ofNat 0 => 0
  | _ => 1

example (x : Int) : mysgn (mysgn x) = mysgn x := by
  -- mysgn x を k と置く
  set k := mysgn x with h
  -- h の mysgn の定義を展開する
  dsimp [mysgn] at h
  -- h の match の結果によって場合分け
  -- すべての場合 (k = -1, 0, 1) に関して mysgn の定義に従い計算する
  split at h
  all_goals
    rw [h]
    rfl

suffices

suffices は、数学でよくある「~を示せば十分である」という推論を行うタクティクです。

ゴールが ⊢ P であるときに suffices h : Q from を実行すると、以下が実行されます。

  • suffices h : Q from のブロック内で、仮定に h : Q が追加される。
  • suffices h : Q from 以降で、ゴールが ⊢ Q に書き換えられる。

apply タクティクと似ていますが、apply と違って「十分条件になっていること」の証明が明らかでないときにも使うことができます。

example (P : Prop) : ¬ ¬ (P ∨ ¬ P) := by
  intro h

  -- `¬ P` を示せば十分である。
  suffices hyp : ¬ P from by
    -- 仮定に `¬ P` が追加される。
    guard_hyp hyp : ¬ P

    -- このとき、特に `P ∨ ¬ P` が成り立つので、示すべきことが言える。
    have : P ∨ ¬ P := by simp_all
    contradiction

  -- 無事ゴールを `¬ P` に帰着させることができた。
  -- 以下 `¬ P` を示す。
  guard_target =ₛ ¬ P

  intro hP
  have : P ∨ ¬ P := by simp_all
  contradiction

構文

suffices Q from by ... という構文では、タクティクによって証明を構成するモードになります。suffices Q from ... という構文では、証明項を直接構成するモードになります。

example (n : Nat) (h : n ≤ 0) : n = 0 := by
  -- `n = 0` を示すためには、`n ≤ 0` であることを示せば十分である。
  suffices hyp : n ≤ 0 from Nat.eq_zero_of_le_zero h

  -- `n ≤ 0` であることを示す。
  assumption

symm

symm は、等式や同値性などの対称な関係の向きを反転するタクティクです。

たとえばゴールが b = a であるとき、symm を実行するとゴールは a = b に変わります。

variable {a b c : Nat}

example (h : a = b) : b = a := by
  symm

  -- ゴールが反転する
  guard_target =ₛ a = b

  assumption

仮定を反転する

symm at h と書くと、ローカルコンテキストにある仮定 h の向きを反転できます。

example (h : a = b) (ha : a + c = 0) : b + c = 0 := by
  symm at h

  -- 仮定の向きが反転する
  guard_hyp h : b = a

  rw [h]
  assumption

同値関係にも使える

symm は等式だけではなく、命題の同値性 にも使えます。

example {P Q : Prop} (h : Q ↔ P) : P ↔ Q := by
  symm

  -- ゴールが反転する
  guard_target =ₛ Q ↔ P

  assumption

example {P Q : Prop} (h : P ↔ Q) (hq : Q) : P := by
  symm at h

  -- 仮定の向きが反転する
  guard_hyp h : Q ↔ P

  rw [h] at hq
  assumption

カスタマイズ

一般の二項関係に対して symm を使いたい場合は、その関係が対称であることを示す定理に [symm] 属性を付与します。

/-- 2つの自然数の差が高々1であることを表す関係 -/
def Close (x y : Nat) : Prop :=
  x ≤ y + 1 ∧ y ≤ x + 1

/-- `Close` は対称な関係 -/
theorem Close.symm {x y : Nat} (h : Close x y) : Close y x := by
  exact ⟨h.right, h.left⟩

example {x y : Nat} (h : Close x y) : Close y x := by
  -- 登録していないと `symm` は使用できない
  fail_if_success symm

  exact Close.symm h

-- `Close` が対称であることを登録する
attribute [symm] Close.symm

example {x y : Nat} (h : Close x y) : Close y x := by
  symm

  -- `Close y x` が `Close x y` に反転する
  guard_target =ₛ Close x y

  assumption

tauto

tauto は、トートロジー(恒真式、tautology)であることに基づいてゴールを閉じるタクティクです。 ゴールを閉じることができなければエラーになります。

import Mathlib.Tactic.Tauto -- `tauto` を使うのに必要


variable (P Q R : Prop)

-- 含意の導入
example (h : P) : Q → P := by
  tauto

-- フレーゲの3段論法
example : (P → (Q → R)) → ((P → Q) → (P → R)) := by
  tauto

-- 否定と同値なら矛盾
example : (P ↔ ¬ P) → False := by
  tauto

tauto が扱う対象の「トートロジー」は、命題論理の範囲で記述できるものに限ります。述語論理における恒真な式は、tauto で示せないことがあります。

example (α : Type) (S : α → Prop) : ¬(∀ x, S x) → (∃ x, ¬ S x) := by
  -- `tauto` では示せない
  fail_if_success tauto

  aesop

example (Q : Prop) : ∀ (P : Prop), P → (Q → P) := by
  -- これは量化子を含むが、`tauto` でも示すことができる。
  tauto

排中律

排中律を使わずに示せる命題であっても、tauto は排中律を使って示してしまうことがあります。直観主義論理の枠内で命題を示すには、代わりに itauto タクティクを使用してください。

/-- 命題とその否定は同値ではない -/
theorem not_neg_iff {P : Prop} : ¬ (P ↔ ¬ P) := by tauto

-- 選択原理を使っているが、これは排中律を使っているため
/- info: 'not_neg_iff' depends on axioms: [propext, Classical.choice, Quot.sound] -/
#print axioms not_neg_iff

-- 実際には排中律は必要ない
theorem not_neg_iff' {P : Prop} : ¬ (P ↔ ¬ P) := by
  intro h
  have hnp : ¬ P := by
    intro hp
    have : ¬ P := by rwa [h] at hp
    contradiction
  have hp : P := by rwa [← h] at hnp
  contradiction

/- info: 'not_neg_iff'' depends on axioms: [propext] -/
#print axioms not_neg_iff'

trans

trans は、推移律を利用して示すべきことを分割するタクティクです。

推移的な関係 に対してゴールが a ∼ c であるとき、trans b により2つのサブゴール a ∼ bb ∼ c が生成されます。calc を使っても同じことができますが、calc を使うまでもないときに便利かもしれません。

import Batteries.Tactic.Trans

example {n m : Nat} (h1 : n ≤ 1) (h2 : 1 ≤ m) : n ≤ m := by
  -- 1 を仲介して示す
  trans 1

  · show n ≤ 1
    assumption

  · show 1 ≤ m
    assumption

trivial

trivial は明らかな(trivial)ことを示します。

-- True は何の仮定もなしに示せる
example : True := by trivial

-- 定義から明らかな等式
example : 1 + 1 = 2 := by trivial

-- 矛盾があるので、どんな命題でも証明できる
example (P : Prop) (h : False) : P := by trivial

舞台裏

trivial は、複数の基本的なタクティクを順に試すマクロとして実装されています。trace.Elab.step というオプションを true にすると、展開の様子を順を追って見ることができます。

/-
trace: [Elab.step] ✅️ trivial
  [Elab.step] ✅️ trivial
    [Elab.step] ✅️ trivial
      [Elab.step] 💥️ (apply And.intro✝) <;> trivial
        [Elab.step] 💥️ focus
              apply And.intro✝
              with_annotate_state"<;>" skip
              all_goals trivial
          [Elab.step] 💥️ ⏎
                apply And.intro✝
                with_annotate_state"<;>" skip
                all_goals trivial
            [Elab.step] 💥️ ⏎
                  apply And.intro✝
                  with_annotate_state"<;>" skip
                  all_goals trivial
              [Elab.step] 💥️ apply And.intro✝
      [Elab.step] 💥️ apply True.intro✝
      [Elab.step] 💥️ decide
      [Elab.step] ✅️ contradiction
-/
example (P : Prop) (h : False) : P := by
  set_option trace.Elab.step true in
  trivial

出力が長いのですが、まず apply And.intro を試し、次に apply True.intro を試し、次に decidecontradiction を試していることがわかります。

try

try は、失敗するかもしれないタクティクをエラーにすることなく実行します。try で指定されたタクティクが成功した場合は try なしの場合と変わりませんが、失敗した場合は try 実行前の状態に戻ります。

import Aesop -- `aesop` を使用するため

variable (P Q : Prop)

example : (P → Q) → (¬ Q → ¬ P) := by
  -- `assumption` は通らないが、エラーにならない
  try assumption

  try
    -- `aesop` が通り、証明が終了する
    aesop
    done

    -- 強制的に失敗させる
    fail

  -- `try` 実行前の状態に戻る
  show (P → Q) → ¬Q → ¬P

  aesop

try?

try? は、証明を探索するための標準的なタクティクです。帰納法や場合分けなどを含む複雑な証明も見つけることができます。

namespace List

variable {α : Type}

@[grind]
def last? (as : List α) : Option α :=
  match as with
  | [] => none
  | [a] => some a
  | _ :: bs => last? bs

example (as : List α) : (reverse as).head? = as.last? := by
  -- grind 単体では証明ができない
  fail_if_success grind

  -- try? なら証明を見つけることができる
  try?

example (as : List α) : (reverse as).head? = as.last? := by
  -- 証明の一例
  fun_induction last? <;> grind [= last?.eq_def]

end List

unfold

unfold は、式の展開(unfolding)を行うタクティクです。定義に基づいて式を展開します。

何も指定しなければゴールを変形しようとします。ローカルコンテキストにある項 h について展開を行うには、unfold ... at h のように at を付けます。

dsimp タクティクを使っても同様のことができます。

def myFun (n : Nat) : Nat :=
  n + 1

example (n : Nat) : myFun n ≥ 1 := by
  -- `dsimp` でも同じことができる
  try
    dsimp [myFun]
    show n + 1 ≥ 1
    fail

  -- `myFun` を定義に展開する
  unfold myFun
  simp

use

use タクティクは、「~を満たす x が存在する」という命題を示すために、証拠になる x を具体的に示します。

ゴールが ⊢ ∃ x, P x のとき、x : X がローカルコンテキストにあれば、use x によりゴールが ⊢ P x に変わります。同時に、P x が自明な場合は証明が終了します。

import Mathlib.Tactic.Use
import Mathlib.Tactic.Linarith

example {α : Type} (P : α → Prop) (x : α) : ∃ y, P y := by
  use x
  -- ゴールが `P x` に変わる
  guard_target =ₛ P x

  sorry

exists との違い

これだけの説明だと exists タクティクと同じに見えますが、use タクティクには exists より優れている点があります。

discharger が指定できる

use は、証拠を与えた後にゴールを閉じるために使うタクティク(discharger と呼ばれます)を指定することができます。

example (x : Rat) (h : 3 * x + 6 > 6) : ∃ (y : Rat), y > 0 := by
  exists x
  linarith

example (x : Rat) (h : 3 * x + 6 > 6) : ∃ (y : Rat), y > 0 := by
  -- `discharger` として `linarith` を指定することができる
  use (discharger := linarith) x

exists タクティクにこの構文は存在しません。

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- `exists (discharger := linarith)` と書くとパースエラーになる
/- error: <input>:1:19: expected ')', ',' or ':' -/
#eval parse `tactic "exists (discharger := linarith) 1"

Exists 以外の型にも使用できる

exists は、ゴールの型が Exists であるという想定をしているため、たとえばフィールドの数が3以上であるような構造体に対して使うとエラーになります。

/-- 例示のための構造体。フィールドの数が `Exists` より多い -/
structure Foo where
  x : Int
  pos : x > 0
  sq : x ^ 2 = 9

/-- `Foo` の項を具体的に与える例 -/
example : Foo := ⟨3, by simp, by simp⟩

/-
error: Insufficient number of fields for `⟨...⟩` constructor: Constructor `Foo.mk` has 3 explicit field, but only 2 were provided
-/
example : Foo := by
  exists 3

しかし、use タクティクであれば対応することができます。

example : Foo := by
  -- 最初のフィールドを `3` で埋めるように指示する
  use 3

  -- 残りのフィールドは `simp` で証明することができる
  all_goals simp

example : Foo := by
  -- `discharger` を指定するバージョン
  use (discharger := simp) 3

with_reducible

with_reducible は、後に続くタクティクの透過度(transparency)を reducible に指定して実行します。透過度 reducible では、[reducible] 属性を持つ定義だけが展開されます。

用途

with_reducible はタクティクを定義するマクロを書く際に有用です。推移律を利用して、不等式を分割するタクティクを定義する例を示しましょう。

まず、不等式の推移律を使う証明の例を示します。

variable (a b c : Nat)

example (h₁ : a ≤ b) (h₂ : b ≤ c) : a ≤ c := by
  -- b を経由して示す
  apply Nat.le_trans (m := b) <;> assumption

example (h₁ : a < b) (h₂ : b < c) : a < c := by
  -- b を経由して示す
  apply Nat.lt_trans (m := b) <;> assumption

今からすることは、この2つの命題を1つのタクティクで証明できるようにすることです。コードを素直に共通化しようとして次のようにマクロを定義すると上手くいきません。

section
  /- ## マクロによるタクティクの定義が上手くいかない例 -/

  /-- 推移律を扱うタクティク -/
  syntax "my_trans" term : tactic

  -- `<` に対するルール
  local macro_rules
    | `(tactic| my_trans $e) => `(tactic| apply Nat.lt_trans (m := $e))

  -- `≤` に対するルール
  local macro_rules
    | `(tactic| my_trans $e) => `(tactic| apply Nat.le_trans (m := $e))

  example (h₁ : a ≤ b) (h₂ : b ≤ c) : a ≤ c := by
    -- 成功
    my_trans b <;> assumption

  example (h₁ : a < b) (h₂ : b < c) : a < c := by
    -- 失敗
    my_trans b <;> try assumption

    exact Nat.le_of_succ_le h₂

end

マクロ展開のルールとして、Lean は後に定義されたルールを先に適用するので、常に Nat.le_trans を先に適用します。ところが < を使って定義されているため、< に対しても apply Nat.le_trans が成功してしまいます。その結果、< に対して Nat.lt_trans を使ってくれないという結果になっています。

/-- `<` は `≤` を使って定義されている -/
example (n m : Nat) : (Nat.lt n m) = (Nat.le n.succ m) := rfl

example (h₁ : a < b) (h₂ : b < c) : a < c := by
  -- < に対しても Nat.le_trans が成功してしまう
  apply Nat.le_trans (m := b)
  · exact h₁
  · exact Nat.le_of_succ_le h₂

with_reducible を使用すると、[reducible] とマークされていない定義は展開されなくなるので、この挙動を防ぐことができます。

example (h₁ : a < b) (h₂ : b < c) : a < c := by with_reducible
  -- < に対して Nat.le_trans が成功しなくなった!
  fail_if_success apply Nat.le_trans (m := b)

  apply Nat.lt_trans (m := b) <;> assumption

section
  /- ## タクティクマクロを with_reducible で定義する例 -/

  -- `<` に対するルール
  local macro_rules
    | `(tactic| my_trans $e) => `(tactic| with_reducible apply Nat.lt_trans (m := $e))

  -- `≤` に対するルール
  local macro_rules
    | `(tactic| my_trans $e) => `(tactic| with_reducible apply Nat.le_trans (m := $e))

  example (h₁ : a ≤ b) (h₂ : b ≤ c) : a ≤ c := by
    -- 成功
    my_trans b <;> assumption

  example (h₁ : a < b) (h₂ : b < c) : a < c := by
    -- 成功
    my_trans b <;> try assumption

end

wlog

wlog は、数学でよく使われる、一般性を失うことなく(without loss of generarity)何々と仮定してよいというフレーズの Lean での対応物です。

import Mathlib.Tactic -- 大雑把に import する

-- `n` と `m` は自然数
variable {n m : ℕ}

example (h : n ≠ m) : 0 < |(n - m : ℤ)| := by
  -- 一般性を失うことなく `m < n` と仮定して良い
  wlog hnm : m < n with H

  -- `m < n` の時に成り立つのであれば、そうでないときも成り立つことを示す
  case inr =>
    -- `m < n` ではないので、`n < m` が成り立つ
    have : m = n ∨ n < m := Nat.eq_or_lt_of_not_lt hnm
    replace : n < m := by aesop

    -- `m < n` の時に成り立つという仮定を利用できる
    replace : 0 < |(m - n : ℤ)| := @H m n h.symm this

    rw [abs_sub_comm]
    assumption

  -- `m < n` と仮定してよいことがわかったので、
  -- `m < n` だとして証明する
  apply abs_pos_of_pos
  simp_all

zify

zify タクティクは、自然数 Nat についての命題を整数 Int についての命題に変換します。

import Mathlib.Tactic

example (x : Nat) (h : x ≥ 5) : 15 ≤ 3 * x := by
  -- 仮定とゴールを整数の不等式に変換する
  zify at h ⊢

  -- 整数についての命題に変換した
  guard_hyp h : x ≥ (5 : Int)
  guard_target = (15 : Int) ≤ 3 * ↑x

  have : (15 : Int) ≤ 3 * ↑x := calc
    (15 : Int) = 3 * 5 := by rfl
    _ ≤ 3 * ↑x := by linarith
  assumption

構文

無名コンストラクタ

無名コンストラクタ(anonymous constructor) を使用すると、単一のコンストラクタしか持たない帰納型 T に対して、コンストラクタ名を指定せずに T 型の項を構成することができます。⟨x1, x2, ...⟩ という構文により使うことができます。

/-- 単一のコンストラクタしか持たない帰納型の例 -/
inductive Foo where
  | mk (x y : Nat)
deriving DecidableEq

#guard
  -- コンストラクタを使って項を作る場合
  let foo₁ := Foo.mk 1 2

  -- 無名コンストラクタを使って項を作る場合
  let foo₂ := ⟨1, 2⟩

  -- 両者は同じものを表す!
  foo₁ = foo₂

一般の帰納型に対しては使用できません。

/-- 複数のコンストラクタを持つ帰納型の例 -/
inductive Sample where
  | fst (foo bar : Nat)
  | snd (foo bar : String)

-- 「コンストラクタが一つしかない帰納型でなければ使用できない」というエラーになる
/-
info: Invalid `⟨...⟩` notation: The expected type `Sample` has more than one constructor

Note: This notation can only be used when the expected type is an inductive type with a single constructor
-/
#check_failure (⟨"foo", "bar"⟩ : Sample)

構造体への使用

構造体は単一コンストラクタしか持たない帰納型なので、構造体に対しても無名コンストラクタ構文が使用できます。

/-- 2つのフィールドを持つ構造体 -/
structure Hoge where
  foo : Nat
  bar : Nat

#check (⟨1, 2⟩ : Hoge)

平坦化

コンストラクタが入れ子になっている場合でも平坦化することができます。

#guard
  -- 入れ子にした場合
  let x : Nat × (Int × String) := ⟨1, ⟨2, "hello"⟩⟩

  -- 平坦化した場合
  let y : Nat × (Int × String) := ⟨1, 2, "hello"⟩

  -- 両者は同じものを表している!
  x = y

上の例は構造体の同種のコンストラクタが入れ子になっている例ですが、structure コマンドで定義された型でなくても、同種のコンストラクタでもなくても、同様に平坦化することができます。

/-- Prod を真似て自作した帰納型 -/
inductive MyProd (α β : Type) where
  | mk (fst : α) (snd : β)
deriving DecidableEq

-- Prod のための2項演算子
@[inherit_doc] infixr:35 " ×ₘ " => MyProd

-- 平坦化ができている!
#check (⟨1, 2, 1, 2, 1⟩ : Nat × Nat ×ₘ Nat ×ₘ Nat × Nat)

しかし、時には平坦化ができないケースもあります。

section

  variable {α β : Type} (f : α → Option (α × β))

  example (a : α) (h : ∃ (x : α × β), f a = some x) : True := by
    -- 平坦化ができない
    fail_if_success obtain ⟨a, b, hx⟩ := h

    -- 代わりにネストさせると通る
    obtain ⟨⟨a, b⟩, hx⟩ := h

    trivial
end

リストリテラル

[x₁, x₂, .. , xₙ] は、List α の項を簡単に作るための構文です。

もしこの構文がなければ、List α の項を作るためにはコンストラクタを使用するしかないので、次のように書く必要があります。

/-- 自前で定義したリスト型 -/
inductive MyList (α : Type) where
  | nil
  | cons (head : α) (tail : MyList α)
deriving DecidableEq

/-- 空リスト。標準の`List`のための記法と被るのを避けている。 -/
notation:max " ⟦⟧ " => MyList.nil

/-- `MyList`に要素を追加する。標準の`List`のための記法と被るのを避けている。 -/
infixr:80 " ::: " => MyList.cons

-- 項を作るのが面倒
/- info: 1 ::: 2 ::: 3 ::: ⟦⟧ : MyList Nat -/
#check 1 ::: 2 ::: 3 ::: ⟦⟧

しかし、リストリテラル構文があるおかげで、次のように見やすく簡潔に書くことができます。

/-- 自作のリストリテラル構文。なお末尾のカンマは許可される。
なお標準の`List`のための記法と被るのを避けている。 -/
syntax "⟦" term,*,? "⟧" : term

macro_rules
  | `(⟦ ⟧) => `(⟦⟧)
  | `(⟦$x⟧) => `($x ::: ⟦⟧)
  | `(⟦$x, $xs,*⟧) => `($x ::: (⟦$xs,*⟧))

-- 項を作るのが楽になった!
#guard ⟦1, 2, 3⟧ = 1 ::: 2 ::: 3 ::: ⟦⟧

-- 末尾のコンマは無視される
#guard ⟦1, ⟧ = 1 ::: ⟦⟧

-- 空リストもリストリテラル構文で書ける
#guard ⟦⟧ = MyList.nil (α := Unit)

部分型構文

{x : T // p x} は、部分型を表す構文です。

/- info: { x // x > 0 } : Type -/
#check { x : Int // x > 0 }

もしこの構文がなければ、Subtype を定義するには以下のように書く必要があります。

/-- 標準の `Subtype` を真似て自前で定義した型 -/
structure MySubtype {α : Type} (p : α → Prop) where
  val : α
  property : p val

-- 正の自然数全体を表す部分型
#check MySubtype (fun x : Nat => x > 0)

しかし、この構文があるおかげで、次のように見やすく簡潔に書くことができます。

@[inherit_doc MySubtype] syntax "my{ " ident (" : " term)? " // " term " }" : term

macro_rules
  | `(my{ $x : $type // $p }) => ``(MySubtype (fun ($x:ident : $type) => $p))
  | `(my{ $x // $p }) => ``(MySubtype (fun ($x:ident : _) => $p))

-- 正の自然数全体を表す部分型
#check my{ x // x > 0 }

-- 正の整数全体の型
#check my{ x : Int // x > 0 }

暗黙の引数

暗黙の引数とは、def コマンドや theorem コマンド、variable コマンドなどが受け取る構文の一つで、関数や定理の引数をユーザが明示的に与えなくても、Lean が文脈を読んで推論してくれるようになります。波括弧 {} で囲んで、{x y : A} のように書きます。

典型的な使用例

たとえば、暗黙の引数を使わなかった場合にどうなるかを見てみましょう。次の関数 List.subs は型パラメータ α : Type を受け取っていますが、第二引数 xs : List α を見れば α : Type が何であるかは分かるので、List.subs を使用する際に毎回 α を指定するのは冗長だと考えられます。

/-- 与えられたリストの部分リストを全て返す(明示的引数バージョン) -/
def List.subs_exp (α : Type) (xs : List α) : List (List α) :=
  match xs with
  | [] => [[]]
  | x :: xs =>
    let xss := subs_exp α xs
    xss ++ xss.map (x :: ·)

-- 型引数 α を明示的に与えて書いた場合
#eval List.subs_exp Nat [1, 2]

-- ホールを使って推論させる場合
#eval List.subs_exp _ [1, 2]

-- 型引数を与えないと(当然ながら)エラーになってしまう
#check_failure List.subs_exp [1, 2]

引数 α を暗黙の引数として受け取るように変更すれば、Lean が α : Type の内容を推論してくれるようになり、α を省略できるようになります。

/-- 与えられたリストの部分リストを全て返す(暗黙引数バージョン) -/
def List.subs_imp {α : Type} (xs : List α) : List (List α) :=
  match xs with
  | [] => [[]]
  | x :: xs =>
    let xss := subs_imp xs
    xss ++ xss.map (x :: ·)

-- 型引数を省略できるようになった
#eval List.subs_imp [1, 2]

-- 型引数を位置引数として与えると、今度はエラーになる
#check_failure List.subs_imp Nat [1, 2]

明示的引数モード

暗黙の引数を受け取るものとして定義された関数や定理に対して、@ 記号を先頭に付けると全ての暗黙の引数の自動挿入が行われなくなります。つまり、すべての引数を手動で与える必要が生じます。

-- 2 つの暗黙引数を持つ関数
def List.map' {α β : Type} (f : α → β) : List α → List β
  | [] => []
  | x :: xs => f x :: map' f xs

-- 普通は次のように使う
#check List.map' (fun x => x == 1) [1, 2, 3]

-- `@` 記号を付けると手動で型引数を与えないといけなくなる
#check @List.map' Nat Bool (fun x => x == 1) [1, 2, 3]

少し、というかかなり細かい注意ですが、@ を付けても「暗黙引数が明示的引数に変わる」わけではありません。その証拠に、#check コマンドの出力を見ると暗黙引数のままになっています。

/- info: id.{u} {α : Sort u} (a : α) : α -/
#check id

-- `α` のバインダーが波括弧のままになっている
/- info: @id : {α : Sort u_1} → α → α -/
#check @id

構文的な性質

Lean.Parser.Term.implicitBinder というパーサが暗黙引数の構文に対応しており、このパーサのドキュメントコメントに次のように書かれている通り、構文としては暗黙の引数に型を指定しないことも許されます。

Implicit binder, like {x y : A} or {x y}. In regular applications, whenever all parameters before it have been specified, then a _ placeholder is automatically inserted for this parameter. Implicit parameters should be able to be determined from the other arguments and the return type by unification.

In @ explicit mode, implicit binders behave like explicit binders.

-- `x : α` と書いたので、`α` が何かの型であることは分かる
def myId {α} (x : α) := x

配列リテラル

#[x₁, x₂, .. , xₙ] は、Array α の項を簡単に作るための構文です。

/- info: #[1, 2, 3] : Array Nat -/
#check #[1, 2, 3]

もしこの構文がなければ、Array α の項を作るにはコンストラクタを利用するしかないので、次のように書く必要があります。

/-- 標準の Array をまねて自作した配列型 -/
structure MyArray (α : Type) where
  toList : List α

abbrev List.toMyArray {α : Type} (xs : List α) : MyArray α := ⟨xs⟩

/- info: [1, 2, 3].toMyArray : MyArray Nat -/
#check List.toMyArray [1, 2, 3]

しかし、配列リテラル構文があるおかげで、次のように見やすく簡潔に書くことができます。

syntax "my#[" term,*,? "]" : term

macro_rules
  | `(my#[ $elems,* ]) => `(List.toMyArray [ $elems,* ])

-- 項を作るのが楽になった!
#check (my#[1, 2, 3] : MyArray Nat)

記号は基本的に、全称量化を表します。つまり述語 P : α → Prop に対して、∀ x : α, P x は「すべての x : α に対して P x が成り立つ」という意味になります。

しかし、 は述語以外のものに対しても実は使うことができます。

#check ∀ n : Nat, Vector Nat n

この場合何を意味するかというと、∀ a : A, B(a : A) → B と同じ意味になります。

set_option pp.foralls false

/- info: (n : Nat) → Vector Nat n : Type -/
#check ∀ n : Nat, Vector Nat n

実際のところ両者は常に同じです。型が Prop になるときは、カリー・ハワード同型対応により「命題は型、証明はその項」なので全称量化の意味になるというだけで、型としては同じものです。型が Prop になるときだけ、見やすいので 記号を使う慣習になっています。

variable {A : Type} {B : A → Sort u}

example : ((a : A) → B a) = (∀ a : A, B a) := by rfl

左パイプ記法

左パイプ記法(left pipe notation) <| は、パイプ記号の右側の式を左側の関数の引数として渡します。

つまり f <| a と書くことで f a と同じ意味になります。

example (a : α) (f : α → β) : (f <| a) = f a := by
  rfl

複数組み合わせると右側が優先して結合されます。 したがって2つ組み合わせて g <| f <| a のように書くと g (f a) と同じ意味になります。

example (a : α) (f : α → β) (g : β → γ) : (g <| f <| a) = g (f a) := by
  rfl

用途

ただの関数適用として書いても左パイプ記法を使用しても順序は変わりませんが、パイプ記法を使うと括弧を省略できます。

/-- 二次元リストの和を計算して、
ログだけ出して結果を捨てる関数 -/
def sumWithLog (dlist : List (List Nat)) : IO Unit := do
  let mut current := 0
  for (list, i) in dlist.zipIdx do
    IO.println <|
      s!"{i} 番目のリスト\n" ++
      s!"  合計値: {list.sum}\n" ++
      s!"  長さ: {list.length}"
    current := current + list.sum

/-
info:
0 番目のリスト
  合計値: 6
  長さ: 3
1 番目のリスト
  合計値: 9
  長さ: 2
2 番目のリスト
  合計値: 9
  長さ: 1
-/
#eval sumWithLog [[1, 2, 3], [4, 5], [9]]

補足

双対的な概念として右パイプ記法があります。

右パイプ記法

右パイプ記法(right pipe notation) |> は、パイプ記号の左側の式を右側の関数の引数として渡します。

つまり a |> ff a と同じ意味になります。

example (a : α) (f : α → β) : (a |> f) = f a := by
  rfl

複数組み合わせると左側が優先して結合されます。 したがって2つ組み合わせて x |> f |> g のように書くと、g (f x) と同じ意味になります。

example (a : α) (f : α → β) (g : β → γ) : (a |> f |> g) = g (f a) := by
  rfl

用途

関数適用として書くと、先に適用する関数を後に書くことになるので順序が逆になります。 一方で右パイプ記法を使用すると、先に適用する関数を先に書くことができます。

/-- `n` 以下の奇数の自乗の和を計算する -/
def sumOfOddSquares (n : Nat) : Nat :=
  List.range (n + 1)
    |> List.filter (· % 2 = 1)
    |> List.map (· ^ 2)
    |> List.sum

#guard sumOfOddSquares 3 = 1^2 + 3^2
#guard sumOfOddSquares 10 = 1^2 + 3^2 + 5^2 + 7^2 + 9^2

補足

双対的な概念として左パイプ記法があります。

at 構文

at 構文とは、タクティク tac に対してその書き換え対象を指定する構文のことです。rw タクティクや simp タクティクなど、「書き換え」を行う多くのタクティクがこの構文を受け入れるようになっています。

利用可能な構文

at h

ローカルコンテキストにある仮定 h : P に対して at h と書くことで h に対して書き換えを行うことができます。

section
  variable {a b : Nat}

  example (lem : a + 0 = a) (h : a + (a + 0) = a) : a + a = a := by
    -- ローカルコンテキストの `h` に対して書き換えを行う
    rw [lem] at h

    rw [h]

end

at h₁ h₂ ..

また、ローカルコンテキストにある複数の仮定 h₁, h₂, .. に対して書き換えを行うには空白区切りで at h₁ h₂ .. とします。

section
  variable (x m n : ℕ)

  example (left : (x : ℝ) < ↑m + ↑n) (right : ↑m + ↑n < (x : ℝ) + 1) : False := by
    -- `left`と`right`に対して書き換えを行う
    norm_cast at left right

    omega
end

at h ⊢

ローカルコンテキストにある仮定 h : P とゴールに対して同時に書き換えを行うには、at h ⊢ とします。

example (x : Nat) (h : x ≥ 1) : 2 * x ≥ 2 := by
  -- 自然数から有理数にキャストする
  qify at h ⊢

  have : (2 : ℚ) ≤ 2 * x := calc
    _ = 2 * 1 := by simp
    _ ≤ 2 * (x : ℚ) := by gcongr
  assumption

at *

ローカルコンテキストのすべての仮定とゴールに対して書き換えを行うには、at * とします。

section
  set_option linter.flexible false

  variable (P : Nat → Bool)

  example (h1 : P (if 0 = 0 then 1 else 2)) (h2 : P (0 * 1)) : P 0 ∧ P (1 + 0) := by
    simp at *

    exact ⟨h2, h1⟩
end

at 構文を受け入れるタクティクを作る

次に示すのは at 構文を受け入れるようなタクティクをマクロとして構成する例です。

/-- 自前で定義した狭義順序 -/
def Nat.mylt (m n : Nat) : Prop := (m + 1) ≤ n

/-- 単に `m < n` と書いたら上で定義した自前の順序が使われるようにする -/
instance : LT Nat where
  lt m n := m.mylt n

open Lean.Parser.Tactic in

/-- `(· < ·)`を定義に展開して中身を確認するための専用自作タクティク -/
syntax (name := dsimp_lt) "dsimp_lt" (location)? : tactic

macro_rules
  | `(tactic| dsimp_lt $[at $location]?) =>
    `(tactic| dsimp only [(· < ·), Nat.mylt] $[at $location]?)

example : 1 < 4 := by
  dsimp_lt
  guard_target =ₛ 1 + 1 ≤ 4

  decide

example (_h : 1 < 3) : True := by
  dsimp_lt at _h
  guard_hyp _h : 1 + 1 ≤ 3

  trivial

by

Lean においては、命題は型で、証明はその項です。命題 P の証明を構成するとは項 h : P を構成するということです。by は、証明の構成をタクティクで行いたいときに使います。

証明項による証明とは、たとえば次のようなものです。

variable (P Q R : Prop)

-- `P → R` というのは `P` の証明を与えられたときに `R` の証明を返す関数の型
-- したがって、その証明は関数となる
example (hPQ : P → Q) (hQR : Q → R) : P → R :=
  fun hP ↦ hQR (hPQ hP)

同じ命題をタクティクを使って示すと、例えば次のようになります。

-- 同じ命題をタクティクで示した例
example (hPQ : P → Q) (hQR : Q → R) : P → R := by
  intro hP
  exact hQR (hPQ hP)

by?

by の代わりに by? を使うとタクティクモードで構成した証明を直接構成した証明に変換してくれます。詳細は show_term のページを参照してください。

/-
info: Try this:
  [apply] fun hP => hQR (hPQ hP)
-/
example (hPQ : P → Q) (hQR : Q → R) : P → R := by?
  intro hP
  exact hQR (hPQ hP)

構文的な性質

タクティクを使用する際には多くの場合 by を伴うので、by とタクティクの関連は深いのですが、by 自身は構文的にタクティクではありません。

open Lean Parser in

/-- `s : String` をパースして `Syntax` の項を得る。`cat` は構文カテゴリ。-/
def parse (cat : Name) (s : String) : MetaM Syntax := do
  ofExcept <| runParserCategory (← getEnv) cat s

-- `by` 自身は構文的にタクティクではない
/- error: <input>:1:0: expected tactic -/
#eval parse `tactic "by"

by のパーサのドキュメントコメントに次のように書かれている通り、by の後にタクティクを続けたものは項(term)になります。

by tac constructs a term of the expected type by running the tactic(s) tac.

-- `by` の後にタクティクを続けたものは構文的に項(term)になる
#eval parse `term "by rfl"

フィールド記法

フィールド記法(field notation) とは、大雑把に言えば Te の型であるときに、関数適用 T.f ee.f と書き表せるという記法のことです。あたかも fe のフィールドであるかのように見えるのでこの名前があります。

典型的な例は、構造体 S の項 e : S に対して、構造体のフィールドのアクセサ関数 S.f の適用を e.f と書けることです。

/-- 平面 -/
structure Point (α : Type) where
  x : α
  y : α

-- `Point` のフィールドへのアクセサ関数
#check Point.x
#check Point.y

-- フィールド記法を使ってアクセスすることができる
#guard
  let p : Point Nat := { x := 1, y := 2 }
  p.x = Point.x p

ここで S は構造体である必要はなく、任意の型で構いません。すなわち任意の型 S とその項 e : S に対して、e.fS.f (p := e) と解釈されます。ただし、ここで p は関数 S.f の型 S を持つような最初の明示的引数です。

/-- 例示のための意味のない帰納型 -/
inductive S where
  | fst (n : Nat)
  | snd (s : String) (n : Nat)

/-- `_x : Unit` という余計な引数を持つ関数 -/
def S.toNat (_x : Unit) (s : S) : Nat :=
  match s with
  | S.fst n => n
  | S.snd _ n => n

#guard
  let e : S := S.fst 42

  -- フィールド記法が使える
  e.toNat () = 42

関数型に対して

e が関数であるとき、e.fFunction.f (p := e) に翻訳されます。ただし、ここで p は関数 Function.f の、関数型を持つような最初の明示的引数です。

section
  variable {α β γ : Type}

  /-- 関数の単射性 -/
  def Function.injective (f : α → β) : Prop := ∀ {x y}, f x = f y → x = y

  example (f : α → β) (g : β → γ) : (g ∘ f).injective → f.injective := by
    -- `g ∘ f` が単射だと仮定する。
    intro h

    -- このとき `f` は単射である。
    exact show f.injective from by
      -- なぜなら、仮に `f x = f y` とすると
      intro x y hg

      -- `(g ∘ f) x = (g ∘ f) y` が成り立っており
      have : (g ∘ f) x = (g ∘ f) y := by simp [hg]

      -- `g ∘ f` が単射であることから `x = y` が導かれるため。
      exact h this

end

パラメータを取る型に対して

eT ... の項であり、かつ関数 T.f が存在するとき、e.fT.f (p := e) に翻訳されます。ただし、ここで p は関数 T.f の型 T ... を持つような最初の明示的引数です。

/-- リストの最小値をそのインデックスと共に出力する -/
def List.minIdx? {α : Type} [LE α] [DecidableLE α] (xs : List α) : Option (Nat × α) :=
  loop xs 0
where
  loop : List α → Nat → Option (Nat × α)
  | [], _ => none
  | x :: xs, i =>
    match loop xs (i + 1) with
    | none => some (i, x)
    | some (j, y) =>
      if x ≤ y then (i, x) else some (j, y)

-- フィールド記法が使用できている
#guard
  let e := [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
  e.minIdx? = some (1, 1)

詳細な仕様

詳細な仕様は、パーサのドキュメントコメントに以下のように書かれています。

The extended field notation e.f is roughly short for T.f e where T is the type of e. More precisely,

  • if e is of a function type, e.f is translated to Function.f (p := e) where p is the first explicit parameter of function type
  • if e is of a named type T ... and there is a declaration T.f (possibly from export), e.f is translated to T.f (p := e) where p is the first explicit parameter of type T ...
  • otherwise, if e is of a structure type, the above is repeated for every base type of the structure.

The field index notation e.i, where i is a positive number, is short for accessing the i-th field (1-indexed) of e if it is of a structure type.

制約

フィールド記法の制約として、フィールド記法を使用すると型強制が通らなくなります。

/-- 標準のリストのラッパー -/
structure MyList (α : Type) where
  data : List α

/-- `MyList` から `List` への型強制 -/
instance {α : Type} : Coe (MyList α) (List α) where
  coe l := l.data

-- 型強制がはたらくので本来 List が来るべきところに MyList が来ていても通る
#check
  let l : MyList Nat := { data := [1, 2, 3] }
  List.foldl (· + ·) 0 l

-- フィールド記法だと型強制がはたらかない
#check_failure
  let l : MyList Nat := { data := [1, 2, 3] }
  l.foldl (· + ·) 0

match .. with

match .. with は、パターンマッチ(pattern match) に使用されます。

基本的な使い方

コンストラクタによるパターンマッチ

典型的な使用場面は、帰納型 T の項は必ずその有限個のコンストラクタの項のどれかに由来するので、そのどれであるかによって場合分けをしたいときです。

def List.myHead? {α : Type} (xs : List α) : Option α :=
  match xs with
  | [] => none
  | x :: _ => some x

#guard [1, 2, 3].myHead? = some 1

/-- 階乗関数。 -/
def Nat.fatorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | m + 1 => (m + 1) * fatorial m

#guard Nat.fatorial 4 = 1 * 2 * 3 * 4

パターンマッチの | に続くのはコンストラクタの像または、コンストラクタの像に展開される式である必要があります。この挙動を変更して任意の関数を使いたい場合、[match_pattern]属性の使用を検討してください。

無名コンストラクタによるパターンマッチ

match .. with 構文はある程度賢く、無名コンストラクタを展開したりすることができます。

/-- 正の自然数 -/
abbrev Pos := { x : Nat // x > 0 }

/-- 正の自然数に対する階乗関数 -/
def Pos.factorial (n : Pos) : Nat :=
  match n with
  | ⟨1, _⟩ => 1
  | ⟨m + 2, h⟩ => (m + 2) * factorial ⟨m + 1, by omega⟩

#guard Pos.factorial ⟨4, by omega⟩ = 1 * 2 * 3 * 4

マッチ結果の証明を取得する

x : T についてパターンマッチしてコンストラクタ cons の枝に入った時、xcons に由来するという証明を取得したいことがあります。このとき、match h : x with という構文を使用すると、h にその証明が格納されます。

def List.myTail {α : Type} (l : List α) : List α :=
  match h : l with
  | [] => []
  | x :: xs => by
    -- `l` が `x :: xs` という形をしていることの証明が取得できている
    guard_hyp h : l = x :: xs
    exact xs

@ パターン

部分にマッチしつつ、その全体の値を変数として保持したいとき、@ パターンと呼ばれる構文が利用できます。

/-- 自然数を1桁ごとのリストに変換する -/
def Nat.toListNat (n : Nat) : List Nat :=
  match n with
  | 0 => []
  | m@(n + 1) =>
    have : m / 10 < m := by grind
    Nat.toListNat (m / 10) ++ [(m % 10)]

#guard Nat.toListNat 1234 == [1, 2, 3, 4]

panic!

panic! を使うと、その部分が実行されたときにエラーメッセージが表示されます。

/-- 0 で割るときに警告を出すような除算関数 -/
def safeDiv (x y : Nat) : Nat :=
  if y = 0 then panic! "0 で割ることはできません!" else x / y

/-
info: PANIC at safeDiv LeanByExample.Syntax.Panic:8:16: 0 で割ることはできません!
---
info: 0
-/
#eval safeDiv 10 0

panic! で処理は中断しない

注意点として、panic! を使っても処理は中断しません。その部分で期待されている型の Inhabited インスタンスが使用されて、値を返します。(値を返さずに終了することが許されると、パニックするプログラムを Empty の項と見做すことができる可能性が生じて、論理的健全性に問題が生じます。)上記の例でいうと、NatInhabited インスタンスが使用されて 0 が返ります。

-- エラーメッセージが出るだけで処理は中断されていない
#guard safeDiv 10 0 = 0

処理を本当に中断したい場合、たとえば IO モナドで返り値を包んだうえで、例外を throw すればよいでしょう。

/-- ゼロ除算に対して例外を投げるバージョンの割り算 -/
def divIO (x y : Nat) : IO Nat :=
  if y = 0 then
    throw (IO.userError "0 で割ることはできません!")
  else
    pure (x / y)

/- error: 0 で割ることはできません! -/
#eval divIO 10 0

なお環境変数 LEAN_ABORT_ON_PANIC に何か値を設定すると、(たとえば 1) panic! の実行時に処理が実際に中断されるようになります。

panic! は例外を投げない

panic! は例外を投げているわけではなく、try .. catch ブロックで補足することはできません。例外ハンドリングが必要な場合は panic! を使用しない方が良いでしょう。

/-
info: PANIC at safeDiv LeanByExample.Syntax.Panic:8:16: 0 で割ることはできません!
Result: 0
-/
#eval show IO Unit from do
  try
    let result := safeDiv 10 0
    IO.println s!"Result: {result}"
  catch e =>
    IO.println s!"Caught an error: {e}"

生文字列リテラル

文字列リテラルはダブルクォート " で囲って表現しますが、では " を含む文字列を書きたいときはどうすれば良いのでしょうか?エスケープシークエンスを付けて「これは文字列リテラルの開始/終了の意味ではないよ」と示すのが良くある方法ですね。

/- info: I said "Hello" to you. -/
#eval IO.println "I said \"Hello\" to you."

それが面倒あるいは困難であるとき、r#""# で囲むという構文が使用できます。これは 生文字列リテラル(raw string literal) と呼ばれるものです。

def json := r#"
  {
    "name": "Alice",
    "age": 30,
    "isStudent": false
  }
"#

/-
info:
  {
    "name": "Alice",
    "age": 30,
    "isStudent": false
  }
-/
#eval IO.println json

s!

s! は、文字列補間を行うための構文です。

def greet (name : String) : String :=
  s!"Hello, {name}!"

/- info: "Hello, World!" -/
#eval greet "World"

カスタマイズ

s! 構文は組み込まれた変数に ToString を適用します。

section
  -- ## test for ambiguous string

  /-
  info: s!: Hello, world ⏎
  s!: Hello, "world"
  -/
  #eval
    let a := "world "
    dbg_trace s!"s!: Hello, {a}"

    let b := "\"world\""
    dbg_trace s!"s!: Hello, {b}"
    return ()
end

代わりに Repr を使うように置き換えることができます。

section
  open Lean TSyntax.Compat

  def Lean.TSyntax.expandInterpolatedStrChunks' (chunks : Array Syntax) (mkAppend : Syntax → Syntax → MacroM Syntax) (mkElem : Syntax → MacroM Syntax) : MacroM Syntax := do
    let mut i := 0
    let mut result := Syntax.missing
    for elem in chunks do
      let elem ←
        match elem.isInterpolatedStrLit? with
        | none => mkElem elem
        | some str => pure <| Syntax.mkStrLit str
      if i == 0 then
        result := elem
      else
        result ← mkAppend result elem
      i := i+1
    return result

  def Lean.TSyntax.expandInterpolatedStr' (interpStr : TSyntax interpolatedStrKind) (type : Term) (toTypeFn : Term) : MacroM Term := do
    let r ← expandInterpolatedStrChunks' interpStr.raw.getArgs (fun a b => `($a ++ $b)) (fun a => `($toTypeFn $a))
    `(($r : $type))
end

syntax:max "d!" interpolatedStr(term) : term

macro_rules
  | `(d! $interpStr) => do interpStr.expandInterpolatedStr' (← `(String)) (← `(reprStr))

section

  /-
  info: d!: Hello, "world "
  d!: Hello, "\"world\""
  -/
  #eval
    let a := "world "
    dbg_trace d!"d!: Hello, {a}"

    let b := "\"world\""
    dbg_trace d!"d!: Hello, {b}"
    return ()
end

show .. from

show T from e は、型 T の項 e を表す構文です。項に対して型 T を明示することができます。

-- とても単純な使用例
#check show Nat from 1

-- `#eval` コマンドに式を渡す際に、期待される型を明示するために show を用いている例
#eval show IO Unit from do
  IO.println "Hello, world!"
  IO.println "Goodbye, world!"

また、Lean においては命題 P : Prop は型なので、命題 P の証明を show P from ... の形で構成することができます。

example (P : Prop) (h : P) : P :=
  show P from h

よく似た構文を持つものとして、show タクティクがあります。

用途

show .. from 構文を使用すると、have タクティクのように補題を用意することができますが、have と違って補題に名前がつかず、使い捨てになります。たとえば rw タクティクに渡すために一度だけ使いたい補題があるときに有用です。

variable (a b x : ℚ)

example (f : ℚ → ℕ) : f ((a + b) ^ 2) = f (a ^ 2 + 2 * a * b + b ^ 2) := by
  -- `have` をつかって補題を用意しなくても、
  -- `show ... from` で無名の証明を構成してそれを `rw` に渡すことができる
  rw [show (a + b) ^ 2 = a ^ 2 + 2 * a * b + b ^ 2 from by ring]

また証明項のところに メタ変数(metavariable) 1を配置すると、証明を後回しにすることができます。

example (h : a * x < b) (ha : a > 0) : x < b / a := by
  -- `b / a` を `r` とおく
  set r := b / a with hr

  -- ここで `b = a * r` というまだ示していない補題に基づいて `h` を書き換える
  rw [show b = a * r from ?lem] at h
  exact (Rat.mul_lt_mul_left ha).mp h

  -- 本来証明項が入るべきところに `?lem` をおいたので、
  -- `case lem` でフォーカスできる
  case lem =>
    -- `r` の定義を展開する
    rw [hr]

    -- 分母を払う
    field_simp

  1. 頭に ? がついている変数のこと。

Σ

Σ は、依存ペア型(dependent pair type) を表します。α : Type u という添え字族によって添え字付けられた型の族 β : α → Type v があるとき、Σ (a : α), β aα の要素とそれに対応する β a の要素のペアの型を表します。

たとえば、[(Nat, 1), (String, "hello"), (Bool, true)] というようなリストを考えてみます。 このリストに正しく型を付けるにはどうすればいいでしょうか? 通常の直積型では、「右の要素の型が左の要素の型に依存する」ことが許されないので、型を付けることができません。

#check_failure [(Nat, 1), (String, "hello"), (Bool, true)]

しかし、依存ペア型を使うことで解決できます。 この場合、リストの中身の型は Σ (α : Type), α になります。 これは、(α : Type) × α と同じ意味です。

example : (Σ (α : Type), α) = ((α : Type) × α) := by rfl

これを使うと、次のようにリストに型を付けることができます。

def sample : List ((α : Type) × α) :=
  [⟨Nat, 1⟩, ⟨String, "hello"⟩, ⟨Bool, true⟩]

依存和型

依存ペア型は、依存和型(dependent sum type) とも呼ばれます。

これは一見すると奇妙に見えます。(a : α) × β a という表記から見ると、掛け算(×)のように見えるからです。

しかし、依存ペア型がどのような関数の 図式(diagram) の中にいるのかを考えると、(つまり周辺の関数を見ると)和との類似が見えてきます。依存型ではない、ただの和型 A ⊕ B を考えると、この型への自然な関数 inl : A → A ⊕ Binr : B → A ⊕ B が存在し、これは単射です。

variable {A B : Type}

example : (Sum.inl : A → A ⊕ B).Injective := by
  intro a1 a2 h
  simp_all

example : (Sum.inr : B → A ⊕ B).Injective := by
  intro b1 b2 h
  simp_all

依存ペア型についても同様に、各構成要素からの自然な関数が存在します。添え字族 α : Type u と型の族 β : α → Type v があるとき、各構成要素 β a からの自然な関数 β a → (a : α) × β a が存在して、これは単射です。

/-- 依存和型への、各構成要素からの自然な関数 -/
def Sigma.inj {α : Type u} (β : α → Type v) (x : α) : β x → (a : α) × β a :=
  fun b => ⟨x, b⟩

/-- 自然な関数 `Sigma.inj β x` はすべての `x : α` に対して単射 -/
example {α : Type u} (β : α → Type v) (x : α) : (Sigma.inj β x).Injective := by
  intro b1 b2 h
  simpa [Sigma.inj] using h

このように周辺の関数との関係を見ると、依存ペア型はペア(積)ではなくて和のように振る舞っていることがわかります。

付録

付録です。以下のような用途で使います。

  • Lean の構文要素に対応しない話をする。
  • 各ページに載せるには少し長すぎる具体例を紹介する。
  • 用語の解説をする。
  • 数学の背景知識の解説をする。

カントールの定理

カントールの定理(Cantor’s theorem)とは、集合論における基本的な定理で、どんな集合 X に対しても、そのベキ集合 P(X) の方が真に大きいと主張するものです。Lean では集合論における集合そのものは普通扱わないのですが、型の世界でも同様のことが成り立ちます。

集合の大きさ比較

X が有限集合の場合、これは明らかです。X の要素数が n だとしたら、ベキ集合 P(X) の要素数は 2^n になるからです。この定理の重要なところは、無限集合でも成り立つというところです。

その場合、無限集合の要素数を比較する必要がありますが、集合 X, Y に対して以下のように考えます。

  • XY の間に全単射が存在するならば、XY の要素数は同じである。
  • X から Y への全射が存在しないならば、X の要素数は Y より小さい。(単射による定義と同じ)
  • X から Y への単射が存在しないならば、X の要素数は Y より大きい。(全射による定義と同じ)

XY の間にどのような写像が存在するかを見て、有限集合の場合の定義を拡張するわけです。

型の世界への翻訳

Lean は集合論の代わりに型理論を用いますが、型の世界でも同様のことが成り立ちます。

定理のステートメントに登場する集合 X のことは単に型 X : Type u と読み替えれば良いです。ベキ集合をどう表現するかですが、部分集合 A ⊆ X というものを「X の項 x : X に対して、それが A に属するかどうかを表す命題 x ∈ A を対応付けるもの」と読み替えることができて、関数型 X → Prop をベキ集合だと見做すことができます。

/-- ベキ集合 -/
def Set (X : Type u) := X → Prop

全射や単射は Lean に組み込みで定義されているものがあるので、これでカントールの定理のステートメントを述べることができます。

open Function

/-- カントールの定理の全射版 -/
theorem cantor_surjective (f : X → Set X) : ¬ Surjective f := by
  sorry

/-- カントールの定理の単射版 -/
theorem cantor_injective (f : Set X → X) : ¬ Injective f := by
  sorry

集合の内包記法の用意

このまま証明をすることもできるのですが、このままだと記号が数学での表記と違い過ぎてわかりにくいので、記法を用意します。

まず A : Set X に対して x ∈ A と書けるようにします。

instance : Membership X (Set X) where
  mem := fun A x => A x

-- テスト
#check
  let A : Set Nat := fun x => x < 5
  3 ∈ A

次に、{x : X ∣ P x} という集合内包表記を用意します。これは macro_rules を使用してマクロとして用意します。

/-- 述語から、それが内包によって定める部分集合を作る -/
def setOf (P : X → Prop) : Set X := P

/-- binder という構文カテゴリ。
これは変数束縛を表していて、
`{x : X | P x}` の `x : X` の部分とか
`{x ∈ X | P x}` の `x ∈ X` の部分とかを表している -/
declare_syntax_cat binder

/-- `{x : T | P x}` の `: T` の部分。
あってもなくても良いので `( )?` で囲う -/
syntax ident (" : " term)? : binder

/-- `{x ∈ T | P x}` の `∈ T` の部分。
あってもなくても良いので `( )?` で囲う -/
syntax ident (" ∈ " term)? : binder

/-- 集合の内包表記 -/
syntax "{" binder "|" term "}" : term

/-- `{x : T | P x}` と `{x ∈ T | P x}` の形の式を `setOf` の式に変換する -/
macro_rules
  | `({ $var:ident | $body:term }) => `(setOf (fun $var => $body))
  | `({ $var:ident : $ty:term | $body:term }) => `(setOf (fun ($var : $ty) => $body))
  | `({ $var:ident ∈ $s:term | $body:term }) => `(setOf (fun $var => $var ∈ $s ∧ $body))

-- テスト
#check
  let A : Set Nat := {x | x % 2 = 0}
  4 ∈ A

全射版の証明

さて、それでは証明を書いていきましょう。全射版も単射版も証明は同じアイデアでできるのですが、全射版の方が素直に証明できるので全射版を証明しましょう。

open Function

/-- カントールの定理の全射版 -/
theorem cantor_surjective (f : X → Set X) : ¬ Surjective f := by
  -- 仮に全射 `f : X → Set X` が存在したとする
  intro surjf

  -- ここで、自分自身の `f` の像に入らないような `x : X` を集めてきてそれを `T` とする
  let T : Set X := {x | x ∉ f x}

  -- `f` は全射だと仮定したので、`T` は `f` の像にあるはず
  -- そこで `f t = T` となる `t : X` を取る
  obtain ⟨t, ht⟩ := surjf T

  -- `t` が `T` に入るかどうかを考える。
  -- まず、`t ∈ T` だと仮定すると矛盾が得られるので `t ∉ T` がわかる
  have t_nmem : ¬ (t ∈ T) := by
    -- `t ∈ T` だと仮定する
    intro mem

    -- `T` の定義から、`t ∉ f t` であるはず
    have nmem : t ∉ f t := by
      dsimp [T, (· ∈ ·), setOf] at mem ⊢
      assumption

    -- しかし、`f t = T` であることから、これは `t ∉ T` を意味する
    rw [ht] at nmem

    -- これは矛盾
    contradiction

  -- `T = f t` であることから、これは `t ∉ f t` を意味し、
  -- すなわち `t ∈ T` ということになる。
  have t_mem : t ∈ T := by
    rw [← ht] at t_nmem
    dsimp [T, (· ∈ ·), setOf] at *
    assumption

  -- これは矛盾
  contradiction

対角線論法

全射版カントールの定理の証明の中では、全射 f : X → Set X が存在すると仮定したときに、そこから {x | x ∉ f x} という集合を作ってくるところが核心でした。このテクニックは 対角線論法(diagonal argument) と呼ばれます。この議論を最初に考案したのは ゲオルク・カントール(Georg Cantor) であることから、カントールの対角線論法とも呼ばれます。

なんで「対角線」なのか疑問に思われたでしょうか?これは、図解してみるとわかります。

たとえば、X = {x₁, x₂, x₃, x₄, x₅} という有限集合を考えましょう。ここでどのような関数 f : X → Set X が与えられたとしても、f の像に入っていない部分集合 T : Set X を構成する具体的な手続きがあるかどうか考えてみましょう。

まず、X の部分集合は各 xᵢ を含んでいるか含んでいないかの長さ5のビット列で表すことができます。それぞれ 10 で表すことにすれば、X の部分集合はベクトルで表せます。たとえば {x₂, x₃, x₄}[0, 1, 1, 1, 0] のように表せますね。そう考えると、各 f : X → Set X は2次元の表で表すことができます。

たとえば、常に {x₁} という1点集合を返す恒等関数 f は次の表と対応します。

それでは、何か仮に f : X → Set X という関数が与えられたとしましょう。その f を表で表したら、次のような表になっていたとします。(これは具体的に図を描かなければいけないので仮に決めただけで、本当はどんな関数でもよいです)

そうしたら、この表の対角線部分に着目します。

[1, 0, 0, 1, 1] というビット列が得られますね。これを各ビット反転させます。01 に、10 にするわけです。そうすると、[0, 1, 1, 0, 0] というビット列が得られます。これに対応する部分集合は {x₂, x₃} ですね。

そうすると、こうして得られた部分集合 T := {x₂, x₃}f の像に入っていないことがわかります。この例では X は有限集合なので全部チェックすればわかりますが、X が無限集合であろうと何だろうと必ずそうなります。なぜかというと、どの x : X に対しても f xT は「x が属しているかどうか」が逆になるからです。したがって Tf の表には絶対に登場しないと断言することができ、したがって f は全射ではありえないと結論付けることができます。

このようにして得られた T こそ、カントールの定理(全射版)の証明で登場した {x | x ∉ f x} そのものです。だから、対角線論法と呼ばれるわけですね。

クワイン

そのプログラムを実行した時の標準出力が自分自身と等しくなるようなコードを書くことができます。そのようなコードを クワイン(Quine) と呼ぶのですが、Lean 4 ではクワインはたとえば次のようにして作ることができます。1

def s := "\ndef main : IO Unit := do\n  IO.println (\"def s := \" ++ s.quote)\n  IO.println (s)"

def main : IO Unit := do
  IO.println ("def s := " ++ s.quote)
  IO.println (s)

  1. このコード例は leanprover-community/lean4-samples に対するPR からの引用です。

じゃんけんゲーム

じゃんけんゲームを CLI で実装する例を紹介します。以下のような仕様で実装しましょう。

  • ユーザはテキストで「グー」「チョキ」「パー」のいずれかを入力する。
  • コンピュータはランダムに手を選ぶ(後出ししているように見えるが、実際にはランダム)
  • あいこだった場合は勝負がつくまで何度でも繰り返す。

ステップ1: ユーザが入力した手を表示する

Lean でプログラムを書く際はだいたいいつもそうですが、必要なデータ型を定義するところから始めます。じゃんけんの手を表す型 Hand を定義しましょう。グー・チョキ・パーの3種類です。

/-- じゃんけんの手 -/
inductive Hand where
  /-- グー -/
  | gu
  /-- チョキ -/
  | choki
  /-- パー -/
  | pa
deriving BEq, Inhabited

protected def Hand.toString : Hand → String
  | .gu => "グー"
  | .choki => "チョキ"
  | .pa => "パー"

instance : ToString Hand where
  toString := Hand.toString

次に、ユーザから受け取る文字列をパースして Hand に変換する関数を用意しましょう。文字列のパースに成功するとは限りませんが、その場合は成功するまで繰り返し何度でも入力を促すことにします。

/-- ユーザからの入力を受け取る -/
def getUserInput : IO String := do
  -- 入力待ちを表す記号を表示
  IO.print "> "

  let stdin ← IO.getStdin
  let input ← stdin.getLine
  let result := input.trimAscii.copy
  return result

/-- 文字列を読んで Hand に変換する -/
def parseHand (input : String) : Option Hand :=
  match input with
  | "グー" => some Hand.gu
  | "チョキ" => some Hand.choki
  | "パー" => some Hand.pa
  | _ => none

/-- ユーザからの入力を受け取ってパース済みの値を返す。
パースに失敗した場合は、成功するまで繰り返す -/
partial def getUserHand : IO Hand := do
  let userInput ← getUserInput
  let some hand := parseHand userInput |
    IO.println "そんな手はないよ!もう一回!"
    getUserHand
  return hand

ここまで終わったら、いったんここまでのコードを使って main 関数を実装してみます。

def main : IO Unit := do
  IO.println "私とじゃんけんをしよう!"
  IO.println "グー、チョキ、パーのどれかを選んで!"
  let yourHand ← getUserHand
  IO.println s!"{yourHand}を選んだんだね!"

この段階でもしうまく行かなければ、日本語の文字が文字化けしている可能性があります。インストール方法のターミナルでの文字化けの修正方法を試してみてください。

ステップ2: コンピュータとの勝負を実装する

次にやるべきことは、コンピュータの手をランダムに選ぶことです。Lean には乱数を生成するための関数が用意されているので、それを使います。

/-- ランダムに手を選ぶ -/
def getRandomHand : IO Hand := do
  let n ← IO.rand 1 3
  let hand :=
    match n with
    | 1 => Hand.gu
    | 2 => Hand.choki
    | 3 => Hand.pa
    | _ => unreachable!
  return hand

-- ちゃんと3つの手が全部出るか確認する
#eval List.range 10 |>.mapM (fun _ => getRandomHand)

問題がなければ、勝敗判定を実装します。

/-- h1 が h2 に勝つかどうかを判定する -/
def Hand.beat (h1 h2 : Hand) : Bool :=
  match h1, h2 with
  | .gu, .choki => true
  | .choki, .pa => true
  | .pa, .gu => true
  | _, _ => false

ここまで終われば、最後まで実装することができるでしょう。 あいこだった場合は、勝負がつくまで何度でも繰り返すことにします。

open Hand

def main : IO Unit := do
  IO.println "私とじゃんけんをしよう!"
  IO.println "グー、チョキ、パーのどれかを選んで!"

  let mut finish := false
  while !finish do
    let yourHand ← getUserHand

    let cpuHand ← getRandomHand
    IO.println s!"私は{cpuHand}を出したよ!"

    if beat yourHand cpuHand then
      IO.println "あなたの勝ち!負けちゃったなぁ"
      finish := true
    else if beat cpuHand yourHand then
      IO.println "私の勝ち!うれしい!"
      finish := true
    else
      IO.println "あいこだね!もう一勝負!"

三目並べ

Lean で、CLI ゲームとして三目並べを実装してみましょう。

盤面を定義する

まずは三目並べの盤面を作ってみます。盤面がどのようなものが考えてみると、次のようなものです。

  1. 盤面は9マスある
  2. 盤面の各マスは、各プレイヤーの着手したマークが入っているか、あるいは空であるかどちらか
  3. 最初は全てのマスが空

盤面は 3×3 の二次元的な構造を持っているのですが、サイズが小さいので二次元配列として持つよりも一次元配列として持った方が簡単です。そこで、盤面は長さ9のベクトル(長さが固定された配列)として持つことにします。

/-- プレイヤー -/
inductive Player where
  /-- 先手 -/
  | x
  /-- 後手 -/
  | o

/-- 盤面 -/
abbrev Board := Vector (Option Player) 9

/-- 盤面の初期状態 -/
def Board.initial : Board := Vector.replicate 9 none

盤面を表示する

次に、盤面を表示できるようにしましょう。既に埋まっている部分はそのまま表示し、空の部分にはユーザが着手場所を選ぶときわかりやすいように、番号を振って表示することにします。

/-- Player を文字列として表示する。
数字の `0` と `Player.o` の区別がつきやすいようにしてある -/
instance : ToString Player where
  toString := fun p =>
    match p with
    | .x => "×"
    | .o => "●"

/-- 配列を二次元配列に変換する。
サイズが期待と異なる場合は `panic!` を呼ぶ -/
def Array.reshape (m n : Nat) (xs : Array α) : Array (Array α) :=
  if xs.size ≠ m * n then
    panic! s!"{decl_name%}: size mismatch"
  else
    Array.range m |>.map (fun i => xs.extract (n * i) (n * (i + 1)))

#guard
  let actual := Array.reshape (m := 2) (n := 3) #[1, 2, 3, 4, 5, 6]
  let expected := #[#[1, 2, 3], #[4, 5, 6]]
  actual = expected

/-- 配列の要素の間に区切りを挿入する -/
def Array.intersperse (xs : Array α) (sep : α) : Array α :=
  match xs.toList with
  | [] => #[]
  | x :: rest =>
    rest.foldl (fun acc y => acc ++ #[sep, y]) #[x]

#guard Array.intersperse #[1, 2, 3] 0 = #[1, 0, 2, 0, 3]

/-- 盤面を表示するための補助関数 -/
def Board.toStrArray (b : Board) : Array String :=
  let rawStrArray := b.toArray
    |>.zipIdx
    |>.map (fun (p?, idx) =>
      match p? with
      | none => toString idx
      | some p => toString p)
  let rows2D := rawStrArray.reshape (m := 3) (n := 3)
  let rows1D := rows2D.map (fun row =>
    let inner := String.intercalate " | " row.toList
    s!"| {inner} |"
  )
  let boader := "+---+---+---+"
  #[boader] ++ rows1D.intersperse boader ++ #[boader]

/-- 盤面を表示する関数。
IO 部分を少なくするため、補助関数を薄く包むだけにしてある -/
def Board.display (b : Board) : IO Unit := do
  for str in b.toStrArray do
    IO.println str

-- テスト用に、盤面を簡便に構成するための補助定義
def X : Option Player := Player.x
def O : Option Player := Player.o
def E : Option Player := none

/-
info:
+---+---+---+
| × | ● | 2 |
+---+---+---+
| 3 | × | 5 |
+---+---+---+
| ● | 7 | × |
+---+---+---+
-/
#eval
  let board : Board := #v[
    X, O, E,
    E, X, E,
    O, E, X
  ]
  Board.display board

盤面の勝敗判定をする

次に、盤面の勝敗判定を実装してみます。勝敗は、以下の3通りのどれかになります。

  1. 引き分け
  2. X を持っているプレイヤーの勝ち
  3. O を持っているプレイヤーの勝ち
/-- ゲームの結果 -/
inductive Result where
  /-- `winner` を持っているプレイヤーの勝ち -/
  | win (winner : Player)
  /-- 引き分け -/
  | draw

/-- 勝敗判定に使われるライン -/
abbrev Line := Vector (Fin 9) 3

/-- 勝敗判定に使われるラインを全部列挙したもの -/
def allLines : Array Line :=
  let row0 := #v[0, 1, 2]
  let row1 := #v[3, 4, 5]
  let row2 := #v[6, 7, 8]
  let col0 := #v[0, 3, 6]
  let col1 := #v[1, 4, 7]
  let col2 := #v[2, 5, 8]
  let diag1 := #v[0, 4, 8]
  let diag2 := #v[2, 4, 6]
  #[row0, row1, row2, col0, col1, col2, diag1, diag2]

def checkLine (line : Line) (board : Board) (P : Player → Bool) : Bool :=
  line.all (fun pos =>
    let player? := board[pos]
    (P <$> player?).getD false
  )

def Board.checkForLines (board : Board) (P : Player → Bool) : Bool :=
  allLines.any (checkLine · board P)

-- Player 等の DecidableEq インスタンスを自動生成する
deriving instance DecidableEq for Player, Result

/-- 盤面の勝敗判定をする。
まだゲームが続けられる場合は `none` を返す。 -/
@[grind =]
def Board.result? (board : Board) : Option Result :=
  let wins : Player → Bool :=
    fun p => board.checkForLines (· = p)
  let draw := board.all (·.isSome)
  if wins .x then
    some <| .win (Player.x)
  else if wins .o then
    some <| .win (Player.o)
  else if draw then
    some .draw
  else
    none

#guard
  let board : Board := #v[
    X, O, X,
    O, X, O,
    O, X, X
  ]
  board.result? = some (Result.win Player.x)

#guard
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  board.result? = none

着手の実装

Option を返すバージョン

盤面のまだ着手されていない場所を選んで、着手ができるようにしましょう。着手しようとしている場所が既に着手済みの場合にどうするかという問題がありますが、とりあえず素直なのは none を返すようにすることでしょうか。

/-- 盤面上の場所 -/
abbrev Position := Fin 9

/-- 盤面上の場所 `move` にプレイヤー `p` が手を置き、新しい盤面を返す。
ただし、既に着手されている場合は `none` を返す。 -/
def Board.place? (board : Board) (move : Position) (p : Player) : Option Board :=
  match board[move] with
  | some _ => none
  | none => some (board.set move (some p))

#guard show Bool from Id.run do
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  let .some actual := board.place? 2 Player.x
    | return false
  let expected : Board := #v[
    X, O, X,
    E, E, E,
    O, E, X
  ]
  actual == expected

#guard
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  (board.place? 1 Player.x).isNone

合法性の証明を受け取るバージョン

返り値を Option に包むのは簡便ですが、着手済みでないことが分かっている場合でも Option を剥がす処理を書かなければいけなくなります。これを回避することができるように、「着手箇所が空であることの証明」を引数に持たせるバージョンも定義しておきましょう。

/-- 合法な着手 -/
structure LegalMove (b : Board) where
  /-- 盤面 `b` 上の着手箇所 -/
  move : Position
  /-- 着手箇所が空であることの証明 -/
  proof : b[move] = none

/-- `LegalMove` の項を作るための補助関数。
`move` が具体的な数であれば、`decide` により自動的に証明が生成されて通る。 -/
def Board.legalCheck (b : Board) (move : Position) (h : b[move] = none := by decide) : LegalMove b :=
  { move := ⟨move, by simp⟩, proof := h }

/-- 合法な着手箇所が与えられたときに、そこに着手した新しい盤面を返す。-/
def Board.place (board : Board) (move : LegalMove board) (p : Player) : Board :=
  board.set move.move (some p)

/-
info:
+---+---+---+
| × | ● | × |
+---+---+---+
| 3 | 4 | 5 |
+---+---+---+
| ● | 7 | × |
+---+---+---+
-/
#eval
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  let legalMove := board.legalCheck 2
  let newBoard := board.place legalMove Player.x
  Board.display newBoard

合法手の列挙

将来的に「その盤面において合法な手を全列挙する」という関数が欲しくなることが予想されるので、それも実装しておきます。

/-- 合法な着手箇所を全列挙する -/
def Board.legalMoves (b : Board) : List (LegalMove b) :=
  List.finRange 9 |>.filterMap (fun pos =>
    match h : b[pos] with
    | none => some { move := pos, proof := h }
    | some _ => none
  )

#guard
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  board.legalMoves.map (fun legalMove => legalMove.move.val) = [2, 3, 4, 5, 7]

この関数の実装は何も見ずに行うとするとおそらく難しいでしょう。ポイントは List.finRange を使用することです。

補題の用意

後で必要になるので、合法手を全列挙する関数 legalMoves に関する補題をいくつか用意しておきます。おもに「ゲームが進行中なら、合法な手が存在する」というのが欲しい命題です。

/-- ゲームが進行中 -/
@[grind =]
def Board.inProgress (board : Board) : Bool :=
  board.result? = none

theorem Board.noLegalMove_iff_allSome (board : Board) : board.legalMoves = [] ↔ board.all Option.isSome := by
  unfold Board.legalMoves
  simp only [
    Fin.getElem_fin,
    List.filterMap_eq_nil_iff,
    List.mem_finRange,
    forall_const,
    Vector.all_eq_true
  ]
  constructor
  · intro h pos hpos
    specialize h ⟨pos, hpos⟩
    grind
  · intro h
    grind

grind_pattern Board.noLegalMove_iff_allSome => board.legalMoves, []

theorem Board.notInProgress_of_allSome (board : Board) (h : board.all Option.isSome) : ¬board.inProgress := by
  unfold Board.inProgress
  simp [Board.result?]
  grind only [= Vector.all_eq]

grind_pattern Board.notInProgress_of_allSome => board.all Option.isSome, board.inProgress

theorem Board.legalMoveExists (board : Board) (h : board.inProgress) : board.legalMoves ≠ [] := by
  intro hEmpty
  have : board.all Option.isSome := by grind
  grind only [usr notInProgress_of_allSome]

grind_pattern Board.legalMoveExists => board.legalMoves, board.inProgress

CPU の思考アルゴリズムの実装

次はいよいよ、CPU の思考アルゴリズムを実装します。単に「ランダムに手を選ぶ」ような CPU を実装しても良いのですが、せっかくなので賢い CPU を実装してみます。次のような方針で実装しましょう。

  1. 次に着手できる場所をすべてリストアップする。
  2. 各盤面の評価値(自分が有利なのか不利なのかを表す値)を計算して、最も評価値が高いものを選ぶ。

評価値の計算

問題は評価値の計算方法ですが、以下のような方針で計算することができます。

  1. 自分が勝ちの局面では 10
  2. 相手が勝ちの局面では -10
  3. 引き分けの場面では 0
  4. それ以外の局面では、「自分も相手も最善手を打った場合にどうなるか」を考えて決める。 具体的には、次の手で到達可能な局面をまずすべて調べる。 そして自分の手番なら評価値が最大になる手を選んで、その評価値を採用する。 相手の手番なら自分から見た評価値が最小になる手を選んで、その評価値を採用する。
  5. なるべく短い手順で勝利するほど、高い評価値を与えるようにする

これを素直に実装すると、相互再帰関数 になります。

/-- そのプレイヤーの対戦相手 -/
def Player.opponent : Player → Player
  | .x => .o
  | .o => .x

/-- 現在手番を持っているプレイヤー -/
def Board.current (b : Board) : Player :=
  let xCount := b.count (some Player.x)
  let oCount := b.count (some Player.o)
  if xCount ≤ oCount then
    Player.x
  else
    Player.o

/-- 盤面の複雑さの指標。
なるべく短い手順で勝利するほど、高い評価値を与えるために使用 -/
def Board.depth (b : Board) : Nat :=
  b.toArray
    |>.filter Option.isSome
    |>.size

/-- 空であるという性質は、`map` をかましても変わらない。 -/
@[grind norm]
theorem List.map_respect_emptiness (xs : List α) (f : α → β) : xs.map f = [] ↔ xs = [] := by
  simp only [← length_eq_zero_iff, length_map]

mutual

/-- この盤面で `p` が手番を持っている場合に、`p` から見た盤面の評価値 -/
partial def Board.maxScore (board : Board) (p : Player) : Int :=
  match h : board.result? with
  | some (Result.win winner) =>
    if winner = p then 10 - board.depth else board.depth - 10
  | some Result.draw => 0
  | none =>
    let nextMoves := board.legalMoves
    let nextBoards := nextMoves.map (board.place · p)
    let nextScores := nextBoards.map (fun b => Board.minScore b p)

    -- `List.max` 関数は空でないリストに対してしか使えないので、
    -- 空でないことを証明する必要がある。
    have h : nextScores ≠ [] := by grind

    nextScores.max h

/-- この盤面で `p` の対戦相手が手番を持っている場合に、`p` から見た盤面の評価値 -/
partial def Board.minScore (board : Board) (p : Player) : Int :=
  match h : board.result? with
  | some (Result.win winner) =>
    if winner = p then 10 - board.depth else board.depth - 10
  | some Result.draw => 0
  | none =>
    let nextMoves := board.legalMoves
    let nextBoards := nextMoves.map (board.place · p.opponent)
    let nextScores := nextBoards.map (fun b => Board.maxScore b p)

    -- `List.min` 関数は空でないリストに対してしか使えないので、
    -- 空でないことを証明する必要がある。
    have h : nextScores ≠ [] := by grind

    nextScores.min h

end -- mutual の終わり

/-- プレイヤー `p` から見た盤面の評価値 -/
def Board.score (board : Board) (p : Player) : Int :=
  if board.current = p then
    Board.maxScore board p
  else
    Board.minScore board p

-- `x` から見ると、既に勝っている場面
#guard
  let board : Board := #v[
    X, O, E,
    E, X, E,
    O, E, X
  ]
  Board.score board Player.x > 0

-- `x` から見ると、あと一手で勝てる必勝局面
#guard
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  Board.score board Player.x > 0

-- `o` から見ると、あと一手で勝てる必勝局面
#guard
  let board : Board := #v[
    X, O, E,
    X, O, X,
    O, E, X
  ]
  Board.score board Player.o > 0

-- `o` が手番。
-- `o` は `x` の勝ちを防ぐことができるので引き分け
#guard
  let board : Board := #v[
    X, X, E,
    E, O, E,
    E, E, E
  ]
  Board.score board Player.x = 0

-- どちらの盤面も必勝局面だが、
-- 短い手順で勝利している方を高く評価する
#guard
  let board1 : Board := #v[
    O, E, E,
    X, X, X,
    O, E, E
  ]
  let board2 : Board := #v[
    O, X, E,
    X, X, E,
    O, E, E
  ]
  board1.score .x > board2.score .x && board2.score .x > 0

手の選択の実装

評価値が手に入ったら、後はその評価値が最大になる手を選ぶだけです。実際に実装してみましょう。

ここでは、複数の候補手が同じ評価値を持つ場合は最も左上にある手を選ぶことにします。そうすると関数が決定的になってテストがやりやすいからです。

@[simp, grind =]
theorem List.mergeSort_respect_nonEmpty (xs : List α) (f : α → α → Bool) :
    xs.mergeSort f = [] ↔ xs = [] := by
  simp only [← length_eq_zero_iff, length_mergeSort]

/-- `board.inProgress` の証明を埋めるために使うタクティク -/
macro "check_progress" : tactic => `(tactic| first
  | assumption
  | cbv
)

/-- 盤面 `board` において、プレイヤー `p` の最善手を探索する -/
def Board.selectBestMove (board : Board) (p : Player) (h : board.inProgress := by check_progress) : LegalMove board :=
  let nextMoves := board.legalMoves
  let scoredMoves := nextMoves.map (fun move =>
    let newBoard := board.place move p
    let score := Board.score newBoard p
    (move, score)
  )
  let sortedScoredMoves := scoredMoves.mergeSort (fun (pos1, score1) (pos2, score2) =>
    score1 > score2 || (score1 = score2 && pos1.move.val < pos2.move.val)
  )

  -- `List.head` 関数は空でないリストに対してしか使えないので、空でないことを証明する必要がある。
  have h : sortedScoredMoves ≠ [] := by grind

  let ⟨bestMove, score⟩ := sortedScoredMoves.head h
  bestMove

#guard show Bool from
  let board : Board := #v[
    X, O, E,
    E, E, E,
    O, E, X
  ]
  let move := Board.selectBestMove board Player.x
  let actual := board.place move Player.x
  let expected : Board := #v[
    X, O, E,
    E, X, E,
    O, E, X
  ]
  actual = expected

ユーザーの入力を受け取る

ここまで対話的な要素なしで実装を勧めてきましたが、そろそろユーザーからの入力を受け取れるようにしましょう。ユーザーからの入力は文字列の形で受け取ることになるので、それをパースする処理がまず必要です。

パース時には様々なエラーが起こりえますが、Except モナドを使うとエラーメッセージを伝播させる処理を簡単に書くことができます。

/-- 文字列を `Position` としてパースする -/
def Position.parse (s : String) : Except String Position :=
  let s := s.trimAscii.copy
  match s.toNat? with
  | none => throw s!"{s} は数字ではありません"
  | some n =>
    if h : n < 9 then
      return ⟨n, h⟩
    else
      throw s!"0~8 の範囲で入力してください"

/-- 文字列を `LegalMove` としてパースする -/
def LegalMove.parse (board : Board) (s : String) : Except String (LegalMove board) := do
  let s := s.trimAscii.copy
  let pos ← Position.parse s
  match h : board[pos] with
  | none => return { move := pos, proof := h }
  | some _ => throw s!"{s} は既に着手済みです"

後は、標準入力から文字列を受け取る処理を書くだけです。

/-- ユーザーから入力を受け取って合法な手を返す -/
partial def getUserInput (b : Board) : IO (LegalMove b) := do
  IO.print "> " -- 入力待ちの記号を出力する
  let stdin ← IO.getStdin
  let input ← stdin.getLine
  let move? := LegalMove.parse b input
  match move? with
  | Except.ok move => return move
  | Except.error err =>
    -- エラーだった時、即座に終了するのではなく、
    -- 正しい入力が返ってくるまで何回でも繰り返す
    IO.println err
    getUserInput b

ゲームのループを実装する

ここまで終わると、後はゲームのループを回すだけです。

ここまで大変でしたが、CPU が強過ぎるのでユーザーが勝つことはできません。残念ですね。

instance : ToString (LegalMove b) where
  toString move := toString move.move.val

def main : IO Unit := do
  let mut board := Board.initial
  -- ユーザーを先手とする
  let mut currentPlayer := Player.x

  while h : board.inProgress do
    if currentPlayer = Player.x then
      -- ユーザーの手番
      Board.display board
      let move ← getUserInput board
      board := board.place move currentPlayer
    else
      -- CPU の手番
      let move := board.selectBestMove currentPlayer
      IO.println s!"CPU は {move} に着手しました。"
      board := board.place move currentPlayer

    -- 手番を交代する
    currentPlayer := currentPlayer.opponent

  -- ゲーム終了後の結果表示
  IO.println "ゲーム終了!"
  board.display

  match board.result? with
  | some (Result.win winner) =>
    IO.println s!"勝者は {winner} です!"
  | some Result.draw =>
    IO.println "引き分けです!"
  | none =>
    throw <| .userError "予期せぬエラーが発生しました。"

嫉妬深い夫たちの川渡りパズル

嫉妬深い夫の問題(Jealous Husbands Problem) と呼ばれる、以下の古典的なパズルを Lean で解いてみましょう。

3 組の夫婦が川を渡らなければならないが、以下のような制約がある。

  • 船が一隻あるが、一度に2人までしか乗ることができない。
  • 船は、当然だが誰か漕ぐ人がいなければ動かすことができない。
  • 全ての人が船を漕ぐことができる。
  • どの男性も大変嫉妬深いので、川のこちら岸にいるときも、向こう岸にいるときも、船に乗っているときも、自分がいないときに自分の妻と他の男性が一緒にいることを許さない。

この制約条件のもとで全員が川を渡って向こう岸に辿り着くことはできるだろうか?

問題文を Lean で表現する

まずは問題文の状況を Lean で表現してみます。男性と女性が3人ずつ、合計で6人いるのですから次のような型を考えると良さそうです。

/-- 人間 -/
inductive Person where
  /-- 男性 -/
  | man (id : Fin 3)
  /-- 女性 -/
  | woman (id : Fin 3)
deriving Inhabited, BEq

Lean での実装がうまくいっているのか見るために、今いる登場人物全体、男性全体、女性全体といったものを表現できることを確かめておきましょう。

/-- 男性全体 -/
def men := [0, 1, 2].map Person.man

/-- 女性全体 -/
def women := [0, 1, 2].map Person.woman

/-- 登場人物全体 -/
def people := men ++ women

夫婦であるような男女ペアと、そうでない男女ペアが存在するというのも表現しなければいけません。配偶者を取得する関数を用意します。実装がシンプルになるので、同一 id を持つ男性と女性が夫婦であることにします。

/-- 配偶者を取得する -/
def Person.spouse (p : Person) : Person :=
  match p with
  | .man id => .woman id
  | .woman id => .man id

次に、人々の状態を表す型が欲しいです。川の両岸にいま誰がいて、船はどちらに停泊していて、船に誰が乗っていて、船がこちら岸と向こう岸のどちらにいるのか、といった状態をこの型で表現したいです。

/-- 岸 -/
inductive Bank where
  /-- こちら岸 -/
  | here
  /-- 向こう岸 -/
  | there
deriving Inhabited, BEq

/-- 人のいる場所 -/
inductive Place where
  /-- 岸にいる -/
  | ofBank (bank : Bank)
  /-- 船に乗っている -/
  | boat
deriving Inhabited, BEq

/-- 人々の状態 -/
structure State where
  /-- 各人がいる場所 -/
  place : Person → Place

  /-- 船がどちらの岸にいるか -/
  boat : Bank
deriving Inhabited

これで、各時点での状態を表現することができました。初期状態とゴール状態を定義することができるか、確かめておきましょう。

/-- 初期状態。全員こちら岸にいて、船もこちら岸にある -/
def initial : State :=
  { place := fun _ => Place.ofBank .here, boat := .here }

/-- ゴール状態。全員向こう岸にいて、船も向こう岸にある -/
def final : State :=
  { place := fun _ => Place.ofBank .there, boat := .there }

状態の表示

このままだと s : State を見やすく表示する方法が存在せずデバッグに困るので、表示方法を指定しておきます。

protected def Person.toString (p : Person) : String :=
  match p with
  | .man id => s!"🚹{id}"
  | .woman id => s!"🚺{id}"

instance : ToString Person := ⟨Person.toString⟩

protected def State.toString (s : State) : String :=
  -- こちら岸にいる人
  let peopleHere := people.filter (fun p => s.place p == .ofBank .here)
  -- 向こう岸にいる人
  let peopleThere := people.filter (fun p => s.place p == .ofBank .there)
  -- 船の位置と、船に乗っている人
  let peopleOnBoat := people.filter (fun p => s.place p == .boat)
  let boat :=
    match s.boat with
    | .here => s!"__{peopleOnBoat}🚢"
    | .there => s!"🚢{peopleOnBoat}__"
  s!"{peopleThere}{boat}{peopleHere}"

instance : ToString State := ⟨State.toString⟩

/- info: "[]__[]🚢[🚹0, 🚹1, 🚹2, 🚺0, 🚺1, 🚺2]" -/
#eval toString initial

/- info: "[🚹0, 🚹1, 🚹2, 🚺0, 🚺1, 🚺2]🚢[]__[]" -/
#eval toString final

状態の遷移

次は、各状態から「どの状態へは1ステップで遷移できて、どの状態へはできないのか」を表現したいですね。それができれば、状態遷移のグラフが得られたことになり、そのグラフを 幅優先探索(breadth first search) すれば、初期状態からゴール状態までの最短経路が求まるからです。

まず、ある状態が問題文の条件を満たしているかどうかを判定する関数を用意しましょう。

/-- 状態が問題文の条件を満たす。
* 船には2人までしか乗っていない
* すべての女性は「自分の夫と一緒にいるか、あるいはどの男性とも一緒にいない」 -/
def State.isValid (s : State) : Bool :=
  let boatCond : Bool := people
    |>.filter (fun p => s.place p == .boat)
    |> (List.length · ≤ 2)
  let womenCond : Bool :=
    women.all (fun w =>
      s.place w == s.place w.spouse || -- 自分の夫と一緒にいる
      men.all (fun m => s.place m != s.place w) -- どの男性とも一緒にいない
    )
  boatCond && womenCond

-- テスト。初期状態とゴール状態は条件を満たす
#guard initial.isValid
#guard final.isValid

以降、問題文にある条件を満たすことを単に「妥当である」ということにします。

次に、ある状態から1ステップで遷移できる妥当な状態を全列挙する関数を用意しましょう。1ステップでできることというのは、次の3通りですね。

  • 船に乗っている人を1人岸に下ろす
  • 岸にいる人を1人船に乗せる
  • 誰かが船を漕いで対岸に移動させる

(船に2人乗せるのは、1人乗せるのを繰り返せば実現できるのでここでは1ステップと認めないことにしました)

/-- `p` さんの乗船状態をトグルして新しい状態を得る。
`p` さんが乗船済みであれば降ろし、`p` さんが岸にいるなら船に乗せる。

以下の場合は失敗して `none` を返す
* `p` さんがいる岸に船が停泊していない場合。
* 得られた状態が妥当な状態ではない場合。
-/
def State.putOff (s : State) (p : Person) : Option State :=
  let candidate? : Option State :=
    match s.place p with
    -- p さんが船に乗っている場合
    | .boat =>
      let newPlace : Person → Place := fun x =>
        if x == p then
          -- p さんを船から降ろす
          .ofBank s.boat
        else
          -- 他の人はそのまま
          s.place x
      some { s with place := newPlace }

    -- p さんが岸にいる場合
    | .ofBank bank =>
      if bank != s.boat then
        -- p さんがいる岸に、船が停まっていない場合は、乗れない
        none
      else
        let newPlace : Person → Place := fun x =>
          if x == p then
            -- p さんを船に乗せる
            .boat
          else
            -- 他の人はそのまま
            s.place x
        some { s with place := newPlace }
  match candidate? with
  | .some state =>
    if state.isValid then some state else none
  | .none => none

/-- 対岸を取得する -/
def Bank.toggle (b : Bank) : Bank :=
  match b with
  | .here => .there
  | .there => .here

/-- 船を対岸に移動させる。
ただし、誰かが船に乗っていなければ失敗して `none` を返す -/
def State.boatTrip (s : State) : Option State :=
  if people.any (fun p => s.place p == .boat) then
    some { s with boat := s.boat.toggle }
  else
    none

/-- ある状態から、次に1ステップで遷移可能な妥当な状態を全列挙する。-/
def State.nextStates (s : State) : List State :=
  people
    |>.map (State.putOff s ·)
    |> (s.boatTrip :: ·)
    |>.reduceOption

幅優先探索

後は、予告したように幅優先探索を行います。

幅優先探索は、グラフを探索してあるノードから別のノードへの最短経路を求めるためによく用いられるアルゴリズムです。幅優先探索は、グラフを探索しながら「次に訪れるべきノード」を何かしらのキューに保存していくことによって実装できます。Lean では Std.Queue が標準的なキューの実装として用意されているので、それを使います。

話をできるだけ一般的にするために、ここでは Graph という型クラスを定義しておいて、それに対して幅優先探索アルゴリズムを実装します。

class Graph (α : Type u) where
  /-- ノード `v` に隣接しているノードのリストを返す -/
  neighbors : α → List α

variable [BEq α] [Hashable α]

open Std

/-- あるノード `s` から始めて、ノード `t` に到達するまでの幅優先探索を行う。
後で経路を復元するために、親ノードを記録した辞書を返す -/
def Graph.bfs [Graph α] (s t : α) : HashMap α α := Id.run do
  -- キューを空の状態で初期化
  -- このキューは「これから訪問するべきノード」を管理する
  let mut q : Queue α := ∅

  -- 親ノードを記録する辞書
  let mut parent : HashMap α α := ∅

  -- 訪問済みであるかどうか管理する集合
  let mut visited : HashSet α := ∅

  -- 初期ノードをキューに追加
  q := q.enqueue s
  visited := visited.insert s

  -- キューが空になるまでループ
  while !q.isEmpty do
    -- キューからノードを取り出す
    let some (v, q') := q.dequeue? | unreachable!
    q := q'

    -- 目的のノードに出会ったら探索を終了する
    if v == t then
      break

    for u in Graph.neighbors v do
      if u ∉ visited then
        visited := visited.insert u
        parent := parent.insert u v
        q := q.enqueue u

  return parent

/-- グラフの親を記録した辞書から、ノード `t` への経路を復元する -/
def constructPath (parent : HashMap α α) (t : α) : List α := Id.run do
  let mut path : List α := []
  let mut cur? : Option α := t

  while cur?.isSome do
    let some cur := cur? | unreachable!
    path := cur :: path
    cur? := parent[cur]?

  path

/-- 幅優先探索によって、ノード `s` からノード `t` への最短パスを求める -/
def Graph.findShortestPath [Graph α] (s t : α) : List α :=
  let parent := Graph.bfs s t
  constructPath parent t

探索の実行

後は、State に対して幅優先探索を実行しましょう。まず、Graph のインスタンスにします。

instance : Graph State where
  neighbors := State.nextStates

次に BEq のインスタンスにします。

/-- 2つの状態が等しいかどうかを判定する -/
def State.beq (s1 s2 : State) : Bool :=
  s1.boat == s2.boat && people.all (fun p => s1.place p == s2.place p)

/-- `==` という記号が使えるようにする -/
instance : BEq State := ⟨State.beq⟩

経路を出力するために辞書を使う関係で、Hashable のインスタンスも必要です。

deriving instance Hashable for Bank, Place

protected def State.hash (s : State) : UInt64 :=
  let places := .ofBank s.boat :: people.map s.place
  Hashable.hash places

instance : Hashable State := ⟨State.hash⟩

以上の準備の下で、計算を実行することができます。やってみるとパスが見つかるので、全員が川を渡ることは可能であることがわかります。

open Std

/-
info:
[]__[]🚢[🚹0, 🚹1, 🚹2, 🚺0, 🚺1, 🚺2]
[]__[🚺0]🚢[🚹0, 🚹1, 🚹2, 🚺1, 🚺2]
[]__[🚹0, 🚺0]🚢[🚹1, 🚹2, 🚺1, 🚺2]
[]🚢[🚹0, 🚺0]__[🚹1, 🚹2, 🚺1, 🚺2]
[🚹0]🚢[🚺0]__[🚹1, 🚹2, 🚺1, 🚺2]
[🚹0]__[🚺0]🚢[🚹1, 🚹2, 🚺1, 🚺2]
[🚹0]__[🚺0, 🚺1]🚢[🚹1, 🚹2, 🚺2]
[🚹0]🚢[🚺0, 🚺1]__[🚹1, 🚹2, 🚺2]
[🚹0, 🚺0]🚢[🚺1]__[🚹1, 🚹2, 🚺2]
[🚹0, 🚺0]__[🚺1]🚢[🚹1, 🚹2, 🚺2]
[🚹0, 🚺0]__[🚹1, 🚺1]🚢[🚹2, 🚺2]
[🚹0, 🚺0]🚢[🚹1, 🚺1]__[🚹2, 🚺2]
[🚹0, 🚹1, 🚺0]🚢[🚺1]__[🚹2, 🚺2]
[🚹0, 🚹1, 🚺0]__[🚺1]🚢[🚹2, 🚺2]
[🚹0, 🚹1, 🚺0]__[🚺1, 🚺2]🚢[🚹2]
[🚹0, 🚹1, 🚺0]🚢[🚺1, 🚺2]__[🚹2]
[🚹0, 🚹1, 🚺0, 🚺1]🚢[🚺2]__[🚹2]
[🚹0, 🚹1, 🚺0, 🚺1]__[🚺2]🚢[🚹2]
[🚹0, 🚹1, 🚺0, 🚺1]__[🚹2, 🚺2]🚢[]
[🚹0, 🚹1, 🚺0, 🚺1]🚢[🚹2, 🚺2]__[]
[🚹0, 🚹1, 🚹2, 🚺0, 🚺1]🚢[🚺2]__[]
[🚹0, 🚹1, 🚹2, 🚺0, 🚺1, 🚺2]🚢[]__[]
-/
#eval show IO Unit from do
  let path := Graph.findShortestPath initial final
  for p in path do
    IO.println p

選択ソート

実装

以下の疑似コードで表される、選択ソートというソートアルゴリズムがあります。(選択ソートと呼ばれるアルゴリズムにはバリエーションがあり、ここに挙げたものとは異なるものもあります)

  1. 1 番目の要素から最後尾の要素までで最も値の小さいものを探し、それを取り除いて 1 番目に置く
  2. 以降同様に、未ソート部分の最小要素を見つけだし、未ソート部分の先頭へ移す
  3. 未ソート部分が空になったら終了

これを Lean で実装すると、次のようになります。

-- ソートなので大小比較が必要
variable [LE α] [DecidableLE α]

-- リストから最小値を取り除くためには、要素が等しいかどうか判定できる必要がある
variable [DecidableEq α]

-- `List.min` で取得した最小値が実際に最小値であることを保証するのに必要
variable [Std.IsLinearPreorder α]

/-- `LE` インスタンスから `Min` インスタンスを作る -/
instance : Min α := minOfLe

grind_pattern List.le_min_iff => x ≤ _, l.min

namespace List

/-- リストの最小値を先頭に持ってくる -/
@[grind]
def minFirst (xs : List α) : List α :=
  match h : xs with
  | [] => []
  | x :: xs =>
    let μ := List.min (x :: xs) (h := by simp)
    have mem : μ ∈ x :: xs := by grind
    have min : ∀ y ∈ x :: xs, μ ≤ y := by grind
    let rest := List.erase (x :: xs) μ
    have len_eq : (μ :: rest).length = (x :: xs).length := by grind
    μ :: rest

-- 簡単な動作確認
#guard minFirst [3, 1, 4, 15, 9] = [1, 3, 4, 15, 9]

@[grind =]
theorem minFirst_length (xs : List α) :
    (minFirst xs).length = xs.length := by
  fun_cases minFirst xs with grind

/-- 選択ソート -/
def selectionSort (xs : List α) : List α :=
  let ys := minFirst xs
  have : ys.length = xs.length := by grind
  match hy : ys with
  | [] => []
  | z :: zs =>
    have : zs.length < ys.length := by grind
    z :: selectionSort zs
termination_by xs.length

-- 簡単な動作確認
#guard selectionSort [3, 1, 4, 15, 9] = [1, 3, 4, 9, 15]

end List

ソートであることの証明

ソートであることを証明するには、2つのことを証明する必要があります。並び替えになっていることと、昇順に並んでいることです。

並び替えであること

2つのリストが互いの順列であることは List.Perm を使って表現でき、~ という記号で表されます。

open scoped List in

example : [1, 2, 3] ~ [3, 2, 1] := by grind

証明は、grindfun_induction ですぐに終わります。

namespace List

/-- `minFirst` は元のリストの要素を並び替えるだけ -/
theorem minFirst_perm (xs : List α) :
    minFirst xs ~ xs := by
  fun_cases minFirst xs with grind

grind_pattern minFirst_perm => minFirst xs, minFirst _ ~ _

/-- `selectionSort` は元のリストの要素を並び替えるだけ -/
theorem selectionSort_perm (xs : List α) :
    selectionSort xs ~ xs := by
  fun_induction selectionSort xs with grind

grind_pattern selectionSort_perm => selectionSort xs

end List

昇順に並んでいること

昇順に並んでいることは、List.Pairwise を使って表現できます。これも、証明は grindfun_induction ですぐに終わります。

namespace List

@[grind ->]
theorem minFirst_spec (x : α) (xs ys : List α) (h : x :: xs = minFirst ys) :
    ∀ y ∈ xs, x ≤ y := by
  fun_cases minFirst ys with grind

theorem selectionSort_sorted (xs : List α) :
    (selectionSort xs).Pairwise (· ≤ ·) := by
  fun_induction selectionSort xs with grind [List.Pairwise]

end List

天使と悪魔の論理パズル

次の古典的な論理パズルを Lean で解いてみましょう。

目の前に2本の道がある。 片方は天国へ、片方は地獄へ続く。

そこに2人の番人がいて、一方は天使で、もう一方は悪魔である。 天使は常に真実を言い、悪魔は必ず嘘をつく。 どちらが天使で、どちらが悪魔なのかは分からない。

どちらか一人に一つだけ YES / NO で答えられる質問をすることが許されている。 なお番人はすべてを知っており、質問に「わからない」と返答することはないものとする。

天国へ続く道を特定するには、どのような質問をすればよいだろうか?

問題文の状況を Lean で表現する

Lean でプログラムを書くとき往々にしてそうであるように、まずは適切なデータ型を定義するところから始めましょう。2人の番人がいて、2つの道があるという状況は次のように書けます。

/-- 番人 -/
inductive Guardian where
  /-- 左の番人 -/
  | left
  /-- 右の番人 -/
  | right
deriving BEq, Repr

/-- 道 -/
inductive Road where
  /-- 左の道 -/
  | left
  /-- 右の道 -/
  | right
deriving BEq, Repr

問題のゴールを表現するのには、これに比べると少し自由度があります。「どのような質問をすればよいか?」という問いなので、意味のある質問文をすべて含むような型 Question を定義して、その中にある適切な質問文 q : Question を探索して見つけるという風に解釈しましょう。

そこで、「この問題の問題設定において、意味のある質問とはどのようなものか」を考える必要があります。まず確定で言えることとして、以下のような質問は可能であるべきでしょう。

  • 左の人は悪魔ですか?と訊く
  • あなたは天使なのかと訊く
  • 左の道は天国へ続く道なのかと訊く

ここで注意が必要なのは、「あなた」という言葉は、質問に答える相手によって指す番人が変わるという点です。そこで、番人を指す表現として「左の番人」「右の番人」のほかに、「あなた」「あなたではない方」を扱えるようにします。

また、p, q : Question が意味のある質問であるならば、「p または q ですか?」といった質問も意味を持つはずですね。そう考えると、質問全体の型 Question を、次のように定義できそうです。

/-- 質問文の中で番人を指す表現 -/
inductive GuardianRef where
  /-- 左の番人 -/
  | left
  /-- 右の番人 -/
  | right
  /-- 質問に答える番人自身 -/
  | you
  /-- 質問に答える番人ではない方 -/
  | other
deriving BEq, Repr

/-- 反対側の番人 -/
def Guardian.other (g : Guardian) : Guardian :=
  match g with
  | .left => .right
  | .right => .left

inductive Question where
  /-- `gr` は天使ですか?という質問 -/
  | angel (gr : GuardianRef)
  /-- `r` は天国へ続きますか?という質問 -/
  | toHeaven (r : Road)
  /-- `q` を否定した質問。
  「左の人は悪魔ですか?」とか、「左の道は地獄へ続きますか?」という質問ができる -/
  | not (q : Question)
  /-- `q` または `p` ですかという質問 -/
  | or (q p : Question)
  /-- `q` かつ `p` ですかという質問 -/
  | and (q p : Question)
  /-- `q` と `p` は同値ですかという質問 -/
  | iff (q p : Question)
deriving BEq, Repr

このようにして探索する対象の質問全体 Question が定義できたので、次は求める質問が満たすべき条件を定式化します。

問題文では「天国へ続く道を特定するにはどうすればいいか」という表現になっていますが、2人の番人のどちらが天使でどちらが悪魔なのかわからないのですから、「相手が天使であろうと悪魔であるかに関係なく、道の行く先だけを反映して答えが返ってくるような質問」であるというのが満たすべき条件だといえます。それをきちんと定式化すればよさそうです。

/-- 状態 -/
structure State where
  /-- 左の人と右の人、どちらが天使なのか。残り片方が悪魔 -/
  angel : Guardian
  /-- 左の道と右の道、どちらが天国へ続く道なのか。残り片方が地獄行き。 -/
  toHeaven : Road

/-- 質問に答える番人が決まったときに、番人を指す表現を具体的な番人に解釈する -/
def GuardianRef.eval (respond : Guardian) (gr : GuardianRef) : Guardian :=
  match gr with
  | .left => .left
  | .right => .right
  | .you => respond
  | .other => respond.other

/-- `s` という状況下で、`respond` に向けた `q : Question` という質問に対する真偽。
回答ではないので、嘘は入らない -/
def truth (s : State) (respond : Guardian) (q : Question) : Bool :=
  match q with
  | .angel gr => s.angel == gr.eval respond
  | .toHeaven r => s.toHeaven == r
  | .not q => !truth s respond q
  | .or q p => truth s respond q || truth s respond p
  | .and q p => truth s respond q && truth s respond p
  | .iff q p => truth s respond q == truth s respond p

/-- `s` という状況下で、`g : Guardian` に対して `q : Question` という質問をした時の答え。
YES か NO で返答が返ってくるが、`true` が YES に対応する。 -/
def answer (s : State) (respond : Guardian) (q : Question) : Bool :=
  if s.angel == respond then
    truth s respond q
  else
    !truth s respond q

定義がきちんとできたかどうか確かめるために少しテストをしてみます。

/-- 左の人が天使で左の道が天国行き -/
def exampleState : State := { angel := .left, toHeaven := .left }

-- 右の人(悪魔)に向かって、あなたは天使ですか?と訊いた時の返事は YES
#guard answer exampleState .right (.angel .you)

-- 左の人(天使)に向かって、あなたは天使ですか?と訊いたときの返事は YES
#guard answer exampleState .left (.angel .you)

-- 右の人(悪魔)に向かって、あなたは悪魔ですか?と訊いた時の返事は NO
#guard answer exampleState .right (.not (.angel .you)) == false

最後に、求めるべき質問の条件を定式化します。それは、相手が悪魔だろうと天使だろうと、どんな状況でも道の行く先だけに依存して返事が変わるような質問であることです。ListMonad インスタンスを用意すると楽に書けるのでここではそうします。

/-- 番人をすべて並べたリスト -/
def allGuardians : List Guardian := [.left, .right]

/-- 状態をすべて並べたリスト -/
def allStates : List State := [
  { angel := .left, toHeaven := .left },
  { angel := .left, toHeaven := .right },
  { angel := .right, toHeaven := .left },
  { angel := .right, toHeaven := .right }
]

-- `List` をモナドのインスタンスにする
instance : Monad List where
  pure x := [x]
  bind l f := l.flatMap f
  map f l := l.map f

/-- 良い質問かどうかを判定する -/
def Question.good (q : Question) : Bool :=
  let results : List Bool := do
    let respond ← allGuardians
    let s ← allStates
    return answer s respond q == (s.toHeaven == .left)
  results.foldl (· && ·) true

探索

これで「求めるべき質問」を定義することができました。あとはそのような質問を探索して見つけるだけです。

探索する対象は、質問全体の空間 Question なわけですが、この型には無限に多くの項があるので、全部探索するわけにはいきません。複雑さを測る尺度を用意して、ある程度簡単なものに限って探索することにしましょう。例えば、質問 q : Question の構造の深さを次のように定義すれば、深さが一定以下の質問は有限個になるので全探索できるようになります。

/-- 質問の深さ -/
def Question.depth (q : Question) : Nat :=
  match q with
  | .angel _ => 0
  | .toHeaven _ => 0
  | .not q => q.depth + 1
  | .or q p => max q.depth p.depth + 1
  | .and q p => max q.depth p.depth + 1
  | .iff q p => max q.depth p.depth + 1

まず深さが小さいものから調べましょう。深さゼロの原子的な質問は有限個です。番人を指す表現が4個、道が2本なので、ここから作れる原子的質問は全部で6個しかありません。

/-- 原子的な質問をすべて並べたリスト -/
def atomicQuestions : List Question := [
  .angel .left,
  .angel .right,
  .angel .you,
  .angel .other,
  .toHeaven .left,
  .toHeaven .right
]

-- この中に良い質問はない
#guard atomicQuestions.filter Question.good == []

深さゼロの質問の中に良い質問はありませんでした。次は深さ1の質問を調べてみましょう。深さ1の質問は、原始的な質問に二項演算を適用したものからなっていて、全部で 6 + 6 * 6 * 3 = 114 個しかありません。これも全探索できます。

/-- 深さ1の質問文全体 -/
def depth1Questions : List Question :=
  let ofNot : List Question := atomicQuestions.map Question.not
  let ofBinary : List Question := do
    let q1 ← atomicQuestions
    let q2 ← atomicQuestions
    let op ← [Question.or, Question.and, Question.iff]
    return op q1 q2
  ofBinary ++ ofNot

-- 114 個しかない
#guard depth1Questions.length == 114

すると早くもこの中から解が見つかります。複数の解が出ますが、対称性によって複数出ているだけであり、本質的には「あなたは天使である」と「左の道は天国へ続く」は同値ですか?という質問のバリエーションであるようです。

/-
info:
[Question.iff (Question.angel (GuardianRef.you)) (Question.toHeaven (Road.left)),
 Question.iff (Question.angel (GuardianRef.other)) (Question.toHeaven (Road.right)),
 Question.iff (Question.toHeaven (Road.left)) (Question.angel (GuardianRef.you)),
 Question.iff (Question.toHeaven (Road.right)) (Question.angel (GuardianRef.other))]
-/
#eval depth1Questions.filter Question.good

質問している相手が天使だろうと悪魔だろうと、左の道が天国行きであれば YES、左の道が地獄行きであれば NO という返事が返ってきます。これでこのパズルは解けました。

末尾再帰

末尾再帰(tail recursion) とは、再帰呼び出しの結果を加工せずそのまま返り値として返すような再帰のことを指します。そのような再帰関数のことを、末尾再帰的(tail recursive) であると言ったりします。

たとえば以下のリストの和を計算する関数は、末尾再帰的ではありません。再帰呼び出しの結果である sum xs をそのまま返すのではなく、(x + ·) で加工してから返しているからです。

variable {α : Type} [Add α] [Zero α]

/-- 末尾再帰的ではない関数の例 -/
def sum (l : List α) : α :=
  match l with
  | [] => 0
  | x :: xs => x + sum xs

#guard sum [1, 2, 3] = 6

逆に、以下の関数は末尾再帰的です。再帰呼び出し sum xs (acc + x) の結果をそのまま返しているからです。再帰呼び出しの前に、acc + x という計算はしていますが、これは引数に入っているだけなので関係ありません。

variable {α : Type} [Add α] [Zero α]

/-- 末尾再帰的な関数の例 -/
def sumAux (l : List α) (acc : α) : α :=
  match l with
  | [] => acc
  | x :: xs => sumAux xs (acc + x)

def sum (l : List α) : α :=
  sumAux l 0

#guard sum [1, 2, 3] = 6

末尾再帰とコンパイル時最適化

末尾再帰という概念が重要なものとされている主な理由は、末尾再帰的な関数はコンパイル時に最適化してループに変換することができるという事実にあります。

スタックオーバーフロー

まず前提として、再帰関数の計算はメモリ効率が悪く スタックオーバーフロー というエラーを引き起こす可能性が高いということを理解しておく必要があります。たとえば、以下のような関数を考えてみましょう。

/-- 階乗関数 -/
def factorial (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * factorial n

このとき、factorial 5 の計算の過程を丁寧に書くとこうなります。

example : factorial 5 = 120 := calc
  _ = 5 * factorial 4 := by rfl
  _ = 5 * (4 * factorial 3) := by rfl
  _ = 5 * (4 * (3 * factorial 2)) := by rfl
  _ = 5 * (4 * (3 * (2 * factorial 1))) := by rfl
  _ = 5 * (4 * (3 * (2 * (1 * factorial 0)))) := by rfl
  _ = 5 * (4 * (3 * (2 * (1 * 1)))) := by rfl
  _ = 120 := by rfl

ここで重要なのは、最後の factorial 0 の結果が返ってくるまで、外側の (5 * ·)(3 * ·) などの計算がすべて待たされるということです。この待機中の情報はコールスタックという場所に保存されるのですが、容量には限りがあるので大きな入力を計算しようとすると溢れてエラーになります。これがスタックオーバーフローです。

再帰ではなくてループで実装した場合は、入力が大きくなってもコールスタックを食いつぶすことはないので、このことを根拠に「再帰よりループの方がメモリ効率が良い」と言われることがあります。

/-- ループで実装した階乗関数 -/
def factorialLoop (n : Nat) : Nat := Id.run do
  let mut acc := 1
  for i in [1:n+1] do
    acc := acc * i
  return acc

#guard factorialLoop 5 = 120

末尾呼び出し最適化

関数が末尾再帰的であった場合、少し様相が異なります。先ほどの階乗関数の例を使って、どのように変わるのかを見てみましょう。まず、階乗関数を末尾再帰を使って書き換えます。

def factorialAux (n acc : Nat) : Nat :=
  match n with
  | 0 => acc
  | n + 1 => factorialAux n ((n + 1) * acc)

/-- 末尾再帰的な補助関数を使って書き換えた階乗関数 -/
def factorialTR (n : Nat) : Nat :=
  factorialAux n 1

そうすると、factorialTR 5 の計算の過程は以下のようになります。

example : factorialTR 5 = 120 := calc
  _ = factorialAux 5 1 := by rfl
  _ = factorialAux 4 (5 * 1) := by rfl
  _ = factorialAux 3 (4 * (5 * 1)) := by rfl
  _ = factorialAux 2 (3 * (4 * (5 * 1))) := by rfl
  _ = factorialAux 1 (2 * (3 * (4 * (5 * 1)))) := by rfl
  _ = factorialAux 0 (1 * (2 * (3 * (4 * (5 * 1))))) := by rfl
  _ = (1 * (2 * (3 * (4 * (5 * 1))))) := by rfl
  _ = 120 := by rfl

よく見ると、引数の計算だけになっていて、再帰呼び出しの結果を待つ必要がなくなっています。したがって、コンパイル時にループに変換することができます。これが 末尾呼び出し最適化(tail call optimization) です。

末尾呼び出し最適化による高速化

Lean のコンパイラは末尾呼び出し最適化を行うため、実際に末尾再帰的な関数に書き換えることによって関数を省メモリかつ高速にすることができます。より顕著な差が出るように、ここではフィボナッチ数列を計算する関数を例にしましょう。(一度の再帰で2つの再帰呼び出しを計算するため、大きな差が出ます)

/-- n 番目のフィボナッチ数を計算する関数 -/
def fib (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fib (n + 1) + fib n

#guard fib 7 = 13


def fibAux (n a b : Nat) : Nat :=
  match n with
  | 0 => a
  | n + 1 => fibAux n b (a + b)

/-- 末尾再帰的に書き直した fib 関数 -/
def fibTR (n : Nat) : Nat := fibAux n 0 1

#guard fibTR 7 = 13

-- 計算に1秒以上かかる
#eval fib 32

-- 計算がすぐ終わる
#eval fibTR 32

「末尾再帰の形にすると高速になるのは分かったが、複雑になるから証明に使うときは嫌だなあ」という感想を持たれたでしょうか?実は Lean では「証明では実装 A を使用し、実行時は効率的な別の実装 B を使用する」ということができます。詳しくは [csimp] のページを参照してください。

末尾再帰と部分関数

末尾再帰性は、partial_fixpoint で修飾できる関数の条件にも現れます。 詳しくは partial_fixpoint のページを参照してください。

関数を末尾再帰化するテクニック

再帰関数を末尾再帰的な形に書き換えるために、さまざまなテクニックが知られています。

蓄積変数を導入する

再帰呼び出しの結果を加工する代わりに、引数を増やしてその中で計算を行うというテクニックです。このとき増やす引数のことを 蓄積変数(accumulator) と呼びます。

/-- リストを逆順にする関数の、末尾再帰的でない実装 -/
@[grind =]
def reverse (l : List α) : List α :=
  match l with
  | [] => []
  | x :: xs => reverse xs ++ [x]

@[grind =]
def reverseAux (l : List α) (acc : List α) : List α :=
  match l with
  | [] => acc
  | x :: xs => reverseAux xs (x :: acc)

/-- リストを逆順にする関数の、末尾再帰的な実装 -/
@[grind =]
def reverseTR (l : List α) : List α := reverseAux l []

@[simp, grind =]
theorem reverseAux_lem (l : List α) (acc : List α)
    : reverseAux l acc = reverse l ++ acc := by
  induction l generalizing acc with grind

/-- `reverse` と `reverseTR` は等しい -/
theorem reverse_eq_reverseTR (l : List α) : reverse l = reverseTR l := by
  induction l with grind

foldl / foldr を使う

再帰パターンを抽出した高階関数として List.foldlList.foldr がありますが、この2つは「List.foldl の方は末尾再帰的で、List.foldr の方はそうではない」という違いがあります。

しかし、Lean の標準ライブラリにおいて List.foldr は末尾再帰的な実装と [csimp] 属性を利用して置換されています。 したがって foldr 的な再帰構造を持つ関数を List.foldr を使って書き直すだけで、実行時に末尾呼び出し最適化の恩恵を受けることができます。

variable [Add α] [Zero α]

/-- 合計を求める関数 -/
def sum (l : List α) : α :=
  match l with
  | [] => 0
  | x :: xs => x + sum xs

/-- 合計を求める関数を `foldr` で書き直したもの -/
def sumFoldr (l : List α) : α :=
  List.foldr (· + ·) 0 l

theorem sum_eq_sumFoldr (l : List α) : sum l = sumFoldr l := by
  delta sumFoldr sum List.foldr
  rfl

継続渡しスタイルにする

「値を受け取った後に残りの計算をどう続けるか」を表す関数のことを 継続(continuation) と呼びます。 「再帰呼び出しの結果を受け取った後に何をするか」を継続として明示的に渡すという方法です。継続渡しスタイル(continuation-passing style) と呼ばれます。

def map (f : α → β) (l : List α) : List β :=
  match l with
  | [] => []
  | x :: xs => f x :: map f xs

/-- 継続渡しスタイルに書き直すための補助関数 -/
def mapCPSAux (f : α → β) (l : List α) (k : List β → List β) : List β :=
  match l with
  | [] => k []
  | x :: xs =>
    mapCPSAux f xs (fun ys => k (f x :: ys))

/-- 継続渡しスタイルに書き直した map 関数 -/
def mapCPS (f : α → β) (l : List α) : List β :=
  mapCPSAux f l id

theorem mapCPSAux_lem (f : α → β) (l : List α) (k : List β → List β)
    : mapCPSAux f l k = k (map f l) := by
  induction l generalizing k with
  | nil => rfl
  | cons x xs ih => solve_by_elim

/-- `map` と `mapCPS` は等しい -/
theorem map_eq_mapCPS (f : α → β) (l : List α) : map f l = mapCPS f l := by
  delta mapCPS
  simp [mapCPSAux_lem]

列挙型に対する ToString インスタンスの自動生成

やりたいことの説明

帰納型であって、すべてのコンストラクタが一切引数を持たないものを、列挙型(enumeration type) と呼びます。 たとえば、以下に示すのは列挙型になっている例です。

/-- 色 -/
inductive Color where
  | red
  | green
  | blue

おおまかに、「有限個の候補からどれか一つを選ぶ」という情報だけを持っているのが列挙型であると言えます。

さて、列挙型に対して ToString インスタンスを定義する場合、各コンストラクタに対応する文字列を返したい場合が多いでしょう。 上記の例であれば、次のように定義するわけです。

protected def Color.toString (c : Color) : String :=
  match c with
  | .red => "red"
  | .green => "green"
  | .blue => "blue"

instance : ToString Color where
  toString := Color.toString

これは決まりきったルーチンワークなので、「自動化したい」という欲求が湧いてきます。 自動化するにはどうすればいいでしょうか?

列挙型であるか判定する

方針としては、列挙型に対してだけ動作する、ToString の deriving handler を定義したいです。 deriving instance ToString for Color のように書いたら、ColorToString インスタンスが自動生成されるようにしたいわけです。 現状では実装していないので、当然失敗します。 これが成功するようにしましょう。

/- error: No deriving handlers have been implemented for class `ToString` -/
deriving instance ToString for Color

deriving handler が内部で何をするかというと、実は手動で定義するときと同じことを自動でやっているだけです。 以下がおおまかな流れです。

  1. まず列挙型かどうか判定して、列挙型でなければ即終了
  2. 列挙型だったら instance コマンドを生成する
  3. 生成したコマンドを実行して、インスタンスを作る

したがってまずやるべきことは、列挙型かどうか判定することです。

これには専用の関数が用意されており、Lean.isEnumType という関数で判定することができます。

import Lean

open Lean Meta

-- Bool は列挙型
run_meta
  let actual ← isEnumType ``Bool
  assert! actual

-- Nat は列挙型ではない
run_meta
  let actual ← isEnumType ``Nat
  assert! !actual

instance コマンドを自動生成する

次の目標は、列挙型に対して instance コマンドを自動生成することです。 以下のように実装することができます。

import Lean

open Lean Elab Command
open Parser.Term

/-- 与えられた 列挙型を表す `declName : Name` に対して、
`ToString` のインスタンスを与える `instance` コマンドを自動生成する -/
def mkToStringInstForEnum (declName : Name) : TermElabM Command := do
  if !(← isEnumType declName) then
    throwError "{declName} は列挙型ではありません"

  let indVal ← getConstInfoInduct declName
  let mut alts : Array (TSyntax ``matchAlt) := #[]
  for ctorName in indVal.ctors do
    let ctorInfo ← getConstInfoCtor ctorName
    let ctorStr := ctorInfo.name.getString!
    let alt ← `(matchAltExpr| | $(mkIdent ctorName):ident => $(quote ctorStr))
    alts := alts.push alt

  let typeIdent := mkIdent declName
  let ToStringIdent := mkIdent `ToString
  `(command|
    instance : $ToStringIdent:ident $typeIdent:ident := ⟨
      fun x => match x with
      $alts:matchAlt*⟩)

いま定義した、instance コマンドを生成する関数を具体的な列挙型に対して実行して、得られたコマンドを確認してみましょう。 実際に、好ましい instance コマンドが生成されていることが確認できます。

inductive Direction where
  | north
  | south
  | east
  | west

/-
info: instance : ToString Direction :=
  ⟨fun x✝ =>
    match x✝ with
    | Direction.north => "north"
    | Direction.south => "south"
    | Direction.east => "east"
    | Direction.west => "west"⟩
-/
run_cmd
  let cmd ← liftTermElabM <| mkToStringInstForEnum `Direction
  let fmt ← liftCoreM <| PrettyPrinter.ppCommand cmd
  logInfo <| MessageData.ofFormat fmt

得られたコマンドを elabCommand 関数で実行してやることで、Direction に対する ToString インスタンスが自動生成できることも確認できます。

-- 実際にコマンドを実行して、`ToString` インスタンスを生成する
run_cmd
  let cmd ← liftTermElabM <| mkToStringInstForEnum `Direction
  elabCommand cmd

#guard toString Direction.north = "north"
#guard toString Direction.south = "south"

deriving handler を宣言する

ここまで終わったら、後は deriving handler を宣言するだけです。 initialize コマンドで登録することができます。

open Lean Elab Command

private def mkToStringInstForEnumHandler (declNames : Array Name) : CommandElabM Bool := do
  if declNames.isEmpty then
    throwError "型が指定されていません"

  for declName in declNames do
    let cmd ← liftTermElabM <| mkToStringInstForEnum declName
    elabCommand cmd
  return true

initialize
  registerDerivingHandler ``ToString mkToStringInstForEnumHandler

これで、晴れて deriving ToString が列挙型に対して使えるようになりました。 initialize コマンドの効果はモジュール(ファイルのこと)を跨がないと発生しないのですが、ファイルを跨げばこういう感じで書けるようになっているはずです。

inductive Hoge where
  | a
  | b
  | c
deriving ToString

#guard toString Hoge.a = "a"
#guard toString Hoge.c = "c"