- Common Lispのシーケンス関数は、リスト・ベクタ・文字列の3種類に同じ書き方で使える汎用関数群です。
find・remove・countなどには:test・:key・:start・:endといった共通のキーワード引数があり、組み合わせて柔軟な検索や変換ができます。remove系は元のシーケンスを変えない非破壊版で、delete系はメモリ効率を優先した破壊的バリアントです。map・reduce・sort・coerceなどを活用すると、型を問わない汎用的なデータ処理関数を簡潔に書けます。
1. シーケンス関数とシーケンス
1.1. 同じ関数が3種類で動く
find や remove は、リスト・ベクタ・文字列に対して同じ書き方で使えます。
(find 3 '(1 2 3 4 5)) ;=> 3
(find 3 #(1 2 3 4 5)) ;=> 3
(find #\a "banana") ;=> #\a
(remove 3 '(1 2 3 3 4)) ;=> (1 2 4)
(remove 3 #(1 2 3 3 4)) ;=> #(1 2 4)
(remove #\a "banana") ;=> "bnn"Code language: Lisp (lisp)
返り値の型は入力と同じで、リストを渡すとリストが返り、文字列を渡すと文字列が返ります。
このようなリスト・ベクタ・文字列に共通に使える関数を「シーケンス関数」といい、length・elt・find・remove・map・sort などがこれにあたります1。
これは、「リスト・ベクタ・文字列はどれも順序を持つデータだから、同じ操作をできるようにしたい」という考えで、「シーケンス関数」は、言語仕様(HyperSpec)の Chapter 17 で定義されています。
それに対してリスト専用の関数もあります。
たとえば、mapや find はシーケンス関数ですが、mapcar・member・assoc はシーケンス関数ではなく、ベクタや文字列には使えません。
; mapcar はリスト専用
(mapcar #'char-upcase '(#\a #\b #\c)) ;=> (#\A #\B #\C)
(mapcar #'char-upcase "abc") ; エラー
; map はシーケンス汎用
(map 'list #'char-upcase '(#\a #\b #\c)) ;=> (#\A #\B #\C)
(map 'string #'char-upcase "abc") ;=> "ABC"
(map 'vector #'char-upcase #(#\a #\b #\c)) ;=> #(#\A #\B #\C)Code language: Lisp (lisp)
1.2. シーケンス型という抽象型
シーケンス関数が「3種類に使える」を成り立たせるために、Common Lisp はリスト・ベクタ・文字列を束ねる抽象型「シーケンス」を持っています。
型の包含関係はこうなります2。
sequence
├── list
│ ├── null ; 空リスト '()
│ └── cons ; 非空リスト
└── vector
├── string ; element-type が character のベクタ
└── bit-vector ; element-type が bit のベクタCode language: PHP (php)
もともと、文字列は「文字を要素とするベクタ」なので、ベクタに使える関数はそのまま文字列にも使えます。
1.3. 型の確認方法
listp・vectorp・stringp はありますが、シーケンスかどうかを直接確認する関数はありません。
そのため typep を使います3。
(defun sequence-p (x)
(typep x 'sequence))
(sequence-p #(1 2 3)) ;=> T
(sequence-p 42) ;=> NILCode language: Lisp (lisp)
1.4. make-sequence で型を指定して生成する
make-sequence は、make-list・make-array の統一版で、シーケンス型を第1引数で指定して、指定した長さのシーケンスを作ります。
あえて使うのは、型を実行時に切り替えたいときや、シーケンス型を引数で受け取る汎用関数を書くときです4。
(defun make-filled-sequence (type size value)
(make-sequence type size :initial-element value))
(make-filled-sequence 'list 5 0) ;=> (0 0 0 0 0)
(make-filled-sequence 'vector 5 0) ;=> #(0 0 0 0 0)
(make-filled-sequence 'string 5 #\x) ;=> "xxxxx"Code language: Lisp (lisp)
2. 共通キーワード引数
多くのシーケンス関数は、共通のキーワード引数を持っています。
find・remove・count・position・substitute などを一度覚えると横断的に使えます。
2.1. :test と :key
:test は比較に使う関数を指定します。
省略すると eql で比較します。
文字列の比較には #'equal か #'string= が必要です5。
(defun find-player (roster name)
(find name roster :test #'equal))
(find-player '("alice" "bob" "carol") "bob")
;=> "bob"
(find-player '("alice" "bob" "carol") "BOB")
;=> NILCode language: Lisp (lisp)
大文字小文字を無視して探すには string-equal を渡します。
(defun find-player-ci (roster name)
(find name roster :test #'string-equal))
(find-player-ci '("alice" "bob" "carol") "BOB")
;=> "bob"Code language: Lisp (lisp)
:key では比較の前に各要素に適用する関数を指定することもできます。
(defun find-player-record (roster name)
(find name roster :key #'car :test #'equal))
(defparameter *players*
'(("alice" 85) ("bob" 90) ("carol" 70)))
(find-player-record *players* "bob")
;=> ("bob" 90) ; キーの "bob" ではなく要素全体が返るCode language: Lisp (lisp)
返ってくるのはキーではなく元の要素です6。
2.2. :start・:end で範囲を絞る
処理対象を部分シーケンスに絞るには、:start と :end を使います。
:start は含まれ、:end は含まれません7。
(count #\a "banana" :start 2 :end 5)
;=> 2 ; インデックス2〜4で数えるCode language: Lisp (lisp)
また、:start・:end が返すインデックスは部分シーケンスではなく元のシーケンス全体のインデックスです。position に :start 2 を指定したときに返る値は、サブシーケンス内の相対位置ではなく元シーケンス内の絶対位置になります。
2.3. :from-end で末尾から処理する
:from-end t を渡すと末尾から処理します。
find では最後に一致した要素が返り、:count と組み合わせると末尾側から処理します8。
(defun find-last (seq item)
(find item seq :from-end t))
(find-last "banana" #\a)
;=> #\a ; 末尾側で最初に見つかった要素(インデックス5)Code language: Lisp (lisp)
2.4. :count で件数を制限する
:count は処理する件数の上限を指定します。
省略すると全件処理します。
(remove 1 '(1 2 1 2 1) :count 2)
;=> (2 2 1) ; 先頭から2件だけ除くCode language: Lisp (lisp)
3. 長さ・アクセス・コピー
3.1. length で長さを得る
(length '(1 2 3)) ;=> 3
(length #(1 2 3)) ;=> 3
(length "hello") ;=> 5Code language: Lisp (lisp)
fill-pointer を持つベクタの場合、確保した容量ではなく fill-pointer の値が返ります。これはベクタの論理的な長さです9。
3.2. elt でインデックスアクセスする
elt は0始まりのインデックスで要素を返します。setf と組み合わせて書き込みもできます10。
(elt '(10 20 30) 1) ;=> 20
(elt #(10 20 30) 1) ;=> 20
(elt "hello" 1) ;=> #\eCode language: Lisp (lisp)
(defun seq-set! (seq i val)
(setf (elt seq i) val)
seq)
(seq-set! (vector 10 20 30) 1 99)
;=> #(10 99 30)Code language: Lisp (lisp)
型が決まっている場面では型専用のアクセサを選ぶほうが慣用的です。
用途 慣用的な選択 備考
----------- ----------- -----------------------------------
リスト nth elt と同じ動作。
nth の方が意図が明確
ベクタ aref 多用される。
コンパイラの最適化も効きやすい
文字列 char 文字列専用として読みやすい
型を問わない elt 汎用関数を書くときに選ぶ
elt をリストに使うと nth と同様に O(N) になりますが、その点はコードから見えなくなります。
ベクタ専用のコードでは aref の方が意図が明確です11。
3.3. subseq でスライスする
subseq は、開始インデックスから終了インデックスの手前までを切り出して、新しいシーケンスを返します。
終了インデックスを省略すると末尾まで取り出します12。
(subseq '(1 2 3 4 5) 1 4) ;=> (2 3 4)
(subseq #(1 2 3 4 5) 1 4) ;=> #(2 3 4)
(subseq "hello world" 6 11) ;=> "world"Code language: Lisp (lisp)
setf subseq で元のシーケンスの一部を上書きすることもできます。
(defun overwrite-subseq (target replacement start)
(setf (subseq target start) replacement)
target)
(overwrite-subseq (copy-seq "hello world") "Lisp" 6)
;=> "hello Lispd"Code language: Lisp (lisp)
3.4. copy-seq でコピーする
copy-seq はトップレベルの構造だけをコピーした新しいシーケンスを返します。
sort や nreverse のような破壊的操作を元に影響させたくないときに使います13。
(defun safe-sort (seq)
(sort (copy-seq seq) #'<))
(defparameter *scores* #(3 1 4 1 5))
(safe-sort *scores*)
;=> #(1 1 3 4 5)
*scores*
;=> #(3 1 4 1 5) ; 元は変わらないCode language: Lisp (lisp)
3.5. fill で一括書き換えする
fill は指定した値でシーケンスの要素をまとめて書き換えます。:start・:end で範囲を絞れます。
(defun reset-seq (seq)
(fill seq 0))
(reset-seq (make-array 5 :initial-contents '(1 2 3 4 5)))
;=> #(0 0 0 0 0)Code language: Lisp (lisp)
(defun clear-range (seq start end)
(fill seq 0 :start start :end end)
seq)
(clear-range (vector 1 2 3 4 5) 1 4)
;=> #(1 0 0 0 5)Code language: Lisp (lisp)
make-array の :initial-element との違いは、作成済みのシーケンスを後から書き換えられる点です。
テストケースをまたいで配列を使い回すときが典型的な用途です14。
4. 検索・判定
4.1. find・find-if
find は最初に一致した要素を返し、見つからなければ nil を返します15。
(defun find-score (scores target)
(find target scores))
(find-score '(80 90 70 85 95) 70) ;=> 70
(find-score '(80 90 70 85 95) 60) ;=> NILCode language: Lisp (lisp)
find-if は条件関数を受け取り、最初に条件を満たした要素を返します。
(defun find-first-passing (scores threshold)
(find-if (lambda (s) (>= s threshold)) scores))
(find-first-passing #(80 90 70 85 95) 90)
;=> 90
(defun find-first-upper (s)
(find-if #'upper-case-p s))
(find-first-upper "heLLo")
;=> #\LCode language: Lisp (lisp)
4.2. position・position-if
position は要素そのものではなくインデックスを返します。find が「値が存在するか」を調べるのに対し、position は「どこにあるか」を調べます。見つからなければ nil を返します16。
(defun index-of-score (scores target)
(position target scores))
(index-of-score '(80 90 70 85 95) 85) ;=> 3
(index-of-score '(80 90 70 85 95) 60) ;=> NILCode language: Lisp (lisp)
(defun first-failing-index (scores threshold)
(position-if (lambda (s) (< s threshold)) scores))
(first-failing-index #(80 90 70 85 95) 75)
;=> 2Code language: Lisp (lisp)
4.3. search でサブシーケンスを探す
search はシーケンス1がシーケンス2に含まれる先頭位置を返します。見つからなければ nil を返します17。
(defun find-substring (text pattern)
(search pattern text))
(find-substring "hello world" "world") ;=> 6
(find-substring "hello world" "xyz") ;=> NILCode language: Lisp (lisp)
リストやベクタでも同様に使えます。
(defun find-subseq (seq sub)
(search sub seq))
(find-subseq '(1 2 3 4 5) '(2 3)) ;=> 1
(find-subseq #(1 2 3 4 5) #(3 4)) ;=> 2Code language: Lisp (lisp)
4.4. mismatch で不一致位置を見つける
mismatch は2つのシーケンスを先頭から比較して、最初に一致しなくなったインデックスを返します。
完全に一致すれば nil を返します。共通プレフィックスの長さを調べるときに使えます18。
(defun common-prefix-length (a b)
(or (mismatch a b) (length a)))
(common-prefix-length "foobar" "foobaz") ;=> 5
(common-prefix-length "abc" "abc") ;=> 3
(common-prefix-length '(1 2 3) '(1 2 4)) ;=> 2Code language: Lisp (lisp)
4.5. count・count-if
count は一致する要素の個数、count-if は条件を満たす要素の個数を返します19。
(defun count-score (scores target)
(count target scores))
(count-score #(80 90 70 90 95) 90) ;=> 2Code language: Lisp (lisp)
(defun count-passing (scores threshold)
(count-if (lambda (s) (>= s threshold)) scores))
(count-passing '(80 90 70 85 95) 85) ;=> 2
(defun count-vowels (s)
(count-if (lambda (c) (find c "aeiou")) s))
(count-vowels "hello world") ;=> 3Code language: Lisp (lisp)
4.6. every・some・notany・notevery
全要素に対する条件判定です。条件が確定した時点で走査を打ち切ります20。
(defun all-passing-p (scores threshold)
(every (lambda (s) (>= s threshold)) 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)) ;=> 100 ; 値を返す
(any-perfect-p #(80 90 70 85 95)) ;=> NILCode language: Lisp (lisp)
some は条件を満たした要素の値を返す点が他の3つと異なります。every・notany・notevery は T か NIL を返します。
(defun no-zeros-p (scores)
(notany #'zerop scores))
(no-zeros-p #(80 90 70)) ;=> T
(no-zeros-p #(80 0 70)) ;=> NILCode language: Lisp (lisp)
5. 変換・フィルタ
5.1. map でシーケンスを変換する
map は第1引数で返り値の型を指定し、各要素に関数を適用した結果を返します21。
(defun normalize-scores (scores)
(map 'vector (lambda (s) (* s 1.0)) scores))
(normalize-scores #(80 90 70 85 95))
;=> #(80.0 90.0 70.0 85.0 95.0)Code language: Lisp (lisp)
複数のシーケンスを同時に処理できます。
長さが異なる場合は最短に合わせて終了します。
(defun add-sequences (a b)
(map 'list #'+ a b))
(add-sequences '(1 2 3) #(10 20 30))
;=> (11 22 33)Code language: Lisp (lisp)
'nil を渡すと副作用だけ実行して nil を返します。
リストの mapc に対応します。
(defun log-scores (scores)
(map nil (lambda (s) (format t "score: ~a~%" s)) scores))
(log-scores '(80 90 70))
; score: 80
; score: 90
; score: 70
;=> NILCode language: Lisp (lisp)
出力がリスト以外のシーケンスのとき、または文字列・ベクタを入力にするときは map を選びます。
出力もリストでよくリストを入力するなら mapcar の方が短く書けます。
5.2. map-into で既存シーケンスに書き込む
map-into は既存のシーケンスに破壊的に書き込みます。新しいシーケンスを作らないのでメモリを節約できます22。
(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)
5.3. reduce で畳み込む
reduce はシーケンスの要素を二項演算で順に畳み込み、単一の値を返します23。
(defun total-score (scores)
(reduce #'+ scores))
(defun highest-score (scores)
(reduce #'max scores))
(total-score #(80 90 70 85 95)) ;=> 420
(highest-score #(80 90 70 85 95)) ;=> 95Code language: Lisp (lisp)
:initial-value を渡すと、空のシーケンスへの対応と初期値を同時に指定できます。
(defun safe-total (scores)
(reduce #'+ scores :initial-value 0))
(safe-total '()) ;=> 0Code language: Lisp (lisp)
: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)
5.4. remove・remove-if(非破壊)
remove は指定した値と一致する要素を除いた新しいシーケンスを返します。元のシーケンスは変更しません24。
(defun failing-scores (scores threshold)
(remove-if (lambda (s) (>= s threshold)) scores))
(defun passing-scores (scores threshold)
(remove-if-not (lambda (s) (>= s threshold)) 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)
(defun remove-char (s ch)
(remove ch s))
(remove-char "banana" #\a) ;=> "bnn"Code language: Lisp (lisp)
5.5. delete・delete-if(破壊的バリアント)
delete・delete-if は remove 系の破壊的バリアントです。
元のシーケンスを変更してよい場面で使い、新しいシーケンスを作らない分メモリ効率がよくなる可能性があります。返り値を必ず受け取って使います25。
(defun remove-zeros! (scores)
(delete 0 scores))
(let ((v (make-array 5 :initial-contents '(80 0 70 0 95))))
(remove-zeros! v))
;=> #(80 70 95)Code language: Lisp (lisp)
リテラルリスト('(1 2 3) のような式)に対して使うと動作が未定義になります。
5.6. remove-duplicates・delete-duplicates
重複要素を除いたシーケンスを返します。
デフォルトは後ろの重複を残し、:from-end t で先頭側を残します26。
(defun unique-scores (scores)
(remove-duplicates scores :from-end t))
(unique-scores '(80 90 70 90 80)) ;=> (80 90 70)
(unique-scores "banana") ;=> "ban"Code language: Lisp (lisp)
5.7. substitute・substitute-if(非破壊)
substitute は値を指定して一致する要素を置き換えた新しいシーケンスを返します27。
(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)
(defun replace-char (s old new)
(substitute new old s))
(replace-char "banana" #\a #\o)
;=> "bonono"Code language: Lisp (lisp)
5.8. nsubstitute・nsubstitute-if(破壊的バリアント)
破壊的に置き換えます。delete 系と同様に返り値を受け取って使います28。
(defun cap-scores! (scores limit)
(nsubstitute-if limit (lambda (s) (> s limit)) scores)
scores)
(cap-scores! (make-array 5 :initial-contents '(80 110 70 85 120)) 100)
;=> #(80 100 70 85 100)Code language: Lisp (lisp)
5.9. replace で一部を上書きする
replace はシーケンス1の一部をシーケンス2の内容で破壊的に上書きします。:start1・:end1 で書き込み先の範囲を、:start2・:end2 で読み込み元の範囲を指定します29。
(defun overwrite-at (target source start)
(replace target source :start1 start)
target)
(overwrite-at (copy-seq "hello world") "Lisp" 6)
;=> "hello Lispd"
(overwrite-at (vector 1 2 3 4 5) #(90 91) 1)
;=> #(1 90 91 4 5)Code language: Lisp (lisp)
6. 並べ替え・結合・逆順
6.1. sort・stable-sort
sort はシーケンスを破壊的にソートします。
同値要素の元の順序は保証されません。
元のシーケンスを保持したいときは copy-seq してから渡します30。
(defun top-scores (scores)
(sort (copy-seq scores) #'>))
(top-scores #(80 90 70 85 95))
;=> #(95 90 85 80 70)Code language: Lisp (lisp)
stable-sort は同値要素の元の順序を保ちます31。
(defparameter *players*
'(("alice" 85) ("bob" 90) ("carol" 85)))
(defun rank-players (players)
(stable-sort (copy-seq players) #'> :key #'cadr))
(rank-players *players*)
;=> (("bob" 90) ("alice" 85) ("carol" 85))
; alice と carol は元の順序のままCode language: Lisp (lisp)
6.2. merge でソート済みシーケンスを合成する
merge はすでにソートされた2つのシーケンスを1つにマージします。
第1引数で返り値の型を指定します。sort と違い安定した動作をします。入力がソート済みでない場合の動作は未定義です32。
(defun merge-sorted (a b)
(merge 'list a b #'<))
(merge-sorted '(1 3 5) '(2 4 6))
;=> (1 2 3 4 5 6)
(merge 'vector #(10 30 50) #(20 40 60) #'<)
;=> #(10 20 30 40 50 60)Code language: Lisp (lisp)
6.3. reverse・nreverse
reverse は新しいシーケンスを返し、nreverse は破壊的に逆順にします33。
(defun reversed-ranking (scores)
(reverse scores))
(reversed-ranking '(95 85 80 70)) ;=> (70 80 85 95)
(reversed-ranking "hello") ;=> "olleh"
(reversed-ranking #(1 2 3)) ;=> #(3 2 1)Code language: Lisp (lisp)
nreverse を使うときは返り値を必ず受け取ります。
(defun reverse-buffer! (buf)
(setf buf (nreverse buf))
buf)
(reverse-buffer! (list 1 2 3 4 5))
;=> (5 4 3 2 1)Code language: Lisp (lisp)
6.4. concatenate でシーケンスを結合する
concatenate は複数のシーケンスを結合して新しいシーケンスを返します。
第1引数で返り値の型を指定します。リストの append と違い、入力のシーケンス型が混在しても動きます34。
(defun merge-rosters (home away)
(concatenate 'list home away))
(merge-rosters '("alice" "bob") '("carol" "dave"))
;=> ("alice" "bob" "carol" "dave")Code language: Lisp (lisp)
(defun join-words (words separator)
(reduce (lambda (acc w) (concatenate 'string acc separator w))
words))
(join-words '("hello" "world" "lisp") " ")
;=> "hello world lisp"Code language: Lisp (lisp)
7. 型をまたぐ変換
7.1. coerce でシーケンス型を変換する
coerce はシーケンスの型を変換します35。
(defun list->vector (lst)
(coerce lst 'vector))
(defun vector->list (v)
(coerce v 'list))
(list->vector '(1 2 3)) ;=> #(1 2 3)
(vector->list #(1 2 3)) ;=> (1 2 3)Code language: Lisp (lisp)
文字のリストを文字列に変換するのは coerce の典型的な用途です。
(defun chars->string (chars)
(coerce chars 'string))
(defun string->chars (s)
(coerce s 'list))
(chars->string '(#\h #\e #\l #\l #\o)) ;=> "hello"
(string->chars "hello") ;=> (#\h #\e #\l #\l #\o)Code language: Lisp (lisp)
8. 実践パターン
8.1. 文字列から特定の文字を抽出・除去する
(defun extract-digits (s)
(remove-if-not #'digit-char-p s))
(extract-digits "abc123def456")
;=> "123456"
(defun alphanumeric-only (s)
(remove-if-not #'alphanumericp s))
(alphanumeric-only "hello, world! 123")
;=> "helloworld123"Code language: Lisp (lisp)
8.2. 型を問わない汎用関数を書く
シーケンス関数を使うと、リスト・ベクタ・文字列を区別しない関数が書けます。
(defun seq-take (seq n)
(subseq seq 0 (min n (length seq))))
(seq-take '(1 2 3 4 5) 3) ;=> (1 2 3)
(seq-take #(1 2 3 4 5) 3) ;=> #(1 2 3)
(seq-take "hello" 3) ;=> "hel"Code language: Lisp (lisp)
(defun has-duplicates-p (seq)
(< (length (remove-duplicates seq :test #'equal))
(length seq)))
(has-duplicates-p '(1 2 3 2)) ;=> T
(has-duplicates-p #(1 2 3)) ;=> NIL
(has-duplicates-p "banana") ;=> TCode language: Lisp (lisp)
8.3. 各要素の出現頻度を数える
(defun seq-frequencies (seq)
(loop for item across (remove-duplicates seq :test #'equal)
collect (cons item (count item seq :test #'equal))))
(seq-frequencies "banana")
;=> ((#\b . 1) (#\n . 2) (#\a . 3))
(seq-frequencies #("alice" "bob" "alice" "carol" "bob" "bob"))
;=> (("alice" . 2) ("bob" . 3) ("carol" . 1))Code language: Lisp (lisp)
9. 【補足】-IF-NOT 系は非推奨
remove-if-not・count-if-not・find-if-not・position-if-not などの -IF-NOT 系は、ANSI Common Lisp 標準で deprecated に指定されています。代わりに complement と -IF 系を組み合わせます36。
; 非推奨
(remove-if-not #'evenp '(1 2 3 4 5))
; 推奨
(defun keep-if (pred seq)
(remove-if (complement pred) seq))
(keep-if #'evenp '(1 2 3 4 5))
;=> (2 4)Code language: Lisp (lisp)
complement は関数を受け取り、その真偽を反転した関数を返します。
ただし remove-if-not は多くの処理系で問題なく動き、実際のコードでも広く使われています。
読みやすさを優先してどちらを使うか判断するのが現実的です。
10. 【補足】非破壊と破壊的バリアントの対応表
| 非破壊 | 破壊的 | 備考 |
|---|---|---|
remove | delete | 返り値を必ず受け取る |
remove-if | delete-if | 同上 |
remove-duplicates | delete-duplicates | 同上 |
substitute | nsubstitute | 同上 |
substitute-if | nsubstitute-if | 同上 |
reverse | nreverse | 同上 |
sort(非破壊版なし) | sort 自体が破壊的 | copy-seq してから渡す |
stable-sort | 同上 | 同上 |
replace(非破壊版なし) | replace 自体が破壊的 | — |
fill(非破壊版なし) | fill 自体が破壊的 | — |
11. 【補足】elt vs 型専用アクセサの使い分け
elt はリスト・ベクタ・文字列を区別しない汎用アクセサです。型が決まっている場面では型専用のアクセサを選ぶほうが慣用的です37。
; リスト → nth
(defun list-ref (lst i) (nth i lst))
; ベクタ → aref
(defun vec-ref (v i) (aref v i))
; 文字列 → char
(defun str-ref (s i) (char s i))
(list-ref '(10 20 30) 1) ;=> 20
(vec-ref #(10 20 30) 1) ;=> 20
(str-ref "hello" 1) ;=> #\eCode language: Lisp (lisp)
aref はベクタを扱うコードで最もよく使われます。aref を見るとベクタへのアクセスだと即座にわかり、コンパイラが型情報を使った最適化をしやすいためです。elt を選ぶのは、受け取ったシーケンスの型が実行時まで決まらない汎用関数を書くときです。
12. 【補足】map と mapcar が両方ある理由
mapcar は MacLisp 時代(1970年代)からの関数です。
当時の Lisp では map は「副作用だけ実行して値を返さない関数」を指していました。
要素を変換して新しいリストを返す処理が mapcar で、「car を取りながら map する」という意味の名前です38。
1980年代に関数型プログラミングの論文が広まると、学術的な文脈での map は「各要素に関数を適用して新しいコレクションを返す処理」を指すようになりました。
Common Lisp はこの学術的な用法に合わせて map を「シーケンス汎用の変換関数」として再定義し、旧来の「副作用専用の map」は mapl に改名されています。
mapcar は互換性のためそのまま残り、map(シーケンス汎用・型指定あり)と mapcar(リスト専用・歴史的遺産)が並存する形になっています。
maphash が常に nil を返すのも同じ系譜です。
ハッシュテーブルはシーケンスではないので map の対象外で、旧来の「副作用専用」設計のまま別途用意された関数です。mapc・mapl・maphash が揃って nil を返すのはその名残です39。
- HyperSpec Section 17.1 の定義によると、シーケンス関数が新しいベクタを構築して返す場合、常に simple-vector を返します。文字列の場合は simple-string を返します。 – CLHS: Section 17.1
- HyperSpec の定義では「シーケンスはベクタかリストとして実装された要素の順序付きコレクション」とされています。文字列とビットベクタはベクタのサブタイプです。 – CLHS: Section 17.1
nilは長さ0のリストとしてシーケンスとみなされます。(typep nil 'sequence)はTを返します。 – Common Lisp the Language, 2nd Ed. Chapter 14- HyperSpec によると、
result-typeがリストのサブタイプの場合はリストを返し、ベクタのサブタイプの場合は実装がelement-typeを決定できればそれに合わせたベクタを返します。 – CLHS: Function MAKE-SEQUENCE - X3J13 の1988年の決定(FUNCTION-TYPE)により、
:testや:keyに渡せる値はシンボルか関数オブジェクトに限定されています。ラムダ式を直接渡すのではなく、#'を使って関数オブジェクトにして渡す必要があります。 – Common Lisp the Language, 2nd Ed. Chapter 14 - HyperSpec では
:key関数が呼ばれる回数は保証されていません。副作用のある関数を:keyに渡すと結果が予測不能になります。 – CLHS: Function SORT, STABLE-SORT :startにnilを渡すことはできません。省略した場合にのみ 0 とみなされます。:endはnilを渡すと「末尾まで」と同義です。 – Common Lisp the Language, 2nd Ed. Chapter 14- HyperSpec では
:from-endの処理順序はあくまで概念上のもので、実際の実装が末尾から処理する保証はありません。副作用のあるテスト関数と組み合わせると結果が予測不能になります。 – Common Lisp the Language, 2nd Ed. Chapter 14 - Practical Common Lisp では「
lengthとeltはともにfill-pointerを持つベクタに対して fill-pointer の値を長さとして扱う」と説明されています。 – Practical Common Lisp: Collections eltは範囲外のインデックスを渡したときにエラーを発生させます。リストに対してはnthと異なり、範囲外でnilを返さずにエラーになる点に注意が必要です。 – Practical Common Lisp: Collections- Common Lisp Cookbook でも「文字列は配列でありシーケンスでもあるので、
arefやeltが使えます。ただしcharの方が実装によっては効率的に実装されている可能性があります」と説明されています。 – The Common Lisp Cookbook: Strings subseqが返すのは常にコピーで、元のシーケンスと構造を共有しません。ただしベクタの要素がオブジェクトへの参照の場合は浅いコピーになります。 – CLHS: Accessor SUBSEQcopy-seqが返すシーケンスは元と同じ型になります。リストを渡せばリスト、ベクタを渡せばベクタが返ります。ただし要素が指すオブジェクトは元のシーケンスと共有されます。 – CLHS: Function COPY-SEQfillはリストに対しても使えます。(fill '(1 2 3) 0)は(0 0 0)を返します。ただしリストに対しては破壊的な変更が行われます。 – CLHS: Function FILLfindとfind-ifはデフォルトで先頭から処理します。:from-end tを渡すと末尾側で最初に見つかった要素を返します。値がnilの要素が含まれる場合、返り値がnilだと「見つからなかった」のか「nilが見つかった」のか区別できません。そのときはpositionを使うほうが安全です。 – CLHS: Function FIND, FIND-IF, FIND-IF-NOTpositionが返すインデックスは、:startを指定した場合でも元のシーケンス全体に対する絶対インデックスです。:start 2を指定して3番目の要素が最初に見つかった場合、返り値は2ではなく元シーケンスでの位置になります。 – Common Lisp the Language, 2nd Ed. Chapter 14searchの引数の順序は「探したいパターンが第1引数、探す対象が第2引数」です。文字列検索の場合(search "ll" "hello")のように書きます。 – CLHS: Function SEARCHmismatchも:test・:key・:start・:end・:from-endをサポートしています。:from-end tを渡すと末尾から比較して、最初に一致しなくなったインデックスを返します。 – CLHS: Function MISMATCHcountとcount-ifも:start・:end・:from-end・:keyをサポートしています。:from-endは結果の個数には影響しませんが、:key関数の呼び出し順序に影響します。 – CLHS: Function COUNT, COUNT-IF, COUNT-IF-NOTevery・some・notany・noteveryは複数のシーケンスを同時に取れます。(every #'< '(1 2 3) '(2 3 4))のように書くと、対応する位置の要素同士を比較して全ペアで条件を満たすかを確認します。 – CLHS: Function EVERY, SOME, NOTANY, NOTEVERYmapに渡す複数のシーケンスの長さが異なる場合、最短のシーケンスに合わせて処理が終了します。 – CLHS: Function MAPmap-intoの結果シーケンスの長さは、渡したシーケンスのうち最短のものに合わせて決まります。結果シーケンスが他のシーケンスより長い場合、余分な要素は変更されません。 – CLHS: Function MAP-INTOreduceに:initial-valueを指定せず空のシーケンスを渡すと、関数をゼロ引数で呼び出した結果を返します。+の場合は0、*の場合は1が返ります。ただし全ての関数がゼロ引数で定義されているわけではないので、空シーケンスが来る可能性がある場合は:initial-valueを指定するほうが安全です。 – CLHS: Function REDUCE- HyperSpec によると、
removeの結果は場合によっては元のシーケンスと一部の構造を共有することがありますが、元のシーケンスは変更されません。 – CLHS: Function REMOVE, REMOVE-IF, REMOVE-IF-NOT - X3J13 の決定により、リストに対する
deleteはcarやcdrをsetfで変更することが許可されています。配列に対しては、要素をずらして結果を作ることが許可されています。元のシーケンスを再利用するかどうかは実装依存で、結果が元のシーケンスと同一(eq)である保証はありません。 – Common Lisp the Language, 2nd Ed. Chapter 14 - HyperSpec では
remove-duplicatesは「集合を表現するための正規形への変換に役立つ」と説明されています。:from-end tを指定すると最初に出現した要素が保持され、後から出現した重複が除かれます。 – CLHS: Function REMOVE-DUPLICATES, DELETE-DUPLICATES substituteの引数の順序は「新しい値、古い値、シーケンス」の順です。(substitute new old seq)と覚えてください。 – CLHS: Function SUBSTITUTE, SUBSTITUTE-IF, SUBSTITUTE-IF-NOTnsubstituteはシーケンスの要素をsetfで直接書き換えます。結果が元のシーケンスと同一(eq)とは限りませんが、元のシーケンスが変更されるという点でsubstituteと異なります。 – CLHS: Function NSUBSTITUTE, NSUBSTITUTE-IF, NSUBSTITUTE-IF-NOTreplaceはseq1とseq2が同じオブジェクトで、指定した範囲が重複していても正しく動作します。ただしリストで構造を共有しているがeqでない場合に範囲が重複すると動作が未定義になります。 – GNU Emacs Lisp Reference: Sequence Functions- HyperSpec によると、
sortの破壊的操作はベクタに対しては要素をインプレースで並べ替えることが保証されています。リストに対してはnreverseと同様に任意の方法で並べ替えることが許可されています。返り値が元のシーケンスと同一(eq)になる保証はありません。 – CLHS: Function SORT, STABLE-SORT sortでは述語が(funcall pred x y)と(funcall pred y x)の両方でnilを返すとき、xとyは同値とみなされます。この場合の並び順はsortでは保証されませんが、stable-sortでは元の順序が維持されます。 – CLHS: Function SORT, STABLE-SORT- HyperSpec によると
mergeは安定です。同値と判定された要素が2つある場合、第1引数のシーケンス側の要素が第2引数の要素より先に並びます。 – CLHS: Function MERGE nreverseは「n」が destructive を意味するプレフィックスの慣習に従っています。Common Lisp では破壊的なバリアントにはnを付ける慣習があります(nconc・nsubst・nreverseなど)。 – Common Lisp the Language, 2nd Ed.- Common Lisp Cookbook では、多数の文字列を結合する場合は
concatenateを繰り返すより、fill-pointerを持つ可変長ベクタにvector-push-extendで文字を追加していく方が効率的だと説明されています。 – The Common Lisp Cookbook: Strings coerceでリストをベクタに変換した場合、返ってくるのはsimple-vectorです。型特化ベクタ(element-typeがfixnumなど)が必要な場合はmake-arrayに:initial-contentsを渡す方法を使います。 – CLHS: Function COERCE- X3J13 委員会は1989年1月の TEST-NOT-IF-NOT 決議で
-IF-NOT系関数と:test-not引数を非推奨に指定しました。同時にcomplement関数を追加することも決議されています。 – Common Lisp the Language, 2nd Ed. Chapter 14 - Practical Common Lisp では「
eltはlengthとともにシーケンスに対する最も基本的な操作であり、理論的にはすべてのシーケンス操作はこれらの組み合わせに還元できる」と説明されています。 – Practical Common Lisp: Collections - Common Lisp the Language(CLtL1、1984年)の時点では
mapは副作用のみの操作を指していました。HyperSpec(ANSI標準)ではmapがシーケンス汎用の変換関数として再定義されています。 – Common Lisp – Wikipedia - HyperSpec の標準シーケンス関数の一覧(Figure 17-1)には
maphashは含まれていません。maphashはハッシュテーブル専用の関数として Chapter 18 で定義されています。 – CLHS: Section 17.1