Upgrade to Pro — share decks privately, control downloads, hide ads and more …

初めてのLean言語

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 初めてのLean言語

More Decks by NearMeの技術発表資料です

Other Decks in Technology

Transcript

  1. 3 Leanのインストール • Lean 4 VS Code Extensionを以下のURLからインストール ◦ https://marketplace.visualstudio.com/items?itemName=leanprover.lean4

    ▪ インストールマニュアル :https://github.com/leanprover/vscode-lean4/blob/master/vscode-lean4/ manual/manual.md • CLIでインストールしたい⽅はこちらを参考(もしくはVSCodeがうまく⾏かない場合) ◦ https://lean-ja.github.io/lean-by-example/HowToInstall.html
  2. 4 Getting Started • Plus.lean ファイルを作成 • 以下のコードを記載 ◦ def

    onePlusOneIsTwo : 1 + 1 = 2 := rfl ▪ 1 + 1 = 2という命題が正しいことを⽰す定義 ▪ rfl は反射律(reflexivity) t = t の証明 • 左辺と右辺を定義展開‧簡約し、両者が定義上等しいと判断できる場合には、 それらを同じ式として扱う。 • コンパイルが成功するか確認 ◦ VSCode:ファイルを開いた時にコード上にエラーがない ◦ CLI:“lean Plus.lean” コマンドを実⾏してエラーがない • 以下のコードを追加して、コンパイルが失敗することを確認 ◦ def onePlusOneIsFifteen : 1 + 1 = 15 := rfl
  3. 6 初めての証明:⾃然数の和を定義 def sum_of_natural_numbers (n : Nat) : Nat :=

    -- 自然数の和を再帰で定義する。 match n with -- 0 までの和。 | 0 => 0 -- n + 1 までの和は、n までの和に n + 1 を足す。 | n + 1 => sum_of_natural_numbers n + (n + 1) -- 計算例。 #eval sum_of_natural_numbers 5
  4. 7 初めての証明:帰納法で証明1/2 theorem sum_of_natural_numbers_formula (n : Nat) : -- 示したい公式。

    2 * sum_of_natural_numbers n = n * (n + 1) := by -- n について帰納法。 -- `induction ... with` は場合分けしながら帰納法を始める。 induction n with | zero => -- 基底部。 -- `simp` は基本的な定義や定理で式を自動的に簡約する。 simp [sum_of_natural_numbers] ...続く
  5. 8 ...続き | succ n ih => -- `succ` は「次の自然数」の場合、つまり

    `n + 1` のケース。 -- `ih` は induction hypothesis の略で、帰納法の仮定。 -- ih : 2 * sum_of_natural_numbers n = n * (n + 1) -- `calc` は等式変形を1行ずつつないで書く。 calc 2 * sum_of_natural_numbers (n + 1) -- `sum_of_natural_numbers` を展開し、`left_distrib`(乗法の左分配法則)を使う。 = 2 * sum_of_natural_numbers n + 2 * (n + 1) := by simp [sum_of_natural_numbers, Nat.left_distrib] -- 帰納法の仮定で置き換える。 _ = n * (n + 1) + 2 * (n + 1) := by -- `rw` は等式を使って式を書き換える。 rw [ih] -- `add_mul`(乗法の右分配法則)でまとめ、`add_comm`(加法の交換法則)で順序をそろえる。 _ = (n + 2) * (n + 1) := by rw [← Nat.add_mul, Nat.add_comm n 2] -- `mul_comm`(乗法の交換法則)で右辺の形に合わせる。 _ = (n + 1) * (n + 2) := by rw [Nat.mul_comm] 初めての証明:帰納法で証明2/2
  6. 10 -- 信号の色を表す型を定義する。 -- `inductive` は、場合の候補を並べて新しい型を作る。 inductive Light | red

    | yellow | green -- 次の信号の色を返す関数。 def next | Light.red => Light.green | Light.green => Light.yellow | Light.yellow => Light.red 信号機の例:状態と状態遷移を定義
  7. 11 theorem red_never_direct_to_yellow : next Light.red ≠ Light.yellow := by

    -- `simp` は定義を展開して式を簡単にする。 simp [next] theorem next_three_times_returns_same (l : Light) : next (next (next l)) = l := by -- `cases l` は `l` を `red` `yellow` `green` の各場合に分ける。 -- `<;>` は、右側の tactic をすべてのゴールに適用する。 -- `rfl` は左右が定義上同じときに証明を閉じる。 cases l <;> rfl 信号機の例:挙動を証明
  8. 13 配⾞システムの状態遷移の例:状態の定義 inductive RideStatus | requested | rejected | assigned

    | pickedUp structure Driver where name : String -- Ride は現在の状態と、必要なら担当ドライバーを持つ。 -- `driver : Option Driver` の `Option` は -- 「値があるかもしれないし、ないかもしれない」型。 structure Ride where status : RideStatus driver : Option Driver
  9. 14 配⾞システムの状態遷移の例:状態遷移の定義 -- 配車リクエスト直後は、状態は requested でドライバーは未定。 def requestRide : Ride

    := { status := .requested, driver := none } -- requested のときだけドライバーを割り当てられる。 -- 成功すると `some Ride`、不正な状態なら `none` を返す。 -- `some x` は「値がある」ケースで、 `x` を包んだ値。 -- `none` は「値がない」ケース。 def assignDriver (ride : Ride) (driver : Driver) : Option Ride := match ride.status with | .requested => some { ride with status := .assigned, driver := some driver } | _ => none -- requested のときだけ rejected に進める。 -- reject しても driver は割り当てられない。 def rejectRide (ride : Ride) : Option Ride := match ride.status with | .requested => some { ride with status := .rejected, driver := none } | _ => none -- assigned のときだけ pickedUp に進める。 def pickUp (ride : Ride) : Option Ride := match ride.status with | .assigned => some { ride with status := .pickedUp } | _ => none
  10. 15 配⾞システムの状態遷移の例:挙動の証明 -- requested からは reject できる。 theorem request_can_be_rejected :

    rejectRide requestRide = some { status := .rejected, driver := none } := by simp [requestRide, rejectRide] -- requested から、直接 pickedUp に進めることはできない。 theorem request_cannot_be_picked_up_directly : pickUp requestRide = none := by simp [requestRide, pickUp] -- requested から assigned にドライバーを割り当てられる。 theorem request_ride_can_be_picked_up_with_driver (driver : Driver) : -- `>>=` は bind と呼ばれる演算子。 -- ここでは `Option` 用の bind なので、次のように動く: -- `some ride >>= pickUp` は `pickUp ride` -- `none >>= pickUp` は `none` -- つまり「assignDriver が成功したときだけ、その結果を pickUp に渡す」。 (assignDriver requestRide driver >>= pickUp) = some { status := .pickedUp, driver := some driver } := by simp [requestRide, assignDriver, pickUp]
  11. 16 配⾞システムの状態遷移の例: 全状態に対する証明(1/4) • 「pickedUp なら、既にドライバーが割り当 てられている」を証明する • まずは、定義した遷移を使って「どんな Ride

    が到達可能か」を考える -- `Reachable ride` は -- 「その `ride` は、このファイルで定義した正しい遷移だけを使って到達できる」 -- という性質を表す述語。 -- -- `Ride` の型そのものは、例えば -- `{ status := .pickedUp, driver := none }` -- のような不正な値も作れてしまう。 -- そこで「ありうる Ride 全体」ではなく、 -- 「初期状態から正しい操作で到達した Ride」に話を限定する。 -- -- `Prop` は `proposition` の略で、「真か偽かを主張する命題の型」を表す。 -- つまり `Reachable : Ride -> Prop` は、各 `Ride` について -- 「その Ride は到達可能か」という命題を返す、という意味。 -- -- `inductive` で `Prop` を作ると、 -- 「どんなときに `Reachable ride` を証明してよいか」を -- コンストラクタで列挙できる。 inductive Reachable : Ride -> Prop where -- 初期状態 `requestRide` は到達可能。 | request : Reachable requestRide -- 既に到達可能な `ride` に対して、`rejectRide` が成功して `next` になったなら、 -- その `next` も到達可能。 | reject ( ride : Ride) ( next : Ride) : Reachable ride -> rejectRide ride = some next -> Reachable next -- 既に到達可能な `ride` に対して、`assignDriver` が成功して `next` になったなら、 -- その `next` も到達可能。 | assign ( ride : Ride) ( driver : Driver) ( next : Ride) : Reachable ride -> assignDriver ride driver = some next -> Reachable next -- 既に到達可能な `ride` に対して、`pickUp` が成功して `next` になったなら、 -- その `next` も到達可能。 | pickUp ( ride : Ride) ( next : Ride) : Reachable ride -> pickUp ride = some next -> Reachable next
  12. 17 配⾞システムの状態遷移の例: 全状態に対する証明(2/4) -- pickedUp なら、既にドライバーが割り当てられている。 theorem picked_up_has_driver {ride :

    Ride} : Reachable ride -> ride.status = .pickedUp -> ∃ driver, ride.driver = some driver := by -- `->`(含意)が2つ並んでいるので、まず仮定を 1つずつ手元に取り出す。 -- `intro hreach` は「`Reachable ride` という仮定に `hreach` という名前を付けて -- 仮定欄(コンテキスト)に置く」操作。これで証明すべきゴールは -- 「ride.status = .pickedUp -> ∃ driver, ride.driver = some driver 」になる。 intro hreach -- ここがこの証明の山場。 -- いきなり「pickedUp なら driver がある」を帰納法で示そうとすると、 -- pickUp 遷移(assigned -> pickedUp )の場面で詰まる。 -- なぜなら pickUp の「遷移元」は pickedUp ではなく assigned なので、 -- 「pickedUp なら…」という帰納法の仮定がそこでは使えないから。 -- -- そこで、ゴールをあえて「 assigned か pickedUp なら driver がある」という -- 少し強い主張 h に置き換える。`suffices h : P by ...` は -- 「P さえ証明できれば元のゴールが従う。その『従う』部分を今ここで示す」という構文。 -- ここでは元の仮定 hstatus(pickedUp) を「assigned ∨ pickedUp」の右側 `Or.inr` に -- 入れて h に渡すだけで元のゴールが出る。 suffices h : ride.status = .assigned ∨ ride.status = .pickedUp → ∃ driver, ride.driver = some driver by intro hstatus exact h (Or.inr hstatus) -- これ以降のゴールは、上で宣言した強い主張 `h` の中身そのもの。 ...続く • 「assigned か pickedUp なら 既にドラ イバーが割り当てられている」という 少し強い証明をする • そこから「pickedUp なら、既にドライ バーが割り当てられている」を導く
  13. 18 配⾞システムの状態遷移の例: 全状態に対する証明(3/4) • Reachableな状態の場合わけ ◦ request ◦ reject ◦

    …続く ...続き -- `induction hreach with` で「ride はどうやって到達可能になったのか」を -- Reachable のコンストラクタごとに場合分けする。 -- Reachable は request / reject / assign / pickUp の4通りで作られるので、 4ケースになる。 induction hreach with | request => -- ケース1: ride は初期状態 requestRide 。状態は requested 。 -- 強い主張の仮定「 assigned ∨ pickedUp 」を取り出すと … intro hstatus -- requestRide を展開すると status は .requested 。 -- 「.requested = .assigned ∨ .requested = .pickedUp 」はどちらも成り立たないので、 -- simp が矛盾(False)を見つけてゴールを閉じてくれる。 simp [requestRide] at hstatus | reject ride next _ hrej _ => -- ケース2: 直前の ride を rejectRide して next になった。 -- `hrej : rejectRide ride = some next` が手元にある。 intro hstatus -- rejectRide の定義(中身の match)を hrej に展開する。 unfold rejectRide at hrej -- `split` は hrej の中の `match ride.status with ...` を場合分けする。 -- 「requested だったとき」と「それ以外」の 2ケースに割れる。 split at hrej · -- requested だったとき : hrej は `some {status := rejected,..} = some next` 。 -- `injection` は「some a = some b なら a = b」を取り出す(コンストラクタの単射性)。 injection hrej with hnext -- hnext で next の中身が分かったので、ゴール中の next をその値に置き換える。 subst hnext -- next の状態は rejected 。assigned でも pickedUp でもないので hstatus は矛盾。 simp at hstatus · -- それ以外のとき : rejectRide は none を返すので hrej は `none = some next` 。 -- これはあり得ない( none と some は別物)ので contradiction で閉じる。 contradiction ...続く
  14. 19 配⾞システムの状態遷移の例: 全状態に対する証明(4/4) • Reachableな状態の場合わけ ◦ …続き ◦ assign ◦

    pickUp ...続き | assign ride driver next _ hass _ => -- ケース3: 直前の ride に assignDriver driver して next になった。 -- `hass : assignDriver ride driver = some next` が手元にある。 -- 状態仮定は使わないので `_` で受け流す。 intro _ unfold assignDriver at hass split at hass · -- requested だったとき : next は driver := some driver で作られている。 injection hass with hnext subst hnext -- ゴール `∃ d, next.driver = some d` を満たす d として driver を提示。 -- `⟨driver, rfl ⟩` は ∃ の中身(証拠 driver と、その等式 rfl)の組。 -- next.driver はまさに `some driver` なので等式は rfl(自明な反射律)で済む。 exact ⟨driver, rfl⟩ · contradiction | pickUp ride next _ hpick ih => -- ケース4: 直前の ride を pickUp して next になった。 -- `hpick : pickUp ride = some next` 、 -- `ih` は帰納法の仮定 = 「直前の ride について、強い主張が成り立つ」。 intro _ unfold pickUp at hpick split at hpick · -- pickUp が成功するのは遷移元 ride が assigned のときだけ。 -- split が作った「 ride.status = .assigned 」という仮定に名前を付ける。 -- (split は名前を自動で付けるので、 `rename_i` で手前から名付け直す。 ) rename_i hassigned injection hpick with hnext subst hnext -- next.driver は ride.driver をそのまま引き継ぐ。 -- ride は assigned なので、帰納法の仮定 ih に「左側(assigned) 」を渡せば -- 「ride に driver がある」が得られる。あとは simp が next と ride の -- driver が同じことを使ってゴールに合わせてくれる (`simpa`)。 simpa using ih (Or.inl hassigned ) · contradiction ...続く
  15. 20 • 思ったより証明はかなり⻑かった • AIの補助がないと、(初⼼者なので)きつい • 逆に、AIの補助があれば、なんとか書ける • AIの出⼒でコンパイルエラーは何回かあった •

    ただ、⾃⼰修正して証明まで辿り着けた • LeanとAIの共同作業といったところか • ⼀度、このような証明が組めれば、強⼒な品質保証になりうる 配⾞システムの状態遷移の例:全状態に対する証明の所感
  16. 21 • 分散システムにおけるLeader選出の証明 ◦ Leaderは⾼々1つ(同⼀Termで2⼈以上のLeaderは存在しない) ◦ 補題 ▪ 1ノードは1Termにつき1票しか投票しない ▪

    Leaderになるには過半数の票が必要 ▪ 2つの過半数集合は必ず交差する • ドライバーへの順番通知で無限ループにならないことの証明 ◦ 以下のような状況で通知が無限ループする ▪ ドライバーAの通知の後、ドライバーBに通知 ▪ ドライバーBに通知の後、ドライバーCに通知 ▪ ドライバーCの通知の後、ドライバーAに通知 ◦ 通知フローの依存グラフに閉路が存在しないことを証明する その他Leanならではの応⽤例
  17. 22 参考サイト • 本家 ◦ https://lean-lang.org/ • Wikipedia ◦ https://ja.wikipedia.org/wiki/Lean_(%E8%A8%BC%E6%98%8E%E3%82%A2%E

    3%82%B7%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%88) • lean-ja ◦ https://lean-ja.github.io/lean-by-example/index.html • 「証明⽀援系LEANに⼊⾨しよう」 ◦ https://speakerdeck.com/unaoya/zheng-ming-zhi-yuan-xi-leanniru-men-siyou