【Common Lisp】
シーケンス関数の基本の使い方
(sequence)

  • Common Lispのシーケンス関数は、リスト・ベクタ・文字列の3種類に同じ書き方で使える汎用関数群です。
  • findremovecountなどには:test:key:start:endといった共通のキーワード引数があり、組み合わせて柔軟な検索や変換ができます。
  • remove系は元のシーケンスを変えない非破壊版で、delete系はメモリ効率を優先した破壊的バリアントです。
  • mapreducesortcoerceなどを活用すると、型を問わない汎用的なデータ処理関数を簡潔に書けます。

関連記事

1. シーケンス関数とシーケンス

1.1. 同じ関数が3種類で動く

findremove は、リスト・ベクタ・文字列に対して同じ書き方で使えます。

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

返り値の型は入力と同じで、リストを渡すとリストが返り、文字列を渡すと文字列が返ります。

このようなリスト・ベクタ・文字列に共通に使える関数を「シーケンス関数」といい、lengtheltfindremovemapsort などがこれにあたります1

これは、「リスト・ベクタ・文字列はどれも順序を持つデータだから、同じ操作をできるようにしたい」という考えで、「シーケンス関数」は、言語仕様(HyperSpec)の Chapter 17 で定義されています。

それに対してリスト専用の関数もあります。
たとえば、mapfind はシーケンス関数ですが、mapcarmemberassoc はシーケンス関数ではなく、ベクタや文字列には使えません。

; 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. 型の確認方法

listpvectorpstringp はありますが、シーケンスかどうかを直接確認する関数はありません。

そのため 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-listmake-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. 共通キーワード引数

多くのシーケンス関数は、共通のキーワード引数を持っています。

findremovecountpositionsubstitute などを一度覚えると横断的に使えます。

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 はトップレベルの構造だけをコピーした新しいシーケンスを返します。

sortnreverse のような破壊的操作を元に影響させたくないときに使います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つと異なります。
everynotanynoteveryTNIL を返します。

(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(破壊的バリアント)

deletedelete-ifremove 系の破壊的バリアントです。
元のシーケンスを変更してよい場面で使い、新しいシーケンスを作らない分メモリ効率がよくなる可能性があります。返り値を必ず受け取って使います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-notcount-if-notfind-if-notposition-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. 【補足】非破壊と破壊的バリアントの対応表

非破壊破壊的備考
removedelete返り値を必ず受け取る
remove-ifdelete-if同上
remove-duplicatesdelete-duplicates同上
substitutensubstitute同上
substitute-ifnsubstitute-if同上
reversenreverse同上
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 の対象外で、旧来の「副作用専用」設計のまま別途用意された関数です。mapcmaplmaphash が揃って nil を返すのはその名残です39

  1. HyperSpec Section 17.1 の定義によると、シーケンス関数が新しいベクタを構築して返す場合、常に simple-vector を返します。文字列の場合は simple-string を返します。 – CLHS: Section 17.1
  2. HyperSpec の定義では「シーケンスはベクタかリストとして実装された要素の順序付きコレクション」とされています。文字列とビットベクタはベクタのサブタイプです。 – CLHS: Section 17.1
  3. nil は長さ0のリストとしてシーケンスとみなされます。(typep nil 'sequence)T を返します。 – Common Lisp the Language, 2nd Ed. Chapter 14
  4. HyperSpec によると、result-type がリストのサブタイプの場合はリストを返し、ベクタのサブタイプの場合は実装が element-type を決定できればそれに合わせたベクタを返します。 – CLHS: Function MAKE-SEQUENCE
  5. X3J13 の1988年の決定(FUNCTION-TYPE)により、:test:key に渡せる値はシンボルか関数オブジェクトに限定されています。ラムダ式を直接渡すのではなく、#' を使って関数オブジェクトにして渡す必要があります。 – Common Lisp the Language, 2nd Ed. Chapter 14
  6. HyperSpec では :key 関数が呼ばれる回数は保証されていません。副作用のある関数を :key に渡すと結果が予測不能になります。 – CLHS: Function SORT, STABLE-SORT
  7. :startnil を渡すことはできません。省略した場合にのみ 0 とみなされます。:endnil を渡すと「末尾まで」と同義です。 – Common Lisp the Language, 2nd Ed. Chapter 14
  8. HyperSpec では :from-end の処理順序はあくまで概念上のもので、実際の実装が末尾から処理する保証はありません。副作用のあるテスト関数と組み合わせると結果が予測不能になります。 – Common Lisp the Language, 2nd Ed. Chapter 14
  9. Practical Common Lisp では「lengthelt はともに fill-pointer を持つベクタに対して fill-pointer の値を長さとして扱う」と説明されています。 – Practical Common Lisp: Collections
  10. elt は範囲外のインデックスを渡したときにエラーを発生させます。リストに対しては nth と異なり、範囲外で nil を返さずにエラーになる点に注意が必要です。 – Practical Common Lisp: Collections
  11. Common Lisp Cookbook でも「文字列は配列でありシーケンスでもあるので、arefelt が使えます。ただし char の方が実装によっては効率的に実装されている可能性があります」と説明されています。 – The Common Lisp Cookbook: Strings
  12. subseq が返すのは常にコピーで、元のシーケンスと構造を共有しません。ただしベクタの要素がオブジェクトへの参照の場合は浅いコピーになります。 – CLHS: Accessor SUBSEQ
  13. copy-seq が返すシーケンスは元と同じ型になります。リストを渡せばリスト、ベクタを渡せばベクタが返ります。ただし要素が指すオブジェクトは元のシーケンスと共有されます。 – CLHS: Function COPY-SEQ
  14. fill はリストに対しても使えます。(fill '(1 2 3) 0)(0 0 0) を返します。ただしリストに対しては破壊的な変更が行われます。 – CLHS: Function FILL
  15. findfind-if はデフォルトで先頭から処理します。:from-end t を渡すと末尾側で最初に見つかった要素を返します。値が nil の要素が含まれる場合、返り値が nil だと「見つからなかった」のか「nil が見つかった」のか区別できません。そのときは position を使うほうが安全です。 – CLHS: Function FIND, FIND-IF, FIND-IF-NOT
  16. position が返すインデックスは、:start を指定した場合でも元のシーケンス全体に対する絶対インデックスです。:start 2 を指定して3番目の要素が最初に見つかった場合、返り値は 2 ではなく元シーケンスでの位置になります。 – Common Lisp the Language, 2nd Ed. Chapter 14
  17. search の引数の順序は「探したいパターンが第1引数、探す対象が第2引数」です。文字列検索の場合 (search "ll" "hello") のように書きます。 – CLHS: Function SEARCH
  18. mismatch:test:key:start:end:from-end をサポートしています。:from-end t を渡すと末尾から比較して、最初に一致しなくなったインデックスを返します。 – CLHS: Function MISMATCH
  19. countcount-if:start:end:from-end:key をサポートしています。:from-end は結果の個数には影響しませんが、:key 関数の呼び出し順序に影響します。 – CLHS: Function COUNT, COUNT-IF, COUNT-IF-NOT
  20. everysomenotanynotevery は複数のシーケンスを同時に取れます。(every #'< '(1 2 3) '(2 3 4)) のように書くと、対応する位置の要素同士を比較して全ペアで条件を満たすかを確認します。 – CLHS: Function EVERY, SOME, NOTANY, NOTEVERY
  21. map に渡す複数のシーケンスの長さが異なる場合、最短のシーケンスに合わせて処理が終了します。 – CLHS: Function MAP
  22. map-into の結果シーケンスの長さは、渡したシーケンスのうち最短のものに合わせて決まります。結果シーケンスが他のシーケンスより長い場合、余分な要素は変更されません。 – CLHS: Function MAP-INTO
  23. reduce:initial-value を指定せず空のシーケンスを渡すと、関数をゼロ引数で呼び出した結果を返します。+ の場合は 0* の場合は 1 が返ります。ただし全ての関数がゼロ引数で定義されているわけではないので、空シーケンスが来る可能性がある場合は :initial-value を指定するほうが安全です。 – CLHS: Function REDUCE
  24. HyperSpec によると、remove の結果は場合によっては元のシーケンスと一部の構造を共有することがありますが、元のシーケンスは変更されません。 – CLHS: Function REMOVE, REMOVE-IF, REMOVE-IF-NOT
  25. X3J13 の決定により、リストに対する deletecarcdrsetf で変更することが許可されています。配列に対しては、要素をずらして結果を作ることが許可されています。元のシーケンスを再利用するかどうかは実装依存で、結果が元のシーケンスと同一(eq)である保証はありません。 – Common Lisp the Language, 2nd Ed. Chapter 14
  26. HyperSpec では remove-duplicates は「集合を表現するための正規形への変換に役立つ」と説明されています。:from-end t を指定すると最初に出現した要素が保持され、後から出現した重複が除かれます。 – CLHS: Function REMOVE-DUPLICATES, DELETE-DUPLICATES
  27. substitute の引数の順序は「新しい値、古い値、シーケンス」の順です。(substitute new old seq) と覚えてください。 – CLHS: Function SUBSTITUTE, SUBSTITUTE-IF, SUBSTITUTE-IF-NOT
  28. nsubstitute はシーケンスの要素を setf で直接書き換えます。結果が元のシーケンスと同一(eq)とは限りませんが、元のシーケンスが変更されるという点で substitute と異なります。 – CLHS: Function NSUBSTITUTE, NSUBSTITUTE-IF, NSUBSTITUTE-IF-NOT
  29. replaceseq1seq2 が同じオブジェクトで、指定した範囲が重複していても正しく動作します。ただしリストで構造を共有しているが eq でない場合に範囲が重複すると動作が未定義になります。 – GNU Emacs Lisp Reference: Sequence Functions
  30. HyperSpec によると、sort の破壊的操作はベクタに対しては要素をインプレースで並べ替えることが保証されています。リストに対しては nreverse と同様に任意の方法で並べ替えることが許可されています。返り値が元のシーケンスと同一(eq)になる保証はありません。 – CLHS: Function SORT, STABLE-SORT
  31. sort では述語が (funcall pred x y)(funcall pred y x) の両方で nil を返すとき、xy は同値とみなされます。この場合の並び順は sort では保証されませんが、stable-sort では元の順序が維持されます。 – CLHS: Function SORT, STABLE-SORT
  32. HyperSpec によると merge は安定です。同値と判定された要素が2つある場合、第1引数のシーケンス側の要素が第2引数の要素より先に並びます。 – CLHS: Function MERGE
  33. nreverse は「n」が destructive を意味するプレフィックスの慣習に従っています。Common Lisp では破壊的なバリアントには n を付ける慣習があります(nconcnsubstnreverse など)。 – Common Lisp the Language, 2nd Ed.
  34. Common Lisp Cookbook では、多数の文字列を結合する場合は concatenate を繰り返すより、fill-pointer を持つ可変長ベクタに vector-push-extend で文字を追加していく方が効率的だと説明されています。 – The Common Lisp Cookbook: Strings
  35. coerce でリストをベクタに変換した場合、返ってくるのは simple-vector です。型特化ベクタ(element-typefixnum など)が必要な場合は make-array:initial-contents を渡す方法を使います。 – CLHS: Function COERCE
  36. X3J13 委員会は1989年1月の TEST-NOT-IF-NOT 決議で -IF-NOT 系関数と :test-not 引数を非推奨に指定しました。同時に complement 関数を追加することも決議されています。 – Common Lisp the Language, 2nd Ed. Chapter 14
  37. Practical Common Lisp では「eltlength とともにシーケンスに対する最も基本的な操作であり、理論的にはすべてのシーケンス操作はこれらの組み合わせに還元できる」と説明されています。 – Practical Common Lisp: Collections
  38. Common Lisp the Language(CLtL1、1984年)の時点では map は副作用のみの操作を指していました。HyperSpec(ANSI標準)では map がシーケンス汎用の変換関数として再定義されています。 – Common Lisp – Wikipedia
  39. HyperSpec の標準シーケンス関数の一覧(Figure 17-1)には maphash は含まれていません。maphash はハッシュテーブル専用の関数として Chapter 18 で定義されています。 – CLHS: Section 17.1