simp

simp は,ターゲットを決められた規則に基づいて自動で単純化(simplify)するタクティクです.@[simp] と付けることにより単純化に使ってほしい命題を登録することができます.

import Mathlib.Tactic.Tauto -- `tauto` を使うため

variable {P Q R : Prop}

example : (P ∨ Q ∨ R) ∧ R ↔ R := by
  -- simp だけでは証明が終わらない
  simp
  show R → P ∨ Q ∨ R

  -- 残りを適当に証明する
  intro hR
  tauto

@[simp]
theorem or_and : (P ∨ Q ∨ R) ∧ R ↔ R := by
  constructor

  -- 左から右を示す
  case mp =>
    intro ⟨_, hR⟩
    assumption

  -- 右から左を示す
  case mpr =>
    -- R が成り立つと仮定する
    intro hR

    -- R は成り立つので,後は P ∨ Q ∨ R が成り立つことを示せばよい
    refine ⟨?_, hR⟩

    -- R が成り立つことから明らか.
    tauto

-- 一度登録した命題は `simp` で示せるようになる.
example : (P ∨ Q ∨ R) ∧ R ↔ R := by 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
-/
#guard_msgs in
example (n m : Nat) : (n + 0) * m = n * m := by simp

end

simp で使用できる構文

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

example (h : R) : (P ∨ Q ∨ R) ∧ R := by
  simp only [or_and]
  assumption

単に名前を定義に展開したい場合は dsimp を使用します. simp は何も指定しなければゴールを単純化しますが,ローカルコンテキストにある h : P を単純化させたければ simp at h と指定することで可能です.ゴールと h の両方を単純化したいときは simp at h ⊢ とします.

variable {n m : Nat}

example (h : n + 0 + 0 = m) : n = m + (0 * n) := by
  simp at h ⊢
  assumption

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

simpa

simpa は,simp を実行した後 assumption を実行するという一連の流れを一つのタクティクにしたものです.simpa at h 構文は存在せず,simpa using h と書くことに注意してください.

example (h : R) : (P ∨ Q ∨ R) ∧ R := by
  simpa only [or_and]

example (h : n + 0 + 0 = m) : n = m := by
  simpa using h

simp?

simp は自動的に証明を行ってくれますが,何が使われたのか知りたいときもあります.simp? は単純化に何が使われたのかを示してくれるので,simp only などを用いて明示的に書き直すことができます.

example : (P ∨ Q ∨ R) ∧ R ↔ R := by
  simp? says
    simp only [or_and]

simp_arith

simp の設定で arith を有効にすると,算術的な単純化もできるようになります. これはよく使用されるので,simp_arith という省略形が用意されています.

example {x y : Nat} : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  -- `simp` だけでは証明が終わらない
  simp
  show y ≤ x + y + 1

  -- 残りを適当に証明する
  calc y = 0 + y := by simp
    _ ≤ x + 1 + y := by gcongr; simp
    _ = x + y + 1 := by ac_rfl

example {x y : Nat} : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  -- config を与えれば一発で終わる
  simp (config := { arith := true, decide := true })

example {x y : Nat} : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := by
  simp_arith

simp_all

simp_allsimp [*] at * の強化版で,ローカルコンテキストとゴールをこれ以上単純化できなくなるまですべて単純化します.

simps 属性

補題を simp で使えるようにするのは @[simp] タグを付けることで可能ですが,simps 属性(または @[simps] タグ)を利用すると simp で使用するための補題を自動的に生成してくれます.これは Mathlib で定義されている機能であり,使用するには Mathlib.Tactic.Basic の読み込みが必要です.

例えば,ユーザが Point という構造体を定義し,Point 上の足し算を定義したところを考えましょう.このとき,足し算はフィールドの値の足し算で定義されているため,「Point の和の x 座標」は x 座標の和ですが,これはそのままでは simp で示すことができません.simps 属性を Point.add 関数に付与することで,simp で示せるようになります.

@[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

@[simps?] に換えると,生成された補題を確認することができます.

/--
info: [simps.verbose] adding projection Simp.Point.sub_x:
      ∀ (p q : Simp.Point), (p.sub q).x = p.x - q.x
[simps.verbose] adding projection Simp.Point.sub_y: ∀ (p q : Simp.Point), (p.sub q).y = p.y - q.y
-/
#guard_msgs (whitespace := lax) in
@[simps?] def Point.sub (p q : Point) : Point :=
  { x := p.x - q.x, y := p.y - q.y }