2019年4月18日木曜日

このサイトについて


 仕事が忙しく、このサイトもしばらく更新できていなかったのですが、最近またプログラミングしたいなという気持ちが出てきたので、ボチボチ手をつけていこうと思っています。

 具体的には、ブログ形式をやめて、オリジナルのサイト形式に戻そうかと思っています。ブログはあくまで雑記的に使うつもりです。

 久しぶりにCommon Lispに戻ってきて、このまま続けようか、どうしようかとも思いましたが、YouTubeで面白い動画を見つけました。
https://www.youtube.com/watch?v=mbdXeRBbgDM

 やはりCommon Lispは目立たないところでしっかり選ばれ、使われているのだなぁと実感しました。

 「独学Common Lisp」も細々と続けていきますので、引き続きよろしくお願いします。

2018年3月28日水曜日

CL-INTERPOL: 文字列表現の拡張

概要

このページではCommon Lispの文字列表現を拡張するCL-INTERPOLについて簡単に説明します。

正規表現ライブラリであるCL-PPCREと併用すると便利ですので、そちらも参照してください。

cl-interpolはリーダーマクロをフル活用したライブラリです。リーダーマクロに関する理解は必須ですので、リーダーマクロについて初耳の方は標準仕様入門の第2章「構文」を参照してください。

このページではASDFでcl-interpolをロードした後、enable-interpol-syntaxマクロで構文を有効化した状態で説明します。
(asdf:load-system :cl-interpol)
; => T

(interpol:enable-interpol-syntax)
; (返り値なし)

disable-interpol-syntaxマクロで構文を無効化することもできます。
(interpol:disable-interpol-syntax)
; (返り値なし)

文字列の表現(デリミタの切り替え)

ANSI Common Lispでは文字列は文字のベクトルであり、定数表現はダブルクォーテーションで囲みます。
"abc"
; => "abc"

これで通常は何の問題もありませんが、しばしば厄介な場合も出てきます。一つはダブルクォーテーション自体を使いたい場合です。
"a\"b\"c"
; => "a\"b\"c"

(length "a\"b\"c")
; => 5

もう一つの厄介な場合は、バックスラッシュを使用する場合です。バックスラッシュはANSI Common Lispで文字を表すリーダーマクロになっているため、文字列の中で1つで使うと、事実上消滅してしまします。
"a\bc"
; => "abc"

"a\\bc"
; => "a\\bc"

(length "a\\bc")
; => 4

cl-interpolは文字列定数表現をダブルクォーテーション以外にも拡張します。使用するにはシャープ・クエスチョン(#?)ディスパッチマクロを使います。
#?/abc/
; => "abc"

#?/a"b"c/
; => "a\"b\"c"

#?/a\bc/
; => "a\\bc"

ダブルクォーテーション以外に使用可能な区切り文字のリストは*outer-delimiters*スペシャル変数で確認できます。
interpol:*outer-delimiters*
; => ((#\( . #\))
;     (#\{ . #\}) 
;     (#\< . #\>)
;     (#\[ . #\])
;      #\/
;      #\|
;      #\"
;      #\'
;      #\#)

特殊文字の印字

cl-interpolのパワーは単にデリミタを切り替えられるだけではありません。バックスラッシュを用いて改行コードやタブ文字を入れることができます。
#?"abc\ndef"
; => "abc
;    def"

#?"abc\tdef"
; => "abc def"

簡易フォーマット

cl-interpolでは改行やタブだけでなく、文字列定数表現の中に式を埋め込むことができます。

ANSI Common Lispでは非常に高機能なformat関数が定められており、これを使うと文字列をプログラム的に生成することができます(第22章「出力(format関数)」を参照のこと)。ただ、高機能すぎて少し直感的ではなく、一般的に普及しているシェルスクリプトやPerlの流儀とは異なるため、最初は扱いづらいかもしれません。

cl-interpolではシェルスクリプトやPerlに近い形でフォーマットを提供してくれます。
;; 変数のフォーマット
(let ((foo 10)) #?"foo = ${foo}")
; => "foo = 10"

;; リストのフォーマット
(let ((foo '(1 2 3 4 5))) #?"foo = @{foo}")
; => "foo = 1 2 3 4 5"

;; リストのデリミタを切り替えて使う
(let ((foo '(1 2 3 4 5))
      (interpol:*list-delimiter* ", "))
  #?"foo = @{foo}")
; => "foo = 1, 2, 3, 4, 5"

;; 式のフォーマット
(let ((x 1)(y 2))
  #?"x = ${x}, y = ${y}\n(x + y) = ${(+ x y)}")
; => "x = 1, y = 2
;    (x + y) = 3"

一般の変数はドル記号を、リストはアットマークを使います。

もちろん、これらはANSI標準のformat関数でも実現できますが、少し冗長です。
(let ((foo 10)) (format nil "foo = ~a" foo))
; => "foo = 10"

(let ((foo '(1 2 3 4 5))) (format nil "foo = ~{~a~^ ~}" foo))
; => "foo = 1 2 3 4 5"

(let ((foo '(1 2 3 4 5))) (format nil "foo = ~{~a~^, ~}" foo))
; => "foo = 1, 2, 3, 4, 5"

(let ((x 1)(y 2))
  (format nil "x = ~a, y = ~a~%(x + y) = ~a" x y (+ x y)))
; => "x = 1, y = 2
;    (x + y) = 3"


一般に、リーダーマクロは影響力が大きいので慎重に使うべきとされています。ただ、cl-interpolは広く普及したライブラリとして定番となっており、使用を躊躇する必要はありません。特にCL-PPCREで正規表現を使う場合は強力な武器になりますので、積極的に活用してください。

2018年3月25日日曜日

cl-num-utils: 数値計算ユーティリティ

概要

このページでは数値計算ライブラリあるcl-num-utilsについて簡単に説明します。

cl-num-utilsは大きなライブラリで、数学的に専門性の高い関数なども定められています。ただ、多くの場合、バリバリの数値計算(線形代数・行列計算)はBLASやLAPACKなどの専用のライブラリを用います。また、最近ではCPUではなくGPUで計算を行うことも増えています。GPUで計算する場合はnVidiaのCUDAというライブラリが必須です。Common Lispでバリバリの数値計算をやる場合、BLAS/LAPACK/CUDAに対応したMGL-MATを使うのが最も高速だと思いますが、MGL-MATを導入すると依存関係でLLAというライブラリが入ります。これがBLAS/LAPACKを呼び出すのですが、このLLAの実装にcl-num-utilsが使われています。Common Lisp製の数値計算ライブラリはいくつかありますが、cl-num-utilsが最も応用性が高いと判断したため、紹介します。

cl-num-utilsは多くのパッケージに分割され、名前空間が別れています。インポートせずに使用する場合はニックネームのclnuでシンボルにアクセスでき、インポートする場合は細かいパッケージを指定してインポートすることもできます。ただし、meansumなどの衝突を起こしやすそうな名前のシンボルも定義されているため、それらをインポートする場合はshadowing-importを使用してください。(mean, variance, medianの3種はAlexandriaと競合します。)
(asdf:load-system :cl-num-utils)
; => T

このページでは基本的に上の状態でサンプルを示します。

紹介するコンテンツ
  1. ユーティリティ
  2. 算術関数
  3. 要素毎の計算
  4. インターバル
  5. 行列
  6. チェビシェフの多項式
  7. 求根アルゴリズム
  8. 数値積分(ロンバーグ法)

ユーティリティ

cl-num-utilsにはcl-num-utils.utilitiesパッケージの中にいくつかの便利なオペレータが定められています。ここでは、汎用性が高いと思われるものをいくつか紹介します。

Function: within?(範囲内の判定)

3つの値を引数に取り、2番目の値が「1番目の引数以上、3番目の引数未満」に入っているかどうかを判定します。
(clnu:within? 1 2 3)
; => T

(clnu:within? 1 1 3)
; => T

(clnu:within? 1 3 3)
; => NIL

Function: as-double-float(double-flaot型変換)

数値計算は'double-float型で行うことが圧倒的に多いので、as-double-float関数で型変換を行うことができます。
(map 'vector #'clnu:as-double-float #(1 2.0s0 1/3))
; => #(1.0d0 2.0d0 0.3333333333333333d0)

Macro: with-double-floats (double-float型の束縛)

as-double-float関数を用いて型変換を行ってから値を束縛するマクロです。
(clnu:with-double-floats ((x 1)
                          (y 2.0s0)
                          (z 1/3))
  (vector x y z))
; => #(1.0d0 2.0d0 0.3333333333333333d0)

Function: binary-search(二分探索)

ソート済の数列ベクトルから二分探索法によってマッチするインデックスを返します。
;; 乱数による数列をベクトルで生成したいので iterate パッケージを使う
(asdf:load-system :iterate)
; => T

;; 乱数の数列ベクトルを生成
(iter (repeat 10) (collect (random 100) :result-type 'vector))
; => #(18 13 81 0 20 19 40 74 52 68)

;; ソートしておく
;; (* は前の評価結果を束縛したスペシャル変数)
(sort * #'<)
; => #(0 13 18 19 20 40 52 68 74 81)

;; 二分探索をする
(clnu:binary-search * 40)
; => 5

;; 2つ前の評価結果(配列)の中で、
;; 1つ前の評価結果(5)の要素番号の要素を取得する
(aref ** *)
; => 40

算術関数

cl-num-utilsにはいくつかの有用な算術関数が定められています。ここではcl-num-utils.arithmeticパッケージのオペレータを紹介します。

Function: same-sign?(符号の等価性判定)

same-sign?関数は可変長引数の符号が同じかを判定します。
(clnu:same-sign? 1 -2 3)
; => NIL

(clnu:same-sign? 1 2 3)
; => T

Function: square(2乗)

square関数は引数を2乗します。
(map 'vector #'clnu:square '(1 2 3 4 5))
; => #(1 4 9 16 25)

Function: abs-diff(距離)

abs-diff関数は2引数間の距離を求めます。
(clnu:abs-diff 2 -3)
; => 5

Function: log10, log2(対数)

ANSI Common Lisp標準のlogは対数の底がe(ネイピア数)であり、「自然対数」を意味しますが、第2引数を指定すると底を変更できます。log10log2はそのエイリアスとしての関数です。
;; cl-num-utils版
(clnu:log10 10000)
; => 4

;; ANSI Common Lisp標準版
(log 10000 10)
; => 4

Function: 1c(1からの減算)

ANSI標準の1-関数は(- number 1)という意味ですが、cl-num-utilsの1c(- 1 number)という意味です。
(clnu:1c 0.3)
; => 0.7

Function: divides?(割り切れるかの判定)

関数名の通り、割り切れるかどうかを判定します。割り切れる場合は商を、割り切れない場合はnilを返します。
(clnu:divides? 10 3)
; => NIL

(clnu:divides? 9 3)
; => 3

Function: numseq(数列の生成)

数列を生成します。Alexandriaのiotaと似ていて、iotaの方がメジャーですが、numseqも直感的です。
;; from 1 to 9
(clnu:numseq 1 9)
; => #(1 2 3 4 5 6 7 8 9)

;; from 1 to 9 length 5 => by 2 (= (9 - 1) / 5)
(clnu:numseq 1 9 :length 5)
; => #(1 3 5 7 9)

;; from 1 to 9 by 2 => length 5 (= (9 - 1) / 2 + 1)
(clnu:numseq 1 9 :by 2)
; => #(1 3 5 7 9)

;; from 1 to 9 result-type 'list
(clnu:numseq 1 9 :type 'list)
; => (1 2 3 4 5 6 7 8 9)

以下がAlexandriaのiotaの場合ですが、返り値がリスト固定なので大量の数列を生成する場合は不利かもしれません。
(alexandria:iota 5 :start 1 :step 2)
; => (1 3 5 7 9)

Function: ivec(整数ベクトルの生成)

numseqと似ていますが、整数のベクトルを生成することに特化したものです。使い方はiotaによく似ていますが、キーワード引数ではなくオプショナル引数を使用します。
;; from 0 below 10
(clnu:ivec 10)
; => #(0 1 2 3 4 5 6 7 8 9)

;; from 1 below 10
(clnu:ivec 1 10)
; => #(1 2 3 4 5 6 7 8 9)

;; from 1 below 10 by 2
(clnu:ivec 1 10 2)
; => #(1 3 5 7 9)

Generic Function: sum, product(加算・乗算)

sumは加算の縮約を、productは乗算の縮約を行いますが、どちらも総称関数であり、リスト・ベクトルなどのシーケンスに加え、行列をはじめとする多次元配列にも対応しています。
(clnu:sum #(1 2 3 4))
; => 10

(clnu:product #2a((1 2)(3 4)))
; => 24

Function: cumulative-sum, cumulative-product(累積和・累積)

sumproductの計算過程が必要であれば、cumulative-sumcumulative-productを使います。
(clnu:cumulative-sum #(1 2 3 4))
; => #(1 3 6 10) ;
;    10

(clnu:cumulative-product #(1 2 3 4))
; => #(1 2 6 24) ;
;    24

Function: l2norm(L2ノルム)

原点0からの距離の2乗の和の平方根を求めます。
;; (sqrt (+ 4 1 0 1 4))に等しい
(clnu:l2norm #(-2 -1 0 1 2))
; => 3.1622777

;; (sqrt (/ (+ 4 1 0 1 2) 5))に等しい
;; (今回はどちらも平均が0にしている)
(clnu:sd #(-2 -1 0 1 2))
; => 1.5811388300841898d0

Function: normalize-probabilities(発生確率の正規化)

複数の事象が発生するイベントがあるとき、各事象の発生回数をもとに発生確率を計算する関数です。サンプルを示した方がわかりやすいと思うので、サイコロの例で試します。

まず、繰り返しを使うので、iterateパッケージをロードします。また、ANSI標準の乱数ではなく、せっかくなのでメルセンヌ・ツイスタ法を使いたいので、MT19937もロードします。
(asdf:load-system :iterate)
; => T
(use-package :iterate)
; => T

(asdf:load-system :mt19937)
; => T
(shadowing-import 'mt19937:random)
; => T

サイコロを10000回降って、どの目が何回出たかを記録してみます。
(iter (repeat 10000)
      (with result = (make-array 6 :initial-element 0))
      (for r = (random 6))
      (incf (svref result r))
      (finally (return result)))
; => #(1682 1613 1683 1663 1688 1671)

どれも似たような数字ですが、これを正規化します。
;; * は変数。手前の式の評価結果を使うことができる。
(clnu:normalize-probabilities *)
; => #(841/5000 1613/10000 1683/10000 1663/10000 211/1250 1671/10000)

分数になってちょっとよく分からなくなったので、小数に変換してみます。
(map 'vector (lambda (x) (coerce x 'float)) *)
; => #(0.1682 0.1613 0.1683 0.1663 0.1688 0.1671)

正規化すると、どの目も16.13%〜16.88%(幅0.75%pt)で発生したことが分かります。ちなみに、100万回に増やしたところ、16.6354%〜16.7261%(幅0.09%pt)でした。許容範囲が1%ptなら1万回、0.1%ptなら100万回という感じでしょうか。

要素毎の計算

cl-num-utilsには配列の要素毎の計算を行う便利な関数が複数定められています。パッケージはcl-num-utils.elementwiseです。

Function: e+, e-, e*, e/(四則演算)

要素毎の四則演算は以下の通りです。
(clnu:e+ #2a((10 20) (30 40)) #2a((1 2)(3 4)))
; => #2A((11 22) (33 44))

(clnu:e- #2a((10 20) (30 40)) #2a((1 2)(3 4)))
; => #2A((9 18) (27 36))

(clnu:e* #2a((10 20) (30 40)) #2a((1 2)(3 4)))
; => #2A((10 40) (90 160))

(clnu:e/ #2a((10 20) (30 40)) #2a((1 2)(3 4)))
; => #2A((10 10) (10 10))

cl-num-utilsにはe+系オペレータの裏に引数の数が固定されたe2+e1-などのオペレータも定められています。e+e-は引数の個数によってこれらのオペレータを使い分けるように設計されているので、一般的にはe+系オペレータを使った方が便利ですが、引数の個数が1または2で明確ならe1-e2+を直接使う方が若干早いはずです。

Function: eexpt, eexp, elog, esqrt(指数・対数)

これらの関数も要素毎の処理を行いますが、複数の配列に対して処理をするのではなく、1つの配列内の要素に対して処理を行います。
(clnu:eexpt #2a((1 2)(3 4)) 2)
; => #2A((1 4) (9 16))

(clnu:esqrt *)
; => #2A((1 2) (3 4))

(clnu:eexp #2a((1 2)(3 4)))
; => #2A((2.7182817 7.389056) (20.085537 54.59815))

(clnu:elog *)
; => #2A((0.99999994 2.0) (3.0 4.0))

(clnu:elog #2a((1 2)(4 8)(16 32)) 2)
; => #2A((0 1) (2 3) (4 5))

Function: emax, emin(最大値・最小値)

ANSI Common Lisp標準のmax関数とmin関数は可変長引数をとるオペレータですが、cl-num-utilsのemaxeminは配列・リストを引数に取ります。
(clnu:emax #2a((3 1)(4 2)))
; => 4
(clnu:emin #2a((3 1)(4 2)))
; => 1

(clnu:emax '(3 1 4 2))
; => 4
(clnu:emin '(3 1 4 2))
; => 1

インターバル

cl-num-utilsには「インターバル」というオブジェクトがCLOS (Common Lisp Object System)で実装されています。これは、実数における範囲を示すものです。特徴としては、無限大も指定できることです。

パッケージはcl-num-utils.intervalです。

Function: interval(インターバルの生成)

interval関数で範囲オブジェクト(インターバル)を生成できます。生成されるインターバルは無限大の存在によって4種類に分けられます。
(clnu:interval -1 1)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1,1]>

(clnu:interval -1 :plusinf)
; => #<CL-NUM-UTILS.INTERVAL:PLUSINF-INTERVAL [-1,∞)>

(clnu:interval :minusinf 1)
; => #<CL-NUM-UTILS.INTERVAL:MINUSINF-INTERVAL (-∞,1]>

(clnu:interval :minusinf :plusinf)
; => #<CL-NUM-UTILS.INTERVAL:REAL-LINE (-∞,∞)>

種類によってカッコの付き方も違うのは仕様です。

Function: left, right(インターバルのアクセッサ)

インターバルオブジェクトのスロットにアクセスするには、leftrightを使います。
(defparameter *ivl* (clnu:interval -1 1))
; => *IVL*

(clnu:left *ivl*)
; => -1
(clnu:right *ivl*)
; => 1

これらはアクセッサですが、読み取り専用であるためスロットの変更はできません。

Function: open-left?, open-right?(無限大かの判定)

インターバルに無限大が含まれるかを判断するには、open-left?open-right?を使用します。
(clnu:open-left? *ivl*)
; => NIL
(clnu:open-right? *ivl*)
; => NIL

(clnu:open-left? (clnu:interval :minusinf 1))
; => T

Function: interval-length(インターバルの長さ)

インターバルの左右の距離を求めるにはinterval-length関数を用います。
(clnu:interval-length *ivl*)
; => 2

Function: interval-midpoint(インターバルの中間点)

インターバルの中間点・中心を求めるにはinterval-midpoint関数を使います。
(clnu:interval-midpoint *ivl*)
; => 0

Function: in-interval?(インターバルの範囲内判定)

ある数がインターバルに含まれるかどうかを判定するにはin-interval?関数を使います。
(clnu:in-interval? *ivl* 0.5)
; => T

(clnu:in-interval? *ivl* 3)
; => NIL

Function: split-interval(インターバルの分割)

インターバルを分割して複数のインターバルにするにはsplit-interval関数を使います。どのように分割するかの指定にはrelativespacerを使用します。
;; 相対的な長さで半分 + 残り
;;   => 半分 + 半分
(clnu:split-interval *ivl* (list (clnu:relative 1/2) (clnu:spacer)))
; => #(#<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1,0]>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0,1]>>

;; 絶対的な長さで1/4 + 残り
;;   => 1/4 + 7/4
(clnu:split-interval *ivl* (list 1/4 (clnu:spacer)))
; => #(#<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1,-3/4]>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-3/4,1]>)

Function: shrink-interval (インターバルの縮小・拡大)

インターバルは縮小したり、拡大したりすることができます。
;; 負の数だと拡大する
(clnu:shrink-interval *ivl* -1)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-2,2]>
(clnu:shrink-interval *ivl* -2)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-3,3]>

;; 正の数だと縮小する
(clnu:shrink-interval *ivl* 1/2)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1/2,1/2]>
(clnu:shrink-interval *ivl* 1/3)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-2/3,2/3]>

;; relative も使用できる
(clnu:shrink-interval *ivl* (clnu:relative 1/3))
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1/3,1/3]>

;; 左右を分けて伸縮できる
;; (左は1/2縮小、右は2拡大)
(clnu:shrink-interval *ivl* 1/2 -2)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1/2,3]>

Function: grid-in(グリッドを取得する)

インターバルにおいて等間隔に位置する点を「グリッド」と呼ぶとすると、そのグリッドを取得するのがgrid-in関数です。
(clnu:grid-in *ivl* 2)
; => #(-1 1)

(clnu:grid-in *ivl* 3)
; => #(-1 0 1)

(clnu:grid-in *ivl* 9)
; => #(-1 -3/4 -1/2 -1/4 0 1/4 1/2 3/4 1)

第2引数はグリッドの数で、間隔の数ではありません。そのため、最小は2です(左端と右端の2箇所)。

Function: subintervals-in(インターバルの等分割)

グリッドと同様に等分割でインターバルを生成する場合はsubintervals-in関数を使用できます。
(clnu:subintervals-in *ivl* 2)
; => #(#<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1,0.0)>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0.0,1]>)

(clnu:subintervals-in *ivl* 4)
; => #(#<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-1,-0.5)>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-0.5,0.0)>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0.0,0.5)>
;      #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0.5,1]>)

こちらは間隔の数を指定します。

Generic Function: shift-interval(インターバルのシフト)

関数名の通り、インターバルを左右にシフトします。
(clnu:shift-interval *ivl* 2)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [1,3]>

(clnu:shift-interval *ivl* -2)
; => #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [-3,-1]>

行列

行列はcl-num-utilsの中心的な機能であり、パッケージは3つに別れています。
  • cl-num-utils.matrix-shorthand: 行列やベクトルを手軽に生成するためにオペレータ
  • cl-num-utils.print-matrix: print-matrixを中心とする出力オペレータ
  • cl-num-utils.matrix: transposeなどの処理オペレータ
このうち、上の2つは標準ではclnuパッケージにexportされていません。ただ、パッケージ名を付けて使用するのは長くて冗長なので、使用する場合はシンボルをインポートします。ここでは、この2つのパッケージについてはインポート済である前提でサンプルを示します。
(use-package :cl-num-utils.matrix-shorthand)
; => T
(use-package :cl-num-utils.print-matrix)
; => T

このページの最初に述べている通り、行列の計算は他の線形代数ライブラリを使うことが多いので、行列計算に関するオペレータは含まれていないのが特徴です。あくまでも行列を扱いやすくする、というのが主眼です。

ここではオペレータを絞って紹介しますので、興味があればソースコードを確認してください。

Function: vec (型付きベクトル)

vecは関数で、型指定ありのベクトルを生成します。要素は型に合うように変換されます。
(vec 'double-float 1 2 3 4 5)
; => #(1.0d0 2.0d0 3.0d0 4.0d0 5.0d0)

Macro: mx(型付き行列)

mxはマクロで、型指定ありの行列を生成します。要素は型に合うように変換されます。
(mx 'double-float (1 2) (3 4))
; => #2A((1.0d0 2.0d0) (3.0d0 4.0d0))

要素はリストの可変長引数で指定します。

ちなみに、行列の内容がリストのリストで指定された場合は以下のようなマクロを別途定義しておき、こちらを通すと使えるようになります。
(defmacro mx* (element-type rows)
  `(cl-num-utils.matrix-shorthand:mx ,element-type ,@rows))
; => MX*

(mx* 'double-float ((1 2)(3 4)))
; => #2A((1.0d0 2.0d0) (3.0d0 4.0d0))

どのように指定するかは好みに応じて使い分けてください。

Function: diagonal-mx(対角行列)

対角行列とは、左上から右下の対角成分以外は全て0である行列です。cl-num-utilsでは対角行列専用の構造体が定義されています。
(diagonal-mx 'double-float 1 2 3 4 5)
; => #S(CL-NUM-UTILS.MATRIX:DIAGONAL-MATRIX
;       :ELEMENTS #(1.0d0 2.0d0 3.0d0 4.0d0 5.0d0))

(aops:as-array *)
; => #2A((1.0d0 0 0 0 0)
;        (0 2.0d0 0 0 0)
;        (0 0 3.0d0 0 0)
;        (0 0 0 4.0d0 0)
;        (0 0 0 0 5.0d0))

array-operationsas-array総称関数がdiagonal-matrix構造体用に再定義されているため、as-arrayを通せば普通の行列(ANSI Common Lisp標準の2次元配列)に変わります。

Function: print-matrix(行列の表示)

print-matrix関数はとても便利な出力オペレータです。行列とストリームを指定すると、綺麗に表示してくます。
(print-matrix #2a((1 2 3)(4 5 6)) t)
;   1 2 3
;   4 5 6
; => NIL

(print-matrix #2a((1 2 3)(40000 50000 60000)) t)
;       1     2     3
;   40000 50000 60000
; => NIL

標準では右寄せされますが、:aligned?キーワード引数をnilにすると右寄せされなくなります。
(print-matrix #2a((1 2 3)(40000 50000 60000)) t :aligned? nil)
;   1 2 3
;   40000 50000 60000
; => NIL

行列をCSVで出力したければ、:paddingキーワード引数を","に、:indentキーワード引数を""にします。
(print-matrix #2a((1 2 3)(40000 50000 60000)) t 
              :aligned? nil
              :padding ","
              :indent "")
; 1,2,3
; 40000,50000,60000
; => NIL

Generic Function: diagonal-vector(対角成分の取得)

行列から対角成分を取得します。
(clnu:diagonal-vector #2a((1 2)(3 4)))
; => #(1 4)

この総称関数は(setf diagonal-vector)総称関数も合わせて定義されているため、アクセッサとして機能します。
(let ((a #2a((1 2)(3 4))))
  (setf (clnu:diagonal-vector a) #(10 40))
  a)
; => #2A((10 2) (3 40))

Function: diagonal-matrix(対角行列)

cl-num-utils.matrix-shorthandパッケージに定められたdiagonal-mx関数は、この関数を適用します。
(clnu:diagonal-matrix #(1 2 3))
; => #S(CL-NUM-UTILS.MATRIX:DIAGONAL-MATRIX :ELEMENTS #(1 2 3))

(aops:as-array *)
; => #2A((1 0 0) (0 2 0) (0 0 3))

diagonal-mxと異なり、引数は予めベクトルにしておく必要があり、また、型の変換も行われません。

Function: lower-triangular-matrix(下三角行列)

行列から下三角行列を抜き出して生成します。返り値は独自の構造体です。
(clnu:lower-triangular-matrix #2a((1 2 3)(4 5 6)(7 8 9)))
; => #<CL-NUM-UTILS.MATRIX:LOWER-TRIANGULAR-MATRIX
;      element-type T
;      1 . .
;      4 5 .
;      7 8 9>

print-object総称関数も定義されているため、構造体ですが独自の表示形式で表示されます。

三角行列などの特殊な行列はcl-sliceslice総称関数も定義されているため、手軽に切り出すことができます。
(cl-slice:slice * t 1)
; => #(0 5 8)

Function: upper-triangular-matrix(上三角行列)

上三角行列を抜き出します。
(clnu:upper-triangular-matrix #2a((1 2 3)(4 5 6)(7 8 9)))
; => #<CL-NUM-UTILS.MATRIX:UPPER-TRIANGULAR-MATRIX
;      element-type T
;      1 2 3
;      . 5 6
;      . . 9>
print-object, aops:as-array, cl-slice:sliceなどの総称関数が合わせて定義されています。

Generic Function: transpose(転置行列)

行列の転置を行います。

ANSI Common Lisp標準の2次元配列のほか、ここで紹介してきた三角行列にも対応しています。
(defparameter *a* #2a((1 2 3)(4 5 6)(7 8 9)))
; => *A*

(clnu:transpose *a*)
; => #2A((1 4 7) (2 5 8) (3 6 9))

(clnu:transpose (clnu:lower-triangular-matrix *a*))
; => #<CL-NUM-UTILS.MATRIX:UPPER-TRIANGULAR-MATRIX
;      element-type T
;      1 4 7
;      . 5 8
;      . . 9>

(clnu:transpose (clnu:upper-triangular-matrix *a*))
; => #<CL-NUM-UTILS.MATRIX:LOWER-TRIANGULAR-MATRIX
;      element-type T
;      1 . .
;      2 5 .
;      3 6 9>

チェビシェフの多項式近似

cl-num-utilsには「チェビシェフの多項式」に関する関数も定められており、関数近似を行うことができます。チェビシェフの多項式についてはこちらのサイトなどを参考にしてみてください。

このページでは、cl-num-utilsのテストコードに掲載された式の一つである y = x / (x + 4) のチェビシェフ多項式近似を行うサンプルを示します。チェビシェフの多項式はcl-num-utils.chebyshevパッケージに収録されています。

Function: chebyshev-roots, chebyshev-root(チェビシェフ多項式の根)

チェビシェフ多項式の根、すなわち0に等しくなるときのxの値を求めます。

チェビシェフ多項式はこちらのサイトでも計算したり、グラフ化できますので、0になっていそうか確かめてみてください。
(clnu:chebyshev-roots 3)
; => #(-0.8660254037844387D0 -6.123233995736766D-17 0.8660254037844387D0)

(clnu:chebyshev-root 3 2)
; => 0.8660254037844387D0

Function: chebyshev-approximate(チェビシェフ多項式による近似)

ある関数の特定の区間において、チェビシェフ多項式を用いて近似関数を得ることができます。

近似化したい元の関数が y = x / (x + 4) だとします(cl-num-utilsのテストコードの例1)。この時、x = 1から x = 10までの値を求めておきます。
(defun real-fn1 (x)
  (float (/ x (+ 4 x))))
; => REAL-FN1

(defparameter *real-value1*
  (map 'vector #'real-fn1 (clnu:numseq 1 10)))
; => *REAL-VALUE1*

*read-value1*
; => #(0.2 0.33333334 0.42857143 0.5
;      0.5555556 0.6 0.6363636 0.6666667
;      0.6923077 0.71428573)

次に、元の関数から15次元のチェビシェフ多項式の近似関数を得て、その関数で近似値を取得してみます。)

(defparameter *app-fn1*
  (clnu:chebyshev-approximate #'real-fn1
                              (clnu:interval 1 10) 15))
; => *APP-FN1*

(defparameter *app-value1*
  (map 'vector *app-fn1* (clnu:numseq 1 10)))
; => *APP-VALUE1*

これだけでは近似できているかどうか分からないので、差分を取って比較してみます。
(defparameter *diff1*
  (map 'vector #'clnu:abs-diff *real-value1* *app-value1*))
; => *DIFF1*

(every #'(lambda (x) (< x 1e-5)) *diff1*)
; => T

1e-50.00001ですので、誤差は十分に小さいと言えます。

Function: chebyshev-regression(チェビシェフ多項式の係数)

Common Lispは関数をラムダ式で直接表現できるため、前節で得られる近似式は関数の実体そのものです。ただ、関数の実体ではなく、係数が欲しい場合もあります。

その場合はchebyshev-regression関数を使います。
(clnu:chebyshev-regression #'real-fn1 15)
; => #(-0.03279555898864451d0 0.26236447190915607d0 -0.03332465729595915d0
;      0.004232786458517312d0 -5.376343721793371d-4 6.828851891812111d-5
;      -8.673779165265424d-6 1.1017144053308373d-6 -1.3993607700009866d-7
;      1.777421220205966d-8 -2.2576206755905066d-9 2.867554582675069d-10
;      -3.64223170142471d-11 4.6253204969796496d-12 -5.776545908275921d-13)

係数の一覧を見ると、後半の係数はほとんど0に近くなっており、項の説明力にあまり寄与していないことが分かります。テストコードが15を採用していたので15にしたのですが、試して見ると10でも1e-5の範囲に誤差が収まりました。

求根アルゴリズム

cl-num-utilsには単一根を求めるアルゴリズムも実装されています。

求根アルゴリズムを使って、2の平方根((sqrt 2))を近似的に求めてみます。なお、求根アルゴリズムはcl-num-utils.rootfindingパッケージに収録されています。

Function: root-bisection(単一解の求根)

2の平方根は「ひとよ ひとよに ひとみごろ(1.41421356)」として覚えると思いますが、数値解析的に求める場合、関数を想定する必要があります。

まず、√2 を求めたい訳ですから、これを x とおきます( x = √2 )。次に、両辺を2乗します( x^2 = 2 )。最後に、根を求めたいので、右辺が 0 になるように移項します( x^2 - 2 = 0 )。こうして得られる左辺が、今回の関数です ( f(x) = x^2 - 2 )。

関数を想定すれば、あとは解析的に求める際の範囲を指定します。試しに0から10までを指定します。
(clnu:root-bisection #'(lambda (x) (- (expt x 2) 2))
                     (clnu:interval 0 10))
; => 1.4141845703125d0
;    -8.200109004974365d-5
;    T
;    #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0,10]>

root-bisection関数は第1引数に関数を、第2引数に範囲を指定します。すると範囲内の値を適用して関数を評価し、値が0に近くなるかどうかを調べます。返り値は1つ目が求めた解、2つ目がその解を関数に適用した場合の計算値、3つ目がその計算値が許容誤差の範囲に収まっているかどうか、4つ目が解析に使用した範囲です。

標準では許容誤差の範囲が小数点3〜4桁程度しかないため、1.414までしか一致していません。もう少し許容誤差を狭めるには、*rootfinding-epsilon*スペシャル変数を変更します。
(setf clnu:*rootfinding-epsilon* 1.0d-8)
; => 1.0D-8

(clnu:root-bisection #'(lambda (x) (- (expt x 2) 2))
                     (clnu:interval 0 10))
; => 1.4142135623842478d0
;    3.154454475406965d-11
;    T
;    #<CL-NUM-UTILS.INTERVAL:FINITE-INTERVAL [0,10]>

これで1.4142135623まで一致しました。

数値積分

cl-num-utilsにはロンバーグ法による数値積分を行う関数も定義されています。

数値積分に関してはこちらのサイトが参考になります。

Function: romberg-quadrature(数値積分)

ある関数の特定の範囲を積分した値を返します。ロンバーグ法を使うのに適切かどうかはさておき、ここでは f(x) = x^2 + 2x + 1 という2次関数の1から2までの範囲の数値積分をしてみます。
(clnu:romberg-quadrature #'(lambda (x) (+ (expt x 2) (* 2 x) 1))
                         (clnu:interval 1 2))
; => 6.333333333333333d0
;    12

こちらの高精度計算サイトでも計算できますが、おそらく返り値の2つ目はステップ(繰り返しの回数)だと思います。(ソースコードのdocstringには明示されていませんが。)

2018年3月24日土曜日

optima: 高速パターンマッチングライブラリ

概要

このページでは高速パターンマッチングライブラリであるoptimaについて簡単に説明します。optimaの作者は日本人ですので、作者自身による日本語ドキュメントも参照してください。

optimaは以下の状態でロードされていることとします。
(asdf:load-system :optima)
; => T

(use-package :optima)
; => T

Clozure CLで名前の衝突が発生する場合は、shadowing-importを使用してからuse-packageしてみてください。
(shadowing-import 'optima:place)
; => T

(use-package :optima)
; => T

なお、optimaにはtriviaという派生ライブラリも存在します。triviaは後発ですがoptimaと互換性があるので、triviaを使用する場合も構文はほぼ同じです。

パターンマッチングとは

パターンマッチングは文字列における正規表現検索の文脈でよく使われますが、optimaがメインの対象とするのはオブジェクトのパターンマッチングです。特に力を発揮するのは、オブジェクトの中身に対するマッチングです。

パターンマッチングは一般に、2つの機能を併せ持っています。
  1. データ構造の分解(分配束縛)を行う「デストラクタ」としての機能
  2. データ構造の判定を行う「条件分岐」としての機能
ANSI Common Lispにはいくつかパターンマッチングに類似するオペレータが定められています。
  • caseマクロ: 定数パターンのマッチングを行います
  • typecaseマクロ: 型パターンのマッチングを行います
  • destructuring-bindマクロ: リストパターンのマッチングを行いますが条件分岐の機能はありません
  • with-slotsマクロ: スロットパターンのマッチングを行いますが条件分岐の機能はありません
それぞれ、標準仕様入門の第5章「データと制御フロー」(case, typecase)、第3章「評価とコンパイル」(destructring-bind)、第7章「オブジェクト」(with-slots)を参照してください。

定番の汎用束縛マクロであるlet-plus(let+)もパターンマッチングを行いますが、条件分岐の機能がありません。複数の分配束縛を行いながら処理をする場合にはlet+が便利ですが、パターンによる条件分岐を行う場合はoptimaが便利です。

このページでは英語の公式ドキュメントを参考にしながら、様々なパターンをのサンプルを紹介します。

パターン一覧
  • 基本パターン
    • 定数
    • 変数: _, otherwise
    • 汎参照: place
    • 述語: guard
    • 否定: not
    • 論理和: or
    • 論理積: and
  • コンストラクタパターン
    • コンス: cons, assoc, property
    • ベクトル: vector, simple-vector
    • スロット: class, structure
  • 派生パターン
    • リスト: list, list*
    • 述語: satisfies, eq, eql, equal, equalp
    • 型: type
  • エキストラパターン
    • 連想リスト: alist
    • 属性リスト: plist
    • 条件分岐マッチ: if-match, when-match, unless-match
    • 束縛マッチ: let-match, let-match*, let-match1
    • 正規表現マッチ:ppcre

基本パターン

基本パターンの中で特徴的なのはplaceパターンです。これはsetfマクロの第1引数の汎参照と同じで、値ではなくコードを束縛します。

また、guardパターンはマッチングを行った後に特定の条件を満たすかどうか、つまり述語関数が真を返すかを判定するために使います。

その他のパターンは自明ですので、以下のサンプルで確認してください。

サンプル1: or, guard, _

;; パターンマッチで実装するフィボナッチ数列
;; orパターン, guardパターン, _パターンを使用
(defun fib (n)
  (match n
    ((or 0 1) n)
    ((guard x (minusp x)) (error "~a is a negative number!" n))
    (_ (+ (fib (1- n)) (fib (- n 2))))))

なお、_パターンは任意の1オブジェクトにマッチします。otherwiseパターンは他にマッチしなかった場合のデフォルト用です。

サンプル2: place

;; placeパターンで汎参照そのものを束縛する
;; この例で x に束縛されるのは '(aref v 1) というコード
(let ((v #(1 2 3)))
  (match v
    ((vector _ (place x) _) (setf x 20) v)))
; => #(1 20 3)

サンプル3: 定数

;; 定数パターンは文字列も使用可能
;; if を使う場合は string= など等価性判定述語関数を使い分ける必要がある
;; match を使う場合は何も考えなくて良い
(let ((string "Hello"))
  (match string
    ("Hello" t)
    (_ nil)))
; => T

コンストラクタパターン

コンストラクタはコンス、ベクトル、クラス(インスタンス)、構造体などに対するマッチングを提供します。どれも直感的に使えますので、以下のサンプルで確認してください。

サンプル4: cons, assoc, property

コンスパターンとしては基本のconsのほか、連想リストに対するassocと属性リストに対するpropertyが定められています。連想リストについては第14章「リスト」を、属性リストについては第10章「シンボル」を参照してください。
(match '(1 . 2)
  ((cons x y) (list x x y y)))
; => (1 1 2 2)

(match '((:x . 1)(:y . 2))
  ((assoc :y value) (format t "y = ~a~%" value)))
; y = 2
; => NIL

(match '(:x 1 :y 2)
  ((property :y value) (format t "y = ~a~%" value)))
; y = 2
; => NIL

なお、連想リストと属性リストはエキストラパターンに複数のマッチングに対応したものが入っていますので、そちらを使用することもできます。

サンプル5: vector, simple-vector

ベクトルに関してはvectorパターンの他にsimple-vectorも用意されています。「シンプル」の意味については標準仕様入門の第15章「配列」を参照してください。
(match #(1 2)
  ((vector x y) (format t "(~a, ~a)~%" x y)))
(1, 2)
; => NIL

(match #(1 2)
  ((simple-vector x y) (format t "(~a, ~a)~%" x y)))
(1, 2)
; => NIL

サンプル6: class

;; クラスの定義
(defclass point ()
  ((x :initarg :x :accessor point-x)
   (y :initarg :y :accessor point-y)))
; => #<STANDARD-CLASS POINT>

;; クラスの実体(インスタンス)の生成
(defparameter *p* (make-instance 'point :x 2 :y 4))
; => *P*

;; class パターンでマッチする
;; (class は省略可)
(match *p*
  ((class point x y)
   (format t "(x, y) = (~a, ~a)~%" x y)))
; (x, y) = (2, 4)
; => NIL

サンプル7: structure

;; 構造体を定義する
(defstruct date year month date)
; => DATE

;; 構造体の実体を生成する
(defparameter *today* (make-date :year 2018
                                 :month 3
                                 :date 24))
; => *TODAY*

;; structure パターンでマッチングする
;; (structureは省略可)
(match *today*
  ((structrure date- year month date)
   (format t "~a/~a/~a" year month date)))
; 2018/3/24
; => NIL

派生パターン

派生パターンは他のパターンで記述できるパターンです。

サンプル8: list, list*

;; proper list
(match '(1 2 3)
  ((list x y z) (list z y x)))
; => (3 2 1)

;; improper list
(match '(1 2 . 3)
  ((list* x y z) (list z y x)))
; => (3 2 1)

サンプル9: satisfies

satisfiesパターンは関数を引数に取りますが、関数の名前を示すシンボルをクォートなしで指定します。
(match 1
  ((satisfies evenp) 'even)
  ((satisfies oddp) 'odd))
; => ODD

サンプル10: type

(match 1.0
  ((type single-float) 'single-float)
  ((type double-float) 'double-float))
; => SINGLE-FLOAT

(match 1.0d0
  ((type single-float) 'single-float)
  ((type double-float) 'double-float))
; => DOUBLE-FLOAT

エキストラパターン

エキストラパターンはoptima.extraパッケージに入っていますので、使用する場合は追加でインポートしてください。
(use-package :optima-extra)
; => T
最後に紹介している正規表現パターンppcreCL-PPCREを使用したものです。ASDFのレベルから分離されているため、ASDFでロードしてから使用してください。
(asdf:load-system :optima.ppcre)
; => T

(use-package :optima.ppcre)
; => T

サンプル11: alist, plist

連想リストや属性リストのエントリーのうち、複数にマッチした場合を分岐させたいなら、andパターンとassoc/propertyを併用するよりもalist/plistを使用した方が直感的です。
(match '((:x . 1)(:y . 2))
  ((alist (:x . x)(:y . y)) (list x y)))
; => (1 2)

(match '(:x 1 :y 2)
  ((plist :x x :y y) (list x y)))
; => (1 2)

サンプル12: if-match, when-match, unless-match

条件分岐の中でパターンマッチを使えるように拡張したエクストラパターンです。matchの中に条件分岐の機能が含まれますが、構文的にANSI Common Lisp標準のif, when, unlessに近いため、あえて定義されているようです。
(if-match (type string) "Hello" t nil)
; => T

(if-match (type string) 1 t nil)
; => NIL

上の例は、matchで記述した以下のバージョンと等価です。
(match "Hello" ((type string) t)(otherwise nil))
; => T

(match 1 ((type string) t)(otherwise nil))
; => NIL

サンプル13: let-match, let-match*, let-match1

letを拡張するマクロとしてはlet+が有名ですが、optimaにも束縛に特化したパターンマッチマクロがあります。let-matchをはじめとするマクロの中では、matchで使用できるパターンを使って分配束縛することができます。

let-match1は束縛対象が1つだけの場合のシンタックスシュガーです。
;; 束縛部分にパターンを使用できる
(let-match (((list x y z) '(1 2 3)))
  (list z y x))
; => (3 2 1)

;; let-match1は束縛が1つだけの場合に使用できる
(let-match1 (list x y z) '(1 2 3)
  (list z y x))
; => (3 2 1)

サンプル14: ppcre

optima.ppcreを使用すると、Cl-PPCREをより直感的に使用することができます。
(match "独学 Common Lisp"
  ((ppcre "(.+)\\s(.+)\\s(.+)" v1 v2 v3)
   (list v1 v2 v3)))
; => ("独学" "Common" "Lisp")

これをCl-PPCREのscan-to-stringsだけで記述すると、以下のようになります。
(multiple-value-bind (match regs)
    (ppcre:scan-to-strings "(.+)\\s(.+)\\s(.+)" "独学 Common Lisp")
  (list (svref regs 0) (svref regs 1) (svref regs 2)))
; => ("独学" "Common" "Lisp")

2018年3月21日水曜日

iterate: 拡張可能でLispらしいループマクロ

概要

このページでは高機能ループマクロであるiterateについて簡単に説明します。ANSI標準の繰り返し構文については第6章「繰り返し」を参照してください。

iterateは以下の状態でロードされていることとします。
(asdf:load-system :iterate)
; => T

(use-package :iterate)
; => T

なお、Clozure CLの場合はiterateのシンボルのいくつかがcommon-lisp-userパッケージのシンボルと衝突するようです。一つ一つshadowing-importしても構いませんが、最初からパッケージを分けると問題を回避できます。
(asdf:load-system :iterate)
; => T

(defpackage :iter-test (:use :cl :iterate))
; => #<Package "ITER-TEST">

(in-package :iterate)
; => #<Package "ITERATE">

loopiterate

ANSI Common Lispには標準で極めて高機能なloopマクロが定められているため、多くのニーズはloopマクロの節(forcollectなど)を使用することで満たされると思います。

しかし、loopマクロで満たされないニーズもあります。
  • loopマクロの内部は独自の構文であるため、Lispらしくないと言われます。これは単に感覚的なものだけでなく、EmacsやVimなどのエディタで適切なインデントを自動で行うことができない、という明確な弱点も存在します。
  • loopマクロは高機能ですが、loopマクロの内部で使う独自の節を定義することはできず、拡張性はありません。
ANSI Common Lispの標準には含まれないサードパーティ製のマクロであるiterateは主にこの2点に対するアプローチを提供します。つまり、iterateloopと異なるのは、「自動インデントに対応したLispらしい構文を持つ拡張可能な繰り返しマクロ」であるという点に尽きます。

実際には様々な違いがありますので興味があれば、公式マニュアル(英語)のp.22 "Difference Between Iterate and Loop"を参照してください。当サイトでは標準仕様入門の第6章「繰り返し」でloopマクロについて網羅的に紹介しているため、このページではiterateマクロを網羅的に紹介するのではなく、loopとの違いに着目して要点を紹介していきます。

構文上の違い

カッコの有無

loopiterの最大の違いは、カッコの有無です。
(loop for i from 1 to 10
      collect i)
; => (1 2 3 4 5 6 7 8 9 10)

(iter (for i from 1 to 10)
      (collect i))
; => (1 2 3 4 5 6 7 8 9 10)

単純な違いですが、これによってEmacsにおける自動インデントなども変わってきますので、とても大きな違いです。

宣言のスタイル

そして、もう一つの大きな違いが、型の宣言の方法です。
(loop for i fixnum from 1 to 10
      collect i)
; => (1 2 3 4 5 6 7 8 9 10)

(iter (for i from 1 to 10)
      (declare (type fixnum i))
      (collect i))
; => (1 2 3 4 5 6 7 8 9 10)

iterateではdeclareシンボルを使うこともtheスペシャルオペレータを使うこともできます。
(iter (for (the fixnum i) from 1 to 10)
      (collect i))
; => (1 2 3 4 5 6 7 8 9 10)

変数束縛の位置

loopマクロは変数の束縛について、マクロ内の先頭部分で行う必要があります。他方、iterateでは場所は選びません。
(loop for x from 1 to 5
      do (print x)
      for y = (expt x 2)
      do (print y))
; WARNING: LOOP: FOR clauses should occur before the loop's main body
; 1 
; 1 
; 2 
; 4 
; 3 
; 9 
; 4 
; 16 
; 5 
; 25 
; => NIL

(iter (for x from 1 to 5)
      (print x)
      (for y = (expt x 2))
      (print y))
; 
; 1 
; 1 
; 2 
; 4 
; 3 
; 9 
; 4 
; 16 
; 5 
; 25 
; => NIL

節の違い

マクロ内の節については細かい違いが多くありますが、最も顕著なのはfor節です。

対象loopiterate
リストinin
ベクトルacrossin-vector
シーケンスなしin-sequence
文字列なしin-string
添字番号なしindex-of-vector,
index-of-sequence,
index-of-string
ハッシュテーブルbeing the
hash-keys in,
being the
hash-values in
in-hashtable
シンボルbeing the&nbsp
symbols in
in-package
公開シンボルbeing the
externl-symbols in
in-package ...
external-only t
ファイルなしin-file ...
using #'read-line
ストリームなしin-stream ...
using #'read-line

表を見ていただくと一目瞭然ですが、iterateはわかりやすいキーワードで豊富な機能が提供されています。

loopにはない機能

多値の束縛

ANSI Common Lisp標準のloopにもリストの分配機能はありますが、多値を直接束縛できる機能はありません。

他方、iterateにはvalues小節があるため、多値を分配束縛できます。
(iter (repeat 5)
      (for (values ss mm hh d m y) = (get-decoded-time))
      (format t "~a/~a/~a ~a:~a:~a~%" y m d hh mm ss)
      (sleep 1))
; 2018/3/20 23:49:14
; 2018/3/20 23:49:15
; 2018/3/20 23:49:16
; 2018/3/20 23:49:17
; 2018/3/20 23:49:18
; => NIL

previousによる前方参照

ループの最中に一つ手前の値を参照したい場合があります。
(loop for v in '(1 2 3 4 5)
      with b = 0
      collect (cons b v)
      do (setf b v))
; => ((0 . 1) (1 . 2) (2 . 3) (3 . 4) (4 . 5))

iterateにはこのような手前の値を参照する機能がついています。for節などでprevious小節を使うと、対象の変数の手前の値を参照して束縛することができます。
(iter (for v in '(1 2 3 4 5))
      (for b previous v initially 0)
      (collect (cons b v)))
; => ((0 . 1) (1 . 2) (2 . 3) (3 . 4) (4 . 5))

前方参照も1つ手前だけでなく、back小節を使うことで2つ以上遡ることもできます。
(iter (for v in '(1 2 3 4 5))
      (for b previous v initially 0 back 2)
      (collect (cons b v)))
; => ((0 . 1) (0 . 2) (1 . 3) (2 . 4) (3 . 5))

generate

ANSI標準のloopマクロにはない機能として、iterateには簡易的なジェネレータの機能がついています。ジェネレータはイテレーション(繰り返し処理)をより一般的に表現するものであり、「繰り返しの中で値がどのように変化するか」という処理と、「どのタイミングで値を変化させるか」というタイミングを分けて扱うことができます。

例えば、シーケンスの中からnil以外の値を行番号と共に表示するプログラムは、一般に以下のように書くことができるように見えます。
(defun non-nil-printer (seq)
  (iter (for i from 1)
        (for v in-sequence seq)
        (when v (format t "~a: ~a~%" i v))))
; => NON-NIL-PRINTER

この関数は、nilがない場合は正常に動作しますが、肝心のnilがある場合に、行番号がズレてしまいます。
(non-nil-printer '(a b c d e))
; 1: A
; 2: B
; 3: C
; 4: D
; 5: E
; => NIL

(non-nil-printer '(a b nil d e))
; 1: A
; 2: B
; 4: D
; 5: E
; => NIL

これは行番号が1から順に増えていくことはfor節で明示できても、どのタイミングで増やすか(増やさない場合はどのような時か)を明示できないため、条件分岐の有無に関わらず繰り返しの回数だけ値が変化することになります。

ジェネレータを使うと、値を増やすタイミングを明確に分離することができます。
(defun non-nil-printer (seq)
  (iter (generate i from 1)
        (for v in-sequence seq)
        (when v (format t "~a: ~a~%" (next i) v))))
; => NON-NIL-PRINTER

iterateのジェネレータはgenerate節で生成され、next節でイテレーション(値を順次変化させる処理)が行われます。when節の条件分岐を満たさない場合はnextが実行されないため、値が変化しないまま再度繰り返しを行うことが可能となります。
(non-nil-printer '(a b c d e))
; 1: A
; 2: B
; 3: C
; 4: D
; 5: E
; => NIL

(non-nil-printer '(a b nil d e))
; 1: A
; 2: B
; 3: D
; 4: E
; => NIL

iterateの拡張

iterateは拡張可能であることを前提に設計されているため、繰り返し処理を行いたいデータ構造に合わせた機能の追加を柔軟に行うことが可能です。

iterateマクロの拡張は、大きく分けると2通りの方法が用意されています。
  1. 節の追加: defmacro-clause
  2. データ構造の追加: defmacro-driver
どのようなデータから値を繰り返し取得するのか、という点が後者で、取り出した値をどのように処理するかというのが前者です。

iterateはすでにデータ構造も処理方法も多様な機能がありますので、そもそも拡張の必要性があるかどうかは十分に検討する必要があると思いますが、このページでは学習向けにサンプルを考えてみましたので、参考にしてみてください。

節の追加: defmacro-clause

この小節では「移動平均」をサンプルとして、iterateマクロの節の追加の方法を示します。

時系列データを扱う際、1期ごとの変動ではなく中長期的なトレンドを見たい場合、「移動平均法」を用いてデータを加工する場合があります。移動平均法は、隣り合うN個の標本の平均を取っていく処理で、データの種類にもよりますが、最も簡単な例だとN=3を使用し、t-1期・t期・t+1期の3つのデータの平均をt期の値とするパターンがあります。

今回は2017年の消費者物価指数のデータを移動平均化してみます。
(defparameter *cpi* #(100.0 99.8 99.9 100.3
                      100.4 100.2 100.1 100.3
                      100.5 100.6 100.9 101.2))
; => *CPI*

まずは関数を使用して、移動平均のアルゴリズムを実装します。
(defun moving-average (sequence)
  (iter (for v in-vector sequence)
        (for b2 previous v back 2)
        (for b1 previous v back 1)
        (with result = '(na))
        (when b2 (push (/ (+ b2 b1 v) 3) result))
        (finally (return (nreverse (push 'na result))))))
; => MOVING-AVERAGE

せっかくなのでpreviousを使用してみました。この場合、t-2期・t-1期・t期の3点の平均を取り、t-1期の値とする処理になりますが、結果は同じです。3期平均の場合は最初の1期目と最後のN期目の平均値は取れませんから、'na (Not Available)を置いています。
(moving-average *cpi*)
; => (NA 99.9 100.0 100.200005 100.30001 100.23334 100.19999 100.299995 100.46667
;     100.666664 100.9 NA)

このmoving-average関数をそのままマクロにしてしまえば、iterateの節になります。節を追加するにはdefmacro-clauseを使用します。マクロを記述するので、Alexandriawith-gensymsマクロを使用します。
(asdf:load-system :alexandria)
; => T

(defmacro-clause (MOVE expr)
  (alexandria:with-gensyms (b2 b1 result)
    `(progn (for ,b2 :previous ,expr :back 2)
            (for ,b1 :previous ,expr :back 1)
            (with ,result = '(na))
            (when ,b2 (push (/ (+ ,b2 ,b1 ,expr) 3) ,result))
            (finally (return (nreverse (push 'na ,result)))))))
; => (MOVE EXPR)

これでiterateに新しい節moveを導入できます。
(iter (for v in-vector *cpi*) (move v))
; => (NA 99.9 100.0 100.200005 100.30001 100.23334 100.19999 100.299995 100.46667
;     100.666664 100.9 NA)

defmacro-clauseのラムダリストは大文字がキーワード、小文字が変数として扱われます。

せっかくなので、move節をもう一歩拡張しましょう。
  • 変数束縛の追加: intoを使用できるようにする
  • 同義語の追加: movingも併用できるようにする
;; &optional引数も使用できる
(defmacro-clause (MOVE expr &optional INTO var)
  (alexandria:with-gensyms (b2 b1)
    ;; iterate::*result-var*は返り値用の内部変数
    (let ((result (or var iterate::*result-var*)))
      `(progn (for ,b2 :previous ,expr :back 2)
              (for ,b1 :previous ,expr :back 1)
              (with ,result = '(na))
              (when ,b2 (push (/ (+ ,b2 ,b1 ,expr) 3) ,result))
              (finally (setf ,result (nreverse (push 'na ,result))))))))
; WARNING: replacing clause (MOVE)
;         with (MOVE &OPTIONAL INTO)
; => (MOVE EXPR &OPTIONAL INTO VAR)

;; defsynonym で同義語を簡単に追加できる
(defsynonym moving move)
; => MOVE

intoを使うときは(or var iterate::*result-var*)という書き方を使用します。ダブルコロンを使用するスタイルは一般には推奨されません。なぜなら、exportされていないシンボルにアクセスすることになるからです。そのため、標準仕様入門の第11章「パッケージ」でもあえて紹介していません。しかし、iterateの場合は公式マニュアルに記載があるので、この書き方を用いても問題ありません。

defsynonymマクロは同義語を手軽に追加できます。一般にloopiterateには現在進行形も合わせて定義されているため、このように同義語を追加しておきます。

(iter (for v in-vector *cpi*)
      (move v into rslt)
      (finally (return (remove 'na rslt))))
; => (99.9 100.0 100.200005 100.30001 100.23334 100.19999 100.299995 100.46667
;     100.666664 100.9)

intoがあるとこのように返り値を細かくコントロールできます。removeはANSI Common Lisp標準の関数で、指定された要素をシーケンスから削除します。標準仕様入門の第17章「シーケンス」をご参照ください。

なお、defmacro-clauseで節を定義するとiterate内の構文シンボルはキーワードとしても同時に使用可能になります。
(iter (for v :in-vector *cpi*)
      (move v :into rslt)
      (finally (return (remove 'na rslt))))
; => (99.9 100.0 100.200005 100.30001 100.23334 100.19999 100.299995 100.46667
;     100.666664 100.9)

:in-vector:intoがキーワードでも正常に動作します。

データ構造の追加: defmacro-driver

この小節ではiterateを行列(2次元配列)向けに拡張する例を通じて、for/generate節の拡張、すなわちデータ構造の追加の方法を示します。

行列をイテレーションする場合、行単位、または列単位による繰り返しができると便利です。そこで、ある行を指定して全ての列を繰り返すin-rowsと、ある列を指定して全ての行を繰り返すin-colsを作ってみます。

for節を拡張するにはdefmacro-driverを使用します。
;; あらかじめalexandriaとarray-operationsをロードしておく
(asdf:load-system :alexandria)
; => T
(asdf:load-system :array-operations)
; => T

;; in-rowsの定義
(defmacro-driver (FOR var IN-ROWS v ROW r)
  (alexandria:with-gensyms (array length index)
    (let ((kwd (if generate 'generate 'for)))
      `(progn
         (with ,array = ,v)
         (with ,length = (aops:nrow ,array))
         (with ,index = -1)
         (,kwd ,var next (progn (incf ,index)
                                (when (<= ,length ,index) (terminate))
                                (aref ,array ,r ,index)))))))
; => (FOR VAR IN-ROWS V ROW R)

;; in-colsの定義
(defmacro-driver (FOR var IN-COLS v col c)
  (alexandria:with-gensyms (array length index)
    (let ((kwd (if generate 'generate 'for)))
      `(progn
         (with ,array = ,v)
         (with ,length = (aops:ncol ,array))
         (with ,index = -1)
         (,kwd ,var next (progn (incf ,index)
                                (when (<= ,length ,index) (terminate))
                                (aref ,array ,index, c)))))))
; => (FOR VAR IN-COLS V COL C)

defmacro-driverの内部では暗黙的にgenerate変数を使うことができます。これはforではなくgenerateでイテレーションする場合をキャッチするための変数です。

terminateiterateの節の一つで、繰り返しを終了することができます。

以下のように使います。
(defparameter *arr* #2a((1 2 3)(4 5 6)(7 8 9)))
; => *ARR*

;; 2行目の各列に対するイテレーション(横移動)
(iter (for v in-rows *arr* row 1) 
      (collect v result-type 'simple-vector))
; => #(4 5 6)

;; 2列目の各行に対するイテレーション(縦移動)
(iter (for v in-cols *arr* col 1) 
      (collect v result-type 'simple-vector))
; => #(2 5 8)

ついでに、各列の和を求める関数も定義してみます。
(defun sum-cols (array)
  (iter (for i :from 0 :below (aops:ncol array))
        (collect (iter (for v :in-cols array :col i) (sum v)))))
; => SUM-COLS

(sum-cols *arr*)
; => (12 15 18)

iterateはこのように対応可能なデータ構造を自由に増やすことができます。

2018年3月19日月曜日

array-operations: 配列操作ライブラリ

概要

このページでは配列操作ライブラリであるarray-operationsについて簡単に説明します。ANSI標準の配列操作については第15章「配列」を参照してください。

array-operationsは以下の状態でロードされていることとします。
(asdf:load-system :array-operations)
; => T

なお、公式ドキュメントではパッケージのニックネームがaoとなっていますが、ソースコードではaopsであるため、このページでもaopsを使用します。
(package-nicknames :array-operations)
; => ("AOPS")

一般的なオペレータ

array-operationsには汎用的なオペレータがいくつか定められています。

dims総称関数: 配列のディメンションの取得

ANSI Common Lispにおけるarray-dimensions関数に相当するのがdims総称関数です。
(aops:dims #2a((1 2 3)(4 5 6)))
; => (2 3)

この場合、2行・3列の行列(2次元配列)であることを意味します。

dim総称関数: 配列の特定のディメンションの取得

配列の特定の次元における要素数を取得したければdim総称関数を用います。ANSI標準のarray-dimension関数に相当します。
(aops:dim #2a((1 2 3)(4 5 6)) 1)
; => 3

行列では0が行、1が列を表す軸番号です。

rank総称関数: 配列のランクの取得

一般的にN次元配列と呼ぶ場合のNを取得します。ANSI標準のarray-rank関数に相当します。
(aops:rank #2a((1 2 3)(4 5 6)))
; => 2

nrow, ncol総称関数: 行数、列数の取得

配列の中でも頻繁に利用される2次元配列(行列)の行数を取得するにはnrowを、列数を取得するにはncolを使用するのが最も簡単です。
(aops:nrow #2a((1 2 3)(4 5 6)))
; => 2

(aops:ncol #2a((1 2 3)(4 5 6)))
; => 3

matrix?, square-matrix?関数: 行列・正方行列の判定

配列が行列であるかどうかを判定するにはmatrix?述語関数を、正方行列(行数と列数が等しい2次元配列)であるかどうかを判定するにはsquare-matrix?述語関数を使用します。
(aops:matrix? #2a((1 2 3)(4 5 6)))
; => T

(aops:square-matrix? #2a((1 2 3)(4 5 6)))
; => NIL

Common Lispのコーディングスタイルでは述語関数にはpを付けるのが好まれますが、SchemeというLispの他の方言では疑問符が好まれます。ここではSchemeスタイルが使用されています。

次元に関するオペレータ

array-operationsでは配列の次元を変更してベクトルにしたり、変形したりするオペレータが定められています。

displace関数: 参照配列の作成

ANSI Common Lisp標準のmake-array関数には標準仕様入門では紹介していない2つのキーワード引数があります。:displaced-to:displaced-index-offsetです。

これらのキーワード引数はdisplaced arrayを作るために使用されます。これは訳が難しいのですが、あえて付けるとすれば「参照配列」でしょうか。個別の配列ではなく、他の配列への参照を格納した配列です。つまり、元の配列の値が変われば自動的に参照配列も変わります。

array-operationsのdisplace関数は前述の2つのキーワード引数を使って参照配列を作るものです。以下の例を参照してください。
;; 実体のある通常の配列を生成し、束縛しておく
;;   2次元, 8要素 = 2 * 4
(defparameter *a* #2a((1 2 3 4)(5 6 7 8)))
; => *A*

;; 1次元への変更
;;  (1次元, 8要素)
(aops:displace *a* '(8))
; => #(1 2 3 4 5 6 7 8)

;; 3次元への変更
;;  (3次元, 8要素 = 2 * 2 * 2)
(aops:displace *a* '(2 2 2))
; => #3A(((1 2) (3 4)) ((5 6) (7 8)))

これだけだと単に次元を変更しているだけのように見えますが、返り値である参照配列は*a*へのポインタを含んでいるため、*a*を変更すると値が変わります。
;; 1次元に変更した参照配列を*b*に束縛する
(defparameter *b* (aops:displace *a* '(8)))
; => *B*

*b*
; => #(1 2 3 4 5 6 7 8)

;; 参照先である*a*を変更する
(setf (aref *a* 0 1) 20)
; => 20

;; *b*も変更されている
*b*
; => #(1 20 3 4 5 6 7 8)

このように元の配列の要素への参照を使いながら別の次元を持つ配列を作るのがANSI標準のmake-array関数における:displaced-toキーワード引数の役割であり、それを簡単に利用するのがarray-operationsのdisplace関数です。

ちなみに、もう一つのキーワード引数である:displaced-index-offsetは配列における参照の始点を指定するのに使用します。例えば、3のある位置から4つの要素を参照して2行2列の参照配列を作るには、以下のようにします。
;; 2次元への変更(オフセットあり)
;;  (2次元, 4要素 = 2 * 2, 参照起点 = 2 (3番目の要素))
(aops:displace *a* '(2 2) 2)
; => #2A((3 4) (5 6))

flatten関数: ベクトル化

flatten関数はdisplace関数の特殊ケースで、常に全ての要素への参照を持つ1次元の参照配列(ベクトル)を生成します。
(aops:flatten #2a((1 2)(3 4)))
; => #(1 2 3 4)

split関数: ランクの切り下げ

split関数もdisplace関数の特殊ケースですが、元の配列の構造に近い重層構造を保ちながら、配列の次元を切り下げるという関数です。

例えば行列(2次元配列)をベクトル(1次元配列)に切り下げると、通常は素の構造が失われます。
;; displaceで1次元に切り下げる場合
(aops:displace #2a((1 2)(3 4)) '(4))
; => #(1 2 3 4)

;; flattenで1次元に切り下げる場合
(aops:flatten #2a((1 2)(3 4)))
; => #(1 2 3 4)

しかし、split関数で次元を切り下げると、多次元配列から「多重配列」のような形式に切り替わります(ネイティブで多次元配列がサポートされないC言語のようなプログラミング言語では、このような配列を「多次元配列」と呼びますが、実際には「配列の配列」であるため、「多重配列」という言葉の方がふさわしいと思います)。
;; 1次元への変更(多重配列化)
(aops:split #2a((1 2)(3 4)) 1)
; => #(#(1 2) #(3 4))

このように多重配列にして何が嬉しいかというと、データの一部を直接「シーケンス」として扱うことができるようになるという点です。
(reduce #'+ (svref * 0))
; => 3

上の例では、多重配列の1行目(添字番号0)のデータを全て合計しています(1 + 2)。なお*というのは処理系において直前の評価結果を取得するスペシャル変数です。標準仕様入門の第25章「周辺分野」で説明しています。

実際には行ではなく列の和や平均、分散を求めることが多いため、行列を転置してから扱うことが一般的です。そのような処理は同じくarray-operationsのmargin関数を使う方が便利です。

なお、ランク(一般的な意味での次元)に0または元の配列と同じランクを指定した場合、元の配列がそのまま返り値となります。

subアクセッサ: 配列の部分取得

配列の部分取得は一般的にはcl-sliceを利用すべきですが、参照配列の機能を使って部分的に取得する場合はarray-operationsのsubアクセッサを使用することができます。
(aops:sub #2a((1 2)(3 4)) 0)
; => #(1 2)

(aops:sub #2a((1 2)(3 4)) 1)
; => #(3 4)

(aops:sub #2a((1 2)(3 4)) 0 0)
; => 1

(aops:sub #2a((1 2)(3 4)) 0 1)
; => 2

(aops:sub #2a((1 2)(3 4)) 1 0)
; => 3

N次元配列においてN個の部分指定を行うとANSI標準のarefアクセッサと同じ動作を行います。

このsubはアクセッサとして定義されていますので、setfマクロと共に使用することができます。
(defparameter *a* #2a((1 2)(3 4)))
; => *A*

(setf (aops:sub *a* 1) #(30 40))
; => #(30 40)

*a*
; => #2A((1 2) (30 40))

partitionアクセッサ: 配列の範囲取得

行列に対してsubアクセッサを使用する場合は特定の行または要素にしか使用できませんが、partitionアクセッサを使用すると行を範囲で指定することができます。
(aops:partition #2a((1 2)(3 4)(5 6)(7 8)) 1 3)
; => #2A((3 4) (5 6))

cl-sliceを使用する場合は以下と等価です。
;; cl-sliceをロードしておく
(asdf:load-system :cl-slice)
; => T

;; 切り取る範囲を指定する
(cl-slice:slice #2a((1 2)(3 4)(5 6)(7 8)) '(1 . 3) t)
; => #2A((3 4) (5 6))

partition(setf partition)関数が同時に定義されているのでアクセッサであり、setfマクロと共に使用することが可能です。

なお、cl-sliceのslice総称関数を使用する場合は行だけでなく列も個別に指定できるため、より複雑な切り取りが可能です。
;; 2次元配列の2〜3行目の2列目を取得する
(cl-slice:slice #2a((1 2)(3 4)(5 6)(7 8)) '(1 . 3) 1)
; => #(4 6)

subvecアクセッサ: ベクトル専用のpartition

配列が1次元(ベクトル)である場合はpartitionよりもsubvecを使用した方が余計な処理がないため高速です。
(aops:subvec #(1 2 3 4 5) 1 3)
; => #(2 3)

;; 結果は同じだが、配列の次元の取得など余計な処理がある
(aops:partition #(1 2 3 4 5) 1 3)
; => #(2 3)

combine関数: ランクの切り上げ

split関数とは逆に、低い次元の多重配列から高次元の多次元配列を生成するのがcombine関数です。
(aops:combine #(#(1 2) #(3 4)))
; => #2A((1 2) (3 4))

reshape, reshape-col, reshape-row関数: 自動調整機能付きdisplace

reshape関数はdisplace関数をより便利にしたものです。

多次元配列の次元(ディメンション)は、要素の数が特定されていればフル指定しなくても自動的に計算して求めることができます。例えば、要素数が6の2次元配列の場合、行数が2と決まれば列数は自動的に3と決まります。一般的に言えば、要素数が決まっている状態でN次元配列のディメンションはN-1個分かれば残りの1つは自動的に計算されます。

displace関数にこの機能を追加したのがreshape関数です。例えば、3行・2列の行列を2行・3列に変更するには、displaceの場合、ディメンションを明示する必要があります。
(aops:displace #2a((1 2)(3 4)(5 6)) '(2 3))
; => #2A((1 2 3) (4 5 6))

しかし、reshapeの場合は行または列のどちらかを指定すれば構いません。
(aops:reshape #2a((1 2)(3 4)(5 6)) '(2 t))
; => #2A((1 2 3) (4 5 6))

(aops:reshape #2a((1 2)(3 4)(5 6)) '(t 3))
; => #2A((1 2 3) (4 5 6))

tを指定した部分が自動で計算されます。

reshape-row関数とreshape-col関数は行列に特化したreshapeで、1行N列またはN行1列に変更するものです。
;; 行優先(1行N列)
(aops:reshape-row #2a((1 2)(3 4)(5 6)))
; => #2A((1 2 3 4 5 6))

;; 列優先(N行1列)
(aops:reshape-col #2a((1 2)(3 4)(5 6)))
; => #2A((1) (2) (3) (4) (5) (6))

処理に関するオペレータ

この節では配列に対する処理を行うオペレータを紹介します。

generate, generate*関数: 関数を使った配列の生成

generate関数は高階関数を使って配列を生成します。第1引数には高階関数を、第2引数には次元(ディメンション)を指定します。第3引数は高階関数に渡す引数の決め方を指定しますが、オプショナル引数であるため、無指定の場合は高階関数に引数が渡されません。
(aops:generate #'(lambda () (random 10)) '(2 3))
; => #2A((1 3 6) (5 1 5))

第3引数に:positionキーワードを指定すると、行優先で数えた場合のインデックスが渡されます。
(aops:generate #'1+ '(2 3) :position)
; => #2A((1 2 3) (4 5 6))

:subscriptsキーワードを指定すると、要素を示す添字指定子がリストで渡されます。行列の場合は、行インデックスと列インデックスです。
(aops:generate #'identity '(2 3) :subscripts)
; => #2A(((0 0) (0 1) (0 2)) ((1 0) (1 1) (1 2)))

:position-and-subscriptsキーワードを指定すると、両方が渡されます(つまり引数は2つです)。
;; format関数の使い方については当サイト標準仕様入門第22章を参照のこと。
(defun format-position (pos sub)
  (format nil "~:r=(~{~a~^, ~})" pos sub)) 
; => FORMAT-POSITION

(aops:generate #'format-position '(2 3) :position-and-subscripts)
; => #2A(("zeroth=(0, 0)" "first=(0, 1)" "second=(0, 2)")
;        ("third=(1, 0)" "fourth=(1, 1)" "fifth=(1, 2)"))

generateを使うと九九を表す行列も簡単に作れます。
(aops:generate #'(lambda (sub) (reduce #'* (mapcar #'1+ sub))) '(9 9) :subscripts)
; => #2A((1 2 3 4 5 6 7 8 9)
;        (2 4 6 8 10 12 14 16 18)
;        (3 6 9 12 15 18 21 24 27)
;        (4 8 12 16 20 24 28 32 36)
;        (5 10 15 20 25 30 35 40 45)
;        (6 12 18 24 30 36 42 48 54)
;        (7 14 21 28 35 42 49 56 63)
;        (8 16 24 32 40 48 56 64 72)
;        (9 18 27 36 45 54 63 72 81))

単位行列も簡単です。
(aops:generate #'(lambda (sub) (if (apply #'= sub) 1 0)) '(9 9) :subscripts)
; => #2A((1 0 0 0 0 0 0 0 0)
;        (0 1 0 0 0 0 0 0 0)
;        (0 0 1 0 0 0 0 0 0)
;        (0 0 0 1 0 0 0 0 0)
;        (0 0 0 0 1 0 0 0 0)
;        (0 0 0 0 0 1 0 0 0)
;        (0 0 0 0 0 0 1 0 0)
;        (0 0 0 0 0 0 0 1 0)
;        (0 0 0 0 0 0 0 0 1))

なお、generate*は第1引数に要素の型を指定できるバージョンで、第2引数以降はgenerateの第1引数以降と同じです。

each, each*関数: 要素毎の関数適用

each関数はその名の通り、全ての要素に高階関数を適用します。
(aops:each #'print #2a((1 2)(3 4)))
; 
; 1 
; 2 
; 3 
; 4 
; => #2A((1 2) (3 4))

複数の配列を指定した場合は、同一位置の要素を同時に引数としながら高階関数を適用します。
(aops:each #'+ #2a((1 2)(3 4)) #2a((10 20)(30 40)))
; => #2A((11 22) (33 44))

each*generate*と同様に第1引数に要素の型を指定します。

margin, margin*関数: 行単位、列単位の計算

margin関数は複雑ですが、一般的には以下のような使い方をします。

read-csvで使った試験結果の表 があるとして、列単位の平均点を求める場合は以下の通りです。
(aops:margin #'alexandria:mean #2a((66 78)(72 62)(85 75)) 0)
; => #(223/3 215/3)

行単位の場合は1にします。
(aops:margin #'alexandria:mean #2a((66 78)(72 62)(85 75)) 1)
; => #(72 67 80)

2018年3月18日日曜日

cl-slice: シーケンス・配列の部分取得

概要

このページでは、リストや配列などの一部を取得するライブラリであるcl-sliceについて説明します。

cl-sliceは拡張可能に設計されていますが、拡張の仕方は説明しません。また、cl-sliceは配列を扱う際には非常に汎用性が高く利用できますので、sliceシンボルはインポートして使います。

このページでは以下の形式で準備されていることを前提に説明します。
(asdf:load-system :cl-slice)
; => T

(shadowing-import 'cl-slice:slice)
; => T

なお、cl-sliceは2017年の10月にオリジナルの作者であるTamas K Pappによるメンテナンスが終了しましたが、alexandria, anaphora, let-plusという代表的な3つのライブラリにしか依存していないため、特に問題なく使用することができます。作者自身が後継のメンテナを募集中ということですので、興味があればこちらの記事(英語)を参照してください。

整数:対応する要素の取得

cl-sliceはslice関数でリストや配列を操作します。最も基本的なのは、整数値で添字(インデックス)を指定し、対応する要素を取得することです。
(slice #(1 2 3 4 5) 1)
; => 2

これがANSI Common Lisp標準のnth(リスト)、aref(配列)、svref(シンプルベクトル)という汎参照(アクセッサ)と異なるのは、逆順でも指定できる点です。-1から始まる負値を指定すると、逆順から見た要素番号に対応する要素を取得します。
(slice #(1 2 3 4 5) -2)
; => 4

また、slicedefunで定義される一般の関数ではなく、defgenericdefmethodで定義される総称関数です。そのため、リストでも配列でも区別することなくアクセスできます。(総称関数はCommon Lisp Object System(CLOS)の一部で、ANSIの第7章で標準化されています。標準仕様入門の第7章「オブジェクト」を参照してください。)
(slice '(1 2 3 4 5) -2)
; => (4)

多次元配列の場合は要素番号を可変長引数のように列挙するだけです。
(slice #2a((1 2)(3 4)) 1 0)
; => 3

t:全ての要素の取得

前節の指定方法において、tシンボルを使うと、対応する次元の全ての要素を取得できます。これは、2次元配列(行列)の行のみ、列のみを取得するのに便利です。

特定の行のみを抽出するには、以下のようにします。
(slice #2a((1 2)(3 4)) 1 t)
; => #(3 4)

特定の列のみを抽出するには、以下のようにします。
(slice #2a((1 2)(3 4)) t 1)
; => #(2 4)

コンス:範囲を指定した取得

要素番号を整数で特定するのではなく、範囲で指定する場合はコンスを使用します。
(slice #(1 2 3 4 5) '(1 . 4))
; => #(2 3 4)

この場合、コンスで指定する数字は対応する要素番号の手前の区切りを意味します。つまり、1の場合は2番目の要素の手前を、4の場合は5番目の要素の手前を意味しますので、2番目から4番目までの要素を取得することになります。

なお、終端指定でnilを指定すると最後の要素まで取得されます。
(slice #(1 2 3 4 5) '(1 . nil))
; => #(2 3 4 5)

このコンスによる指定でも負の値を使うことができます。例えば、最初と最後の要素を除く範囲を指定したい場合は、以下のようになります。
(slice #(1 2 3 4 5) '(1 . -1))
; => #(2 3 4)

ただし、範囲の指定はあくまでも始点から終点までですので、同一の番号を指定することはできません。また、逆順にすることもできません。

範囲の指定は多次元配列でも使用できますし、整数やtの指定と混ぜて使うこともできます。
(slice #2a((1 2 3)(4 5 6)(7 8 9)) t '(1 . nil))
; => #2A((2 3) (5 6) (8 9))

ベクトル:要素の個別取得

指定方法がベクトルの場合、対応する要素を個別に取得することができます。
(slice #(10 20 30 40 50 60 70 80 90) #(2 3 5 7))
; => #(30 40 60 80)

ここでももちろんコンスを用いた指定を組み合わせることができます。
(slice #(10 20 30 40 50 60 70 80 90) #((1 . 4) (-3 . nil)))
; => #(20 30 40 70 80 90)

ビット型のベクトル:フラグとしての要素取得

少し変わり種としての利用法ですが、ベクトルがbit型の場合(つまりbit-vectorの場合)、フラグとして使用することができます。
(slice #(10 20 30 40 50 60 70 80 90) #*010101010)
; => #(20 40 60 80)

その他の機能

以上がcl-sliceのメインですが、他にも少しだけ機能を紹介します。

headtail

言葉の通りですが、headで先頭から、tailで終端から指定することができます。この2つのシンボルは関数ですので、使用する場合はインポートするかパッケージ名も付けて使用する必要があります。
(slice #(1 2 3 4 5) (cl-slice:head 2))
; => #(1 2)

(slice #(1 2 3 4 5) (cl-slice:tail 2))
; => #(4 5)

maskwhich

cl-sliceパッケージに含まれるslice以外の関数としてmaskwhichを紹介します。

mask総称関数は述語関数とシーケンスを引数に取り、述語関数がtを返す部分のフラグを立てたビットマスクを返します。
(cl-slice:mask #'evenp #(1 2 3 4 5))
; => #*01010

また、which総称関数も述語関数とシーケンスを引数に取り、述語関数がtを返す部分の要素番号をベクトルにして返します。
(cl-slice:which #'evenp #(11 22 33 44 55))
; => #(1 3)

この例の場合、2244evenp(偶数かどうか)で真になるため、22の要素番号である144の要素番号である3をベクトルにして返します。

ANSI Common Lispにもposition-ifという同様の関数がありますが、こちらは条件を満たす1つの要素しか取得できません。条件に合致する複数の要素を取得したい場合は、maskwhichなどを利用すると大変便利なため、データのフィルタリングなどを行う場合は積極的に活用してください。

2018年3月17日土曜日

CL-FAD: ファイル・ディレクトリ操作ライブラリ

概要

CL-FADは可搬性のあるファイル・ディレクトリ操作ライブラリです。

ANSI Common Lispの標準仕様では第19章に「ファイルネーム」が、第20章に「ファイル」が定められていますが、処理系・環境依存のものも多く、Windows, macOS, Linuxなど多様なOSで共通して用いることができるオペレータは多くありません。

CL-FADは処理系や環境に依存せず、ポータブルに利用できることを念頭に設計されており、また、便利系のオペレータも実装されていることから、ファイル・ディレクトリ操作のデファクトスタンダードとして広く利用されています。

モジュール管理ツールであるASDFが3に移行してからはファイルネーム関係の環境依存を吸収するためにUIOPが独立して容易されているため、ASDFを使用可能な環境ではUIOPを用いることも可能です。実際、UIOPはCL-FADなど複数のライブラリの代替となることを目指して設計されていますが、そのカバー範囲がかなり広いことやドキュメントが未整備であることなどから、依然CL-FADが広く愛用されているという状況にあります。

CL-FADはANSI Common Lispの第19章・第20章を大幅に補強するものですので、使いたい機能がCL-FADに含まれる場合は積極的に活用してください。

なお、CL-FADはBordeaux ThreadsAlexandriaに依存していますので、あらかじめASDFなどでロードできる状態にしておいてください。このページでは、以下の状態でCL-FADをロード済として説明します。

(require "asdf")
; => T
(asdf:load-system :cl-fad)
; => T

調査に関するオペレータ

この節では、ファイルやディレクトリに関する調査を行うオペレータを紹介します。

directory-exists-p関数: ディレクトリの存在確認

指定されたディレクトリが存在するかどうかを確認します。存在する場合はそのパスを、存在しない場合はnilを返します。
(fad:directory-exists-p #p"common-lisp")
; => #P"common-lisp/"

file-exists-p関数: ファイルの存在確認

指定されたファイルが存在するかどうかを確認します。存在する場合はそのパスを、存在しない場合はnilを返します。
(fad:file-exists-p #p".ccl-init.lisp")
; => #P"/Users/satoshi/.ccl-init.lisp"

directory-pathname-p関数: ディレクトリ指定かどうかの確認

指定されたパスがディレクトリを意味する場合はそのパスを、ディレクトリでない場合はnilを返します。
(fad:directory-pathname-p #p"common-lisp")
; => NIL

(fad:directory-pathname-p #p"common-lisp/")
; => #P"common-lisp/"

pathname-absolute-p関数: 絶対パスかどうかの確認

指定された引数が絶対パスを意味する場合はtを、絶対パスではない場合はnilを返す述語関数です。
(fad:pathname-absolute-p "/Users/satoshi/")
; => T

(fad:pathname-absolute-p "./")
; => NIL

pathname-relative-p関数: 相対パスかどうかの確認

指定された引数が相対パスを意味する場合はtを、相対パスではない場合はnilを返す述語関数です。
(fad:pathname-relative-p "./")
; => T

pathname-root-p関数: ルートディレクトリかどうかの確認

指定された引数がルートを意味する場合はtを、ルートではない場合はnilを返す述語関数です。
(fad:pathname-root-p #p"/")
; => T

(fad:pathname-root-p "/")
; -> T

パスネームの処理に関するオペレータ

この節ではパスネームオブジェクト(ファイルネーム)の処理に関するオペレータを紹介します。

canonical-pathname関数: パスネームの正規化

冗長なパスネームを正規化します。
(fad:canonical-pathname "/a/b/../../a/b/c.txt")
; => #P"/a/b/c.txt"

merge-pathnames-as-directory関数: パスネーム(ディレクトリ)の連結

ディレクトリを意味するパスネームを連結します。ファイルを意味する部分は破棄されます。
(fad:merge-pathnames-as-directory "a/" "b/" "c/d")
; => #P"a/b/c/"

merge-pathnames-as-file関数: パスネーム(ファイル)の連結

パスネームを連結し、ファイルを意味するパスネームを生成します。
(fad:merge-pathnames-as-file "a/" "b/" "c/d")
; => #P"a/b/c/d"

pathname-as-directory関数: ディレクトリを意味するパスネームへの変換

パスネームがディレクトリを意味する場合はそのまま返し、ファイルを意味する場合はディレクトリに変換して返します。
(fad:pathname-as-directory #p"a/b/c")
; => #P"a/b/c/"

pathname-as-file関数: ファイルを意味するパスネームへの変換

パスネームがファイルを意味する場合はそのまま返し、ディレクトリを意味する場合はファイルに変換して返します。
(fad:pathname-as-file #p"a/b/c/")
; => #P"a/b/c"

pathname-directory-pathname関数: ディレクトリ部分のパスネームの取得

pathname-as-directory関数と似ていますが、こちらはファイルを意味するパスネームが指定された場合にディレクトリに変換するのではなく、そのファイルが属するディレクトリ部分のパスネームのみを取得します。
(fad:pathname-directory-pathname #p"a/b/c")
; => #P"a/b/"

pathname-parent-directory関数: 一つ上のディレクトリに存在する場合のパスネーム

この関数の動作は少し複雑です。

引数がディレクトリを意味する場合、そのディレクトリが属する親ディレクトリを取得します。
(fad:pathname-parent-directory #p"a/b/c/")
; => #P"a/b/"

他方、引数がファイルを意味する場合、そのファイルが一つ上のディレクトリに存在したとしたら、という仮定で得られるパスネームを取得します。
(fad:pathname-parent-directory #p"a/b/c")
; => #P"a/c"

この例の場合、cというファイルはbというディレクトリに属しているのが引数の状態ですが、返り値ではbの親ディレクトリであるaに属している場合のパスとなっています。

ディレクトリの探索に関するオペレータ

この節ではディレクトリ内のファイル一覧を取得するなど、探索に関するオペレータを紹介します。

list-directory関数: ディレクトリ内の一覧取得

指定されたディレクトリ直下に属するファイルやディレクトリなどのパスネームをまとめてリストとして返す関数です。ディレクトリを再帰的に探索することはありません。
(fad:list-directory "./")
; => (#P"/Users/satoshi/lisp/asdf.fas" #P"/Users/satoshi/lisp/cv_test.txt"
;     #P"/Users/satoshi/lisp/test.txt" #P"/Users/satoshi/lisp/convert.lisp"
;     #P"/Users/satoshi/lisp/asdf.lib" #P"/Users/satoshi/lisp/asdf.lisp"
;     #P"/Users/satoshi/lisp/convert.dx64fsl" #P"/Users/satoshi/lisp/convert.fas"
;     #P"/Users/satoshi/lisp/convert.lib" #P"/Users/satoshi/lisp/.DS_Store"
;     #P"/Users/satoshi/lisp/old/")

walk-directory関数: ディレクトリ内の再帰的探索

この関数は高機能ですので、サンプルを複数示しながら説明します。

まず、必須の引数は2つで、第1引数がディレクトリ、第2引数が関数です。返り値はありません。
(fad:walk-directory "./a/" #'print)
; 
; #P"/Users/satoshi/lisp/a/d.txt" 
; #P"/Users/satoshi/lisp/a/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/e.txt" 
; #P"/Users/satoshi/lisp/a/b/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/c/f.txt" 

標準ではディレクトリに対しては関数を適用しませんが、:directoriesキーワード引数をnil以外に指定するとディレクトリにも適用するようになります。
(fad:walk-directory "./a/" #'print :directories t)
; 
; #P"/Users/satoshi/lisp/a/d.txt" 
; #P"/Users/satoshi/lisp/a/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/e.txt" 
; #P"/Users/satoshi/lisp/a/b/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/c/f.txt" 
; #P"/Users/satoshi/lisp/a/b/c/" 
; #P"/Users/satoshi/lisp/a/b/" 
; #P"a/" 

ディレクトリの探索は「深さ優先」と「幅優先」があり、標準では「深さ優先」が採用されます。つまり、標準では:directoriesキーワード引数に:depth-firstを指定した場合と同じ動作になります。
(fad:walk-directory "./a/" #'print :directories :depth-first)
; 
; #P"/Users/satoshi/lisp/a/d.txt" 
; #P"/Users/satoshi/lisp/a/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/e.txt" 
; #P"/Users/satoshi/lisp/a/b/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/c/f.txt" 
; #P"/Users/satoshi/lisp/a/b/c/" 
; #P"/Users/satoshi/lisp/a/b/" 
; #P"a/" 

これを「幅優先」に変更する場合は、:breadth-firstを指定します。
(fad:walk-directory "./a/" #'print :directories :breadth-first)
; 
; #P"a/" 
; #P"/Users/satoshi/lisp/a/d.txt" 
; #P"/Users/satoshi/lisp/a/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/" 
; #P"/Users/satoshi/lisp/a/b/e.txt" 
; #P"/Users/satoshi/lisp/a/b/.DS_Store" 
; #P"/Users/satoshi/lisp/a/b/c/" 
; #P"/Users/satoshi/lisp/a/b/c/f.txt" 

さらに、関数を適用するかどうかを判定するための述語関数を個別に指定することもできます。その指定は:testキーワード引数を使います。例えば、拡張子を使って判定を行う場合、ANSI Common Lisp標準のpathname-type関数を使うことができますから、あらかじめ以下のような関数を定めておくことで簡単にファイルの抽出を行うことができます。
(defun filetype-fn (type)
  #'(lambda (pathspec)
      (string= (pathname-type pathspec) type)))
; => FILETYPE-FN

(fad:walk-directory "./a/" #'print :test (filetype-fn "txt"))
; 
; #P"/Users/satoshi/lisp/a/d.txt" 
; #P"/Users/satoshi/lisp/a/b/e.txt" 
; #P"/Users/satoshi/lisp/a/b/c/f.txt" 

一時ファイルに関するオペレータ

CL-FADには一時ファイルを扱うオペレータも複数定義されていますが、その中でも最も便利なwith-output-to-temporary-fileマクロについて簡単に説明します。

プログラムがファイルシステムの中に新しいファイルを生成する際、ファイルの重複には気をつけなければなりません。誤って既存のファイルを上書きしてしまうようなことはあってはなりません。そのため、自身のプログラム名を冠したオリジナルのファイル名を使って、重複がなさそうなディレクトリに保存するのが一般的です。

しかし、ファイルを複数保存する必要がある場合など、名前の重複を丁寧に避けるのは意外と大変な作業です。そこで、重複なくファイルを保存できるようにファイル名を自動で考えてくれるのが「一時ファイル」を扱うオペレータの最も重要な役割です。

CL-FADを用いて一時ファイルを作成するのはとても簡単で2ステップで終わります。

まず、fad:*default-template*スペシャル変数を自分のプログラムの名前などに応じて変更します。
(setf fad:*default-template* "my-program-%")
; => "my-program-%"

最後のパーセント文字は乱数に基づくファイル名が入るので消してはいけません。それ以外の部分を適宜設定します。

そして、with-output-to-temporary-fileマクロで一時ファイルの操作をすると、返り値として一時ファイルのパスネームが返されます。
(fad:with-output-to-temporary-file (temp) (print "Hello" temp))
; => #P"my-program-IADWU5S2"

これだけで一時ファイルを手軽に使うことができるので、必要があればぜひ使ってみてください。

read-csv: シンプルなCSVリーダー

概要

このページでは、シンプルなCSVファイルのリーダーであるread-csvパッケージについて説明します。

read-csv: ストリームから1行読み取り、CSVを解析して返す

read-csvパッケージには外部シンボルが2つしかありません。一つは行単位の読み取りを行うread-csv関数で、もう一つはファイル単位の読み取りを行うparse-csvです。

read-csv関数は4つの引数を取りますが、最初の一つだけが必須で、残りはオプショナルです。引数はそれぞれ以下の意味を持ちます。
  1. 第1引数: 入力ストリーム
  2. 第2引数: セパレータ(標準は#\,
  3. 第3引数: ファイル終端時にエラーを通知するか否か(標準はt
  4. 第4引数: ファイル終端読み取り時に返すオブジェクト(標準はnil
個人的には、セパレータはカンマを使うことが多いので変更する可能性が低く、引数の並びはセパレータを最後に持って来る設計の方がいいのではないかという気もします。なぜなら、第3引数のエラー通知を行うとエラーハンドリングが必要になるため、通常はnilにしてエラーの通知を抑制するためです。

第4引数はnilのままでも構いませんが、read-csvパッケージの中では:eofキーワードを使って制御していますので、その流儀に従ってここでは:eofキーワードを使用します。

以下にサンプルを示します。まず、test.csvというファイル名で、次のデータを保存してください。国語と英語の試験の点数のデータとします。

Member,Japanese,English
A,66,78
B,72,62
C,85,75

今回は単純にCSVファイルを読み取るのではなく、指定された生徒の点数の合計点を返すような関数を考えてみます。今回はコンディションシステムを使ってファイル末尾まで読み込んだ場合の処理を記述しています。コンディションシステムについては標準仕様入門の第9章「コンディション」を参照してください。

(require "asdf")
(asdf:load-system :read-csv)


(defparameter *file* "test.csv")

(defun sum-from-string (list)
  (reduce #'+ (mapcar #'read-from-string list)))

(defun sum-score (name)
  (with-open-file (in *file*)
    (handler-case
        (loop for row = (read-csv:read-csv in)
              when (string= (first row) name)
              return (sum-from-string (rest row)))
      (end-of-file ()))))

read-csv関数はデータを全て文字列として処理するため、数に変換する場合はread-from-string関数を使います。ここでは国語と英語の点数ですが、試験の種類を増やしても対応できるようにmapcarreduceという高階関数を2つ組み合わせています。

以下のように呼び出します。

(sum-score "A")
; => 144
(sum-score "B")
; => 134
(sum-score "C")
; => 160
(sum-score "D")
; => NIL

データを扱う場合はリストではなくベクトルにしたい場合もありますし、今回のように文字列から数に変換してから使いたい場合もあります。データ処理は臨機応変に細かい調整を伴いながら行いますので、read-csv関数を使うことで1行単位の処理に対応できます。

parse-csv関数: 全データの一括読み込み

read-csvパッケージのもう一つの関数であるparse-csvは全データを一括で読み込み、リストのリストとして返します。使い方は単純です。

先ほどの続きで、以下のように実行してみてください。

(with-open-file (in *file*) (read-csv:parse-csv in))
; => (("Member" "Japanese" "English") ("A" "66" "78") ("B" "72" "62")
;     ("C" "85" "75"))

parse-csv関数の引数は2つだけで、read-csvの最初の2つと同じです。まとめてリストのリストとしてデータを取得したい場合は便利です。

2018年2月5日月曜日

anaphora: itによる前方参照マクロ集

概要

このページでは、itシンボルで前方参照可能な「アナフォリックマクロ」を集めたanaphoraを紹介します。

アナフォリックマクロとは

おそらくCommon Lisp以外ではほとんど目にすることのない言葉の一つが「アナフォリックマクロ」です(他には「総称関数」「コンディション」などもCommon Lisp固有かもしれません)。

この言葉を広めたのはPaul GrahamのOn Lispに間違いありません。On Lispはマクロに関する決定版のテキストです。私も読みましたし、おすすめできるのでLisp Linksでも紹介しています。

私はCommon Lispを学ぶ前にPerlを知っており、Perlの独特な特殊変数を好んで使っていました。Perlには$_という特殊変数があり、変数(引数)を省略するとこの変数が使われるという機能があります(デフォルト変数)。

アナフォリックマクロはプログラミング的にはPerlの$_に近く、変数の束縛を省略すると、itというシンボルが変数として使われる、という機能を実現します。アナフォラとは前方参照を意味します(On Lispでは「前方照応」と訳しています)。

もっとも、難しく考える必要はなく、itというシンボルはまさに英語のitと同じです。itは文脈に応じて指し示すものが変わりますが、アナフォリックマクロのitも同じで、使用するシチュエーションによって変わります。

Common Lispの代表的なパッケージの一つであるanaphoraはいくつかのANSI標準オペレータのアナフォリック版を提供します。また、単純に「値」をletスペシャルオペレータでitに束縛するa-系マクロだけでなく、「式」をsymbol-macroletスペシャルオペレータでitに束縛するs-系マクロも提供します。シンボルマクロは式をシンボルに束縛するというCommon Lisp秘伝の奥義のようなものですが、混乱しやすいため一般にはあまり使われません。

このページではanaphoraのオペレータを3種類に分けて簡単に紹介します。

なお、anaphoraのオペレータは基本的に全てシンボルを継承して使われます。ASDFでロードし、use-packageもしくはdefpackage:useでシンボルを継承してから以下のサンプルを使ってください。
(asdf:load-system :anaphora)
; => T
(use-package :anaphora)
; => T

なお、エラーが出る場合はこのページの最後でシンボルの競合について解説していますので、そちらを参照して解決してください。

値を束縛するアナフォリックマクロ

まず、anaphoraで最もよく使われるのは第1引数の評価結果(値)をitに束縛するというオペレータです。

例えば、letスペシャルオペレータのアナフォリック版であるaletマクロは以下のように使うことができます。
(alet (+ 1 2)
  (format nil "~a" it))
; => "3"

itというシンボルは表面上未定義(未束縛)に見えますが、aletというアナフォリックマクロの働きによって(+ 1 2)の評価結果である3が束縛されています。そのため、エラーにはなりません。

このようなアナフォリックマクロが複数定義されており、aifawhenが代表的です。全12種類についてはanaphoraのフォルダに入っているREADME.mdを見れば書いてありますので、自分で確認してください。

式を束縛するシンボルマクロ型アナフォリックマクロ

関数とマクロの違いは一般的に「関数は値を返すが、マクロは式を返す」と説明されます。同様にletsymbol-macroletの違いは「前者は値を束縛するが、後者は式を束縛する」と説明できます。

値ではなく式を束縛すると何が嬉しいかというと、setfマクロの第1引数で使うことができるのです。

(defparameter *list* '(1 2 3))
; => *LIST*

(sif (second *list*)
     (setf it nil)
     (setf it t))
; => NIL

*list*
; => (1 NIL 3)

setfマクロの第1引数で使うためには(second *list*)の評価結果である2ではなく、(second *list*)という式そのものを束縛しておく必要があります。そのため、この機能はsymbol-macroletスペシャルオペレータを使ったシンボルマクロでしか実装することができません。

もちろん、このようなシンボルマクロは注意が必要です。特に複数回の評価を不意に行ってしまうため、副作用には十分に注意してください。

以下では乱数の発生という複数評価に弱いシチュエーションを作為的に作り出してみます。
;; 0以上n未満の乱数を生成し、表示して返す関数を定義しておく
(defun print-random (n)
  (print (random n)))
; => PRINT-RANDOM

;; 何回か試す
(sif (print-random 2)
  it)
; 
; 1 
; 1 
; => 1

(sif (print-random 2)
  it)
; 
; 0 
; 0 
; => 0

;; 異なる数が表示されることがあるが、
;; この時までミスに気づかない
(sif (print-random 2)
  it)
; 
; 0      ;; sif 直後の条件分岐による1度目の乱数生成(表示分)
; 1      ;; it すなわち (print-random 2) の再評価による乱数生成(表示分)
; => 1   ;; it の返り値

anaphoraは有名なので、s-系マクロを見た場合は多くの人が「これはシンボルマクロだ」ということに気づき、注意してソースコードを読むことになります。しかし、シンボルマクロの怖いところはソースコードにあるのではなく、シンボルに束縛される式が副作用を持っていたり、環境に影響されたりする場合です。これはプログラマでは防ぎがたく、ユーザーの手に委ねられているということになります。

a-系アナフォリックマクロでも十分に「黒魔術」的と呼ばれますが、s-系アナフォリックマクロはそれ以上に不可解なバグの原因になり得ますので、うまく使ってください。

なお、こちらも12種類のオペレータが定められていますので、同様にREADME.mdを参照してください。

asif: ミックス系アナフォリックマクロ

最後にa-系とs-系が合わさったマクロとして唯一asifが定められています。これは、ifthen部分ではletによる値の束縛を、else部分ではsymbol-macroletによる式の束縛を行うというものです。

asifの主な使い道はこうです。まず、汎参照の形でどこかを参照し、その真偽を調べます。真の場合(nil以外の場合)はその値を使いますが、偽(nil)の場合は最初の汎参照を使ってsetfマクロで値を書き換える、というような場合です。

以下でサンプルを示します。
(defparameter *list* '(1 2 3))
; => *LIST*

;; ここではthen節が使われるので、it = 2
(asif (second *list*)
      it
      (setf it 0))
; => 2

*list*
; => (1 2 3)

(setq *list* '(1 nil 3))
; => (1 NIL 3)

;; ここではelse節が使われるので、it = (second *list*)
(asif (second *list*)
      it
      (setf it 0))
; => 0

;; 値が変わっている
*list*
; => (1 0 3)

itシンボルの衝突

情報技術のことをInformation Technologyと言うので、略してITです。例えば、IT人材の人数などをitというシンボルに束縛しておこうと思って、itというシンボルをすでに使っているときにanaphoraパッケージのロードと継承を行うとエラーになります。

処理系を起動した直後などで試してみてください。

;; 単に処理系でitと打ち込んだだけ(変数の定義も束縛もしていない)
'it
; => IT

(require "asdf")
; => T

;; ここまでは普通にロードできる
(asdf:load-system :anaphora)
; => T

;; 急にエラーになる
(use-package :anaphora)
; *** - (USE-PACKAGE (#<PACKAGE ANAPHORA>) #<PACKAGE COMMON-LISP-USER>): 1 name
;       conflicts remain
;       Which symbol with name "IT" should be accessible in
;       #<PACKAGE COMMON-LISP-USER>?
; The following restarts are available:
; ANAPHORA       :R1      #<PACKAGE ANAPHORA>
; COMMON-LISP-USER :R2    #<PACKAGE COMMON-LISP-USER>
; ABORT          :R3      Abort main loop

しかし、怯える必要はありません。Common Lispのパッケージシステムとコンディションシステムは非常に柔軟です(第9章「コンディション」と第11章「パッケージ」を参照してください)。

処理系によってエラーメッセージは違うと思いますが、要は"IT"という名前のあるシンボルに競合が発生しているからどのパッケージのを使うか選んでね、ということです。anaphoraを使う場合は必ずanaphora:itを使うことになるので:r1を選択するのがいいでしょう。間違えてanaphoraをロードしてしまったということや、anaphoraのitは常にanaphora:itという形式でアクセスするので継承はやっぱり不要です、という場合は:r2を選択します。

ここでは:r1を選んでみます。
:r1
; => T

返り値のtはコンディションシステムが発動する直前の式であるuse-packageの返り値です。つまり、エラーは発生したものの、何事もなかったように正常に動作を継続することができたのです。

試しに、itというシンボルがどのパッケージのものか、確認してください。
(symbol-package 'it)
; => #<PACKAGE ANAPHORA>

これは仕様入門第11章「パッケージ」で説明したRestartによる解決策です。一般にはエラーの通知(デバッガの起動)は避けたいはずなので、shadowing-importを使って明示的にanaphoraのitを継承するか、shadowを使って明示的にanaphoraのitを除外するかが推奨されます。

それぞれ、処理系を起動した直後で試してください。まずはanaphoraのitを継承する場合です。
(require "asdf")
; => T

'it
; => IT

(asdf:load-system :anaphora)
; => T

;; 明示的にanaphoraのitを継承する
(shadowing-import 'anaphora:it)
; => T

;; エラーは通知されない
(use-package :anaphora)
; => T

;; anaphoraのitが優先されている
(symbol-package 'it)
; => #<PACKAGE ANAPHORA>

次はCOMMON-LISP-USERit(自分で打ち込んだシンボル)を優先する場合です。
(require "asdf")
; => T

(asdf:load-system :anaphora)
; => T

'it
; => IT

;; 明示的に打ち込んだitを優先する
(shadow 'it)
; => T

;; エラーは通知されない
(use-package :anaphora)
; => T

;; 打ち込んだitが優先されている
(symbol-package 'it)
; => #<PACKAGE COMMON-LISP-USER>

anaphoraは自分で使うことは少なくても、定番のライブラリが仕様していることが多いため、結果的に必要になる場合があります。最も注意が必要なのはこの節で述べたitの衝突と、シンボルマクロの扱いです。その点を注意すれば非常に面白い機能ですので、どんどん使ってみてください。