6. Matematika Diskret

Matematika diskret mempelajari himpunan, objek, dan struktur berhingga. Kita dapat menghitung banyaknya elemen suatu himpunan berhingga, menghitung jumlah atau hasil kali berhingga atas elemen-elemennya, menentukan nilai maksimum dan minimum, dan sebagainya. Kita juga dapat mempelajari objek yang dibangkitkan melalui sejumlah berhingga penerapan fungsi pembangkit tertentu, mendefinisikan fungsi melalui rekursi struktural, dan membuktikan teorema melalui induksi struktural. Bab ini menjelaskan bagian-bagian Mathlib yang mendukung kegiatan tersebut.

6.1. Finset dan Fintype

Menangani himpunan dan tipe berhingga dalam Mathlib dapat membingungkan karena pustaka ini menyediakan beberapa cara untuk menanganinya. Bagian ini membahas cara-cara yang paling umum.

Kita telah menjumpai tipe Finset dalam Bab 5.2 dan Bab 5.3. Sesuai namanya, sebuah elemen bertipe Finset α adalah himpunan berhingga yang elemen-elemennya bertipe α. Kita akan menyebut objek semacam ini "finset". Tipe data Finset dirancang agar memiliki interpretasi komputasional, dan banyak operasi dasar pada Finset α mengasumsikan bahwa kesamaan pada α dapat diputuskan. Hal ini menjamin adanya algoritma untuk menguji apakah a : α merupakan elemen dari suatu finset s.

section
variable {α : Type*} [DecidableEq α] (a : α) (s t : Finset α)

#check a  s
#check s  t

end

Jika deklarasi [DecidableEq α] dihapus, Lean akan mengeluh pada baris #check s t karena tidak dapat menghitung irisannya. Namun, semua tipe data yang secara wajar diharapkan dapat digunakan dalam komputasi memiliki kesamaan yang dapat diputuskan. Jika Anda bekerja secara klasik dengan membuka namespace Classical dan mendeklarasikan noncomputable section, Anda dapat menalar tentang finset yang beranggotakan elemen dari tipe apa pun.

Finset mendukung sebagian besar operasi teori himpunan yang juga tersedia bagi himpunan biasa:

open Finset

variable (a b c : Finset )
variable (n : )

#check a  b
#check a  b
#check a \ b
#check ( : Finset )

example : a  (b  c) = (a  b)  (a  c) := by
  ext x; simp only [mem_inter, mem_union]; tauto

example : a  (b  c) = (a  b)  (a  c) := by rw [inter_union_distrib_left]

Perhatikan bahwa kita telah membuka namespace Finset, tempat teorema-teorema khusus tentang finset berada. Jika Anda menelusuri contoh terakhir di bawah langkah demi langkah, Anda akan melihat bahwa penerapan ext yang diikuti oleh simp mereduksi identitas tersebut menjadi masalah dalam logika proposisional. Sebagai latihan, Anda dapat mencoba membuktikan beberapa identitas himpunan dari Bab 4 yang diadaptasi untuk finset.

Anda telah melihat notasi Finset.range n untuk himpunan berhingga bilangan asli \(\{ 0, 1, \ldots, n-1 \}\). Finset juga memungkinkan Anda mendefinisikan himpunan berhingga dengan mencacah elemen-elemennya:

#check ({0, 2, 5} : Finset Nat)

def example1 : Finset  := {0, 1, 2}

Ada berbagai cara untuk membuat Lean mengenali bahwa urutan elemen dan kemunculan berulang tidak berpengaruh pada himpunan yang disajikan dengan cara ini.

example : ({0, 1, 2} : Finset ) = {1, 2, 0} := by decide

example : ({0, 1, 2} : Finset ) = {0, 1, 1, 2} := by decide

example : ({0, 1} : Finset ) = {1, 0} := by rw [Finset.pair_comm]

example (x : Nat) : ({x, x} : Finset ) = {x} := by simp

example (x y z : Nat) : ({x, y, z, y, z, x} : Finset ) = {x, y, z} := by
  ext i; simp [or_comm, or_left_comm]

example (x y z : Nat) : ({x, y, z, y, z, x} : Finset ) = {x, y, z} := by
  ext i; simp; tauto

Anda dapat menggunakan insert untuk menambahkan satu elemen ke sebuah finset dan Finset.erase untuk menghapus satu elemen. Perhatikan bahwa erase berada dalam namespace Finset, sedangkan insert berada dalam namespace akar.

example (s : Finset ) (a : ) (h : a  s) : (insert a s |>.erase a) = s :=
  Finset.erase_insert h

example (s : Finset ) (a : ) (h : a  s) : insert a (s.erase a) = s :=
  Finset.insert_erase h

Sebenarnya, {0, 1, 2} hanyalah notasi untuk insert 0 (insert 1 (singleton 2)).

set_option pp.notation false in
#check ({0, 1, 2} : Finset )

Dengan sebuah finset s dan predikat P, kita dapat menggunakan notasi pembentuk himpunan {x s | P x} untuk mendefinisikan himpunan yang terdiri atas elemen-elemen s yang memenuhi P. Notasi ini berarti Finset.filter P s, yang juga dapat ditulis s.filter P.

example : {m  range n | Even m} = (range n).filter Even := rfl
example : {m  range n | Even m  m  3} = (range n).filter (fun m  Even m  m  3) := rfl

example : {m  range 10 | Even m} = {0, 2, 4, 6, 8} := by decide

Mathlib mengetahui bahwa citra sebuah finset di bawah suatu fungsi juga merupakan finset.

#check (range 5).image (fun x  x * 2)

example : (range 5).image (fun x  x * 2) = {x  range 10 | Even x} := by decide

Lean juga mengetahui bahwa hasil kali Kartesius s ×ˢ t dari dua finset merupakan finset dan bahwa himpunan kuasa suatu finset juga merupakan finset. (Perhatikan bahwa notasi s ×ˢ t juga berlaku untuk himpunan.)

#check s ×ˢ t
#check s.powerset

Mendefinisikan operasi pada finset berdasarkan elemen-elemennya tidak mudah karena setiap definisi semacam itu harus tidak bergantung pada urutan penyajian elemen. Tentu saja, Anda selalu dapat mendefinisikan fungsi dengan menyusun operasi yang sudah ada. Cara lain adalah menggunakan Finset.fold untuk melipat suatu operasi biner atas elemen-elemennya, asalkan operasi tersebut asosiatif dan komutatif. Sifat-sifat ini menjamin bahwa hasilnya tidak bergantung pada urutan penerapan operasi. Jumlah, hasil kali, dan gabungan berhingga didefinisikan dengan cara tersebut. Dalam contoh terakhir di bawah, biUnion berarti "gabungan berindeks terbatas". Dalam notasi matematika konvensional, ekspresi itu ditulis \(\bigcup_{i ∈ s} g(i)\).

#check Finset.fold

def f (n : ) : Int := (n)^2

#check (range 5).fold (fun x y : Int  x + y) 0 f
#eval (range 5).fold (fun x y : Int  x + y) 0 f

#check  i  range 5, i^2
#check  i  range 5, i + 1

variable (g : Nat  Finset Int)

#check (range 5).biUnion g

Terdapat prinsip induksi alami pada finset: untuk membuktikan bahwa setiap finset memiliki suatu sifat, tunjukkan bahwa himpunan kosong memiliki sifat tersebut dan bahwa sifat itu tetap berlaku ketika kita menambahkan satu elemen baru ke sebuah finset. (Simbol @ dalam @insert diperlukan pada langkah induksi dalam contoh berikut untuk memberi nama pada parameter a dan s karena keduanya ditandai implisit.)

#check Finset.induction

example {α : Type*} [DecidableEq α] (f : α  )  (s : Finset α) (h :  x  s, f x  0) :
     x  s, f x  0 := by
  induction s using Finset.induction_on with
  | empty => simp
  | @insert a s anins ih =>
    rw [prod_insert anins]
    apply mul_ne_zero
    · apply h; apply mem_insert_self
    apply ih
    intros x xs
    exact h x (mem_insert_of_mem xs)

Jika s adalah sebuah finset, Finset.Nonempty s didefinisikan sebagai x, x s. Anda dapat menggunakan pilihan klasik untuk memilih satu elemen dari finset tak kosong. Demikian pula, pustaka mendefinisikan Finset.toList s, yang menggunakan pilihan untuk mengambil elemen-elemen s dalam suatu urutan.

noncomputable example (s : Finset ) (h : s.Nonempty) :  := Classical.choose h

example (s : Finset ) (h : s.Nonempty) : Classical.choose h  s := Classical.choose_spec h

noncomputable example (s : Finset ) : List  := s.toList

example (s : Finset ) (a : ) : a  s.toList  a  s := mem_toList

Anda dapat menggunakan Finset.min dan Finset.max untuk memilih elemen minimum atau maksimum dari finset yang elemennya berada dalam suatu tatanan linear. Demikian pula, Anda dapat menggunakan Finset.inf dan Finset.sup untuk finset yang elemennya berada dalam suatu kisi, tetapi ada satu kendala. Apa yang harus dijadikan elemen minimum dari finset kosong? Anda dapat memeriksa bahwa versi bertanda prima dari fungsi-fungsi di bawah menambahkan prasyarat bahwa finset tersebut tak kosong. Untuk menangani finset kosong, versi tanpa prima Finset.min dan Finset.max menambahkan elemen terbesar atau terkecil, secara berturut-turut, pada tipe keluaran. Versi tanpa prima Finset.inf dan Finset.sup mengasumsikan bahwa kisi tersebut dilengkapi elemen terbesar atau terkecil, secara berturut-turut.

#check Finset.min
#check Finset.min'
#check Finset.max
#check Finset.max'
#check Finset.inf
#check Finset.inf'
#check Finset.sup
#check Finset.sup'

example : Finset.Nonempty {2, 6, 7} := 6, by trivial
example : Finset.min' {2, 6, 7} 6, by trivial = 2 := by trivial

Setiap finset s memiliki kardinalitas berhingga, Finset.card s, yang dapat ditulis #s ketika namespace Finset terbuka.

#check Finset.card

#eval (range 5).card

example (s : Finset ) : s.card = #s := by rfl

example (s : Finset ) : s.card =  _i  s, 1 := by rw [card_eq_sum_ones]

example (s : Finset ) : s.card =  _i  s, 1 := by simp

Bagian berikutnya sepenuhnya membahas penalaran mengenai kardinalitas.

Ketika memformalkan matematika, kita sering harus menentukan apakah definisi dan teorema akan dinyatakan menggunakan himpunan atau tipe. Penggunaan tipe sering menyederhanakan notasi dan pembuktian, tetapi bekerja dengan subhimpunan dari suatu tipe dapat lebih luwes. Padanan berbasis tipe bagi finset adalah fintype, yakni suatu tipe α yang dilengkapi instans Fintype α. Menurut definisi, sebuah fintype hanyalah tipe data yang dilengkapi finset univ yang memuat seluruh elemennya.

variable {α : Type*} [Fintype α]

example :  x : α, x  Finset.univ := by
  intro x; exact mem_univ x

Fintype.card α sama dengan kardinalitas finset yang bersesuaian.

example : Fintype.card α = (Finset.univ : Finset α).card := rfl

Kita telah melihat contoh prototipikal fintype, yaitu tipe Fin n untuk setiap n. Lean mengenali bahwa kelas fintype tertutup terhadap operasi seperti hasil kali.

example : Fintype.card (Fin 5) = 5 := by simp
example : Fintype.card ((Fin 5) × (Fin 3)) = 15 := by simp

Setiap elemen s dari Finset α dapat dikoersikan menjadi tipe (↑s : Type), yaitu subtipe yang terdiri atas elemen-elemen α yang termuat dalam s. Selain itu, Lean mengetahui bahwa ↑s merupakan fintype.

variable (s : Finset )

example : (s : Type) = {x :  // x  s} := rfl
example : Fintype.card s = s.card := by simp

Lean dan Mathlib menggunakan inferensi kelas tipe untuk melacak struktur tambahan pada fintype, yakni finset universal yang memuat seluruh elemen. Dengan kata lain, Anda dapat memandang fintype sebagai struktur aljabar yang dilengkapi data tambahan tersebut. Bab 7 menjelaskan cara kerjanya.

6.2. Argumen Pencacahan

Kemahiran mencacah berbagai objek merupakan bagian utama kombinatorika. Mathlib memuat beberapa identitas dasar untuk menghitung elemen finset:

open Finset

variable {α β : Type*} [DecidableEq α] [DecidableEq β] (s t : Finset α) (f : α  β)

example : #(s ×ˢ t) = #s * #t := by rw [card_product]
example : #(s ×ˢ t) = #s * #t := by simp

example : #(s  t) = #s + #t - #(s  t) := by rw [card_union]

example (h : Disjoint s t) : #(s  t) = #s + #t := by rw [card_union_of_disjoint h]
example (h : Disjoint s t) : #(s  t) = #s + #t := by simp [h]

example (h : Function.Injective f) : #(s.image f) = #s := by rw [card_image_of_injective _ h]

example (h : Set.InjOn f s) : #(s.image f) = #s := by rw [card_image_of_injOn h]

Dengan membuka namespace Finset, kita dapat menggunakan notasi #s untuk s.card serta nama-nama ringkas seperti card_union dan seterusnya.

Mathlib juga dapat menghitung elemen fintype:

open Fintype

variable {α β : Type*} [Fintype α] [Fintype β]

example : card (α × β) = card α * card β := by simp

example : card (α  β) = card α + card β := by simp

example (n : ) : card (Fin n  α) = (card α)^n := by simp

variable {n : } {γ : Fin n  Type*} [ i, Fintype (γ i)]

example : card ((i : Fin n)  γ i) =  i, card (γ i) := by simp

example : card (Σ i, γ i) =  i, card (γ i) := by simp

Ketika namespace Fintype tidak terbuka, kita harus menggunakan Fintype.card, bukan card.

Berikut adalah contoh penghitungan kardinalitas suatu finset, yaitu gabungan range n dengan salinan range n yang digeser sekurang-kurangnya sejauh n. Penghitungan tersebut mengharuskan kita menunjukkan bahwa kedua himpunan dalam gabungan itu saling lepas. Baris pertama pembuktian menghasilkan syarat samping Disjoint (range n) (image (fun i m + i) (range n)), yang dibuktikan pada akhir pembuktian. Predikat Disjoint terlalu umum untuk langsung berguna bagi kita, tetapi teorema disjoint_iff_ne mengubahnya ke bentuk yang dapat kita gunakan.

#check Disjoint

example (m n : ) (h : m  n) :
    card (range n  (range n).image (fun i  m + i)) = 2 * n := by
  rw [card_union_of_disjoint, card_range, card_image_of_injective, card_range]; omega
  . apply add_right_injective
  . simp [disjoint_iff_ne]; omega

Sepanjang bagian ini, omega akan menjadi alat andalan kita untuk menangani penghitungan aritmetika dan pertidaksamaan.

Berikut contoh yang lebih menarik. Pertimbangkan subhimpunan dari \(\{0, \ldots, n\} \times \{0, \ldots, n\}\) yang terdiri atas pasangan \((i, j)\) dengan \(i < j\). Jika pasangan-pasangan ini dipandang sebagai titik kisi pada bidang koordinat, semuanya membentuk segitiga atas dari persegi yang bersudut \((0, 0)\) dan \((n, n)\), tanpa menyertakan diagonalnya. Kardinalitas persegi penuh adalah \((n + 1)^2\). Setelah ukuran diagonal dikurangkan dan hasilnya dibagi dua, diperoleh kardinalitas segitiga \(n (n + 1) / 2\).

Sebagai cara lain, perhatikan bahwa baris-baris segitiga tersebut berukuran \(0, 1, \ldots, n\), sehingga kardinalitasnya adalah jumlah \(n\) bilangan bulat positif pertama. Pernyataan have pertama dalam pembuktian di bawah menggambarkan segitiga sebagai gabungan baris-barisnya; baris \(j\) terdiri atas bilangan \(0, 1, ..., j - 1\) yang masing-masing dipasangkan dengan \(j\). Dalam pembuktian di bawah, notasi (., j) menyingkat fungsi fun i (i, j). Selebihnya hanyalah penghitungan kardinalitas finset.

def triangle (n : ) : Finset ( × ) := {p  range (n+1) ×ˢ range (n+1) | p.1 < p.2}

example (n : ) : #(triangle n) = (n + 1) * n / 2 := by
  have : triangle n = (range (n+1)).biUnion (fun j  (range j).image (., j)) := by
    ext p
    simp only [triangle, mem_filter, mem_product, mem_range, mem_biUnion, mem_image]
    constructor
    . rintro ⟨⟨hp1, hp2⟩, hp3
      use p.2, hp2, p.1, hp3
    . rintro p1, hp1, p2, hp2, rfl
      omega
  rw [this, card_biUnion]; swap
  · -- tangani dahulu sifat saling lepas
    intro x _ y _ xney
    simp [disjoint_iff_ne, xney]
  -- lanjutkan penghitungan
  transitivity ( i  range (n + 1), i)
  · congr; ext i
    rw [card_image_of_injective, card_range]
    intros i1 i2; simp
  rw [sum_range_id]; rfl

Variasi pembuktian berikut melakukan penghitungan dengan fintype, bukan finset. Tipe α β adalah tipe ekuivalensi antara α dan β. Sebuah ekuivalensi terdiri atas pemetaan maju, pemetaan balik, serta bukti bahwa keduanya saling invers. Pernyataan have pertama dalam pembuktian menunjukkan bahwa triangle n ekuivalen dengan gabungan saling lepas dari Fin i ketika i berkisar pada Fin (n + 1). Menariknya, fungsi maju dan fungsi balik dibangun dengan taktik, bukan ditulis secara eksplisit. Karena kedua fungsi tersebut hanya memindahkan data dan informasi, rfl membuktikan bahwa keduanya saling invers.

Setelah itu, rw [←Fintype.card_coe] menulis ulang #(triangle n) sebagai kardinalitas subtipe { x // x triangle n }, dan selebihnya merupakan penghitungan.

example (n : ) : #(triangle n) = (n + 1) * n / 2 := by
  have : triangle n  Σ i : Fin (n + 1), Fin i.val :=
    { toFun := by
        rintro ⟨⟨i, j⟩, hp
        have : (i  n  j  n)  i < j := by simpa [triangle] using hp
        exact ⟨⟨j, by linarith⟩, i, by linarith⟩⟩
      invFun := by
        rintro i, j
        use j, i
        suffices j  n  i  n by simpa [triangle]
        constructor <;> linarith [i.2, j.2]
      left_inv := by intro i; rfl
      right_inv := by intro i; rfl }
  rw [Fintype.card_coe]
  trans; apply (Fintype.card_congr this)
  rw [Fintype.card_sigma, sum_fin_eq_sum_range]
  convert Finset.sum_range_id (n + 1)
  simp_all

Berikut pendekatan lain lagi. Baris pertama pembuktian di bawah mereduksi masalah menjadi pembuktian 2 * #(triangle n) = (n + 1) * n. Kita dapat melakukannya dengan menunjukkan bahwa dua salinan segitiga tersebut tepat mengisi persegi panjang range n ×ˢ range (n + 1). Sebagai latihan, cobalah melengkapi langkah-langkah penghitungan tersebut. Dalam solusi, kita banyak mengandalkan omega pada langkah kedua dari belakang, tetapi tetap perlu mengerjakan cukup banyak bagian secara manual.

example (n : ) : #(triangle n) = (n + 1) * n / 2 := by
  apply Nat.eq_div_of_mul_eq_right (by norm_num)
  let turn (p :  × ) :  ×  := (n - 1 - p.1, n - p.2)
  calc 2 * #(triangle n)
      = #(triangle n) + #(triangle n) := by
          sorry
    _ = #(triangle n) + #(triangle n |>.image turn) := by
          sorry
    _ = #(range n ×ˢ range (n + 1)) := by
          sorry
    _ = (n + 1) * n := by
          sorry

Anda dapat meyakinkan diri bahwa kita memperoleh segitiga yang sama, hanya digeser ke bawah, jika dalam definisi triangle kedua kemunculan range (n + 1) diganti dengan range n dan < diganti dengan . Latihan di bawah meminta Anda menggunakan fakta ini untuk menunjukkan bahwa kedua segitiga tersebut berukuran sama.

def triangle' (n : ) : Finset ( × ) := {p  range n ×ˢ range n | p.1  p.2}

example (n : ) : #(triangle' n) = #(triangle n) := by sorry

Mari menutup bagian ini dengan sebuah contoh dan latihan dari tutorial tentang kombinatorika yang disampaikan Bhavik Mehta dalam Lean for the Curious Mathematician pada tahun 2023. Misalkan kita memiliki graf bipartit dengan himpunan simpul s dan t. Untuk setiap a dalam s, sekurang-kurangnya tiga sisi keluar dari a, sedangkan untuk setiap b dalam t, paling banyak satu sisi masuk ke b. Dengan demikian, jumlah sisi dalam graf sekurang-kurangnya tiga kali kardinalitas s dan paling banyak kardinalitas t. Akibatnya, tiga kali kardinalitas s tidak lebih besar daripada kardinalitas t. Teorema berikut menerapkan argumen ini dengan relasi r sebagai representasi sisi-sisi graf. Pembuktiannya merupakan penghitungan yang elegan.

open Classical
variable (s t : Finset ) (a b : )

theorem doubleCounting {α β : Type*} (s : Finset α) (t : Finset β)
    (r : α  β  Prop)
    (h_left :  a  s, 3  #{b  t | r a b})
    (h_right :  b  t, #{a  s | r a b}  1) :
    3 * #(s)  #(t) := by
  calc 3 * #(s)
      =  a  s, 3                               := by simp [mul_comm]
    _   a  s, #({b  t | r a b})              := sum_le_sum h_left
    _ =  a  s,  b  t, if r a b then 1 else 0 := by simp
    _ =  b  t,  a  s, if r a b then 1 else 0 := sum_comm
    _ =  b  t, #({a  s | r a b})              := by simp
    _   b  t, 1                               := sum_le_sum h_right
    _  #(t)                                     := by simp

Latihan berikut juga diambil dari tutorial Mehta. Misalkan A merupakan subhimpunan dari range (2 * n) dengan n + 1 elemen. Mudah dilihat bahwa A pasti memuat dua bilangan bulat berurutan, sehingga memuat dua elemen yang saling prima. Jika Anda menonton tutorial tersebut, Anda akan melihat bahwa cukup banyak usaha dihabiskan untuk membuktikan fakta berikut, yang kini dibuktikan secara otomatis oleh omega.

example (m k : ) (h : m  k) (h' : m / 2 = k / 2) : m = k + 1  k = m + 1 := by omega

Solusi latihan Mehta menggunakan prinsip rumah merpati, dalam bentuk exists_lt_card_fiber_of_mul_lt_card_of_maps_to, untuk menunjukkan bahwa terdapat dua elemen berbeda m dan k dalam A sedemikian sehingga m / 2 = k / 2. Cobalah melengkapi pembenaran fakta tersebut, lalu gunakan hasilnya untuk menyelesaikan pembuktian.

example {n : } (A : Finset )
    (hA : #(A) = n + 1)
    (hA' : A  range (2 * n)) :
     m  A,  k  A, Nat.Coprime m k := by
  have :  t  range n, 1 < #({u  A | u / 2 = t}) := by
    apply exists_lt_card_fiber_of_mul_lt_card_of_maps_to
    · sorry
    · sorry
  rcases this with t, ht, ht'
  simp only [one_lt_card, mem_filter] at ht'
  sorry

6.3. Tipe yang Didefinisikan secara Induktif

Landasan Lean memungkinkan kita mendefinisikan tipe induktif, yaitu tipe data yang instansnya dibangkitkan dari bawah ke atas. Sebagai contoh, tipe data List α yang berisi daftar elemen-elemen α dibangkitkan dengan memulai dari daftar kosong, nil, lalu menambahkan elemen satu demi satu ke bagian depan daftar. Di bawah ini kita akan mendefinisikan tipe pohon biner BinTree. Elemen-elemennya dibangkitkan dengan memulai dari pohon kosong, lalu membangun pohon baru dengan menautkan sebuah simpul baru pada dua pohon yang sudah ada.

Dalam Lean, kita dapat mendefinisikan tipe induktif yang objeknya tak berhingga, seperti pohon berfondasi baik dengan percabangan terhitung. Namun, definisi induktif berhingga lazim digunakan dalam matematika diskret, terutama dalam cabang-cabang yang berkaitan dengan ilmu komputer. Lean tidak hanya menyediakan sarana untuk mendefinisikan tipe semacam itu, tetapi juga prinsip induksi dan pendefinisian melalui rekursi. Sebagai contoh, tipe data List α didefinisikan secara induktif:

namespace MyListSpace

inductive List (α : Type*) where
  | nil  : List α
  | cons : α  List α  List α

end MyListSpace

Definisi induktif tersebut menyatakan bahwa setiap elemen List α adalah nil, yaitu daftar kosong, atau cons a as, dengan a sebuah elemen α dan as sebuah daftar elemen-elemen α. Nama lengkap konstruktornya adalah List.nil dan List.cons, tetapi Anda dapat menggunakan notasi yang lebih pendek ketika namespace List terbuka. Ketika namespace List tidak terbuka, Anda dapat menulis .nil dan .cons a as di mana pun Lean mengharapkan sebuah daftar; Lean akan menyisipkan kualifikasi List secara otomatis. Sepanjang bagian ini, kita akan menempatkan definisi sementara dalam namespace terpisah seperti MyListSpace agar tidak berbenturan dengan pustaka standar. Di luar namespace sementara tersebut, kita kembali menggunakan definisi pustaka standar.

Lean mendefinisikan notasi [] untuk nil dan :: untuk cons. Anda juga dapat menulis [a, b, c] untuk a :: b :: c :: []. Fungsi penambahan dan pemetaan didefinisikan secara rekursif sebagai berikut:

def append {α : Type*} : List α  List α  List α
  | [],      bs => bs
  | a :: as, bs => a :: append as bs

def map {α β : Type*} (f : α  β) : List α  List β
  | []      => []
  | a :: as => f a :: map f as

#eval append [1, 2, 3] [4, 5, 6]
#eval map (fun n => n^2) [1, 2, 3, 4, 5]

Perhatikan bahwa terdapat kasus dasar dan kasus rekursif. Pada masing-masing kasus, kedua klausa pendefinisi berlaku secara definisional:

theorem nil_append {α : Type*} (as : List α) : append [] as = as := rfl

theorem cons_append {α : Type*} (a : α) (as : List α) (bs : List α) :
    append (a :: as) bs = a :: append as bs := rfl

theorem map_nil {α β : Type*} (f : α  β) : map f [] = [] := rfl

theorem map_cons {α β : Type*} (f : α  β) (a : α) (as : List α) :
    map f (a :: as) = f a :: map f as := rfl

Fungsi append dan map telah didefinisikan dalam pustaka standar, dan append as bs dapat ditulis sebagai as ++ bs.

Lean memungkinkan Anda menulis pembuktian dengan induksi yang mengikuti struktur definisi.

variable {α β γ : Type*}
variable (as bs cs : List α)
variable (a b c : α)

open List

theorem append_nil :  as : List α, as ++ [] = as
  | [] => rfl
  | a :: as => by rw [cons_append, append_nil as]

theorem map_map (f : α  β) (g : β  γ) :
     as : List α, map g (map f as) = map (g  f) as
  | [] => rfl
  | a :: as => by rw [map_cons, map_cons, map_cons, map_map f g as]; rfl

Anda juga dapat menggunakan taktik induction'.

Tentu saja, teorema-teorema ini sudah tersedia dalam pustaka standar. Sebagai latihan, cobalah mendefinisikan fungsi reverse dalam namespace MyListSpace3 (agar tidak berbenturan dengan List.reverse dari pustaka standar) yang membalik urutan sebuah daftar. Anda dapat mengujinya dengan #eval reverse [1, 2, 3, 4, 5]. Definisi reverse yang paling langsung memerlukan waktu kuadratik, tetapi hal itu tidak perlu dikhawatirkan di sini. Anda dapat melompat ke definisi List.reverse dalam pustaka standar untuk melihat implementasi yang berjalan dalam waktu linear. Cobalah membuktikan reverse (as ++ bs) = reverse bs ++ reverse as dan reverse (reverse as) = as. Anda dapat menggunakan cons_append dan append_assoc, tetapi mungkin perlu merumuskan dan membuktikan beberapa lema bantu.

def reverse : List α  List α := sorry

theorem reverse_append (as bs : List α) : reverse (as ++ bs) = reverse bs ++ reverse as := by
  sorry

theorem reverse_reverse (as : List α) : reverse (reverse as) = as := by sorry

Sebagai contoh lain, perhatikan definisi induktif pohon biner berikut beserta fungsi untuk menghitung ukuran dan kedalaman suatu pohon biner.

inductive BinTree where
  | empty : BinTree
  | node  : BinTree  BinTree  BinTree

namespace BinTree

def size : BinTree  
  | empty    => 0
  | node l r => size l + size r + 1

def depth : BinTree  
  | empty    => 0
  | node l r => max (depth l) (depth r) + 1

Kita akan menghitung pohon biner kosong sebagai pohon biner berukuran 0 dan berkedalaman 0, karena konvensi ini memudahkan pembahasan. Dalam literatur, objek-objek dari tipe data ini kadang disebut pohon biner diperluas. Dengan menyertakan pohon kosong, misalnya, kita dapat mendefinisikan pohon node empty (node empty empty) yang terdiri atas sebuah simpul akar, subpohon kiri kosong, dan subpohon kanan yang hanya terdiri atas satu simpul.

Berikut sebuah pertidaksamaan penting yang menghubungkan ukuran dan kedalaman:

theorem size_le :  t : BinTree, size t  2^depth t - 1
  | empty    => Nat.zero_le _
  | node l r => by
    simp only [depth, size]
    calc l.size + r.size + 1
       (2^l.depth - 1) + (2^r.depth - 1) + 1 := by
          gcongr <;> apply size_le
    _  (2 ^ max l.depth r.depth - 1) + (2 ^ max l.depth r.depth - 1) + 1 := by
          gcongr <;> simp
    _  2 ^ (max l.depth r.depth + 1) - 1 := by
          have : 0 < 2 ^ max l.depth r.depth := by simp
          omega

Cobalah membuktikan pertidaksamaan berikut, yang sedikit lebih mudah. Ingat, jika Anda melakukan pembuktian dengan induksi seperti pada teorema sebelumnya, Anda harus menghapus := by.

theorem depth_le_size :  t : BinTree, depth t  size t := by sorry

Definisikan pula operasi flip pada pohon biner, yang secara rekursif menukar subpohon kiri dan kanan.

def flip : BinTree  BinTree := sorry

Jika definisi Anda benar, pembuktian berikut semestinya cukup dengan rfl.

example: flip  (node (node empty (node empty empty)) (node empty empty)) =
    node (node empty empty) (node (node empty empty) empty) := sorry

Buktikan pernyataan berikut:

theorem size_flip :  t, size (flip t) = size t := by sorry

Kita menutup bagian ini dengan sedikit logika formal. Berikut adalah definisi induktif formula proposisional.

inductive PropForm : Type where
  | var (n : )           : PropForm
  | fls                   : PropForm
  | conj (A B : PropForm) : PropForm
  | disj (A B : PropForm) : PropForm
  | impl (A B : PropForm) : PropForm

Setiap formula proposisional merupakan variabel var n, konstanta salah fls, atau formula majemuk berbentuk conj A B, disj A B, atau impl A B. Dalam notasi matematika biasa, semuanya lazim ditulis secara berturut-turut sebagai \(p_n\), \(\bot\), \(A \wedge B\), \(A \vee B\), dan \(A \to B\). Konektif proposisional lain dapat didefinisikan menggunakan konektif-konektif ini. Sebagai contoh, kita dapat mendefinisikan \(\neg A\) sebagai \(A \to \bot\) dan \(A \leftrightarrow B\) sebagai \((A \to B) \wedge (B \to A)\).

Setelah mendefinisikan tipe data formula proposisional, kita mendefinisikan arti mengevaluasi sebuah formula proposisional terhadap penetapan nilai kebenaran Boolean v pada variabel-variabelnya.

def eval : PropForm  (  Bool)  Bool
  | var n,    v => v n
  | fls,      _ => false
  | conj A B, v => A.eval v && B.eval v
  | disj A B, v => A.eval v || B.eval v
  | impl A B, v => ! A.eval v || B.eval v

Definisi berikut menentukan himpunan variabel yang muncul dalam suatu formula. Teorema sesudahnya menunjukkan bahwa mengevaluasi formula pada dua penetapan nilai kebenaran yang bersepakat pada variabel-variabel formula tersebut menghasilkan nilai yang sama.

def vars : PropForm  Finset 
  | var n    => {n}
  | fls      => 
  | conj A B => A.vars  B.vars
  | disj A B => A.vars  B.vars
  | impl A B => A.vars  B.vars

theorem eval_eq_eval :  (A : PropForm) (v1 v2 :   Bool),
    ( n  A.vars, v1 n = v2 n)  A.eval v1 = A.eval v2
  | var n, v1, v2, h    => by simp_all [vars, eval]
  | fls, v1, v2, h      => by simp_all [eval]
  | conj A B, v1, v2, h => by
      simp_all [vars, eval, eval_eq_eval A v1 v2, eval_eq_eval B v1 v2]
  | disj A B, v1, v2, h => by
      simp_all [vars, eval, eval_eq_eval A v1 v2, eval_eq_eval B v1 v2]
  | impl A B, v1, v2, h => by
      simp_all [vars, eval, eval_eq_eval A v1 v2, eval_eq_eval B v1 v2]

Dengan memperhatikan pengulangan tersebut, kita dapat memanfaatkan otomatisasi secara lebih cerdik.

theorem eval_eq_eval' (A : PropForm) (v1 v2 :   Bool) (h :  n  A.vars, v1 n = v2 n) :
    A.eval v1 = A.eval v2 := by
  cases A <;> simp_all [eval, vars, fun A => eval_eq_eval' A v1 v2]

Fungsi subst A m C menyatakan hasil penggantian setiap kemunculan variabel var m dalam formula A dengan formula C.

def subst : PropForm    PropForm  PropForm
  | var n,    m, C => if n = m then C else var n
  | fls,      _, _ => fls
  | conj A B, m, C => conj (A.subst m C) (B.subst m C)
  | disj A B, m, C => disj (A.subst m C) (B.subst m C)
  | impl A B, m, C => impl (A.subst m C) (B.subst m C)

Sebagai contoh, tunjukkan bahwa mengganti variabel yang tidak muncul dalam suatu formula tidak mengubah formula tersebut:

theorem subst_eq_of_not_mem_vars :
     (A : PropForm) (n : ) (C : PropForm), n  A.vars  A.subst n C = A := sorry

Teorema berikut menyatakan sesuatu yang lebih halus dan menarik: mengevaluasi A.subst n C pada penetapan nilai kebenaran v sama dengan mengevaluasi A pada penetapan yang memberikan nilai C kepada var n. Cobalah membuktikannya.

theorem subst_eval_eq :  (A : PropForm) (n : ) (C : PropForm) (v :   Bool),
  (A.subst n C).eval v = A.eval (fun m => if m = n then C.eval v else v m) := sorry