プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(48)

よっしゃ! やっていきます「実践Common Lisp」。

高階関数

シーケンス反復関数(という名で呼んでいいのか)には高階関数の亜種が2種類用意されているそうです。
両方とも、アイテム引数の代わりにシーケンスの各要素について呼び出される関数を引数にするそう。
1種類目は基本関数に-ifをつけ足した名前の関数で、
引数で渡した関数が真を返す場合に基本関数の動作を行なうようです。
もう1つは、名前に-if-notがついたもので、1つめと逆の動作をします。

CL-USER> (count-if #'evenp '(1 2 3 4 5))
2
CL-USER> (count-if-not #'evenp '(1 2 3 4 5))
3
CL-USER> (remove-if-not #'(lambda (animal) (eql (car animal) '猫))
                        '((猫 たま) (犬 ぽち) (猫 みー) (猿 ぼぶ)))
                        
((猫 たま) (猫 みー))
CL-USER> 

後者の-if-not関数は言語の標準で非推奨とされているそうですが、
どうも非推奨にすべきではないという意見が多数らしく、標準が変わるとしたら非推奨でなくなるだろうということ。
特にremove-if-notは名前から受ける印象と違い、述語を満たすものを返すので使い勝手がいいとべた褒め。

これらの高階関数もキーワード引数を取ることができます。関数を引数でもともと持つので:test以外ですが。
と言うことは、:keyを使えば先程の例がもっとスマートに書けますね。

CL-USER> (remove-if-not #'(lambda (animal) (eql animal '猫))
                        '((猫 たま) (犬 ぽち) (猫 みー) (猿 ぼぶ)) :key #'car)
                        
((猫 たま) (猫 みー))
CL-USER>

removeにはremove-duplicateという更なる亜種がいて、引数のシーケンスの重複を取り除いて
結果を返すようです。シーケンス内の全ての要素が対象になるので:countはキーワードにできないそう。

シーケンス全体の操作

シーケンス全体を一度に操作する関数もいくつかあるそうです。
copy-seq,reverseはいずれもシーケンス1つを引数にとり、同じ型の新しいシーケンスを返すそう。
注意点として、どちらも新たに作成されるオブジェクトは全体の容器としてのシーケンスだけであり、
その中身の各要素はコピーされて新しく作られるわけではないということです。
ちょっと難解な言い回しな気がしますが、浅いコピーということですかね。

CL-USER> *vec*
#(Z B D E F H)
CL-USER> (reverse *vec*)
#(H F E D B Z)
CL-USER> (copy-seq *vec*)
#(Z B D E F H)
CL-USER> (defvar *copy-vec* (copy-seq *vec*))
*COPY-VEC*
CL-USER> (vector-pop *copy-vec*)
; Evaluation aborted on #<TYPE-ERROR expected-type: (AND VECTOR (NOT SIMPLE-ARRAY))
             datum: #<(SIMPLE-VECTOR 6) {1002F5F05F}>>.
CL-USER> (vector-pop *vec*)
H
CL-USER> *copy-vec*
#(Z B D E F H)
CL-USER> *vec*
#(Z B D E F)
CL-USER> (elt *copy-vec* 0)
Z
CL-USER> (setf (elt *copy-vec* 0) 'a)
A
CL-USER> *copy-vec*
#(A B D E F H)
CL-USER> *vec*
#(Z B D E F)
CL-USER> (type-of *copy-vec*)
(SIMPLE-VECTOR 6)
CL-USER> (type-of *vec*)
(VECTOR T 10)
CL-USER>

なんか思ってたのと違いました。型が違っている...。うーん、わからん。
それ以外どれぐらい違うのでしょう。とりあえずほとんど普通にベクタとして使えるのかな。
配列,ベクタ,シーケンス : セマンティックウェブ・ダイアリー
ありがたい記事。「生成後動的に大きさが変わらない」というところにひっかかったようです。
コピーをするとフィルポインタまでは引き継がれない、よし。

CL-USER> (type-of (make-array 5))
(SIMPLE-VECTOR 5)
CL-USER> (type-of (make-array 5 :fill-pointer t))
(VECTOR T 5)
CL-USER>

concatenate関数は、任意の数のシーケンスを繋ぎ合わせ新しいシーケンスを返してくれます。
そうか、文字列もリストもシーケンスという共通点があってconcatenateで使えていたんですね。

CL-USER> (concatenate 'vector #(1 2 3) '(4 5 6))
#(1 2 3 4 5 6)
CL-USER> (concatenate 'list #(1 2 3) "456")
(1 2 3 #\4 #\5 #\6)
CL-USER> (concatenate 'string #(1 2 3) '(4 5 6))
; Evaluation aborted on #<TYPE-ERROR expected-type: CHARACTER datum: 1>.
CL-USER> (concatenate 'string #(#\1 #\2 #\3) '(#\4 #\5 #\6))
"123456"
CL-USER>

stringは文字しか受けつけない...以前もやらかしてました...。

ソートとマージ

シーケンスをソートするには、sortとstable-sortという2種類の関数が使えるそう。
いずれも引数にシーケンスと述語を取り、ソートされたシーケンスを返します。そして、破壊的関数。
stable-sortのsortと違う点は、述語によって等価だと判断された値の順序を絶対に変えないというとこ。
ついでに破壊的関数とは言っても書き方には注意があるそうなのでそれは下記で。

CL-USER> (defparameter *vec* (vector "foo" "bar" "baz"))
*VEC*
CL-USER> *VEC*
#("foo" "bar" "baz")
CL-USER> (setf *vec* (sort *vec* #'string<)) ;ソート関数はシーケンスを好き勝手に変更し得るので
#("bar" "baz" "foo")                    ;必ず戻り値を利用することを心がける
CL-USER>

merge関数は2つのシーケンスと述語を取り、2つのシーケンスを述語に照らし合わせてマージした結果を返します。
戻り値のシーケンスは、元のシーケンスを両方とも同じ述語でソートしてから
マージしたシーケンスと同じになるそうです。
また第一引数として、生成されるシーケンスの型を明示してあげる必要があるそうです。

CL-USER> (merge 'list #(1 6 3) #(2 8 0) #'<)
(1 2 6 3 8 0)
CL-USER> (merge 'list #(1 6 3) #(2 8 0) #'>)
(2 8 1 6 3 0)
CL-USER>

めちゃくちゃですね。どういう順序で並んでいるのかわかりません。
各シーケンスの先頭要素を述語で比較して、残ったほうと次の要素を比較して、という感じ?
引数のシーケンスはソートされている必要があるのかな。

CL-USER> (merge 'list #(1 3 6) #(0 2 8) #'<)
(0 1 2 3 6 8)
CL-USER> (merge 'list #(1 3 6) #(0 2 8) #'>)
(1 3 6 0 2 8)
CL-USER> (merge 'list (reverse #(1 3 6)) (reverse #(0 2 8)) #'>)
(8 6 3 2 1 0)
CL-USER>

そうっぽいですね。

部分シーケンス操作

既存のシーケンスの部分シーケンスを操作するための一番基本的な関数は、subseqというそう。
開始インデックスから終了インデックス、あるいは開始からシーケンスの終わりまでを取り出せます。

CL-USER> (subseq "foobarbaz" 3 6)
"bar"
CL-USER> (subseq "foobarbaz" 3)
"barbaz"
CL-USER> (subseq '(foo bar baz fiz foo bar) 3 5)
(FIZ FOO)
CL-USER>

また、setfもできますが、それによってシーケンスが伸長することはないそうです。

CL-USER> (defparameter *str* (copy-seq "foobarbaz"))
*STR*
CL-USER> (defun subseq-test (str s e)
           (setf (subseq *str* s e) str)
           *str*)
SUBSEQ-TEST
CL-USER> (subseq-test *abc* 3 6)
; Evaluation aborted on #<UNBOUND-VARIABLE *ABC* {10044623F3}>.
CL-USER> (subseq-test "abc" 3 6)
"fooabcbaz"                             ;部分シーケンスと新しい値が同じ長さ。問題なし。
CL-USER> (subseq-test "barrrr" 3 6)
"foobarbaz"                             ;新しい値が長すぎ。余分なとこは無視。
CL-USER> (subseq-test "xx" 3 6)
"fooxxrbaz"                             ;新しい値が短すぎ。あるぶんだけ変更。
CL-USER>

シーケンス中の複数の要素を1つの値に設定するときは、fillというのを使うそう。
必要な引数は、対象のシーケンスと埋める値。キーワード引数の:startと:endで範囲を指定します。
デフォルトの範囲はシーケンス全体だそう。

CL-USER> (fill *str* #\x :start 3 :end 6)
"fooxxxbaz"
CL-USER>

シーケンスから部分シーケンスを探すときはsearchが使え、
似た関数であるpotitionは要素を探すときに使い、searchはシーケンスを探すという違いがあるようです。

CL-USER> (position #\a *str*)
4
CL-USER> (position "bar" *str*)
NIL
CL-USER> (search "bar" *str*)
3
CL-USER> (search #\a *str*)
; Evaluation aborted on #<TYPE-ERROR expected-type: SEQUENCE datum: #\a>.
CL-USER> (search "a" *str*)
4
CL-USER>

searchの方が便利な気がしてしまします。

共通のプリフィックスを持つ2つのシーケンスから最初の差分になる場所を見つけたいときは、
mismatch関数が使えます。

CL-USER> (mismatch "ヨヨヨヨヨヨヨヨ" "ヨヨヨヨEヨヨヨ")
4
CL-USER> (mismatch "ヨヨヨヨヨヨヨヨ" "ヨヨヨヨヨヨヨ")
7
CL-USER> (mismatch "ヨヨヨヨヨヨヨヨ" "ヨヨヨヨヨヨヨヨ")
NIL
CL-USER>

最近出てきた標準的なキーワード引数は全て取ることができるそうです。

久しぶりに長めになりました。今日はここまで!