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の衝突と、シンボルマクロの扱いです。その点を注意すれば非常に面白い機能ですので、どんどん使ってみてください。

2018年2月4日日曜日

let-plus: 汎用束縛系マクロ

概要

このページでは、定番の汎用束縛系マクロであるlet-plusを紹介します。

let-plusAlexandriaanaphoraに依存しています。各パッケージをASDFまたはQuicklispでロード可能な状態にしてから利用してください。また、let-plusは一般に全てのシンボルを継承して使われることが多いため、use-packageまたはdefpackage:useで継承して使用してください。

以下ではASDFを用いてlet-plusを使用可能な状態にするサンプルを示します。
(asdf:load-system :let-plus)
; => T

(use-package :let-plus)
; => T

let+の使い方一覧

let-plusパッケージをロードすると、let+マクロを使用可能になります。これはANSI Common Lispで定められた様々な束縛系スペシャルオペレータ・マクロを一つのオペレータだけで実現できる高機能なマクロです。

代表的な使い方を以下に列挙し、各節で使い方のサンプルを示します。
  1. シンプルな束縛
  2. リストの分配束縛
  3. ラムダリストの束縛
  4. インスタンス(スロット・アクセッサ)の束縛
  5. 構造体の束縛
  6. 多値の束縛
  7. 配列の束縛(分配束縛・指定束縛)
  8. 関数の束縛
  9. 属性リストの束縛
  10. ハッシュテーブルの束縛
  11. 複素数の束縛
let-plusパッケージにはlet+以外にもdefun+というマクロも用意されていますが、これはlet+の機能をdefunに対応させたものです。ここではlet+だけを例にとって紹介します。

1. シンプルな束縛

let+は特に気にしなければlet*と同じように使うことができます。
(let+ ((a 1) (b 2) (c (+ a b))) (list a b c))
; => (1 2 3)

初期値を指定しない場合、nilが用いられます。
(let+ (a b c) (list a b c))
; => (NIL NIL NIL)

2. リストの分配束縛

let+にはリストのパターンマッチによる分配機能が付いており、destructuring-bindのように使うことができます。
(let+ (((a b c) '(1 2 3))) (vector a b c))
; => #(1 2 3)

便利な機能として、シンボル&ignを使うとその部分にマッチした値は無視されます。
(let+ (((a &ign c) '(1 2 3))) (vector a c c))
; => #(1 3 3)

この&ignシンボルは他の様々な機能でも汎用的に使用できます。

3. ラムダリストの束縛

ラムダリストとは関数の引数の並びのことで、Common Lispではシンプルな引数の並び以外にも&optionalシンボルを用いたオプショナル引数や&keyシンボルを用いたキーワード引数などを使用することができます。この機能は「ラムダリスト」が使用可能と定められているdefunlambdaなど特定のオペレータでしか使うことができませんが、let+ではそれを一般の束縛に拡張して使うことができます。

まずは&optionalによるオプショナル引数です。
(let+ (((a &optional (b 2)) '(1))) (vector a b))
; => #(1 2)

(let+ (((a &optional (b 2)) '(1 3))) (vector a b))
; => #(1 3)

次に、&keyによるキーワード引数の例です。
(let+ (((a &optional (b 2) &key (c 3)) '(1))) (vector a b c))
; => #(1 2 3)

(let+ (((a &optional (b 2) &key (c 3)) '(1 20))) (vector a b c))
; => #(1 20 3)

(let+ (((a &optional (b 2) &key (c 3)) '(1 20 :c 30))) (vector a b c))
; => #(1 20 30)

キーワード引数はANSI標準と同様に、指定されたかどうかをチェックする機能もあります。
(let+ (((a &optional (b 2) &key (c 3 c?)) '(1 20))) (vector a b c c?))
; => #(1 20 3 NIL)

4. インスタンス(スロット・アクセッサ)の束縛

let+にはクラスのインスタンスを束縛する機能もあります。ANSI標準のwith-slotsマクロやwith-accessorsマクロと同じです。インスタンスの束縛には&slotsシンボルまたは&accessorsシンボルを使用します。

まず、サンプルとなるクラスを定義し、そのクラスを使ってインスタンスを生成します。
(defclass date ()
  ((year :initarg :year
         :accessor year-of)
   (month :initarg :month
          :accessor month-of)
   (date :initarg :date
         :accessor date-of)))
; => #<STANDARD-CLASS DATE>

(defparameter *today* (make-instance 'date :year 2018 :month 2 :date 4))
; => *today*
*date*
; => #<DATE #x302000E3A52D>

スロットの名前で束縛するときは&slotsシンボルを使います。
(let+ (((&slots year month date) *today*))
  (format nil "~a/~a/~a" year month date))
; => "2018/2/4"

スロットの名前は別名のエイリアスで利用することも可能です。
(let+ (((&slots (yy year) (mm month) (dd date)) *today*))
  (format nil "~a/~a/~a" yy mm dd))
; => "2018/2/4"

アクセッサで束縛するときは、&accessorsシンボルを使います。
(let+ (((&accessors (yy year-of) (mm month-of) (dd date-of)) *today*))
  (format nil "~a/~a/~a" yy mm dd))
; => "2018/2/4"

5. 構造体の束縛

クラスのインスタンスに対する束縛と同様に、let-plusは構造体に対する束縛も可能です。構造体の場合は&structureシンボルを使うとともに、直後に構造体のスロットへのアクセッサの命名規則を配置します。構造体はデフォルトで「構造体名+ハイフン」がアクセッサの命名規則であるため、これを配置します。

以下がサンプルです。

;; 構造体の定義
(defstruct date
  year month date)
; => DATE

;; 構造体の実体の生成
(defparameter *today* (make-date :year 2018 :month 2 :date 4))
; => *TODAY*

;; 確認
*today*
; => #S(DATE :YEAR 2018 :MONTH 2 :DATE 4)

;; let+ の &structure による束縛
(let+ (((&structure date- year month date) *today*))
  (format nil "~a/~a/~a" year month date))
; => "2018/2/4"

6. 多値の束縛

let+はANSI標準のmultiple-value-bindの代わりにもなるため、多値の束縛も可能です。

多値の束縛には&valuesシンボルを使います。
(let+ (((&values a b) (floor 5 2)))
  (format nil "~a ... ~a" a b))
; => "2 ... 1"

floorは多値を返す典型的な変数で1つ目の返り値が商、2つ目の返り値が余りです。剰余の扱いについてはANSI Common Lispの標準で様々なオペレータが定めらていますので、気になる方は仕様入門の第12章「」を参照してください。

7. 配列の束縛

配列の束縛は主に2通りの方法があります。

まずは「分配束縛」です。こちらはリストの束縛と同様にマッチングに基づいて値と変数が紐づけられます。
(let+ ((#(a b c d e) #(1 2 3 4 5)))
  (format nil "~{~a~^, ~}" (list a b c d e)))
; => "1, 2, 3, 4, 5"

もちろん、配列一般に対応しているため、ベクトル以外の多次元配列も束縛可能です。
(let+ ((#2a((a b)(c d)) #2a((1 2)(3 4))))
  (list a b c d))
; => (1 2 3 4)

もう一つの方法は配列のインデックスを指定する束縛です。こちらはマッチングではないので、配列の一部を束縛することができます。インデックス指定型の束縛を使うには&array-elementsシンボルを使います。
(let+ (((&array-elements (a 0 1)(b 1 0)) #2a((1 2)(3 4))))
  (vector a b))
; => #(2 3)

もちろん、&ignシンボルを使うこともできるため、小さい配列なら&ignを使って分配束縛を行うのも手です。
(let+ ((#2a((&ign a)(b &ign)) #2a((1 2)(3 4))))
  (vector a b))
; => #(2 3)

8. 関数の束縛

関数の束縛は&flet&labelsを使うことができます。両者の違いは再帰に対応しているか否かという点だけです。再帰を使わない場合は&fletを、再帰を使う場合は&labelsを使用します。

(let+ (((&flet double (x) (* x 2))))
  (double 10))
; => 20

9. 属性リストの束縛

let+&plistシンボルを使うと属性リストを便利に使うことができます。
(let+ (((&plist a b) '(a 1 b 2)))
  (vector a b))
; => #(1 2)

10. ハッシュテーブルの束縛

let+&hash-tableシンボルを使うと属性リストと同様にハッシュテーブルを使うことができます。
(defparameter *ht* (make-hash-table))
; => *HT*

(setf (gethash 'a *ht*) 1)
; => 1
(setf (gethash 'b *ht*) 2)
; => 2

(let+ (((&hash-table a b) *ht*))
  (vector a b))
; => #(1 2)

11. 複素数の束縛

メインで使うことはあまりないのではないかと思いますが、let+&complexシンボルで複素数の実部と虚部の束縛に使うことができます。
(let+ (((&complex a b) #c(2 3)))
  (vector a b))
; => #(2 3)

2018年2月3日土曜日

シーケンス - Alexandria

概要

このページでは、Common Lispの汎用ユーティリティであるAlexandriaの「シーケンス」に関するオペレータを紹介します。

sequence-of-length-p関数: シーケンスの長さの判定

Alexandriaのsequence-of-length-p関数は、シーケンスとその長さを引数として指定し、その情報が正しいかどうかを判定する述語関数です。型がシーケンスではない場合はエラーが通知されます。
(alexandria:sequence-of-length-p '(1 2 3) 3)
; => T

rotate関数: シーケンスの要素の順序を変更

車のタイヤを入れ替えることを「ローテーション」と言いますが、シーケンスの要素の順番を入れ替えるのがAlexandriaのrotate関数です。この関数はかなり高機能で、どのように入れ替えるかを指定することができます。
  • オプショナル引数の第2引数を指定しない場合、最後の要素が先頭に配置されます。
  • 第2引数を指定すると、その個数だけ移動されます。
  • 第2引数を負数で指定しすると、入れ替えの方法が逆順になり、先頭の要素が最後に配置されます。
以下でサンプルを示します。
(alexandria:rotate '(a b c d))
; => (D A B C)

(alexandria:rotate '(a b c d) 2)
; => (C D A B)

(alexandria:rotate '(a b c d) -1)
; => (B C D A)

shuffle関数: シーケンスの要素の順序をランダムに配置

Alexandriaのshuffle関数はその中の通りシーケンスの要素をシャッフルします。:start, :end の各キーワード引数で、シャッフルする範囲を指定することも可能です。
(alexandria:shuffle '(a b c d))
; => (D A B C)

random-elt関数: シーケンスの要素をランダムに取得

random-elt関数も名前を見れば分かる通り、シーケンスの要素をランダムに取得するオペレータです。
(alexandria:random-elt '(a b c d))
; => B

removef, deletefマクロ: 要素を削除するモディファイマクロ

ANSI Common Lispではremove, deleteという関数が定められており、共にシーケンスから特定の要素を削除します。両者の違いは引数を破壊的に変更するかどうかで、引数を再利用する場合はremoveを使い、引数を再利用しない場合はdeleteを使います。

Alexandriaはこれらの関数のモディファイマクロを提供します。モディファイマクロは関数の返り値を変数に束縛し直すものです。

以下にサンプルを示します。
(defparameter *list* '(a b c d))
; => *LIST*

(alexandria:removef *list* 'b)
; => (A C D)

*list*
; => (A C D)

emptyp関数: ベクトルにも対応したendp

ANSI Common Lispではendpという関数が定められており、リストを引数にとってnil(空リスト)であるかどうかを判定します。このendpは引数がlist型以外であればtype-errorコンディションを通知します。

Alexandriaのemptyp関数はベクトルにも拡張してシーケンス全般に対応させたendpです。
(alexandria:emptyp #())
; => T

length=関数・コンパイラマクロ: シーケンスの長さの一致の判定

Alexandriaのlength=というオペレータは関数としても、コンパイラマクロとしても定められています。

まず、関数として使用する場合は、複数のシーケンスを引数に並べます。すると全てのシーケンスの長さが等しいかどうかを判定します。
(alexandria:length= '(a b c) '(1 2 3))
; => T

次に、コンパイラマクロとして使用する場合は、第1引数に一致を確認したい長さをあらかじめ指定します。
(alexandria:length= 3 '(a b c) '(1 2 3))
; => T

あらかじめ検証したい長さを取得できる場合はコンパイラマクロとして使用した方が効率的です(私が検証した訳ではありませんが、ドキュメント文字列に書いてあります)。

copy-sequence関数: 型変換機能付のcopy-seq

ANSI Common Lispの標準仕様ではcopy-seqという関数が定められており、リストまたはベクトルの「シーケンス」一般に対してコピー機能を提供します。

Alexandriaのcopy-sequenceはそれを少しだけ拡張するもので、コピー後の型を指定することができます。コピー後の型が引数と同じであれば単にcopy-seqを呼び出し、異なればcoerce関数を呼び出します。

あらかじめ引数の型とコピー後の型の一致・不一致が分かっていればcopy-seqまたはcoerceを直接呼び出した方が効率的ですが、copy-sequeceには判定のコードも含まれているため、型の一致・不一致が分かっていない場合には便利です。

(alexandria:copy-sequence 'vector '(a b c))
; => #(A B C)

first-elt関数: ベクトルにも対応したfirst

ANSI Common Lispではfirstという関数(アクセッサ)が定められており、リストの最初の要素を取得することができます。

Alexandriaのfirst-elt関数はリストに限らすベクトルの最初の要素も取得することができます。また、(setf first-elt)関数も定められているため、setfマクロで使用することもできます。(このような関数を「アクセッサ」と呼びます。)
(defparameter *vec* #(1 2 3))
; => *VEC*

(alexandria:first-elt *vec*)
; => 1

(setf (alexandria:first-elt *vec*) 1.0d0)
; => 1.0d0

*vec*
; => #(1.0d0 2 3)

last-elt関数: ベクトルにも対応したlast

前節のfirst-eltとほぼ同じですが、最初ではなく最後の要素を参照します。

starts-with-subseq関数: シーケンスの先方一致判定

シーケンスの先頭のいくつかの要素が指定した引数と一致するかどうかを判定します。:return-suffixキーワード引数はデフォルトではnilですが、tにすると先頭の一致した要素を取り除いた残りの部分だけが返されます。

返り値は多値で、1つ目の返り値は一致したかどうか、2つ目の返り値は:return-suffixtで指定した場合の残りのシーケンスです。

以下のサンプルで確認してください。

(alexandria:starts-with-subseq #(1 2) #(1 2 3 4 5))
; => T ;
;    NIL

(alexandria:starts-with-subseq #(1 2) #(1 2 3 4 5) :return-suffix t)
; => T ;
;    #(3 4 5)

(alexandria:starts-with-subseq #(1 3) #(1 2 3 4 5))
; => NIL ;
;    NIL

ends-with-subseq関数: シーケンスの後方一致判定

前節のstarts-with-subseq関数の後方一致版です。

starts-with関数: シーケンスの先頭の要素の一致判定

starts-with-subseq関数は先頭の部分一致を判定しますが、先頭の要素一つだけの判定でいいならstarts-with関数が便利です。
(alexandria:starts-with 1 #(1 2 3))
; => T

一致判定を行う関数に共通ですが、等価性の判定にはデフォルトでeql述語関数が使われているため、文字や文字列などを判定する場合は:testキーワード引数で等価性判定述語を切り替える必要があります。

(alexandria:starts-with #\a "abc" :test #'char=)
; => T

ends-with関数: シーケンスの最後の要素の一致判定

starts-with関数の最後の要素版です。

map-combinations関数: シーケンスの要素の組み合わせに対する繰り返し

高校数学でコンビネーション記号Cを習ったことがある人は、それを思い浮かべてください。Alexandriaのmap-combinations関数は高階関数で、シーケンスの要素の組み合わせを取りながら、その組み合わせに対して関数を適用するというオペレータです。返り値は常に引数のシーケンスなので、副作用を目的に利用されます。

サンプルを見た方が早いと思いますので、以下の例を見てください。

(alexandria:map-combinations #'print '(a b c) :length 2)
; 
; (A B) 
; (A C) 
; (B C) 
; => (A B C)

(alexandria:map-combinations #'print '(a b c d e) :length 2)
; 
; (A B) 
; (A C) 
; (A D) 
; (A E) 
; (B C) 
; (B D) 
; (B E) 
; (C D) 
; (C E) 
; (D E) 
; => (A B C D E)

便利ですが、高校数学で組み合わせの宿題があったとしても、この関数は使わず自分の頭で考えて解きましょう。ただし、Alexandriaの実装を見てアルゴリズムを理解した上で使うなら構わないと思いますが。

map-permutations関数: シーケンスの要素の順列に対する繰り返し

map-combinationsは組み合わせですが、map-permutationsは順列です。どちらも高校数学の範囲です。
(alexandria:map-permutations #'print '(a b c) :length 2)
; 
; (A B) 
; (B A) 
; (A C) 
; (C A) 
; (B C) 
; (C B) 
; => (A B C)

map-derangements関数: シーケンスの要素の完全順列に対する繰り返し

derangementsとは完全順列(撹乱順列)を意味します。完全順列とは、元のシーケンスと並びが被らないような要素の並びです。

これもサンプルを見た方が早いと思うので、以下で示します。

(alexandria:map-derangements #'print '(a b c))
; 
; (C A B) 
; (B C A) 
; => (A B C)

元のシーケンスは'(a b c)という並びなので、1番目が'a、2番目が'b、3番目が'cです。このシーケンスを並び変えた時に、元の順序と一部が一致するような順列は完全順列ではありません。例えば'(a c b)は1番目が'aで元と同じなので、これは完全順列ではありません。

map-derangements関数はこのような完全順列を調べ、その順列に対して第1引数の関数を適用します。

extremum関数: ソートした場合の先頭要素の取得

extremumとは「極値」という意味らしいですが、Alexandriaのextremum関数はそこまで難しい関数ではなく、定義を見る限り「並べ替えた場合の先頭の要素」というくらいの意味のようです。ただし、ソートは行なっていないので、いわゆる「最大値」や「最小値」だけを取得する場合に効率的なのかもしれません。最も、ANSI Common Lispではmaxminが定められているので、数で使うときはベクトルに対して、数以外の場合は比較関数を指定して使う、ということが想定されます。
(alexandria:extremum '(3 2 5 4 1) #'<)
; => 1

(alexandria:extremum '(3 2 5 4 1) #'>)
; => 5

上の例だとリストに対して適用しているので、maxminなどのANSI標準の範囲を使って簡単に書き換えることができます。
(apply #'min '(3 2 5 4 1))
; => 1

(apply #'max '(3 2 5 4 1))
; => 5

ベクトルの場合はloopマクロのminimize, maximize節で対応可能です。
(loop for v across #(3 2 5 4 1) minimize v)
; => 1

(loop for v across #(3 2 5 4 1) maximize v)
; => 5

要素の型が実数(real型)ではない場合は便利かもしれません。なぜならAlexandriaのextremumでは型に応じた比較関数を指定できるからです。
(alexandria:extremum "LISP" #'char<)
; => #\I

2018年2月1日木曜日

Common Lisp Runs Anywhere.

私はCommon Lispを様々な場所で動かしています。

自宅のPCはMacbook Proなので、macOSです。主にClozure CLとCLISPを使っています。SBCLも入っていますが、あまり使っていません。

サーバーサイドではFreeBSDを使っています。Amazon Web ServiceのEC2インスタンスを一つ持っており、そこでHunchentootを使ったWebサーバーを動かしています。Amazon Prime Photoと組み合わせて遠くに住んでいる親族向けに子供の写真を配信しています。HTTPSとBASIC認証を組み合わせた簡易的なプライベートページです。HunchentootはCLISPでは動かないので、FreeBSDでもClozure CLを使っています。

仕事では統計分析を行うことがあり、かなりハイスペックなWindows PCを使っています。メモリは32GBもあり、統計解析専門のソフトも入っていますが、そのソフトでうまく読み込める形にデータを加工したり、オリジナルな推計式でシミュレーションをしたりするときにCommon Lispを使っています。Maximaを入れるとCLISPが付いてきますが、CLISPは数値解析が遅いので、Clozure CLを使っています。SBCLもWindowsで動きますが、一応ホームページにexperimental(実験的)と書かれてあるので、結果が重要となる分析には使いません。

仕事のWindows PCにはVirtual Boxで仮想化したGNU/Linux (CentOS)が入っています。どうしてもFortranを動かす必要があり、Linuxで構築した方が早かったためです。その中ではSBCLとClozure CLが入っています。

PythonやPerlもインストーラを使えば簡単に導入できますが、意外と大変なのがライブラリです。WindowsのPythonでnumpyの性能を十分に発揮しようと思えば、BLASなどのFortranやC++で書かれた別の言語環境を整える必要があります。

Common LispのASDFはよくできていて、WindowsでもmacOSでもLinuxでもFreeBSDでも、ユーザーのホームディレクトリのcommon-lispフォルダにライブラリをそのまま展開しておけば動きます。FFIを使う場合は別ですが、Common Lispだけで書かれたライブラリを導入することは驚くほど簡単です。

また、仕事のPCはネットに繋がっていないため、Quicklispを手軽に使うことができず、ライブラリを自分で個別に導入する必要があります。PerlのcpanやPythonのpipは便利ですが、これら無しでライブラリ環境を構築しようと思うと結構骨が折れると思います。C/C++やFortranならなおさらです。

地味に便利なのは、Common Lispの主要な処理系がきちんとCP932にも対応していることです。WindowsのCLISPはデフォルトでCP932ですし、Clozure CLやSBCLでもexternal-formatで簡単に文字符号化方式を指定することができます。Common Lispで文字列処理、というのは意外かもしれませんが、CL-PPCREさえあればPerlと同じ正規表現を使うこともできますし、個人的には結構便利だと思っています。

Common Lispには成熟した処理系が多く存在しており、実は環境を選ばずに高速かつ手軽なプログラミング環境を構築することができます。私は基本的には趣味でCommon Lispを使っていますが、実用的に利用するなら「どこでも動く」というのは大きなメリットになるのではないかと思います。