simp

simp は、ターゲットを決められた規則に基づいて自動で単純化(simplify)するタクティクです。

基本的には、A = B または P ↔ Q という形の補題を登録しておくと、AB に、または PQ に自動で単純化してくれます。左辺を右辺に書き換え、右辺を左辺に戻すことはないため、右辺は左辺よりも「単純」であることが求められます。等価性に基づいて書き換えるので 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

等式・同値性以外のルールを登録した場合

等式や同値性以外のルールを登録した場合、等式・同値性への変換が自動的に行われたうえで登録されます。

たとえば下記の例のように、1 ≠ 0 というルールを simp 補題として登録した場合、(1 = 0) = False という定理が自動生成されそれに基づいて単純化が行われるようになります。

instance : One MyNat where
  one := MyNat.succ MyNat.zero

theorem MyNat.one_neq_zero : (1 : MyNat) ≠ 0 := by
  intro h
  injection h

-- `simp`属性を追加したことによって、
/-
info: theorem MyNat.one_neq_zero._simp_1 : (1 = 0) = False :=
eq_false MyNat.one_neq_zero

-- Lean.Meta.simpExtension extension: 1 new entries
-/
whatsnew in attribute [simp] MyNat.one_neq_zero

example (h : (1 : MyNat) = 0) (P : Prop) : P := by
  simp at h

一般に P という命題を登録すると P = True という定理が、¬ P という命題を登録すると P = False という定理が自動生成されて単純化に使用されます。

/-- 偶数を表す帰納的述語 -/
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
-/
whatsnew in attribute [simp] MyEven_two

simp で使用できる構文

補題の指定

既知の h : P という命題を使って単純化させたいときは、明示的に simp [h] と指定することで可能です。複数個指定することもできます。また simp only [h₁, ... , hₖ] とすると h₁, ... , hₖ だけを使用して単純化を行います。

example {P Q : Prop} (h : Q) : (P → P) ∧ Q := by
  simp only [imp_self, true_and]
  assumption

単に名前を定義に展開したい場合は dsimp を使用します。

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 [add_zero, zero_mul] at h ⊢
  assumption

ローカルコンテキストとゴールをまとめて全部単純化したい場合は simp at * とします。

注意点: simp 補題のループ

なお、[simp] 属性を付与した命題は「左辺を右辺に」単純化するルールとして登録されます。 左辺と右辺を間違えて登録すると、無限ループになって simp の動作が破壊されることがあります。[simp] 属性は慎重に登録してください。

section

  -- 何もしていなければ 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, 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

arith オプション

simp の設定で arith を有効にすると、算術的な単純化もできるようになります。

example (x y : Nat) : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  -- `simp` だけでは証明が終わらない
  fail_if_success solve
  | simp

  -- 適当に証明する
  suffices y ≤ x + y + 1 from by
    simp_all

  have : y ≤ x + y + 1 := calc
    _ = 0 + y := by simp
    _ ≤ x + 1 + y := by gcongr; simp
    _ = x + y + 1 := by ac_rfl
  assumption

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: 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