Array
Array α
は配列を表す型です。特定の型 α : Type u
の要素を一直線に並べたものです。
#[a₁, ..., aₖ]
という記法で Array α
の項を作ることができます。
#check (#[1, 2, 3] : Array Nat)
#check (#["hello", "world"] : Array String)
定義と実行時の性質
Array α
は次のように連結リスト List α
のラッパーとして定義されているように見えます。
structure Array.{u} (α : Type u) : Type u where
toList : List α
しかしドキュメントコメントに以下のように書かれている通り、実行時には List
とは大きく異なる動的配列(dynamic array)としての振る舞いを見せます。
Array α
is the type of dynamic arrays with elements fromα
. This type has special support in the runtime.Arrays perform best when unshared. As long as there is never more than one reference to an array, all updates will be performed destructively. This results in performance comparable to mutable arrays in imperative programming languages.
An array has a size and a capacity. The size is the number of elements present in the array, while the capacity is the amount of memory currently allocated for elements. The size is accessible via
Array.size
, but the capacity is not observable from Lean code.Array.emptyWithCapacity n
creates an array which is equal to#[]
, but internally allocates an array of capacityn
. When the size exceeds the capacity, allocation is required to grow the array.From the point of view of proofs,
Array α
is just a wrapper aroundList α
.
List
のラッパーとしての定義は、証明を行おうとしたときに参照されます。
基本的な操作
インデックスアクセス
配列は GetElem
のインスタンスであり、i
番目の要素を取得するために a[i]
という記法が使用できます。Array α
は実行時には動的配列として振る舞うので、インデックスアクセスは高速に行うことができます。
#guard #[1, 2, 3][0] = 1
#guard #[1, 2, 3][3]? = none
要素の追加
xs : Array α
に対して Array.push
関数で末尾に要素を追加できます。
ドキュメントコメントに次のように書かれている通り、この操作は高速に行うことができます。
Adds an element to the end of an array. The resulting array's size is one greater than the input array. If there are no other references to the array, then it is modified in-place.
This takes amortized
O(1)
time becauseArray α
is represented by a dynamic array.
#guard #[1, 2, 3].push 4 = #[1, 2, 3, 4]
List と比較した特長
List
も同様にインデックスアクセスをサポートしていますが、Array
の方がより高速にアクセスすることができます。
なぜかというと、List
は連結リストとして実装されており、i : Nat
番目にアクセスしようとすると最初の要素から順に辿っていく必要があるからです。一方で Array
は動的配列として実装されているので、i
番目の要素に直接アクセスできます。
open IO
variable {α : Type} {valid : α → Nat → Prop}
variable [GetElem α Nat Nat valid] [GetElem? α Nat Nat valid]
/--
コレクション型に対して、そのインデックスアクセスの実行時間を計測する関数
* `col`: 対象となるコレクション
* `accessPoint`: 計算の中では、同じインデックスに対して何度もアクセスして平均時間を計測する。
そこでアクセスするインデックス。
-/
@[noinline]
def evalIdxAccess (col : α) (accessPoint : Nat) : IO Nat := do
-- アクセスを試みる回数
let access_times := 1000
-- コンパイラに最適化されて結果が変わることを防ぐために
-- どうでもいい計算をする
let mut sum := 0
let mut sum_time := 0
for _ in [0:access_times] do
-- インデックスアクセスにかかる時間を測定する
let start_time <- monoNanosNow
let x := col[accessPoint]!
let end_time <- monoNanosNow
let time := end_time - start_time
-- アクセス時間を合計する
sum := (sum + x) % 2
sum_time := sum_time + time
IO.println s!"{sum} is computed..."
return sum_time / access_times
def main : IO Unit := do
let size := 10_000_000
let accessPoint := size / 10
let listTime ← evalIdxAccess (List.range size) accessPoint
println s!"List access time: {listTime}ns"
let arrayTime ← evalIdxAccess (Array.range size) accessPoint
println s!"Array access time: {arrayTime}ns"
if listTime < arrayTime then
throw <| userError "List is faster than Array"
#eval main
使用例
配列の特徴を生かしてプログラムを組んでいる例をいくつか紹介します。
特定の要素だけ末尾に移動させる
arr : Array α
に対しては、インデックスアクセスと「指定されたインデックスにある要素の更新」が高速に行えるので、それを生かして「特定の要素だけ末尾に移動させる」操作を効率的に実装することができます。
variable {α : Type} [BEq α]
/-- 配列`arr`の要素`a`が与えられたときに、
他の要素の位置関係を維持したまま、すべての`a`を配列の末尾に移動させる
-/
def Array.move (arr : Array α) (a : α) : Array α := Id.run do
let mut arr := arr
let mut write := 0
-- まず `a` 以外の要素を前詰めで配置
for x in arr do
if x != a then
arr := arr.set! write x
write := write + 1
-- 残りの部分を `a` で埋める
for i in [write:arr.size] do
arr := arr.set! i a
return arr
#guard Array.move #[1, 0, 2, 0, 3] 0 == #[1, 2, 3, 0, 0] -- `0` を末尾に移動
#guard Array.move #[1, 2, 3] 0 == #[1, 2, 3] -- `0` がないので変化なし
#guard Array.move #[] 0 == #[] -- 空配列はそのまま
#guard Array.move #[0, 0, 0] 0 == #[0, 0, 0] -- `0` しかないので変化なし
スタックの実装
arr : Array α
に対しては「末尾に要素を追加する操作」と「末尾から要素を取り出す操作」が高速に行えるので、スタックの実装に利用することができます。
/-- 括弧が対応しているか判定する -/
def matchParen (c1 c2 : Char) : Bool :=
match c1, c2 with
| '(', ')' => true
| '{', '}' => true
| '[', ']' => true
| _, _ => false
/--
文字列 `s` に含まれる括弧が正しく対応しているかどうかを判定する関数。
開き括弧と閉じ括弧が対応しており、正しい順序で閉じられている場合に `true` を返す。
-/
def validParen (s : String) : Bool := Id.run do
-- 括弧のスタックを空で初期化
let mut stack : Array Char := #[]
-- 文字列の各文字に対してループ
for c in s.toList do
-- スタックの末尾の要素(最後に追加された開き括弧)を取得
let last := stack.back?
match last with
| none =>
-- スタックが空なら、文字をスタックに追加(開き括弧のはず)
stack := stack.push c
| some last =>
-- スタックの末尾と現在の文字が対応する括弧なら、スタックから取り除く(ペアが閉じられた)
if matchParen last c then
stack := stack.pop
else
-- 対応していない場合は、新たにスタックに追加(新しい開き括弧)
stack := stack.push c
-- すべての括弧が正しく閉じられていればスタックは空になっている
return stack.isEmpty
-- テストケース
#guard validParen "()"
#guard validParen "()[]{}"
#guard !validParen "(]"
#guard !validParen "([)]"
#guard validParen "{[]}"
#guard !validParen "{"
#guard !validParen "}"
#guard validParen "([{}])({}){}"