Leanのロゴ | |
パラダイム | 関数型プログラミング |
---|---|
登場時期 | 2013年 |
開発者 |
Leonardo de Moura LeanFRO |
最新リリース | v4.13.0/ 2024年11月1日 |
型付け | 推論される, 強い, 静的 |
影響を受けた言語 |
ML Coq Haskell Prolog Rust Scheme |
影響を与えた言語 | koka |
プラットフォーム | クロスプラットフォーム |
ライセンス | Apache License 2.0 |
ウェブサイト |
lean-lang |
Lean は純粋関数型プログラミング言語のひとつである。また、同時に証明支援系(theorem prover)でもある。帰納型を伴うCalculus of constructionsと呼ばれる型システムに基づいている。純粋関数型言語であるが、functional but in-place と呼ばれるプログラミングパラダイムに基づいており、効率性を重視している。
Lean はGitHubでホストされているオープンソースプロジェクトである。2013年にMicrosoft ResearchのLeonardo de Mouraによって立ち上げられた[1]。
Lean は最初の定理証明支援系ではない。Lean の開発時点で Coq や Agda など他の定理証明支援系は存在しており、Lean の言語仕様はそれらから大きく逸脱したものではない。新しい証明支援系を考案した理由として、次の2点がある。[2]
最初のプロトタイプは Lean 0.1 (2014 年) である。Lean 0.1 では ML ライクな構文が導入され、それは後のすべての Lean のバージョンで継承されることになる。単純な simp
タクティクが既にこのバージョンから存在した。帰納型のサポートはまだなく、手で公理(axiom)を追加する必要があった。Lean 0.1 では、Lua スクリプトによる構文と戦術の拡張がサポートされていたが、この部分は後の Lean 3 で削除されることになる。[3]
2015 年、Lean の最初の公式リリースである Lean 2 が発表された。帰納型の適切なサポートや組み込みタクティクの拡張など、欠けていた重要な機能が追加されたほか、主要な機能として Lean 2 ではホモトピー型理論 (HoTT) のサポートが追加された。[3]
最初にリリースされた比較的安定したバージョンは Lean 3 で、2017年の1月20日にリリースされた。[4] Lean 3 では、あまり使用されていなかった Lua による構文拡張機能が削除され、根本的に異なるアプローチが採用された。Lean 自体がプログラミング言語とされ、Lean 自体によりタクティクの定義やそのほかのメタプログラミングが可能になった。Lean 2 からのもう一つの大きな変更は、ホモトピー型理論 (HoTT) のサポートの廃止である。HoTT のサポートが廃止された理由としては、
が挙げられる。また、Lean で数学を形式化するライブラリである mathlib がコミュニティ管理の 独立したプロジェクトとして分離された。[3]
バージョン3.4.2以降、Lean 3の開発は正式に終了し、Lean 4の開発が始まった。
2021年、Lean 4の最初のマイルストーンリリースが発表された。[5]C++ではなく Lean 自身によって再実装され、「Lean は定理証明支援系であると同時に、汎用プログラミング言語でもある」と標榜するようになった。
Lean 4 より以前のバージョンでは、次のような問題点が認識されていたが、Lean 4 では大幅に改善された。[6]
Lean 4 は完全に拡張可能であり、パーサ、エラボレータ、タクティク、決定手続き(decision procedure)、プリティプリンタ、コードジェネレータを変更・拡張することができる。また Lean 4 は対話的証明のためにカスタマイズされた衛生的なマクロ(hygienic macro)を持つ。Lean の構文をユーザが改変する際に C++ コードに触れる必要はなくなった。[6]
さらに Lean 4 ではメモリ管理手続きが改善されたほか、テーブル解決に基づく新しい型クラス解決アルゴリズムが使用され、パフォーマンスが改善された。また、Lean 4 は functional but in-place と呼ばれる新しいプログラミングパラダイムに基づくようになった。[6]
Lean 4 には Lean 3 との後方互換性はない。Lean3 で開発されていた主要なライブラリとして、2017年ごろから開発が行われていた[7]mathlib が挙げられるが、コミュニティにより Lean4 への書き直しが行われた。これには100万行以上のコードを書き換える必要があったが、この移行作業は2023年7月に完了した。[8]
2023年7月、Lean Focused Research Organization (FRO) が設立された。[9]形式数学革命のために、証明のUI改善、スケーラビリティの改善、証明の自動化といった問題に取り組むとしている。また2023年9月、最初のLean 4 の公式リリースが発表された。[10]
プログラミング言語としての Lean は、関数などの式を部分的に実行して評価することが容易にできるように設計されている。#eval
というコマンドが存在し,関数などをその場で評価することができる。いわば、Jupyter Notebook のような対話的な実行環境が最初から利用可能であるということができる。
def frac (n : Nat) : Nat :=
match n with
| 0 => 1
| n + 1 => (n + 1) * frac n
-- エディタ上でコードを開いているとき
-- `#eval` の上にマウスオーバーすると 120 と表示される
#eval frac 5
Lean はオブジェクト指向言語ではなく、オブジェクトやクラスに相当する概念は存在しないが、関数適用を「まるでフィールドにアクセスするかのように」書くことができる記法が用意されている。これは Nim 言語における Uniform Function Call Syntax に似ている。
structure Point (α : Type) : Type where
x : α
y : α
-- アクセサ
#check (Point.x : {α : Type} → (Point α) → α)
#check (Point.y : {α : Type} → (Point α) → α)
def origin : Point Int := { x := 0, y := 0 }
-- 通常の関数適用の書き方
#guard Point.x origin = 0
-- フィールド記法。`.x` を付けるだけで値にアクセスできる
#guard origin.x = 0
同じく純粋関数型言語である Haskell とは異なり、Lean は正格評価である。つまり、関数を評価する前に関数に渡された引数をすべて評価する。
/-- 条件 `cond` が true なら最初の引数を,
false なら2番目の引数を返す関数 -/
def selectFst (cond : Bool) (fst snd : Nat) :=
if cond then
fst
else
snd
/-- 与えられた自然数をそのまま返す関数.
実行されると infoview に表示が出る. -/
def trace (n : Nat) : Nat :=
dbg_trace "trace is called"
n
/-
trace is called
10
-/
#eval selectFst true 10 (trace 20)
自然数は帰納型として定義することができる。この定義はペアノの公理に基づいており、すべての自然数はゼロもしくは他の自然数の後者であると述べている。
inductive Nat : Type where
| zero : Nat
| succ : Nat → Nat
自然数の加算はパターン・マッチングを使用して、再帰的に定義できる。
def Nat.add (n m : Nat) : Nat :=
match n with
| zero => m
| succ n => succ (add n m)
Lean ではカリーハワード同型対応を利用して証明を行う。ある命題 P
の証明 h
は,型 P
を持つような項 h : P
と同一視される。したがって、Lean を仲介することによって「証明する」ということは、「正確にある型を持つような項を作る」ということに言い換えられる。
これは Lean で簡単な命題の証明項を実際に構成した例である。
theorem and_swap (P Q : Prop) : P ∧ Q → Q ∧ P :=
fun h => { left := h.right, right := h.left }
Lean の機能として、タクティクによって証明項を構成することができる。by
キーワードによってタクティクモードに入ることができる。タクティクモードの中では、「現在利用できる仮定や命題」「今示すべきこと」が常に infoview に表示され、対話的に証明を書くことができる。
theorem and_swap (P Q : Prop) : P ∧ Q → Q ∧ P := by
-- P ∧ Q と仮定する
intro h
-- Q ∧ P を示すには Q と P をそれぞれ示せばよい
constructor
-- Q を示す
case left =>
exact h.right
-- P を示す
case right =>
exact h.left
Lean は依存型を持つ。つまり、関数 f : A → B
があったときに、f
による t : A
の返り値 f t
の型が t
に依存していてもよい。このとき f
の型を f : (t : A) → B
のように表記する。これにより、命題に正しく型を付けることが可能である。
theorem one_add : ∀ n, n + 1 ≥ 1 := by simp_arith
/-
one_add 1 : 1 + 1 ≥ 1
-/
#check one_add 1
/-
one_add 3 : 3 + 1 ≥ 1
-/
#check one_add 3
依存型がなければ命題(これは一種の型である)が項に依存することができないので、表現することができる命題の範囲がとても狭まってしまう。定理証明支援系をプログラミング言語として実現するうえで、依存型があることはとても重要な役割を果たす。
また、依存型はより柔軟にプログラムに型を付けることを可能にし、型で表現できるソフトウェアの仕様の範囲を広げる。以下は「長さが n
であるようなリスト」を Subtype
として表現する型である。
def Vect (α : Type) (n : Nat) : Type := {l : List α // l.length = n }
Lean は強力なメタプログラミング機能を持っており、新しい記法の定義や証明の自動化が行えるようになっている。以下は、マクロとして記法を定義する例である。
def modulo (k n m : Int) : Prop := k ∣ (n - m)
-- modulo を二項関係らしく書けるようにする
notation:55 x:55 " ≃ " y:55 " mod " k:55 => modulo k x y
#check (1 ≃ 6 mod 5)
Lean は対話的に証明を書くことをサポートするだけでなく、タクティクによる自動証明機能も提供する。以下は複雑な命題に見えるが、たった一つのタクティクで証明が終了する例である。
example (a₁ a₂ a₃ : Nat) : 3 ∣ a₁ + 10 * a₂ + 100 * a₃ ↔ 3 ∣ a₁ + a₂ + a₃ := by
omega
Agda や Coq, Isabelle などの他の定理証明支援系と比べると Lean にはどのような特徴があるだろうか。この中で Isabelle は依存型がなく、依拠している基礎が異なるという著しい差異がある。Agda と Coq と比較すると、基礎はほぼ同じであるが、Lean には以下のような特徴がある。
型クラスは、プログラミングと定理証明の両方において、アドホック多相性 [12] を実現してくれる。しかし、Lean の急成長中の数学ライブラリ Mathlib の中で、型クラスが広く使われるにつれ既存の型クラス解決手続きの理論的限界が進歩の妨げになるようになった。既存の型クラス解決手続きの主要な理論的限界とは、次のようなものである:
Lean 4 では、この2つの問題を解決する新しいアルゴリズムであるテーブル化型クラス解決が実装されている。このアルゴリズムは Prolog に対して1998年に [13] 提案された型クラス解決アルゴリズムに基づく。 [11]
Lean は純粋関数型言語であるため、手続き型言語では暗黙に扱われる副作用を、モナドという再利用可能な抽象的要素で再定義することで扱っている。この再定義によって、副作用をより厳密に制御したり、派生的な副作用を導入したりすることを可能にしている。モナドは Haskell の最もよく知られた抽象化機能であり、そのユビキタスな糖衣構文である do 記法と常に結びついている。
Lean では、メタプログラミングフレームワークによる高い拡張可能性を生かして、do 構文が Haskell と比べて拡張されている。具体的には以下のような記法を最初からサポートしている。[14]
let mut
で宣言できるようにする Rust ライクな記法for
ループ、break
や continue
といった制御フローたとえば、以下は Lean 4 で実装したエラトステネスの篩である。
def eratosthenes (n : Nat) : Array Bool := Id.run do
let mut isPrime := Array.mkArray (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
let mut q := p * p
while q <= n do
isPrime := isPrime.set! q false
q := q + p
return isPrime
ほとんどの関数型言語は、メモリ管理を自動で行うためにガベージ・コレクタを利用している。一方で、各値の正確な参照カウントを保持することで、破壊的更新などの最適化が可能になる。Lean は、純粋な関数型言語でありながら参照カウントを利用してメモリ管理を行う。参照カウントがちょうど1の値(つまり共有されていない値)を更新するとき、自動的に破壊的更新が行われる。このことを指して、Lean のプログラミングパラダイムのことを functional but in-place (FBIP) と呼ぶ。[6] 特に、Lean で配列のようなデータ構造を扱うとき、FBIP によりコードの純粋性を保ちながら効率的なコードを生成することが可能である。
これにより Lean のコンパイラが生成するコードは効率的になり、 他の静的型付け言語と比較しても遜色のないものになった。
実際に Lean が値の破壊的変更を行っていることは、次のようなコードで確認することができる。
-- Lean のオブジェクトのメモリ上でのアドレスを取得する関数
-- 参照透過性を壊すため,unsafe である
#eval ptrAddrUnsafe #[1, 2, 3]
/-- フィボナッチ数列を計算する -/
unsafe 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` のメモリアドレスを表示させている
dbg_trace ptrAddrUnsafe fib
fib := fib.push (fib[i-1]! + fib[i-2]!)
return fib
-- 値がコピーされていれば異なるメモリアドレスが表示されるはずだが...?
#eval fibonacci 15
ITP(対話的定理証明支援系)において、構文を拡張可能にすることは複雑な数学的対象をわかりやすく表現するのに重要であるのみならず、ライブラリ開発においても再利用可能な抽象化を行うために役立つ。このように表現力の高いマクロシステムを持つことは ITP にとって極めて重要なのだが、Lean 3 のものを含め既存の ITP のマクロシステムには問題があった。
問題点は主に以下の2点である:
Lean 4 はこれらの問題を Scheme ファミリーに着想を得た新しいマクロシステムを開発することにより同時に解決した。新しいマクロシステムは、複数のマクロ抽象化レベルを単一で統一的なシステムで実装することを可能にし、高い表現力と安全性を両立した。[16]
たとえば、以下は Coq のライブラリ math-comp における和のΣ記号の定義である。[17] 見てわかるように少しずつ異なる同様の定義が 12 回繰り返されている。
Reserved Notation "\sum_ i F"
(at level 41, F at level 41, i at level 0,
right associativity,
format "'[' \sum_ i '/ ' F ']'").
Reserved Notation "\sum_ ( i <- r | P ) F"
(at level 41, F at level 41, i, r at level 50,
format "'[' \sum_ ( i <- r | P ) '/ ' F ']'").
Reserved Notation "\sum_ ( i <- r ) F"
(at level 41, F at level 41, i, r at level 50,
format "'[' \sum_ ( i <- r ) '/ ' F ']'").
Reserved Notation "\sum_ ( m <= i < n | P ) F"
(at level 41, F at level 41, i, m, n at level 50,
format "'[' \sum_ ( m <= i < n | P ) '/ ' F ']'").
Reserved Notation "\sum_ ( m <= i < n ) F"
(at level 41, F at level 41, i, m, n at level 50,
format "'[' \sum_ ( m <= i < n ) '/ ' F ']'").
Reserved Notation "\sum_ ( i | P ) F"
(at level 41, F at level 41, i at level 50,
format "'[' \sum_ ( i | P ) '/ ' F ']'").
Reserved Notation "\sum_ ( i : t | P ) F"
(at level 41, F at level 41, i at level 50). (* only parsing *)
Reserved Notation "\sum_ ( i : t ) F"
(at level 41, F at level 41, i at level 50). (* only parsing *)
Reserved Notation "\sum_ ( i < n | P ) F"
(at level 41, F at level 41, i, n at level 50,
format "'[' \sum_ ( i < n | P ) '/ ' F ']'").
Reserved Notation "\sum_ ( i < n ) F"
(at level 41, F at level 41, i, n at level 50,
format "'[' \sum_ ( i < n ) '/ ' F ']'").
Reserved Notation "\sum_ ( i 'in' A | P ) F"
(at level 41, F at level 41, i, A at level 50,
format "'[' \sum_ ( i 'in' A | P ) '/ ' F ']'").
Reserved Notation "\sum_ ( i 'in' A ) F"
(at level 41, F at level 41, i, A at level 50,
format "'[' \sum_ ( i 'in' A ) '/ ' F ']'").
ほぼ同じことが Lean の数学ライブラリ mathlib4 では次のように表現されている。[18] 明らかに繰り返しが少なく、簡潔な書き方になっていることがわかるだろう。
syntax bigOpBinder := term:max ((" : " term) <|> binderPred)?
syntax bigOpBinderParenthesized := " (" bigOpBinder ")"
syntax bigOpBinderCollection := bigOpBinderParenthesized+
syntax bigOpBinders := bigOpBinderCollection <|> (ppSpace bigOpBinder)
syntax (name := bigsum) "∑ " bigOpBinders ("with " term)? ", " term:67 : term
これは Lean では syntax
および declare_syntax_cat
というコマンドが用意されていることと関係がある。Lean ではユーザが構文カテゴリを定義し、パーサの拡張を抽象化された高水準言語で行うことができる。これは Lean 3 までの静的なマクロでは不可能だった。
×
に対する \times
のようなLaTeXに似たシーケンスを使用して入力ができるようにする。