Lean 構文早見表

Lean 構文早見表(Lean by Example) は,Lean の基本的な構文や機能を紹介するためのサンプルコード集です.Lean の数多くの機能の全体を概観できる資料となることを目指しています.

本書は逆引きタクティクリストの姉妹編です.mathlibにあるタクティクや,# 付きの対話的コマンドについてはあちらを参照してください.

コメント

多くのプログラミング言語にある機能ですが,Lean でもコメントアウトの構文が存在します.

行コメント

-- はインラインコメントを表します.各行の -- 以降の文字列は無視されます.

-- これはコメントです

ブロックコメント

/--/ で囲まれた部分はブロックコメントとして認識され,やはり実行されません.

/-
これはブロックコメントです
複数行を一度にコメントアウトします.
-/

モジュールドキュメントと異なり,import 文より前に書くことができるという性質があります.

ドキュメントコメント

/---/ で囲まれた部分はドキュメントコメントとして認識されます.これは直後に続く関数や変数に対する補足説明として認識されます.ドキュメントコメントとして与えた説明は,エディタ上でその関数や変数にカーソルを合わせることで表示されます.

/-- 挨拶です -/
def foo := "Hello World!"

/-- 1を加える関数 -/
def bar (n : Nat) : Nat := n + 1

モジュールドキュメント

/-!-/ で囲まれた部分はモジュールドキュメントです.セクショニングコメントと呼ばれることもあります.これは慣習的にファイルやセクション, 名前空間の冒頭でそこで何が行われるかを説明するために使われます.1

namespace Fibonacci

/-! ### フィボナッチ数列に関する定義 -/

/-- フィボナッチ数列の n 番目の要素 -/
def fib (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fib (n - 1) + fib (n - 2)

end Fibonacci

なお細かい話として,モジュールドキュメントは import 文より前に書くとエラーになります.

1

mathlib のドキュメント規約を参照してください.

フィールド記法

フィールド記法(ドット記法とも)とは,ある条件の下で関数適用 f xx.f と書くことができる記法のことです.Nim 言語における Uniform Function Call Syntax に似た機能です.

典型的な使い方

Lean で構造体を定義すると,自動的にその構造体の各フィールドにアクセスするための関数が生成されます.構造体の名前が S で,フィールドの名前が x ならアクセサは S.x という名前になります.

/-- 平面上の点 -/
structure Point where
  x : Int
  y : Int

def p : Point := { x := 10, y := 20 }

-- `Point.x` で構造体の `x` フィールドにアクセスできる.
example : Point.x p = 10 := rfl

このとき,構造体の項 s : S に対して,S.x ss.x と略記することができます.これが典型的なフィールド記法の使い方です.

example : p.x = 10 := rfl

一般のフィールド記法

構造体を例にしましたが,構造体に限ったことではありません.一般に関数 f があり,項 t : T があったとすると「fT 型の非暗黙の引数のうち,最初のものに x を代入したもの」を x.f で表すことができます.

例えば,以下は同じことを表現したコードです.

example : List.map (fun x => x + 1) [1, 2, 3] = [2, 3, 4] := rfl

example : [1, 2, 3].map (fun x => x + 1) = [2, 3, 4] := rfl

よくあるエラー

以下のようなエラーが出てフィールド記法が使えないことがあります.このエラーの対処法については _root_ を参照してください.

invalid field 'foo', the environment does not contain 'Bar.foo'

依存型

Lean では型が値に依存することができます.これを依存型と呼びます.他の言語でこれができるものとしては,Idris などがあります.

これにより,たとえば関数 f : A → B の返り値の型が引数 a : A の値に依存することができ,そのような関数の型を (a : A) → B と書きます.これを特に依存関数型といいます.

全称命題と ∀ 記号

典型的なのは全称命題の表現です.

-- 自然数から命題への関数の型
-- `n + 0 = 0` 自体が型であることに注意
#check (n : Nat) → (n + 0 = n : Prop)

/-- 自然数から `n + 0 = n : Prop` への関数.
カリーハワード同型対応により,これは `∀ n, n + 0 = n` の証明であるとみなせる. -/
def add_zero : (n : Nat) → n + 0 = n := fun _n => rfl

-- 通常の書き方をした証明.
-- 返り値の型が `Prop` の項である場合,このように `∀` 記号を使うのが自然
theorem add_zero' : ∀ (n : Nat), n + 0 = n := by
  intro n
  rfl

型の中の条件分岐

「ある b : Bool の値に応じて,真なら自然数,偽なら文字列を返す」という関数も,型の中に if 式を組み込んで次のように表現できます.

def sample (b : Bool) : if b then Nat else String :=
  match b with
  | true => (3 : Nat)
  | false => "three"

ただし,コードの設計としては NatString を合成した型を自分で定義したほうがよいでしょう.

-- 自然数と文字列の和集合
inductive NatOrString : Type
  | natVal : Nat → NatOrString
  | strVal : String → NatOrString

-- コンストラクタを省略できるように型強制を定義する
instance : Coe Nat NatOrString := ⟨NatOrString.natVal⟩
instance : Coe String NatOrString := ⟨NatOrString.strVal⟩

def sample' (b : Bool) : NatOrString :=
  match b with
  | true => (3 : Nat)
  | false => "three"

引数のデフォルト値

Python や PHP などの言語と同様に,Lean でも関数の引数にデフォルト値を指定することができます.

たとえば,割り算で「分母がゼロのとき」への安全装置を実装した関数 safeDiv を定義してみましょう.複数のやり方がありますが,ここでは「分母がゼロでないという証明」を引数に取るようにしてみます.

/-- 分母がゼロでないことを要求する割り算 -/
def safeDiv (n m : Nat) (_h : m > 0) : Nat := n / m

-- 分母がゼロでないことの証明を渡す必要がある
#guard safeDiv 10 2 (by decide) = 5

しかし, こうすると割り算を行うたびに毎回引数に証明を渡す必要が生じます.これでは面倒ですね.ここで,引数の証明項にデフォルト値を設定することができます.そうすれば,毎回証明を渡さなくても済みます.

/-- 分母がゼロでないことを自動で証明しようとする割り算 -/
def safeDiv' (n m : Nat) (_h : m > 0 := by decide) : Nat := n / m

-- 分母がゼロでないことの証明を省略できる
#guard safeDiv' 10 2 = 5

-- 分母をゼロにするとエラー
#check_failure safeDiv' 10 0

デフォルト値を上書きするには,普通に値を渡せばよいです.

-- `contradiction` では示せないので失敗する
#check_failure safeDiv' (12) 3 (by contradiction)

正格評価

Lean は Haskell と同様に純粋関数型言語ですが,Haskell とは異なり 正格評価 です.つまり,関数を評価する前に引数をすべて評価します.これは純粋関数型ではない Python や Rust, Julia などと同様です.

次のコード例の selectFstcond として true を渡されれば,単に fst を返します.つまり trace 20 は返り値には必要ないので,もし遅延評価なら評価されません. しかし実際には評価されるので,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

/--
info: trace is called
10
-/
#eval selectFst true 10 (trace 20)

なお if 式を直接評価した場合は,必要のない枝は評価されません.

/-- info: 10 -/
#eval (if true then 10 else trace 20)

無名コンストラクタ

無名コンストラクタは,構造体 T に対して T 型の項を構成する方法のひとつです.

これは以下に示すように,⟨x1, x2, ...⟩ という構文により使うことができます.

/-- 2つのフィールドを持つ構造体 -/
structure Hoge where
  foo : Nat
  bar : Nat

def hoge : Hoge := ⟨1, 2⟩

コンストラクタが入れ子になっていても平坦化することができます.例えば,以下の2つの定義は同じことをしています.

def foo : Nat × (Int × String) := ⟨1, ⟨2, "foo"⟩⟩

def foo' : Nat × (Int × String) := ⟨1, 2, "foo"⟩

一般の帰納型に対しては使用できません.

inductive Sample where
  | fst (foo bar : Nat) : Sample
  | snd (foo bar : String) : Sample

-- 「コンストラクタが一つしかない型でなければ使用できない」というエラーになる
#check_failure (⟨"foo", "bar"⟩ : Sample)

-- コンストラクタを指定しても使用できない
#check_failure (Sample.snd ⟨"foo", "bar"⟩ : Sample)

名前付き引数

Lean では,関数に引数を渡すときに順序ではなく名前で引数を指定することができます.

/-- 足したり掛けたりする関数 -/
def addMul (n m : Nat) := n + m * m

-- 通常, 引数は順序で指定されるが
#guard addMul 2 3 = 11

-- 名前付き引数を使うと,引数の順序を変えることができる
#guard addMul (m := 3) (n := 2) = 11

Functional but in-place

参照透過性

Lean は 純粋関数型言語 です.これはすべての関数が純粋で 参照透過性(referential transparency) を持つことを意味します.ここで関数 f が参照透過であるとは,次を意味します.

  • 引数として同じ値を与えると毎回同じ値を返し,
  • 式の値の評価以外の 副作用(side effect) を生まない

特に変数 a の値を参照する関数を考えると,関数の値が変わらないことから,変数 a の値は変更できないということがわかります.実際に Lean ではすべての変数は一度値を束縛すると値を変更できません.参照透過性を守るために,Lean は必要に応じて変数の値のコピーを行います.

-- foo という変数を定義する
def foo := "hello"

-- 再定義しようとするとエラーになる
/-- error: 'foo' has already been declared -/
#guard_msgs in def foo := "hello world"

-- 関数の内部で同名の `foo` という変数を定義することはできる
#eval show IO Unit from do
  let foo := foo ++ " world"
  IO.println foo

-- しかしグローバル変数としての `foo` の値は "hello" のまま変わっていない
#guard foo = "hello"

値の破壊的変更

しかし,Lean で変数の値の破壊的変更ができないわけではありません.できないどころか,容易に変数の値の破壊的変更を行うことができます.ここで破壊的変更とは,変数の値をコピーすることなく,in-place に値が変更されることを指します.次の例では,配列の値の変更が in-place に行われていることを確かめることができます.

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

Functional but in-place

なぜこのような現象が起こるのでしょうか?答えは,Lean が値の不要なコピーを行わないためです.具体的には,Lean は参照カウントがちょうど 1 であるような値を更新する際に,コピーして新しい値を生成する代わりに破壊的変更を行います.参照カウントが 1 の値はグローバル変数ではありませんし,その値を束縛している変数がその値を参照しているだけであるため,その値を変更しても参照透過性は保たれます.これを指して,Lean 言語のパラダイムのことを Functional but in-place と呼びます.1

変数の参照カウントを調べるための関数も用意されており,次の例のようなコードで検証を行うことができます.

-- `α` は型で,`α` の要素の大小比較ができる
variable {α : Type} [Ord α]

-- コンパイラによる最適化が行われないようにし,
-- `dbgTraceIfShared` の直観的でない挙動を防ぐために `noinline` 属性を付与
@[noinline]
def Array.checkedSwap (a : Array α) (i j : Fin a.size) : Array α :=
  -- `dbgTraceIfShared` 関数は,引数の参照カウントが 1 より大きいときにメッセージを表示する
  -- これは,破壊的変更ができない場合に警告を出すと言ってもよい
  dbgTraceIfShared "array shared!" a |>.swap i j

/-- 挿入ソートのヘルパ関数.
要素 `arr[i]` を `arr[i-1]` と比較して,
`arr[i-1] ≤ arr[i]` となるように要素を入れ替える -/
def insertSorted (arr : Array α) (i : Fin arr.size) : Array α :=
  match i with
  | ⟨0, _⟩ => arr
  | ⟨i' + 1, _⟩ =>
    have : i' < arr.size := by
      simp [Nat.lt_of_succ_lt, *]
    match Ord.compare arr[i'] arr[i] with
    | .lt | .eq => arr
    | .gt =>
      -- 再帰呼び出しのたびに "called swap" を表示させる
      dbg_trace "called swap"

      -- 上で定義した `checkedSwap` を呼んで配列の値の swap を行う
      insertSorted
        (arr.checkedSwap ⟨i', by assumption⟩ i)
        ⟨i', by simp [Array.checkedSwap, dbgTraceIfShared, *]⟩

/-- 挿入ソートのヘルパ関数 -/
partial def insertionSortLoop (arr : Array α) (i : Nat) : Array α :=
  if h : i < arr.size then
    insertionSortLoop (insertSorted arr ⟨i, h⟩) (i + 1)
  else
    arr

/-- 挿入ソート -/
def insertionSort (arr : Array α) : Array α :=
  insertionSortLoop arr 0

def sample_array := #[1, 3, 2, 8, 10, 1, 1, 15, 2]

-- `swap` は何度も実行されているが,最初の一回だけ `shared RC ...` メッセージが表示される
-- 最初の一回はグローバル変数 `sample_array` が引数として与えられているので,破壊的変更は許されない
-- 後の呼び出しでは参照カウントが 1 になっていて,破壊的変更が許される状況
/--
info: called swap
shared RC array shared!
called swap
called swap
called swap
called swap
called swap
called swap
called swap
called swap
called swap
called swap
called swap
called swap
#[1, 1, 1, 2, 2, 3, 8, 10, 15]
-/
#guard_msgs in #eval insertionSort sample_array
1

Moura, L.d., Ullrich, S. (2021). The Lean 4 Theorem Prover and Programming Language. In: Platzer, A., Sutcliffe, G. (eds) Automated Deduction – CADE 28. CADE 2021. Lecture Notes in Computer Science(), vol 12699. Springer, Cham. https://doi.org/10.1007/978-3-030-79876-5_37

_root_

_root_ は名前空間のルートを表します.主に名前空間を一時的に抜けるために使います.名前空間 Hoge の中で foo を定義すると Hoge.foo という名前になりますが,_root_.foo と定義すればこの挙動を避けて foo という名前にすることができます.

たとえば以下のように名前空間の中で List 配下の関数を定義し,フィールド記法を使おうとしてもうまくいきません.こういう場合に _root_ を使用すると,名前空間を閉じることなくエラーを解消できます.

namespace 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'
  [[1, 2], [3]]
has 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

abbrev

abbrev は,別名を宣言する構文です.

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

def NaturalNumber : Type := Nat

しかし,ここで定義した Nat の別名を項に対して使用するとエラーになります.これは,Lean が NaturalNumber を定義に簡約(reduce)するよりも先に,42 : NaturalNumber という表記が定義されているか OfNat のインスタンスを探そうとするためです.1

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

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

abbrev NaturalNumber : Type := Nat

#check (42 : NaturalNumber)

舞台裏

abbrev@[reducible] 属性のついた def と同じである2ため,定義に reducible という属性を与えても機能します.

@[reducible]
def NaturalNumber : Type := Nat

#check (42 : NaturalNumber)

attribute

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

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

theorem foo {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  constructor <;> intro h
  · obtain ⟨hPQ, hP⟩ := h
    constructor
    · apply hPQ
      assumption
    · assumption
  · obtain ⟨hP, hQ⟩ := h
    constructor
    · intro _
      assumption
    · assumption

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

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

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

与えた属性を削除することもできます.削除するには - を属性の頭に付けます.

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

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

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

@[simp]
theorem bar {P Q : Prop} : (P → Q) ∧ P ↔ Q ∧ P := by
  constructor <;> intro h
  · obtain ⟨hPQ, hP⟩ := h
    constructor
    · apply hPQ
      assumption
    · assumption
  · obtain ⟨hP, hQ⟩ := h
    constructor
    · intro _
      assumption
    · assumption

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

class

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 として上で定義したものを,もう一度 structure として定義してみます.

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

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

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

/-- info: Monoid'.e {α : Type} (self : Monoid' α) : α -/
#check Monoid'.e

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

#check_failure (Monoid'.e : Nat)

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

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

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

インスタンス暗黙引数と型クラス解決

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

/-- info: Monoid.e {α : Type} [self : Monoid α] : α -/
#check Monoid.e

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

def

def は,関数や項を定義するための基本的な構文です.

def foo := "hello"

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

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

#guard factorial 7 = 5040

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

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

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

where

where キーワードを使うと,定義をする前に変数を使用することができます.主に,ヘルパー関数を宣言するために使用されます.

/-- 整数 `m, n` が与えられたときに `m` 以上 `n` 以下の整数のリストを返す -/
def range (m n : Int) : List Int :=
  loop m (n - m + 1).toNat
where
  loop (start : Int) (length : Nat) : List Int :=
    match length with
    | 0 => []
    | l + 1 => loop start l ++ [start + l]

deriving

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

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

inductive Color : Type where
  | red
  | green
  | blue

deriving instance Repr for Color

#eval Color.red

対象の型の直後であれば,省略して deriving C だけ書けば十分です.また複数の型クラスに対してインスタンスを生成するには,クラス名をカンマで続けます.

structure People where
  name : String
  age : Nat

deriving Inhabited, Repr

#eval (default : People)

よくあるエラー

なお,deriving で実装を生成できるのは,決まりきったやり方で実装できて,実装方法が指定されている型クラスのみです.実装方法が指定されていなければ使うことはできません.

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

/--
error: default handlers have not been implemented yet, class: 'Deriving.Callable' types: [Deriving.People]
-/
deriving instance Callable for People

example

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

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

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

-- `rfl` という項の型が `1 + 1 = 2` であると言っても同じこと
#check (rfl : 1 + 1 = 2)

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

-- `{n : Nat} → n + 0 = n` という依存関数型の項を `rfl` で構成できる, と言い換えられる
#check (rfl : {n : Nat} → n + 0 = n)

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

inductive

inductive コマンドは,帰納型(inductive type)を定義することができます.Lean でユーザが新しい型を定義する主要な方法です.

列挙型

帰納型の最も基本的な例は,次のような列挙型です.列挙型とは,固定された値のどれかを取るような型です.

/-- 真か偽のどちらかの値をとる型 -/
inductive Bool : Type where
  | true
  | false

#check (Bool.true : Bool)

列挙型は,帰納型の中でもすべてのコンストラクタが引数を持たないような特別な場合といえます.

一般の帰納型

一般には,帰納型のコンストラクタは引数を持つことができます.

universe u v

/-- `α` と `β` の直和型 -/
inductive Sum (α : Type u) (β : Type v) where
  | inl (a : α) : Sum α β
  | inr (b : β) : Sum α β

コンストラクタの引数の型が定義しようとしているその帰納型自身であっても構いません.

/-- ペアノの公理によって定義された自然数 -/
inductive Nat : Type where
  | zero : Nat
  | succ (n : Nat) : Nat

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)

舞台裏

instance@[instance] タグが付けられた def と同じように振る舞います.

-- `List` 同士を足すことはできない
#check_failure [1] + [2]

-- インスタンスを宣言する
@[instance]
def instListAdd {α : Type} : Add (List α) where
  add := List.append

-- リスト同士を足すことができるようになった
-- 実装としては,上で指定した通り `List.append` が使われる
#check [1] + [2]

namespace

namespace は,定義に階層構造を与えて整理するための構文です.名前空間 Foo の中で bar を定義すると,それは Foo.bar という名前になり,名前空間 Foo の中では短い名前 bar でアクセスできますが,名前空間を出るとアクセスにフルネームが必要になります.

なおここでは説明のために namespace の中をインデントしていますが,これは一般的なコード規約ではありません.

namespace Nat

  def isEven (n : Nat) : Bool := n % 2 == 0

  -- 同じ名前空間の中なら短い名前でアクセスできる
  #check isEven

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

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

section

section は,variable で宣言された引数のスコープを区切ることができます.

section
  variable (a : Type)

  -- 宣言したので有効
  #check a
end

-- `section` の外に出ると無効になる
#check_failure a

なお上記の例ではセクションの中をインデントしていますが,インデントする必要はありません.

また,セクションに名前を付けることもできます.名前を付けた場合は,閉じるときにも名前を指定する必要があります.

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

structure

structure は構造体を定義するためのコマンドです.構造体とは,複数のデータをまとめて一つの型として扱えるようにしたものです.

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 : origin.x = 0 := by rfl

-- コンストラクタ
#check (Point.mk : {α : Type} → α → α → Point α)

コンストラクタに mk 以外の名前を使いたい場合,:: を使って次のようにします.

structure Prod (α : Type) (β : Type) where
  gen ::
  fst : α
  snd : β

-- コンストラクタ
#check Prod.gen

項を定義する

構造体の項を定義したい場合,複数の方法があります.波括弧記法が好まれますが,フィールド名が明らかな状況であれば無名コンストラクタを使用することもあります.

-- コンストラクタを使う
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

値の更新

Lean は純粋関数型言語なので「構造体のフィールドを更新する」ことはできませんが,既存の構造体のフィールドの一部だけを変更した新しい構造体の項を作ることはできます.

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

舞台裏

構造体は,帰納型の特別な場合であり,コンストラクタが一つしかないケースに対応します.上記の Point は以下のように定義しても同じことです.ただしこの場合,アクセサ関数が自動的に作られないため,フィールド記法は自分で実装しないと使用できないほか,波括弧記法が使えません.

inductive Point' (α : Type) : Type where
  | mk : (x : α) → (y : α) → Point' α

-- フィールド記法が利用できない
#check_failure Point'.x

-- 無名コンストラクタは使用できる
def origin' : Point Int := ⟨0, 0⟩

-- 波括弧記法は使用できない
#check_failure ({ x := 1, y := 2 } : Point' Int)

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 'Theorem.frac' is not a proposition
  Nat → Nat
-/
theorem frac (n : Nat) : Nat :=
  match n with
  | 0 => 1
  | n + 1 => (n + 1) * frac n

variable

variable は,定理や関数の引数を宣言するための構文です.

たとえば以下の関数と命題に対して,引数の α : Typel : List α は共通です.

/-- 連結リストの最後の要素を取り出す -/
def last? {α : Type} (l : List α) : Option α :=
  match l with
  | [] => none
  | [a] => some a
  | _ :: xs => last? xs

theorem nng_list_length {α : Type} (l : List α) : l.length >= 0 := by simp

variable コマンドを利用すると,共通の引数を宣言してまとめておくことができます.

variable {α : Type} (l : List α)

/-- 連結リストの最後の要素を取り出す -/
def last? (l : List α) : Option α :=
  match l with
  | [] => none
  | [a] => some a
  | _ :: xs =>last? xs

theorem nng_list_length : l.length >= 0 := by simp

上記の2つの例で引数の l : List α を省略できるかどうかに違いがありますが,これは返り値の型に現れているかどうかに依ります.

partial

partial は部分関数(partial function)を定義するための構文です.

Lean では再帰関数も再帰的でない関数と同様に定義できますが,扱いは異なります.再帰的な関数 f を定義すると,Lean は f がすべての入力に対して必ず有限回の再帰で停止することを証明しようとします.自動的な証明が失敗すると,エラーになってしまいます.エラーを消すには,

  • 停止することを手動で証明するか
  • 停止性は保証しないと明示的にマークするか

どちらかの対応が必要です.関数の定義に partial とマークすると, 停止性は保証しないとマークしたことになります.

/--
error: fail to show termination for
  WithoutPartial.alternate
with errors
argument #2 was not used for structural recursion
  failed to eliminate recursive application
    alternate ys xs

argument #3 was not used for structural recursion
  failed to eliminate recursive application
    alternate ys xs

structural recursion cannot be used

Could not find a decreasing measure.
The arguments relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
            xs ys
1) 38:24-39  ?  ?
Please use `termination_by` to specify a decreasing measure.
-/
#guard_msgs (whitespace := lax) in
def alternate {α : Type} (xs ys : List α) : List α :=
  match xs, ys with
  | [], ys => ys
  | x :: xs, ys => x :: alternate ys xs

-- `partial` を使うと,停止性の証明が不要になる
partial def alternate {α : Type} (xs ys : List α) : List α :=
  match xs, ys with
  | [], ys => ys
  | x :: xs, ys => x :: alternate ys xs

#guard alternate [1, 3, 5] [2, 4, 6] = [1, 2, 3, 4, 5, 6]

なお,partial とマークされた定義を元に新たに関数を定義するとき,再度 partial とマークする必要はありません.

def more_alternate := @alternate

private

private は,その定義があるファイルの中でだけ参照可能になるようにする修飾子です.他のファイルからはアクセス不能になります.

不安定なAPIなど,外部に公開したくないものに対して使うのが主な用途です.

なお private コマンドでセクションや名前空間にスコープを制限することはできません.

namespace Hoge
  section
    -- private とマークした定義
    private def addOne (n : Nat) : Nat := n + 1
  end
end Hoge

open Hoge

-- アクセスできる
#check addOne

以下の例 1 のように,変数のスコープを制限するようなセクションの変種を自作することは可能です.

open Lean Elab Command

elab "private section" ppLine cmd:command* ppLine "end private section": command => do
  let old ← get
  try
    for cmd in cmd do
      elabCommandTopLevel cmd
    throwAbortCommand
  finally
    set old

private section
  def foo := "hello!!"

  -- セクションの中なら当然アクセスできる
  #check foo
end private section

-- `foo` にアクセスできない
#check_failure foo
1

Alex J. Best さんによる Zulip Chat/lean4/private section での投稿を元にした例です.

protected

protected は,ある名前空間 Hoge にある定義 foo に対して,必ずフルネームの Hoge.foo でアクセスすることを強要するものです.

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 }

  -- 名前空間の中にいても,短い名前ではアクセスできない
  #check_failure sub

  -- フルネームならアクセスできる
  #check Point.sub

end Point

open Point

-- 名前空間を開いていても,短い名前でアクセスできない
#check_failure sub

-- フルネームならアクセスできる
#check Point.sub

noncomputable

noncomputable は,宣言された関数が計算可能でないことを Lean に伝えるために使われます.

Lean は,def で定義された関数はすべて計算可能であると想定しています.したがって,計算可能でない関数を定義すると,エラーが発生します.

計算可能でない関数が生じるのは,選択原理 Classical.choice を使用したときです.選択原理は,型が「空ではない」という証明だけから,その型の項を魔法のように構成できると主張している公理です.空ではないという情報から具体的な項の情報は得られないため,選択原理を使用した関数は計算不能になります.

variable {X Y : Type}

-- 写像 `f : X → Y` が全射であること
def Surjective (f : X → Y) : Prop := ∀ y, ∃ x, f x = y

-- 全射な関数の逆写像を構成する
-- しかし,全射という情報だけからは逆写像を具体的に作ることはできないので,
-- 計算不能になりエラーになってしまう
/--
error: failed to compile definition, consider marking it as 'noncomputable'
because it depends on 'Classical.choice', and it does not have executable code
-/
#guard_msgs in 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', and it does not have executable code
-/
#guard_msgs in #eval id_inverse 3

Fin

Fin は,ある数 i : Nat とそれがある数 n : Nat 未満であるという情報を組にしたものです.Fin nn 未満の自然数全体の集合を表します.

/--
info: structure Fin : Nat → Type
number of parameters: 1
constructor:
Fin.mk : {n : Nat} → (val : Nat) → val < n → Fin n
fields:
val : Nat
isLt : ↑self < n
-/
#guard_msgs in #print Fin

variable (n : Nat)

-- 自然数 `n : Nat` と,`n` より小さい自然数 `val : Nat` から,`Fin n` の項が得られる
-- つまり `Fin n` は `n` 未満の自然数全体の集合
#check (Fin.mk : (val : Nat) → val < n → Fin n)

パイプライン演算子

関数型言語らしく,Lean にもパイプライン演算子があります.

|> 演算子

関数 f : α → βx : α に対して x |> ff x と同じです.

def pipe₁ := [1, 2, 3, 4, 5]
  |> List.map (· + 10)
  |> List.filter (· % 2 == 0)

#guard pipe₁ = [12, 14]

|>. を使用すればフィールド記法が利用できます.

def pipe₂ := [1, 2, 3, 4, 5]
  |>.map (· + 10)
  |>.filter (· % 2 == 0)

#guard pipe₂ = [12, 14]

<| 演算子

関数 f : α → βx : α に対して f <| xf x と同じです.関数適用と変わらないようですが,パイプラインを使うと括弧が要らなくなるという利点があります.

#check ([[]] : List <| List Nat)

#check ([[[]]] : List <| List <| List Nat)

<|$ で書くこともできます.同じ意味になりますが,<| を使うべきであるとされています.1

#check ([[]] : List $ List Nat)

#check ([[[]]] : List $ List $ List Nat)
1

Lean Manual を参照.

Coe

Coe は,型強制(coercion)と呼ばれる仕組みをユーザが定義するための型クラスです.

ある型 T が期待される場所に別の型の項 s : S を見つけると,Lean は型エラーにする前に自動的に型変換を行うことができないか試します.ここで行われる「自動的な型変換」が型強制です.型強制を明示的に指定するには, 記号をつけて ↑x などのようにします.

具体的には Coe S T という型クラスのインスタンスが定義されているとき,型 S の項が型 T に変換されます.

例えば,正の自然数からなる型 Pos を定義したとします.包含関係から Pos → Nat という変換ができるはずです.この変換を関数として定義するだけでは,必要になるごとに毎回書かなければなりませんが,型強制を使うと自動化することができます.

inductive Pos where
  | one : Pos
  | succ : Pos → Pos

def one : Pos := Pos.one

-- `List.drop` の引数は `Nat` なのに,`Pos` を渡したのでエラーになっている
#check_failure [1, 2, 3].drop one

/-- `Pos` から `Nat` へ変換する -/
def Pos.toNat : Pos → Nat
  | one => 1
  | succ n => n.toNat + 1

instance : Coe Pos Nat where
  coe n := n.toNat

-- 自動的に `Pos` から `Nat` への変換が行われてエラーにならない
#check [1, 2, 3].drop one

-- 明示的に型強制を行った例
#check [1, 2, 3].drop (↑one)

CoeDep

CoeDep は型強制を行うための型クラスですが,Coe と異なり「項に依存する型強制」(dependent coercion)を行うことができます.

たとえば空でないリストからなる型 NonEmptyList を定義したとします.空リストを変換する方法がないため,List α → NonEmptyList α という変換を定義する自然な方法はありません.しかし CoeDep を使えば空でないリストに限って NonEmptyList に変換するという型強制を定義することができます.

structure NonEmptyList (α : Type) : Type where
  head : α
  tail : List α

-- リストから `NonEmptyList` に変換することができない
#check_failure ([1, 2] : NonEmptyList Nat)

variable {α : Type}

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
  /-- 関数部分 -/
  func : Nat → Nat

  /-- 加法を保つ -/
  additive : ∀ x y, func (x + y) = func x + func y

/-- 恒等写像 -/
def identity : AdditiveFunction := ⟨id, by intro x y; rfl⟩

#check (identity : AdditiveFunction)

-- `identity` の型は `AdditiveFunction` であって,関数ではないのでこれはエラーになる
#check_failure (identity 1)

-- `CoeFun` を使って `AdditiveFunction` の項を関数型 `Nat → Nat` に強制する
instance : CoeFun AdditiveFunction (fun _ => Nat → Nat) where
  coe f := f.func

-- まるで関数のように使えるようになる
#check (identity 1)

上記の例ではどんな t : AdditiveFunction も同じ型 Nat → Nat に強制していますが,実際には依存関数型に強制することができます.

CoeSort

CorSortCoe と同じく型強制を定義するための型クラスですが,項ではなく型に対して適用されるところが違います.

たとえば,モノイドという数学的対象をどう形式化するか考えてみましょう.モノイドとは,集合 M とその上の二項演算 * : M → M → M であって,結合律を満たし,単位元を持つものです.インフォーマルには,「くっつける」演算が定義されている集合と考えても良いでしょう.たとえば自然数は加法に対してモノイドですし,文字列は連結に対してモノイドです.

ここでは以下のように形式化します.1

structure Monoid where
  /-- 台集合 -/
  Carrier : Type

  /-- 単位元 -/
  neutral : Carrier

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

自然数やリストがモノイドであることを,上記の定義に基づいて形式化すると次のようになります.

def natAddMonoid : Monoid :=
  {
    Carrier := Nat,
    neutral := 0,
    op := (· + ·)
  }

def listMonoid (α : Type) : Monoid :=
  {
    Carrier := List α,
    neutral := [],
    op := (· ++ ·)
  }

ここでこの2つのモノイドの間の関数として,リストの長さを与える関数を考えてみます.

def length {α : Type} (xs : (listMonoid α).Carrier) : (natAddMonoid).Carrier :=
  List.length xs

型アノテーションの中で Carrier が繰り返し現れています.上記でモノイドを「集合と演算の組」として定義したため,モノイドを集合とみなすことができないためです.毎回 Carrier を補うのは面倒に感じますね.CoeSort を使えば,このようなルーティンな型変換を自動で行うことができます.

instance : CoeSort Monoid Type where
  coe m := m.Carrier

-- 単に `listMonoid α` と書いただけで,`Carrier` への変換が暗黙的に行われる
def length' {α : Type} (xs : listMonoid α) : natAddMonoid :=
  List.length xs
1

ここで紹介するモノイドの定義は CoeSort の紹介のために最低限必要な部分だけを取り出したもので,正確ではありません.上記の定義では二項演算が結合的であること,単位元が本当に単位元として振る舞うことが保証されません.また,そもそもモノイドは型クラスとして実装するべきです.そうすれば,自然数やリストにモノイドの構造を入れるために毎回新しい型を導入する必要はありません.

Inhabited

Inhabited は,ある型にデフォルトの項があることを示す型クラスです.

Inhabited のインスタンスである型は,default という項を持ちます.

variable {α : Type} [Inhabited α]

#check (default : α)

Inhabited の注意すべき使われ方として,クラッシュする関数の返り値として呼ばれるというものがあります.

たとえば a : Array に対して i 番目の要素を取り出す処理を考えます.i 番目の要素が存在するとは限らないので,例外の処理が必要です.一つの方法は,「i 番目の要素がなければエラーにする」というものです.

def get (a : Array α) (i : Nat) : α :=
  if h : i < a.size then
    a.get ⟨i, h⟩
  else
    panic! "index out of bounds"

何気ない定義のように見えますが,この定義には Inhabited α が必要です.panic! でプログラムをクラッシュさせているのですが,このときに α が空でないことが要求されています.

これは Lean が定理証明支援系としてもプログラミング言語としても使えるようにするための技術的な制約からくる仕様です.もし空の型を返してプログラムがクラッシュすることが許されたとすると,「空の型は False に等しい」ので,クラッシュするプログラムを False の証明として扱える可能性が生じてしまいます.

Repr

Repr は,その型の項をどのように表示するかを指示する型クラスです.

たとえば,以下のように新しく構造体 Point を定義したとき,特に何も指定しなければ Point の項を #eval で表示することはできません.

-- 平面上の点を表す構造体
structure Point (α : Type) : Type where
  x : α
  y : α

-- 原点
def origin : Point Nat := ⟨0, 0⟩

/--
error: expression
  origin
has type
  Point Nat
but instance
  Lean.Eval (Point Nat)
failed to be synthesized, this instance instructs Lean on how to display the resulting value, recall that any type implementing the `Repr` class also implements the `Lean.Eval` class
-/
#eval origin

エラーメッセージが示すように,これは Point が型クラス Lean.Eval のインスタンスではないからです.エラーメッセージには,Repr クラスのインスタンスであれば自動的に Lean.Eval のインスタンスにもなることが書かれています.通常,Lean.Eval のインスタンスを手で登録することはなく,Repr インスタンスによって自動生成させます.

Repr インスタンスの登録方法ですが,通り一遍の表示で構わなければ deriving コマンドで Lean に自動生成させることができます.

deriving instance Repr for Point

#eval origin

あるいは,Point の定義時に以下のようにしても構いません.

structure Point (α : Type) : Type where
  x : α
  y : α
deriving Repr

def origin : Point Nat := ⟨0, 0⟩

#eval origin

ToString

ToString は,文字列への変換を行う型クラスです.

structure Point (α : Type) where
  x : α
  y : α

def origin : Point Int := ⟨0, 0⟩

-- 文字列補完により文字列に変換しようとしても,
-- 最初はどう変換したらいいのかわからないのでエラーになる
#check_failure s!"{origin}"

/-- `ToString` のインスタンスを登録する -/
instance : ToString (Point Int) where
  toString p := s!"Point: ({p.x}, {p.y})"

-- これで文字列に変換できる
#eval s!"{origin}"

なお,Repr のインスタンスがないが,ToString のインスタンスはある状態で #eval しようとすると,Repr の代わりに ToString のインスタンスが使用されます.Repr のインスタンスを与えればそちらが優先して使用されます.

/-- info: Point: (0, 0) -/
#eval origin

-- `Repr` のインスタンスを自動生成して登録する
-- 以降は,`#eval` 時には `Repr` のインスタンスが使用される
deriving instance Repr for Point

/-- info: { x := 0, y := 0 } -/
#eval origin

notation

notation は,新しい記法を導入するためのコマンドです.

/-- 各 `k` に対して,二項関係 `a ≃ b mod k` を返す -/
def modulo (k a b : Int) : Prop :=
  k ∣ (a - b)

notation a " ≃ " b " mod " k => modulo k a b

#check (3 ≃ 7 mod 4)

次のような例も作ることができます.

notation "[" x " ... " y "]" => List.range y |> List.drop x

example : [1 ... 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9] := by rfl