独学Common Lisp

My Practical Utilities

概要

ここでは私が用いた関数やマクロなどをメモしていきます。

他のページと比べて、解説は少なめです。

Function: dotback

ファイルを上書きで変更したい場合、まずファイル名を変更してからそのファイルを読み込み、元のファイル名で出力します。その際、どのようなファイル名に変更してもいいのですが、私は「元のファイル名+".back"」に変更するようにしています。

Common Lispのファイル名は単純な文字列で与える場合もあればpathnameオブジェクトとして与える場合もあります。そのため、文字列の場合とpathnameの場合を分けて処理します。

(defun dotback (file)
  (if (pathnamep file)
      (pathname (concatenate 'string (namestring file) ".back"))
      (pathname (concatenate 'string file ".back"))))

pathnameから普通の文字列に戻すのはnamestringで、文字列からパスネームを構築するのはpathnameです。

Macro: with-output-to-file

Common Lispのファイル入出力マクロであるwith-open-fileは自由度が高いですが、出力に使うには色々とキーワードオプションを指定する必要があり、ソースコードが長くなりがちです。そこで、私は出力用だけ別のマクロを作って対応しています。

(defmacro with-output-to-file 
      ((out file &optional (enc :utf-8)) &body body)
  `(with-open-file (,out ,file :direction :output
                         :external-format ,enc
                         :if-exists :supersede)
     ,@body))

Macro: overwrite

ファイルを上書きしたい処理は意外と多くあります。私の場合、他の人が書いたC/C++及びFortranのソースコードの内容を修正する機会があり、Common Lispを用いて自動で処理したことがあります。その時の処理が文字コードの変更、行の挿入、行単位の置換だったので、上書き用の汎用マクロを書き、その上に3つの関数を構築しました。

私はアナフォリックマクロが好きなので、アナフォリックマクロで書いたものを関数から利用するようにし、このマクロ自体は内部用として利用しています。

itが行の内容、NRが行番号を指し、マクロの外から明示的な定義・束縛なく利用することができます。

下の方で出てくるwith-gensymマクロを使っています。

(defmacro overwrite 
    (file &optional (f-e :utf-8) (t-e :utf-8) &body body)
  "Anaphoric macro: it (file line string) and NR (line number)"
  (with-gensym (back-file in out)
    `(let ((,back-file (dotback ,file)))
       (rename-file ,file ,back-file)
       (with-open-file (,in ,back-file :external-format ,f-e)
         (with-output-to-file (,out ,file ,t-e)
           (do ((it (read-line ,in nil)) (NR 1 (1+ NR)))
               ((null it) nil)
             (setf it (string-trim '(#\return) it))
             ,@body
             (write-line it ,out)
             (setf it (read-line ,in nil)))))
       (delete-file ,back-file))))

Function: jis-to-utf8

前述の通り、overwriteマクロの上に構築した文字コード変換関数です。

(defun jis-to-utf8 (file)
  (overwrite file :cp932 :utf-8 nil))

Function: insert-line

同じくoverwriteマクロで構築している行挿入関数です。

(defun insert-line (n string file &optional (enc :utf-8))
  (overwrite file enc enc
    (when (= NR n)
      (setf it (format nil "~a~%~a" it string)))))

Function: replace-line

同じくoverwriteマクロで構築している行置換関数です。

(defun replace-line (n string file &optional (enc :utf-8))
  (overwrite file enc enc
    (when (= NR n)
      (setf it string))))

Function: grep-search

ファイルに検索文字列が含まれるかどうかを検索する関数です。

(defun grep-search (string file)
  (with-open-file (in file)
    (loop
       for line = (read-line in nil)
       when (search string line) return t
       when (null line) return nil)))

Function: read-file

テキストファイルの内容を行単位で読み込み、リストにして返します。基本はread-lineで行単位で読み込みますが、readで読み込む時もあったため、リーダーを指定できるようにしています。

(defun read-file (file &optional (reader #'read-line))
  (with-open-file (in file)
    (loop
       for line = (funcall reader in nil)
       while line
       collect line)))

read-lineを使う時は以下のようにオプションは必要ありません。

(read-file "test.txt")

readを使う時は、明示します。

(read-file "test.txt" #'read)

Function: file-range

上で紹介したread-fileと、下で紹介するstring-split, list-range, list-columnを組み合わせた関数です。ファイルの特定の行が区切り文字(デフォルトは#\,)で区切られている場合、何行目から何行目までの何列目という指定の仕方でデータを拾ってきます。インデックスは当然0からです。

公開されている統計データはエクセルかCSVのことが多く、整形されていないことが大半です。しかし、最初から最後まで整形されていなくても、欲しいデータの部分だけはきちんと整形されていることが多いです。整形されている、というのは要するに区切り文字で区切られている、ということです。

そのような時にいちいちエクセルで整形し直してから処理するのは面倒なので、この関数で一気に処理してしまいます。

(defun file-range (file start end col-num)
  (list-column
   (mapcar #'string-split (list-range (read-file file) start end))
   col-num))

Function: string-split

文字列を区切り文字で分割してリストにします。これを使った時はstring-split自体を高階関数mapcarの引数にして使うため、区切り文字はオプショナル引数にしています。

(defun string-split (string &optional (dlim #\,))
  (read-from-string
    (format nil "(~a)" (substitute #\space dlim string))))

Function: list-range

リストの特定の要素を取得するにはnthを使いますが、何番目から何番目というように範囲で指定したいことがあります。特に上で紹介したread-fileを使ってファイルを行単位で読み込んだ場合は、テキストファイルの何行目から何行目という指定をする場合があります。

そのような時に、リストの範囲を取得する関数list-rangeを使います。脱出条件をつけると長いリストの最初の方だけを取得する場合も効率的になりますが、ここではつけていません。

(defun list-range (list start end)
  (loop
     for i from 0
     for item in list
     when (<= start i end) collect item))

Function: list-column

データが二次元のリストになっているような場合、行単位のデータを列単位として取り出したいことがあります。その時に、list-columnを使います。似たようなことはmapcarでもできます。

(defun list-column (list col-num)
  (loop
     for item in list
     collect (nth col-num item)))

Function: lag

時系列データを扱う際、1期ラグを取るということがあります。例えば1期前のGDPが当期のGDPに影響する、という場合、GDPのデータは入ったリストのラグを取ると処理しやすくなります。また、対数を用いた変化率を取る場合もラグを取っておけばlogを取って引くだけで変化率の列を得ることができます。

ベクタには対応していませんが、リストの場合はこのlag関数でラグを取ることができます。

(defun lag (list)
  (let ((len (length list)))
    (push 0.0d0 list)
    (loop
       for i from 1
       for v in list
       while (<= i len) collect v)))

当然ながら、一番最初のデータはラグを取ることができませんので、0になります。

Function: change

前述の通り、ラグを取っておくと簡単に変化率を求めることができますから、関数changeを高階関数mapcarに渡すことで変化率のリストを求めることができます。

(defun change (x y)
  (if (or (zerop x) (zerop y))
      0
      (- (log x) (log y))))

このように使います。

(mapcar #'change data (lag data))

Function: db-create

簡易的なデータベース用ハッシュテーブル作成のための関数です。Stataという商用統計解析ソフトでは一度に扱えるデータフレームは一つだけであり、その代わりいちいちデータベースとなる変数を指定する必要がありません。Stataと同じようにデータベースを隠蔽するため、ハッシュテーブルとする大域変数を一つ定めています。下のinsertselectと共に使いました。

(defvar *df* '())

(defun db-create ()
  (setf *df* (make-hash-table)))

Macro insert (Function: =insert)

db-createで作成したハッシュテーブルにデータを追加する関数とマクロです。

データフレームの変数名でハッシュケーブルのkeyになるものにいちいち'をつけるのが嫌なので、insertをマクロとして定義しておいて、'だけを付加して内部関数=insertを呼び出します。

(values)をつけておかないとインタラクティブな場面で何か色々返り値が表示されたりするのでつけています。

(defun =insert (col-name data)
  (setf (gethash col-name *df*) data)
  (values))

(defmacro insert (col-name data)
  `(=insert ',col-name ,data))

Macro: select (Function: =select)

db-createで作成したハッシュテーブルからデータを取得する関数とマクロです。

insertと同じように表面上はマクロで定義し、内部関数を使って処理しています。この時はデータの中身はリストでしたので、オプショナル引数として「何番目」というインデックスも取れるようにしています。

(defun =select (col-name &optional index)
  (if (null index)
      (gethash col-name *df*)
      (nth index (gethash col-name *df*))))

(defmacro select (col-name &optional index)
  `(=select ',col-name ,index))

Macro: with-gensym

Paul GrahamのOn Lispにも出てくる有名なマクロです。マクロを構築するためのマクロで、多くの内部変数を(gensym)するのが面倒な時に使います。

(defmacro with-gensyms (syms &body body)
  `(let ,(mapcar #'(lambda (x) `(,x (gensym))) syms)
     ,@body))

Macro: if-math

数値の比較で、「未満・同値・より大きい」の3通りに分岐したいことがあります。そのような時、いちいちcondによる3方向分岐を書くのが面倒なので、マクロで条件分岐を作って使っています。

(defmacro if-math ((n1 n2) f1 f2 f3)
  `(cond
     ((< ,n1 ,n2) ,f1)
     ((= ,n1 ,n2) ,f2)
     (t ,f3)))

Common Lispのマクロには「分配(distribution)」という機能がついていて、通常のラムダリストの部分に複雑な構文構造を記述することができます。マクロ展開のフェーズでは、ラムダリストの構造の通りにマッチングされます。

私は最初、このif-mathと同じことをloopマクロのwhen節とelse節で記述しましたが、とても見にくくなってしまったためマクロで実装し、do節の中に入れて使いました。

Macro: list-return

リストに対して繰り返し処理をしたいが、ある条件の時は繰り返しを脱出して特定の値を返したいという処理が複数あったので、マクロを作って使いました。リストに対して繰り返し処理をしているので、そのリストの要素を脱出判定や返り値として使うため、アナフォリックマクロで定義しています。

(defmacro list-return (list test rslt)
  "Anaphoric Macro: it (element of list)"
  `(dolist (it ,list)
     (when ,test
       (return ,rslt))))

これを使った時は二次元リストの処理をしており、以下のように使用しました。

(list-return data-list
  (< (second it) test-value) (first it))

リストの各行に対して繰り返し処理を行いながら、その2列目の値を脱出判定に用い、条件に合致したら1列目の値を返すような場合です。

一回だけならマクロを使わずに実装すべきですが、複数回ある場合はマクロが便利です。


Copyright © 2017- satoshiweb.net All rights reserved.