【Common Lisp】
make-array で作る1次元配列の
基本の使い方
(vector)

  • Common Lispの「配列」は特に「ベクタ」といい、make-arrayで作る。
  • 任意の位置にO(1)でアクセスできる。
  • arefで読み書きし、map・reduce・remove-ifなどのシーケンス関数がリストと同じように使える。
  • fill-pointerとadjustableを組み合わせると可変長スタックとして使え、vector-push-extendで自動拡張できる。
  • element-typeやdeclareで型を特化させるとSBCLでは内部表現が最適化され、演算が速くなる。

関連記事

1. ベクタを作る・読む・書く・ループ

Common Lisp では配列を make-array で作ります。
1次元配列はベクタとも呼ばれ、メモリ上に要素が連続して並びます。

ベクタ vs リスト ベクタ(配列) メモリ上に連続して並ぶ 10 20 30 40 50 0 1 2 3 4 aref v 2 → 直接 ● ランダムアクセス O(1) ● メモリ連続 → キャッシュ効率◎ ● サイズ固定(原則) ● 先頭への挿入は O(N) リスト(連結リスト) ポインタでノードをつなぐ 10 20 30 40 nil nth 2 → 先頭からたどる ● 先頭への追加・削除 O(1) ● 長さが動的に変わる ● ランダムアクセス O(N) ● ポインタ分のメモリ消費

そのため、リストが n 番目の要素に到達するには先頭から順にたどって O(N) になるのに対し、ベクタでは任意の位置にインデックスで O(1) アクセスできます。

1.1. make-array で空の配列を確保する

配列の場合は、まず領域を確保し、それから値を書き込むのが本筋です。

make-arrayは、長さと初期値を指定して配列オブジェクトを作ります。

(defun make-score-table (n)
  (make-array n :initial-element 0))

(make-score-table 5)
;=> #(0 0 0 0 0)Code language: Lisp (lisp)

:initial-element を省くとデフォルト初期値になりますが、実装によって nil0 か異なります。

1.2. vector 関数と #(…) リテラル

要素が決まっている場合には、かんたんにベクタオブジェクトを生成できます。

リストの list に対応するのが vector です。
引数をそのままベクタにして返します。

(defun make-rgb (r g b)
  (vector r g b))

(make-rgb 255 128 0)
;=> #(255 128 0)Code language: Lisp (lisp)

また、リテラルでベクタオブジェクトを生成することもできます。

#(1 2 3 4 5)
;=> #(1 2 3 4 5)Code language: Lisp (lisp)

#(...) はリーダーマクロで、'x(quote x) に展開されるのと同じ仕組みです。
コードを読み込む段階で処理系がベクタオブジェクトを生成するため、実行時には新しいオブジェクトを作りません。

ただし、リーダーマクロは読み込み時に確定するため、中に変数を書いても評価されない点には注意が必要です。

(defun literal-vs-vector (x)
  (list #(x 20 30)        ; x はシンボルのまま
        (vector x 20 30))) ; x が評価される

(literal-vs-vector 10)
;=> (#(X 20 30) #(10 20 30))Code language: Lisp (lisp)

また #(...) は、別の場所に書いてもコンパイラによって同じオブジェクトが再利用される可能性があるため変更は禁止です。
つまり、内容が固定でコードに埋め込むなら #(...)、実行時に値が決まる・変更する前提なら vectormake-array を使います。

1.3. length で長さを得る

配列の長さは、lengthで求められます。

(defun count-students (scores)
  (length scores))

(count-students #(80 90 70 85 95))
;=> 5Code language: Lisp (lisp)

注意点としては、ベクタの場合、配列の長さは入っているデータの個数なのか、確保している要領なのか、という問題があります。
この問題については、fill-pointer と可変長ベクタの項で考えます。

1.4. aref で読む

aref は、0 始まりインデックスを受け取り、対応する要素を返します。
範囲外を参照しようとすると、実装固有のエラーになります。

リストの nth に対応しますが、aref は O(1) です。

(defun first-score (scores)
  ; (nth 0 scores) に対応、ただし O(1)
  (aref scores 0))

(first-score #(80 90 70 85 95))
;=> 80Code language: Lisp (lisp)
(defun last-score (scores)
  (aref scores (1- (length scores))))

(last-score #(80 90 70 85 95))
;=> 95Code language: Lisp (lisp)

1.5. setf aref で書く

setfaref を組み合わせて要素を更新します。
リストに対する破壊的な書き換えと同じ setf の構文で統一されています。

(defun update-score (scores i new-score)
  (setf (aref scores i) new-score)
  scores)


(defparameter v (make-array 5 :initial-contents '(80 90 70 85 95)))
(update-score v 2 99)

;=> #(80 90 99 85 95)Code language: Lisp (lisp)

1.6. loop で配列の要素に処理をする

areflengthを使うと、loopで各要素に対する処理を書けます。
インデックスは、lengthより小さい必要があるので below を使います。

(defun print-indexed-scores (scores)
  (loop for i below (length scores)
        do (format t "~a番: ~a~%" (1+ i) (aref scores i))))

(print-indexed-scores #(80 90 70 85 95))
; 1番: 80
; 2番: 90
; ...Code language: Lisp (lisp)

1.7. loop across でベクタを走査する

ベクタの全要素は、loopacross 節でも順に取り出せます。
リストに対する for x in list に対応します。

(defun print-scores (scores)
  (loop for s across scores
        do (format t "~a " s)))

(print-scores #(80 90 70 85 95))
; 80 90 70 85 95Code language: Lisp (lisp)

2. ベクタ全体への操作

ベクタ全体への操作 map 変換して新しいベクタ (map ‘vector fn v) reduce 畳み込み・合計・最大 (reduce #’+ v) remove-if 条件でフィルタ 元のベクタは変更なし find 最初の一致要素 なければ nil #(80 90 70) map #(80.0 90.0 70.0) reduce 240(合計) remove-if #(90) 80未満を除去

2.1. map で変換する

(defun normalize-scores (scores max-score)
  (map 'vector (lambda (s) (* 100.0 (/ s max-score))) scores))

(normalize-scores #(80 90 70 85 95) 100)
;=> #(80.0 90.0 70.0 85.0 95.0)Code language: Lisp (lisp)

第1引数で返り値の型を指定します。
'nil を渡すと、リストの mapc に相当するような副作用だけ実行することもできます。

(defun print-each (v)
  (map nil (lambda (x) (format t "~a~%" x)) v))

(print-each #(80 90 70))
; 80
; 90
; 70Code language: Lisp (lisp)

リストでのマップ関数との対応関係を整理します。

; (mapcar (lambda (s) ...) lst) に対応
; (map 'vector (lambda (s) ...) v))

; (mapc (lambda (s) ...) lst) に対応
; (map nil (lambda (s) ...) v)

2.2. reduce で畳み込む

リストの reduce は、そのままベクタにも使えます。

(defun total-score (scores)
  (reduce #'+ scores))

(defun highest-score (scores)
  (reduce #'max scores))


(defparameter v #(80 90 70 85 95))
(total-score v)
;=> 420
(highest-score v)
;=> 95Code language: Lisp (lisp)

2.3. remove-if でフィルタする

条件で要素をフィルタするには、リストの remove-ifremove-if-not がそのままベクタにも使えます。
どちらも新しいベクタを返し、元のベクタは変更しません。

(defun failing-scores (scores passing-mark)
  (remove-if (lambda (s) (>= s passing-mark)) scores))

(defun passing-scores (scores passing-mark)
  (remove-if-not (lambda (s) (>= s passing-mark)) scores))

(failing-scores #(80 90 70 85 95) 85)
;=> #(80 70)

(passing-scores #(80 90 70 85 95) 85)
;=> #(85 95)Code language: Lisp (lisp)

値を直接指定して除くには remove を使います。

(defun remove-score (scores target)
  (remove target scores))

(remove-score #(80 90 70 90 95) 90)
;=> #(80 70 95)Code language: Lisp (lisp)

2.4. find で検索する

find は最初に一致した要素を返し、見つからなければ nil を返します。

(defun score-exists-p (scores target)
  (if (find target scores) t nil))

(score-exists-p #(80 90 70 85 95) 70)
;=> T

(score-exists-p #(80 90 70 85 95) 60)
;=> NILCode language: Lisp (lisp)

リストの member に似ていますが、member が一致した要素以降のリストを返すのに対し、find は要素そのものを返します。
なおリスト専用の member はベクタには使えません。

(defun find-passing (scores passing-mark)
  (find-if (lambda (s) (>= s passing-mark)) scores))

(find-passing #(80 90 70 85 95) 85)
;=> 85Code language: Lisp (lisp)

3. ベクタとシーケンス

ベクタは、Common Lispの「シーケンス」の一種です。

ベクタとシーケンス シーケンス リスト (a b c) ベクタ #(1 2 3) 文字列 “abc” 共通シーケンス関数 map / reduce find / remove-if length / subseq copy-seq / sort coerce で型変換 リスト coerce ベクタ coerce 文字列 文字列は element-type ‘character のベクタと同等

シーケンスとは順序を持つデータの総称で、リスト、ベクタ、文字列がこれに当たります。
シーケンスは、mapreducefindremove-if などの関数が同じように使えます。
さらに、文字列は element-type 'character のベクタと同じ扱いになる場面も多く、lengtharefsubseq などは、ベクタと同じように使えます。

ベクタはサイズを固定して作るのが基本ですが、可変長にするには fill-pointer:adjustable t の組み合わせが必要になります。

用途の目安はこうなります。
インデックスで頻繁にアクセスする、サイズが事前にわかる、メモリ効率を気にするならベクタを選びます。
ただし、先頭への追加や削除が多く、長さが動的に変わるならリストの方が自然です。

3.1. coerce で型を変換する

coerce はシーケンスの型を変換します。
ベクタとリスト・文字列を自由に行き来できます。

リストをベクタに変換したいときには、coerceの第2引数に 'vectorを渡します。

(defun list->vector (lst)
  (coerce lst 'vector))

(list->vector '(1 2 3 4 5))
;=> #(1 2 3 4 5)Code language: Lisp (lisp)

反対に、ベクタをリストに変えることもできます。

(defun vector->list (v)
  (coerce v 'list))

(vector->list #(1 2 3 4 5))
;=> (1 2 3 4 5)Code language: Lisp (lisp)

3.2. vectorp で型を確認する

vectorp は、ベクタかどうかを判定します。
たとえば、リストとベクタを両方受け付ける関数の分岐に使います。

(defun sequence-first (seq)
  (cond
    ((vectorp seq) (aref seq 0))
    ((listp seq) (car seq))
    (t seq)))

(sequence-first #(1 2 3))
;=> 1

(sequence-first '(4 5 6))
;=> 4

(sequence-first "hello")
;=> #\hCode language: Lisp (lisp)

ちなみに、文字列もベクタの一種なので vectorpt を返します。

3.3. make-array でリストから配列を作る

make-arrayは、:initial-contents で、内容をリストやほかのベクタでまとめて渡せます。
ただし、長さと要素数が一致しないとエラーになるので注意が必要です。

(defun vector-from-list (lst)
  (make-array (length lst) :initial-contents lst))

(vector-from-list '(10 20 30 40 50))
;=> #(10 20 30 40 50)

; 実行時に生成したリストも渡せる
(defun squares-vector (n)
  (make-array n :initial-contents
              (loop for i from 1 to n collect (* i i))))

(squares-vector 5)
;=> #(1 4 9 16 25)Code language: Lisp (lisp)

coerceとの違いは、make-arrayは作成するベクタの性質を細かく指定できる点です。

; 整数専用ベクタを作りたいなら make-array
(make-array (length lst)
            :element-type 'fixnum
            :initial-contents lst)Code language: Lisp (lisp)

要素型を:element-typeで絞ったり、サイズ変更可能にする:adjustableを付けたりできます。
coerceはそういった制御ができず、汎用ベクタが返ります。

sort は破壊的な関数で、与えたベクタの値を変更します。

3.4. subseq でスライスする

subseqは、新しいベクタを作って返します。
start は含まれ、end は含まれません。

(defun top-three (scores)
  (subseq (sort (copy-seq scores) #'>) 0 3))

(top-three #(80 90 70 85 95))
;=> #(95 90 85)Code language: Lisp (lisp)

リストの subseq と同じ関数がそのまま使えます。

3.5. copy-seq でコピーする

copy-seq は浅いコピー(shallow copy)を作ります。
元のベクタを壊さずに操作したいときに使います。

(defun double-first (scores)
  (let ((copy (copy-seq scores)))
    (setf (aref copy 0) (* 2 (aref copy 0)))
    copy))

(let ((scores (make-array 5 :initial-contents '(80 90 70 85 95))))
  (list (double-first scores) scores))
;=> (#(160 90 70 85 95) #(80 90 70 85 95))Code language: Lisp (lisp)

ベクタの要素が数値・文字・シンボルなど不変のオブジェクトだけで構成されているときは、shallow copyとdeep copyの差を気にしないでも大丈夫です。

3.6. 【補足】deep copyとオブジェクトの配列

shallow copyとdeep copyの違いが出るのは、ベクタの要素がほかのオブジェクト(リストなど)への参照である場合です。

;; 要素がリストのベクタ
(let* ((original (vector (list 1 2) (list 3 4)))
       (shallow  (copy-seq original)))
  (push 99 (aref shallow 0))   ; shallow[0] のリストを変更
  (list original shallow))

;=> (#((99 1 2) (3 4)) #((99 1 2) (3 4)))Code language: Lisp (lisp)

shallow[0]とoriginal[0]は同じリストオブジェクトを指しているので、片方を変えるともう片方も変わってしまいます。

まったく別のコピーを作るには、Common Lispには標準のdeep-copyがないので、再帰的にコピーする関数を自前で書く必要があります。

(defun deep-copy (obj)
  (typecase obj
    (vector
     (let ((copy (copy-seq obj)))
       (dotimes (i (length copy))
         (setf (aref copy i) (deep-copy (aref copy i))))
       copy))
    (list
     (mapcar #'deep-copy obj))
    (t obj)))  ; 数値・シンボル等はそのまま返す


(let* ((original (vector (list 1 2) (list 3 4)))
       (deep     (deep-copy original)))
  (push 99 (aref deep 0))
  (list original deep))
;=> (#((1 2) (3 4)) #((99 1 2) (3 4)))
;   original は変わらないCode language: Lisp (lisp)

ただし、これは素朴な実装で、循環構造があると無限ループする点には注意が必要です。
データ設計でコピーが必要な範囲を限定するほうが安全です。

3.7. sort で並べ替える

一時的にソート済みの配列が必要なだけなら、先に copy-seq してから渡すのが非破壊ソートの慣用句です。

(defun sorted-scores (scores)
  (sort (copy-seq scores) #'>))

(sorted-scores #(80 90 70 85 95))
;=> #(95 90 85 80 70)Code language: Lisp (lisp)

stable-sort も破壊的操作です。
これは、同値要素があれば、その順序を元のまま保ちます。

3.8. elt によるシーケンス汎用アクセス

ベクタでもリストでも区別なく同じ書き方ができる関数がいくつかありました。
これは、「シーケンス」と総称して、同じように扱える仕組みです。

インデックスアクセスには、elt という汎用アクセサがあります。

(defun nth-element (seq n)
  (elt seq n))

(nth-element #(10 20 30 40 50) 2)
;=> 30

(nth-element '(10 20 30 40 50) 2)
;=> 30Code language: Lisp (lisp)

リストかベクタかを問わず動く汎用関数を書くときに elt を選びます。
ベクタだと aref、 リストだと nth に相当する操作になります。

ただし、リストに対して elt を使うと O(N) になる点は変わりません。
つまり、コードから計算オーダーが予測できなくなるデメリットもあります。
ベクタ専用のコードでは aref を使うほうが、意図が明確になる利点もあるわけです。

4. 実践パターン

4.1. 標準入力から整数をベクタに読み込む

競技プログラミングで頻出の「N 個の整数を読む」パターンです。
read は標準入力から1トークン読むので、スペース区切りでも改行区切りでも動きます。

(defun read-int-vector (n)
  (let ((v (make-array n :element-type 'fixnum)))
    (loop for i below n
          do (setf (aref v i) (read)))
    v))

; 入力: 1 2 3 4 5
; (read-int-vector 5)
;=> #(1 2 3 4 5)Code language: Lisp (lisp)

4.2. コマンドライン引数をベクタで扱う

SBCL では sb-ext:*posix-argv* でコマンドライン引数にアクセスできます。
coerce でベクタに変換すると、インデックスで特定の引数に直接アクセスできます。

(defun argv-vector ()
  (coerce sb-ext:*posix-argv* 'vector))

(defun get-args ()
  (let ((all (argv-vector)))
    (when (> (length all) 1)
      (subseq all 1))))Code language: Lisp (lisp)

4.3. 頻度表(英字カウント)

char-code で文字コードを取得し、#\a のコードを引くと英小文字を 0〜25 のインデックスに変換できます。

(defun count-lowercase (s)
  (let ((cnt (make-array 26 :initial-element 0)))
    (loop for ch across s
          when (and (char<= #\a ch) (char<= ch #\z))
            do (incf (aref cnt (- (char-code ch) (char-code #\a)))))
    cnt))

(count-lowercase "abacaba")
;=> #(4 2 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)Code language: Lisp (lisp)

5. ベクタに関するまとめ表

そのほか、ベクタに関係する関数を一覧にします

用途関数・オプション
作成make-array, vector, :initial-element, :initial-contents
読み書きaref, setf aref, svref, elt
長さlength
同一性equal, equalp
型確認vectorp, arrayp, array-dimensions, array-element-type
コピー・変換copy-seq, coerce, concatenate, subseq, displaced-to, displaced-index-offset
一括操作fill, substitute, substitute-if
ソートsort, stable-sort, reverse, nreverse
関数型操作map, map-into, reduce
フィルタremove-if, remove-if-not, remove, remove-duplicates
検索find, find-if, position, position-if, count, count-if
条件判定every, some, notany, notevery
可変長:fill-pointer, :adjustable, vector-push-extend, vector-pop, adjust-array
型特化:element-type 'fixnum, 'single-float, 'bit, 'character, declare, simple-array, optimize

また、make-arrayで作れる配列の分類です。

汎用
element-type なし(= t
型特化
element-type 指定あり
1次固定長
adjustable なし
fill-pointer なし
simple-vector
(simple-array t (*))
型特化 simple-array
(simple-array fixnum (*)) など
1次元可変長
adjustable t または
fill-pointer あり
汎用可変ベクタ型特化可変ベクタ
多次元固定長
adjustable なし
simple-array(多次元)
(simple-array t (* *)) など
型特化 simple-array(多次元)
(simple-array fixnum (* *)) など
多次元可変長
adjustable t
汎用可変多次元配列
aref のみ
型特化可変多次元配列
aref のみ

6. ベクタと効率化

ベクタは、リストのような柔軟性の代わりに、マシンのメモリを効率に扱えます。

ベクタと効率化 ① element-type ‘fixnum 整数を unbox 格納 ‘single-float / ‘bit ② declare 型をコンパイラへ明示 (declare (type (simple-array fixnum) v)) ③ optimize (optimize (speed 3) (safety 0)) チェックを省略し高速化 displaced-to:コピーなし参照 src: #(0 1 2 3 4 5 6 7 8 9) view: #(2 3 4 5 6) start=2, size=5 let+defun:キャッシュ閉じ込め (let ((memo (make-array 1000 :initial-element -1))) (defun fib (n) …)) グローバル変数を使わずキャッシュ

6.1. 型特化ベクタ(:element-type)

繰り返し処理の時間効率がネックになっている場合は、 element-typedeclareoptimize の三点セットを試す価値があります。

:element-type を指定すると処理系がその型に特化した内部表現を使えるようになります。

SBCL では fixnum 特化ベクタは整数を unbox して格納するため、汎用ベクタより演算が速くなります。
型と一致しない値を入れると type-error が出ます。

(defun make-int-table (n)
  (make-array n :element-type 'fixnum :initial-element 0))

(make-int-table 5)
;=> #(0 0 0 0 0)Code language: Lisp (lisp)
(defun make-float-table (n)
  (make-array n :element-type 'single-float :initial-element 0.0))

(make-float-table 5)
;=> #(0.0 0.0 0.0 0.0 0.0)Code language: Lisp (lisp)

6.2. simple-vectorとsimple-array

配列要素の型を特化するだけでなく、ベクタそのものの型を宣言することで、計算効率が上がることがあります。
とくに、(simple-array fixnum (*)) の型宣言がよく使われます。

(defun dot-product (a b)
  (declare (type (simple-array fixnum (*)) a b)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i below (length a)
          do (incf sum (* (aref a i) (aref b i))))
    sum))

(dot-product
  (make-array 5 :element-type 'fixnum :initial-contents '(1 2 3 4 5))
  (make-array 5 :element-type 'fixnum :initial-contents '(5 4 3 2 1)))
;=> 35Code language: Lisp (lisp)

配列の中には、simple-vectorsimple-array というサブタイプがあります。

  • simple-vectorは、型指定が任意固定長1次元配列です。
    つまり、make-array で特にパラメータを指定しないと、simple-vectorになります。
  • 一方、simple-arrayは、多次元を含む固定長の配列のことです。

つまり、simple-vectorは、simple-array に含まれますが、型指定には対応していません。
そこで、パフォーマンス的には、適切に型指定したsimple-arrayの方がよいことが期待できます。

simple-array の特徴は、固定長であることが明示されていることです。
(simple-array fixnum (*))は、「fixnum の固定長1次元配列」を表し、コンパイラはインデックスチェックや間接参照を省ける場合があります。

6.3. 【補足】simple-vectorのsvref

ちなみに、simple-vectorには、svrefという専用アクセサがあります。
しかし、arefでも代用できるので、あまり使う機会はないかもしれません。

(defun simple-first (v)
  (declare (type simple-vector v))
  (svref v 0))

(simple-first (vector 10 20 30))
;=> 10Code language: Lisp (lisp)

6.4. bit型ベクタ

bit 型ベクタはビット列として #*... 形式で表示されます。
フラグの配列やビットマスクを省メモリで持つのに使います。

(defun make-flag-table (n)
  (make-array n :element-type 'bit :initial-element 0))

(make-flag-table 8)
;=> #*00000000Code language: Lisp (lisp)
(defun set-flag (flags i)
  (setf (aref flags i) 1)
  flags)

(set-flag (make-flag-table 8) 3)
;=> #*00010000Code language: Lisp (lisp)

array-element-type はベクタが受け付ける型を返します。
:element-type を省略した汎用ベクタでは t が返ります。

(defun check-element-type (v)
  (array-element-type v))

(check-element-type (make-int-table 5))
;=> FIXNUM

(check-element-type (make-array 5))
;=> TCode language: Lisp (lisp)

6.5. displaced-to で部分ベクタを参照する

displaced-to は別のベクタの一部を、コピーなしで参照する窓を作ります。

大きな配列の一部分だけ関数に渡したいときに、subseqだとコピーが発生します。
そこで、make-array displaced-to で「ビュー配列」を作るとコピーを避けられます。

(defun make-window (src start size)
  (make-array size :displaced-to src :displaced-index-offset start))

(let ((src (make-array 10 :initial-contents '(0 1 2 3 4 5 6 7 8 9))))
  (make-window src 2 5))
;=> #(2 3 4 5 6)Code language: Lisp (lisp)

ビュー配列への書き込みは元のベクタに反映されます。

6.6. let + defun でキャッシュを関数に閉じ込める

関数で扱うベクタを何度も生成し直す代わりにキャッシュとして保持することができますす。

let の中で defun を書くと、グローバル変数を使わずに関数の静的変数のように持つことができます。

(let ((memo (make-array 1000 :initial-element -1)))
  (defun fib (n)
    (cond ((< n 2) n)
          ((>= (aref memo n) 0) (aref memo n))
          (t (setf (aref memo n)
                   (+ (fib (- n 1)) (fib (- n 2)))))))
  (defun fib-reset ()
    (fill memo -1)))

(fib 10)   ;=> 55   ; memo[0..10] が埋まる
(fib 10)   ;=> 55   ; memo を参照するだけ、再計算しない
(fib 15)   ;=> 610  ; memo[11..15] だけ追加で計算する
(fib-reset)         ; memo をリセット(外から memo には触れない)Code language: Lisp (lisp)

let の評価はファイルをロードしたときに一度だけ行われ、memo はそのとき作られた同じベクタをずっと参照し続けます。
そのため、fib を何度呼んでも memo は続きから利用できます。

let の中に defun を書いても、関数名はグローバルな名前空間に登録されますが、memo は、クロージャ変数になり外からアクセスできません。
関数自体もローカルに閉じ込めたいなら fletlabels を使います。

fib-reset も同じ memo を閉じ込めているので、グローバル変数を公開せずにリセットできます。

ちなみに、defun の中に let を書くと意味がまったく変わります。
memo は関数を呼ぶたびに新しく作られるため、キャッシュとして機能しません。

(defun fib-no-cache (n)
  (let ((memo (make-array 1000 :initial-element -1)))  ; 毎回リセットされる
    (cond ((< n 2) n)
          ((>= (aref memo n) 0) (aref memo n))
          (t (setf (aref memo n)
                   (+ (fib-no-cache (- n 1)) (fib-no-cache (- n 2))))))))Code language: Lisp (lisp)

7. 【応用】可変長ベクタのスタック構造

配列の基本的な使い方の一つに、「スタック」があります。
スタックは、配列の最後尾にデータを追加したり、取り出したりできます。

ここまでのベクタは作成時にサイズを固定しますが、要素数が実行中に増えるスタックとして使うために、fill-pointer:adjustable t という仕組みがあります。
ただし、単純なベクタよりは、ちょっと管理がややこしくなります。

7.1. fill-pointerとvetor-push(固定長スタック)

スタック操作の基本は、vector-push です。
ベクタに要素を追加します。

リストの push に対応しますが、先頭ではなく末尾に追加する点が違います。
ただし、vector-pushは、配列サイズを変更しません。
あらかじめ確保した容量内に収まるときだけ追加し、オーバーすると nil を返します。

ここで大事なのは、要素の追加位置です。
固定長配列の場合は最後尾に追加できません。
そこで、代わりに内部に fill-pointer という、「論理的な長さ」を管理するカーソルを持っています。

(defun push-until-full (items capacity)
  (let ((v (make-array capacity :fill-pointer 0)))
    (loop for item in items
          for result = (vector-push item v)
          while result)
    v))

(push-until-full '(1 2 3 4 5) 3)
;=> #(1 2 3)Code language: Lisp (lisp)

つまり、fill-pointerの位置に要素を追加し、加算する仕組みです。
空の状態から始めるなら、fill-pointer の初期値に 0 を指定してください。

7.2. fill-pointerとvector-pop

vector-pop は末尾要素を返しながら fill-pointer を1つ減らします。
fill-pointert を渡すと、ベクタのサイズと同じ値が初期値になります。

(defparameter V (make-array 5 :fill-pointer t :initial-element 0))
(fill-pointer V)
;=> 5Code language: Lisp (lisp)

また、空チェックとして (zerop (fill-pointer v)) が必要です。
リストの pop に対応しますが、スタックが空のとき nil を返す pop と違い、vector-pop はエラーを発生させるからです。

ちなみに、fill-pointerlengthが同じ場合は、スタックが満杯扱いの状態で、vector-push は失敗します。

7.3. adjustable で可変長ベクタ

ベクタをサイズを変更できるようにするには、make-array:adjustable t を与えて、adjust-arrayにします。
これは、最大容量を変更できるようになり、大きなベクタが不要になれば縮小も可能です。

(defparameter V (make-array 3 :adjustable t :initial-contents '(1 2 3)))

(defun grow-table (v new-size)
  (adjust-array v new-size :initial-element 0))

(grow-table V 6)
;=> #(1 2 3 0 0 0)Code language: Lisp (lisp)

7.4. vector-push-extend(可変長スタック)

可変長ベクタに対応したスタック操作が、vector-push-extend です。

vector-push-extend は、要素を末尾に追加し、容量が足りなくなれば自動でバッファを拡張します。

(defun collect-evens (limit)
  (let ((result (make-array 0 :fill-pointer 0 :adjustable t)))
    (loop for i from 2 to limit by 2
          do (vector-push-extend i result))
    result))

(collect-evens 10)
;=> #(2 4 6 8 10)Code language: Lisp (lisp)

7.5. 文字列バッファの仕組み

文字列バッファは、可変長スタックと同じ仕組みで作れます。

(defun build-string (&rest parts)
  (let ((buf (make-array 64
                         :element-type 'character
                         :fill-pointer 0
                         :adjustable t)))
    (dolist (s parts)
      (loop for c across s
            do (vector-push-extend c buf)))
    (coerce buf 'string)))

(build-string "hello" ", " "world")
;=> "hello, world"Code language: Lisp (lisp)

element-type 'character を指定したベクタは文字列として扱われます。

8. 【リファレンス】比較と属性確認

8.1. equal と equalp でベクタを比較する

(defun same-scores-p (a b)
  (equal a b))

(same-scores-p #(80 90 70) #(80 90 70))
;=> T

(same-scores-p #(80 90 70) #(80 90 71))
;=> NILCode language: Lisp (lisp)
(defun approx-equal-p (a b)
  ; 数値型が違っても内容が一致すれば真
  (equalp a b))

(approx-equal-p #(1 2 3) #(1.0 2.0 3.0))
;=> TCode language: Lisp (lisp)

equal はリストとベクタを同一視しません。
equalp も同様です。

(defun list-vector-same-p (lst v)
  (list (equal lst v) (equalp lst v)))

(list-vector-same-p '(1 2 3) #(1 2 3))
;=> (NIL NIL)Code language: Lisp (lisp)

8.2. 配列の次元を確認する(array-dimensions)

array-dimensions は各次元のサイズをリストで返します。
1次元なので要素が1つのリストになります。

(defun describe-vector (v)
  (list :dimensions (array-dimensions v)
        :vectorp (vectorp v)
        :arrayp (arrayp v)))

(describe-vector #(10 20 30 40 50))
;=> (:DIMENSIONS (5) :VECTORP T :ARRAYP T)Code language: Lisp (lisp)

arrayp はベクタを含む配列全般に対して t を返します。

9. 検索・フィルタの応用

9.1. count と count-if

リストの countcount-if がそのままベクタにも使えます。

(defun count-passing (scores passing-mark)
  (count-if (lambda (s) (>= s passing-mark)) scores))

(count-passing #(80 90 70 85 95) 85)
;=> 2Code language: Lisp (lisp)
(defun count-occurrences (v target)
  (count target v))

(count-occurrences #(1 2 1 3 1) 1)
;=> 3Code language: Lisp (lisp)

9.2. position と position-if

要素ではなくインデックスが欲しいときに使います。
見つからない場合は nil を返します。

(defun find-first-failing-index (scores passing-mark)
  (position-if (lambda (s) (< s passing-mark)) scores))

(find-first-failing-index #(80 90 70 85 95) 75)
;=> 2Code language: Lisp (lisp)
(defun index-of (v target)
  (position target v))

(index-of #(80 90 70 85 95) 85)
;=> 3Code language: Lisp (lisp)

9.3. every・some・notany・notevery で条件を判定する

リストの everysome がそのままベクタにも使えます。
Rubyなどでの言語の all?any?none? に相当します。

(defun all-passing-p (scores passing-mark)
  (every (lambda (s) (>= s passing-mark)) scores))

(all-passing-p #(80 90 70 85 95) 60)
;=> T

(all-passing-p #(80 90 70 85 95) 75)
;=> NILCode language: Lisp (lisp)
(defun any-perfect-p (scores)
  (some (lambda (s) (= s 100)) scores))

(any-perfect-p #(80 100 70 85 95))
;=> TCode language: Lisp (lisp)
(defun none-failing-p (scores passing-mark)
  (notany (lambda (s) (< s passing-mark)) scores))

(none-failing-p #(80 90 70 85 95) 60)
;=> TCode language: Lisp (lisp)

条件が成立した時点で走査を打ち切るため、長いベクタでも無駄なく動きます。

9.4. reduce の from-end

:from-end t を渡すと、末尾から畳み込みます。

これは、+max のような可換な演算では結果は変わりませんが、引き算のように演算の結合順序が結果に影響する場合に、違いが出ます。

(reduce #'- #(10 3 2 1))
;=> 4   ; ((10 - 3) - 2) - 1

(reduce #'- #(10 3 2 1) :from-end t)
;=> 6   ; 10 - (3 - (2 - 1))
Code language: Lisp (lisp)

ややこしいですが、逆順に引くわけでもありません。

10. ベクタの一括操作

10.1. fill で一括書き換え

(defun reset-table (v)
  (fill v 0)
  v)

(reset-table (make-array 5 :initial-contents '(1 2 3 4 5)))
;=> #(0 0 0 0 0)Code language: Lisp (lisp)

:start:end で範囲を絞れます。

(defun clear-middle (v)
  (fill v 0 :start 1 :end 4)
  v)

(clear-middle (make-array 5 :initial-contents '(1 2 3 4 5)))
;=> #(1 0 0 0 5)Code language: Lisp (lisp)

作成時の初期化なら :initial-element で十分ですが、fill が必要になるのは作成済みのベクタを後から書き換えたい場面です。
競技プログラミングでテストケースをまたいで配列を使い回すときが典型例です。

(defun solve-multiple-cases (test-cases capacity)
  (let ((dp (make-array capacity :initial-element 0)))
    (loop for tc in test-cases
          do (fill dp 0)  ; テストケースごとにリセット
             ; ... dp を使った処理 ...
          )))Code language: Lisp (lisp)

10.2. concatenate でベクタを結合する

リストの append に対応するのが concatenate です。
第1引数に返り値の型を指定します。

(defun merge-scores (a b)
  ; (append a b) に対応
  (concatenate 'vector a b))

(merge-scores #(80 90 70) #(85 95))
;=> #(80 90 70 85 95)Code language: Lisp (lisp)
(defun join-words (&rest words)
  (concatenate 'string (first words)
               (apply #'concatenate 'string
                      (mapcar (lambda (w) (concatenate 'string " " w))
                              (rest words)))))

(join-words "hello" "world")
;=> "hello world"Code language: Lisp (lisp)

10.3. reverse と nreverse で逆順にする

リストの reversenreverse がそのままベクタにも使えます。

(defun reversed-scores (scores)
  (reverse scores))

(reversed-scores #(80 90 70 85 95))
;=> #(95 85 70 90 80)Code language: Lisp (lisp)
(defun reverse-in-place! (v)
  (nreverse v)
  v)

(reverse-in-place! (make-array 5 :initial-contents '(80 90 70 85 95)))
;=> #(95 85 70 90 80)Code language: Lisp (lisp)

10.4. remove-duplicates で重複を除く

(defun unique-scores (scores)
  (remove-duplicates scores))

(unique-scores #(80 90 70 90 80))
;=> #(70 90 80)Code language: Lisp (lisp)
(defun unique-scores-ordered (scores)
  ; :from-end t で後ろのものを除き、先頭側を残す
  (remove-duplicates scores :from-end t))

(unique-scores-ordered #(80 90 70 90 80))
;=> #(80 90 70)Code language: Lisp (lisp)

10.5. map-into で既存のベクタに書き込む

map が新しいシーケンスを返すのに対し、map-into は既存のベクタに破壊的に書き込みます。
メモリを節約したいときに選びます。

(defun scale-scores! (scores factor)
  (map-into scores (lambda (s) (round (* s factor))) scores)
  scores)

(scale-scores! (make-array 5 :initial-contents '(80 90 70 85 95)) 1.1)
;=> #(88 99 77 94 105)Code language: Lisp (lisp)

10.6. substitute と substitute-if で置換する

substitute は値を指定して置換し、substitute-if は条件で置換します。
どちらも新しいベクタを返します。

(defun replace-score (scores old new)
  (substitute new old scores))

(replace-score #(80 90 70 90 95) 90 100)
;=> #(80 100 70 100 95)Code language: Lisp (lisp)
(defun cap-scores (scores limit)
  (substitute-if limit (lambda (s) (> s limit)) scores))

(cap-scores #(80 110 70 85 120) 100)
;=> #(80 100 70 85 100)Code language: Lisp (lisp)

:count で件数を制限できます。
破壊的に書き換えたいなら nsubstitutensubstitute-if を使います。