Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

NumpyのCommon Lisp実装Numclがあるらしい

Pythonの(といいつつCで速度を稼いでいる)Numpyという数値計算用のライブラリがあり、広く使われているのは誰もが知るところだと思う。実際には、WebエンジニアからはPandasといったフレームワークを被せて使うことが多い。

そんな中、NumpyのCommon Lisp実装があることを知った。その名もNumcl。

numcl.github.io

READMEなどを読む限り、Numclは以下のような方針で実装されているようだ:

  • 可能な限りNumpyのAPIを模倣する。ところどころよりLispyな方法を使う。
  • Portabilityを重視して、Numpyのように新たなデータ構造は導入せず、Common Lisp標準のarrayを使う。(Common Lispは、デフォルトで多次元配列をサポートしている)
  • 可能な限りCommon Lispの既存の関数を拡張するような名前を使う。将来的に標準関数を置き換えることもできるようにするため。

ネイティブなCommon Lisp上で実装されているのが面白いけれど、Common Lispは意外と速い(失礼)ので、それでもかまわないようだ。

実際に遊ぶ

installation

いつも通り、Numclはquicklispからインストールできる:

(ql:quickload :numcl)

numclは大きめのヒープを要求するので、処理系をあらかじめ設定しておく必要があるかもしれない。Roswellを使っている場合は以下のコマンドをシェルで実行すればよい:

$ ros config set dynamic-space-size 4gb

これで4GBのヒープが利用可能になる。

使う

NumpyのAPIに準拠しているので、当然といえば当然だがNumpyと同じような動作をする。

(defpackage :numcl-exercise (:use :cl))
(in-package :numcl-exercise)

;; numpy同様にarangeを使うことができる
(defparameter arr (numcl:arange 100))
;; => #(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
;; 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
;; 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
;; 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99)

;; numpy同様にスカラー演算を行うことができる
;; ただし、演算子の名前空間はnumclのもとにある
(defparameter arr2 (numcl:+ arr 1))
;; => #(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
;;   30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
;;   56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
;;   82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100)

;; numpy同様に配列同士の演算を行うことができる
(defparameter arr3 (numcl:* arr arr))
;; => #(0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400 441 484
;;   529 576 625 676 729 784 841 900 961 1024 1089 1156 1225 1296 1369 1444 1521
;;   1600 1681 1764 1849 1936 2025 2116 2209 2304 2401 2500 2601 2704 2809 2916
;;   3025 3136 3249 3364 3481 3600 3721 3844 3969 4096 4225 4356 4489 4624 4761
;;   4900 5041 5184 5329 5476 5625 5776 5929 6084 6241 6400 6561 6724 6889 7056
;;   7225 7396 7569 7744 7921 8100 8281 8464 8649 8836 9025 9216 9409 9604 9801)

;; numpyでおなじみの操作が用意されている
(defparameter arr4 (numcl:reshape arr '(9 9)))
;; => #2A((0 1 2 3 4 5 6 7 8)
;;     (9 10 11 12 13 14 15 16 17)
;;     (18 19 20 21 22 23 24 25 26)
;;     (27 28 29 30 31 32 33 34 35)
;;     (36 37 38 39 40 41 42 43 44)
;;     (45 46 47 48 49 50 51 52 53)
;;     (54 55 56 57 58 59 60 61 62)
;;     (63 64 65 66 67 68 69 70 71)
;;     (72 73 74 75 76 77 78 79 80))

;; numpyにおけるrange accessはarefを使う
;; 行が最初に指定される
(numcl:aref arr4 4) ;; => #(36 37 38 39 40 41 42 43 44)
(numcl:aref arr4 '(4 6)) ;; => #2A((36 37 38 39 40 41 42 43 44) (45 46 47 48 49 50 51 52 53))
;; 全て指定するにはtを使う
(numcl:aref arr4 t 3) ;; #(3 12 21 30 39 48 57 66 75)

;; transposeもある
(numcl:transpose arr4)

;; 行列積(matrix multiply)
(numcl:matmul arr4 (numcl:transpose arr4))

;; 平方根
(numcl:sqrt arr4)

手元に巨大なデータセットがないので、手元でベンチマークをすることができないのだが、Common Lispの場合速度が必要な箇所を最適化したりといった手法が適用できるのでそれなりの速度が出るのだろうと思う。

また、特殊なデータ形式ではなくCommon Lispの標準的なarrayを使っているので、CSVなどの入出力関連の関数は提供されておらず、別途どこかにあるパーサを持ってきて、arrayに変換してくれということらしい。

PandasのCommon Lisp実装、Teddy

ところで、NumpyのCommon Lisp実装があるならPandasのCommon Lisp実装もあるのではないかと思って調べたところ、Teddyというのがヒットする。

github.com

だが、2年前に開発が止まっているし*1、シンボルのexportまわりがおかしいのでちゃんと動かない。がんばれば動くみたいな完成度の状態になっている。

Common Lispの計算系ライブラリを他にも探すと、他にも機械学習系のライブラリであるMGL(こちらはNumclを使わずcl-cudaに依存している)などもあり、細々と更新されているようだ。

github.com

Common Lispには強力なREPLがあるので、このまま頑張ってほしい。

*1:Numclも8ヶ月前に更新が停止しているが…………

Scalaのfor-comprehensionのパチモンをCommon Lispで実装した(多値使って)

何言ってるんだという感じですがこういう感じです。

(defun find-user (username)
  (if (string= username "windymelt")
      (return-left :windymelt)
      (return-right "User Not Found")))

(defun check-passwd (user passwd)
  (if (and (eq user :windymelt)
           (string= passwd "123456"))
      (return-left t)
      (return-right "Password authentication failed")))

(defun get-user-age (user)
  (if (eq user :windymelt)
      (return-left 26)
      (return-right "User Age Not Found")))

(defun ensure-adult (age)
  (if (< age 20)
      (return-right "Should be adult")
      (return-left t)))

(defun purchase-beer (user)
  (declare (ignorable user))
  (print "beer!"))

(defun beer-controller (user-name passwd)
  (for-comprehension (user         <- (find-user user-name))
                     (passwd-guard <- (check-passwd user passwd))
                     (age          <- (get-user-age user))
                     (adult-guard  <- (ensure-adult age))
                     (_            <- (purchase-beer user))))

こういうのが書けます。Maybeしか実装してないのでfor-comprehension実装したというのはちょっと盛りすぎかも。

実際に動作させるとこういう感じで,ちゃんとMaybeな挙動になっています。

CL-USER> (beer-controller "windymelt" "")
NIL
"Password authentication failed"
CL-USER> (beer-controller "windymel" "")
NIL
"User Not Found"
CL-USER> (beer-controller "windymelt" "123456")

"beer!" 
"beer!"
NIL

パチモンなのでちゃんとはしていないのですが,より詳しく仕組みを解説してみます。

for comprehension is 何

Scalaの面白機能だと思ってたけどHaskellとかにもあるはず。

hakobe932.hatenablog.com

モナドとかをがっちゃんこするのに便利なやつですが,詳しくはここでは説明しません。

多値

コード上でMaybeをどうやって表現するかというと,まあ素朴にやったらリストとか構造体を使うと思うんですが,今回は練習を兼ねて多値を使うことにしました。

多値 is 何

Golangのおかげで浸透してきたような気がする概念ですが,ようするに一度に複数の値を返せるという機能です。タプル返すのとは別物です。詳しくは↓見てください。

keens.github.io

Common Lispではvaluesを呼ぶことで多値を返すことができ,multiple-value-bindなどを使って多値を受け取ることができます。 うまくやるとレジスタに値が全部載るらしいので高速でエコになるはずです。リストは作った瞬間にメモリ確保が発生したりします。Common Lispで速度を重視する場合,いかにリスト作成によるメモリ確保(コンシングconsingと呼ばれたりします)を減らすかが勝負です(たぶん)。軽量に動くMaybeモナド,ちょっと欲しかったので作ってしまったというわけです。

多値でMaybeモナド

Maybeモナドを多値で表現しましょう。最初に返される値をleft,2番目の値をrightとしましょう。これは見た目からして自然な定義です。

leftとrightで包んだ値を返せるreturn-leftreturn-rightは,以下のように書けますね。今気付いたけどマクロじゃなくて良かった気がする。

(defmacro return-left (x)
  `(values ,x nil))

(defmacro return-right (x)
  `(values nil ,x))

動かしてみます。

CL-USER> (return-left  12)
12
NIL
CL-USER> (return-right  34)
NIL
34

関数が複数の値を返していますね。これが多値です。

Maybe多値を連鎖させる

モナドがあったらチェインさせたい(ことわざ)

flatmapを実装しましょう。

(defmacro flatmap (f xform)
      `(multiple-value-bind (left right) ,xform
         ;; left projection
         (if left
             ,f
             (return-right right))))

flatmapは第2引数のフォームから多値を受け取り,leftがあるならばfを呼び出します。さもなければ,rightを返してしまいます。

ところでこれ本当にflatmapなのでしょうか。不安になってきましたがパチモンなので可とします。

mapも作りましょう。やってることはほぼ一緒ですが返り値がMaybeにならずに値がそのまま得られる。

(defmacro maybe-map (f xform)
      `(multiple-value-bind (left right) ,xform
         ;; left projection
         (if left
             ,f
             right)))

本当にこれでいいのか不安です。

for-comprehension

さて,scalaのfor-comprehensionはflatmapとfilterとmapとに分解できることが知られています。というかそのシュガーシンタックスです。したがって,今回はfor-comprehensionな式を受け取って,それをがっちょんがっちょんしてflatmapとmapとに組み変えてしまえばいいわけです。filterは面倒だったのでさぼりました。

作戦

紆余曲折あったのですがこういう作戦で実装しています。for-comprehension自体は計算を行わず,マクロによる式変形に留めるスタイルです。

;; 参考
(for-comprehension
  (a <- (f)) ; 1段目
  (b <- (g a)) ; 2段目
  (c <- (h a b))) ; 3段目
  1. まず最も上の段から,flatmapにくるんで呼ぶコードを生成する。これをcodeとする。
  2. 次の段に移り,またflatmapにくるんで呼ぶコードを生成する。このときflatmapの引数としてcodeを渡す。生成されたコードでcodeを上書きする。
  3. 段がある限りcodeを更新しつつflatmapでくるんでいく。

ま要するにflatmapを使う形式に変換しているだけです。ただテクいこともやっていて,a,b,cといった変数のスコープがhandler-bindの外側に及ばない(当然)のでいったん上層にシンボルを用意し,そのplistに保管したりしています。

説明するのが面倒になってきたのでコードを出します。

(ql:quickload '(:iterate :alexandria))
(use-package :iterate)
(use-package :alexandria)

;; 段の値がおさまった変数は,まとめて1つのシンボルのプロパティに格納するようにしている。
;; このため段の変数を参照する箇所を置換する
(defun sanitize-variables-for-for-comprehension (form var)
  (mapcar #'(lambda (x) (etypecase x
                          (list (sanitize-variables-for-for-comprehension x var))
                          (t (if (eq x var)
                                 `(get *for-comprehension-variables* ,(make-keyword (concatenate 'string "%" (string var)))) ;; 名前をそのまま使うと衝突するのでシンボルに%をつける
                                 x))))
          form))

(defmacro for-comprehension (&rest clauses)
  (iter (for c :in clauses)
    (with vars) ;; ここに「どういう変数に代入された?」かが収められる。あとで使う
    (for code
         :first ;; codeの初期値を決める
         (ecase (second c) ;; '<-' or '='
           (<-
            (progn
              (push (first c) vars) ;; 変数名シンボルを退避する
              `(multiple-value-bind (left right) (flatmap ,(third c) t) ;; flatmapするが,1段目なので特になにも渡さなくてよい。ダミーでtを渡す形式にする
                 (setf (get *for-comprehension-variables* ,(make-keyword (concatenate 'string "%" (string (first c))))) (if left left right)) ;; 得られた値をより大域な変数に退避させる
                 (values left right))))) ;; 退避させた後はなにごともなく元の多値を返す
         :then ;; 2段目以降はここが呼ばれる
         (if (eq (first c) 'yield) ;; yieldだったらmap呼ぶ
             `(maybe-map ,(second c) ,code)
             (ecase (second c)
               (<-
                (progn
                  (push (first c) vars)
                  `(multiple-value-bind (left right) (flatmap ,(third c) ,code) ;; 前段で生成されたcodeに対してflatmapするという形式に変換する
                     (setf (get *for-comprehension-variables* ,(make-keyword (concatenate 'string "%" (string (first c))))) (if left left right))
                     (values left right)))))))
    (finally (return
               `(progn
                  (let ((*for-comprehension-variables* nil)) ;; ここにflatmapした結果を詰め込んでおいて,変数名を使って参照できるようにしている
                    ,(progn
                       (dolist (v vars) (setf code (sanitize-variables-for-for-comprehension code v))) ;; 変数名で参照している箇所は↑の変数からの読み込みに置換する
                       code)))))))

これでMaybeモナドもどきが実装できましたね。flatmapの実装をいじったらOptionとかに対応したり,right projectionにできると思います。

なんでright projectionじゃなくてleft projectionなの

デフォでは,普通の関数が多値を受け取ると1つ目の値を受け取るようになっているので,それとの整合性を優先しました。

CL-USER> (identity  (values 1 2 3 4 5))
1

たぶん欲しい値は1番目にあるのが普通だろう,というわけです。

なんでリスト使わなかったの

リストでいいと思います。

まとめ

なんとなく便利そうなのができた。

めでたし

Common Lispで変数名に使える文字が自由すぎる

おおざっぱなタイトルすぎる。

Common LispやSchemeといったLisp族はきわめて構文の構造が簡素である,といったことはわざわざ説明の必要もないでしょう。リストとして式のASTを表現するわけです。

(defun foo (bar) (+ foo bar)) ; あらゆる構文がリスト

構造が簡素ということは,それを表現するのに必要な,言い換えると予約しなければならない文字があまり無い,ということでもあるわけです。

事実,Common Lispでシンボルとして使えない文字を探してみると,他言語よりは少ないのです。ちょっと探してみましょう。

使えなそうな文字

()

言わずもがな,Lispはカッコでリストを表現するのでカッコは使えない。

'

リストをコードではなくデータとして読ませたいときにはクオートquoteという表記でそれを指示する。この記号にはアポストロフィが割り当てられており,シンボル名には使えない。バックティックもだいたい同じ理由で使えない。

空白

そりゃそうだ。

#

ハッシュサインはCommon Lispの読取器に指示を出すために,しばしばクオートと隣接して'#のように使われる。'#()と表現するとベクタである。'#2A((1 2 3) (4 5 6))と書くと2次元配列になる。

ちなみに関数シンボルは#'fooといったように書く。まぎらわしい。

:

コロンはパッケージ名とシンボルとの区切りとして使われる。例えばCL-USER:FOOCL-USERパッケージのFOOというシンボルを指し示す。Perlのパッケージは::というふうに2文字重ねる必要があるのだが,2文字も要らんやろという感情がある。

ちなみにCommon Lispではコロンを重ねて::と書くと,export操作をしていないプライベートなシンボルにアクセスできる。カプセル化を破る,注意が必要な操作だということが見た目で分かるようになっている。

"

他の言語がそうであるように,ダブルクオートは文字列リテラルのために予約されている。

.

ドットはドット対リテラルのために予約されている。

……とおもいきや

* (defparameter foo.bar 123)
FOO.BAR
* (+ foo.bar foo.bar)
246

クオートの外では使えるようだ。

\

バックスラッシュは文字を表現するときに使うことがある。

* (defparameter foo\bar 666)
|FOObAR|

なんか不穏な状態になった。怖いからやめておこう。

いろんな文字で変数定義してみる

さて,前述した以外の文字は(たぶん)だいたいなんでも変数名として,より正確にはシンボル名として使うことができます。実際にやってみましょう。

アスタリスクはSBCL(処理系)のプロンプトです。

@

まずはPerlライクにリストを@で表現してみる。

* (defparameter @array '(1 2 3))
@ARRAY
* (mapcar #'* @array @array)
(1 4 9)

普通に使える。とはいえリストはあまりによく使うのでわざわざ@で修飾するまでもないかもしれない。

ちなみに準クオートquasiquoteでリストを展開するときに@を使うのでまぎらわしいかもしれない。

* (defparameter foo '(1 2 3))
FOO
* `(a b ,@foo)
(A B 1 2 3)

%

次にハッシュを%で表現してみる。

* (defparameter %hash (make-hash-table :test #'equal))
%HASH
* (setf (gethash "foo" %hash) 123)
123
* (gethash "foo" %hash)
123
T

普通に使えたけど,Common Lispでハッシュから値を取り出すためにはgethash関数という専用の関数を使うわけです。このため渡されている変数がハッシュなのは自明といえば自明。 ところでgethashの引数の順序,皆さん納得できてますか?キーが最初でハッシュが最後ですよ???おかしくないですか?????OOPで培われた常識が崩れていきます。

(gethash
  key ; まずキーを指定して...
  hash ; 次にハッシュテーブルを渡す
)

ちなみに関数内でループ用の関数を定義するときにプレフィックスとして%がよく使われている気がします。

不等号

クラスを定義してみましょう。Common Lispではクラス名を不等号で修飾する習慣があった気がします。

* (defclass <foo> () ((x :initarg :x :accessor x) (y :initarg :y :accessor y)))
#<STANDARD-CLASS COMMON-LISP-USER::<FOO>>
* (defparameter fooinstance (make-instance '<foo> :x 12 :y 34))
FOOINSTANCE
* (x fooinstance)
12

普通に使えますね。 defclassをソラで書けるようになったら大人です。

スラッシュ

スラッシュを使ってみます。Perlをはじめとする言語では正規表現リテラルに使われていますね。正規表現を表現する変数名として使ってみます。

* (ql:quickload :cl-ppcre)
To load "cl-ppcre":
  Load 1 ASDF system:
    cl-ppcre
; Loading "cl-ppcre"
..
(:CL-PPCRE)
* (defparameter /regex/ (ppcre:parse-string "regex"))
/REGEX/
* (ppcre:scan /regex/ "abcregexdef")
3
8
#()
#()

あんまりうまみを感じない。

[]

配列用の構文がないので角カッコも自由に使えるけれどあまり使い道が思い付かない。1次元配列であるベクタで使ってみましょうか。

* (defparameter [vec] #(1 2 3))
[VEC]
* (defun dotproduct (v1 v2) (reduce #'+ (map 'vector #'* v1 v2)))
DOTPRODUCT
* (defparameter [vec].[vec] (dotproduct [vec] [vec]))
[VEC].[VEC]
* [vec].[vec]
14

内積が定義できた。

ブレース{}も使えそうだけど用途がまったく思い付かない。

はてなでわっしょい

Common Lispでは述語(tかnilかを返す関数)には末尾にp(predicateの略)をつける慣例があるが,Schemeでは?を末尾に付ける。個人的にはSchemeのスタイルが好きなのだが。

* (defun string? (x) (typecase x ((string) t)))
STRING?
* (string? 123)
NIL
* (string? "foo")
T

->

Schemeでは何かから何かへと変換するような関数は->でつなぐ。Common Lispにそういう慣例はなさそうだ。

* (defun string->integer (s) (parse-integer s))
STRING->INTEGER
* (string->integer "666")
666
3

命名に関してはSchemeのほうがより洗練されているように感じる。

感想

命名規則的な感じでシンボルの中身に対応する記号を修飾するようにすると,もともとLispは無味乾燥として見えがちなので,コードが読みやすくなるかもしれない。

とはいえハンガリアン記法みたいになっても嫌な気がする。

ちなみにGoogleのCommon Lispスタイルガイドというのがあって,一般的な慣例は網羅されているのでこれに従うとよいだろう。

みなさまもLispで見掛けた面白い命名があったら教えてください。

オチ

実はパイプ(バーチカルバー)でくくると,その中ではだいたい何でも使えるようになります。

* (defparameter |foo bar buzz '() ::::| 1)
|foo bar buzz '() ::::|

めでたし

Common LispでGPSを実装した + packageとpackage-inferred-systemまわりハマりどころ

初コーディングはCommon Lispでした。

さて、GPSというものがあります。地球を測位するやつではなく、General Problem Solverです。Generalという夢のような名前が付いていますが、このGPSは任意の形式化された記号問題を解くことができるらしいので遊んでみました。

ちなみにGPSのCommon Lisp実装はPAIPという本に載っています。日本では『実用 Common Lisp』という題で販売されています。

実用Common Lisp

実用Common Lisp

幸いにもPAIP本の中身はMITライセンスで公開されている(太っ腹!!)ので、これを見てコードを移植した。

github.com

なんでこんなことしているかというと、ちょうど先日Factorioの生産関連のデータを作成したので、そのデータを食わせて工場のプランニングとかができないかと試してみたいのだ。 そもそも工場のプランニングは記号問題なのか?という感じもするが、まあ試しに遊んでみたい。

github.com

ハマりどころ

でもって今回もちょっとしたハマりに遭遇したので、メモしておく。だいたいpackageまわりの挙動によるものである。

package-inferred-systemにおけるパッケージ同士の依存関係 - あるはずのシンボルが無い

今回はモジュールシステムにpackage-inferred-systemを採用した。これについては以下記事で説明しているので参照してほしい。

blog.3qe.us

さて、package-inferred-systemでは、パッケージ同士の依存関係を自動認識してモジュールを解決してくれる。 例えばfoobarというpackage-inferred-systemでfoobar/aというパッケージがfoobar/bというパッケージをuseしていた場合、これが依存関係と捉えられ、foobar/bfoobar/aに先立ってコンパイルされるようになる。

;; a.lisp
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl :foobar/b))
(in-package :foobar/a)

(func) ; => "func"
;; b.lisp
(in-pacakge :cl-user)
(defpackage :foobar/b (:use :cl) (:export :func))
(in-package :foobar/b)

(defun func () "func")

さて、外部のパッケージのシンボルを呼ぶ場合にはuseする以外にも、 以下の例のようにfoobar/b:funcとして呼ぶこともできる。こうしてパッケージ名を明に指定することをパッケージ名を修飾する、などという。

;; a.lisp - package-inferred-systemでは動作しない
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl))
(in-package :foobar/a)

;; パッケージ名を修飾して呼び出している
(foobar/b:func) ; => "func"

package-inferred-systemはパッケージのuse関係を依存関係解決に使っている

しかしながら、package-inferred-systemを使う場合は勝手が異なってくる。 useを行わずにパッケージ名を修飾することでシンボルを参照しようとした場合、そのシンボルは呼べない、というか呼び出しに失敗するのである。 なぜなら、シンボルを呼び出そうとしても、シンボル(と、シンボルがあるはずのパッケージ)が存在しないからである。

どういうこと?もうちょっと深く考えてみよう。

まず、どこかのパッケージにあるシンボルを呼び出すためには、シンボルとそれを入れるパッケージが先に存在していなければならない。これは当然の感覚だ。

シンボルが存在するとはどういうことかというと、先に定義されている、ということだ。 特にCommon LispをASDFのようなビルドツールでコンパイルしつつ使うような場合は、ASDFによって該当のソースファイルがコンパイルされることで、事前にシンボルが定義されていなければならない、ということである。

ちなみに、Common Lisp上でのプログラムの論理的な構成単位はパッケージだが、ビルドツールであるASDF上でのプログラムの構成単位は、ソースファイルである(簡略化している)。 ソースファイルとパッケージとの関係は独立しており、必ずしも1対1で対応しているわけではない。

そこに規約を加えるのがpackage-inferred-systemである。ASDFがpackage-inferred-systemが有効なシステムをコンパイルするとき、パッケージ名がそのままソースファイル名に対応しているとみなし、 ソースファイルのコンパイル順序を、パッケージのuse関係をもとに決定するようになる。

このため、直接useしていないパッケージのあるファイルは決してコンパイルされないという挙動が発生する。するとシンボルが定義されないので、呼ぼうとすると失敗してしまう、という現象が現れるのだ。

useを使わない場合には、空のimport-fromをつけることでASDFに依存関係を認識させる、というテクニックが使える。これが無いと依存関係が切断されてしまい、正しく動作できない。

;; a.lisp
(in-pacakge :cl-user)
(defpackage :foobar/a (:use :cl) (:import-from :foobar/b))
(in-package :foobar/a)

;; パッケージ名を修飾して呼び出している
(foobar/b:func) ; => "func"

パッケージを移動するのを忘れる

パッケージを作成するにはdefpackageを使うが、非常によく忘れがちなのが、(in-package :cl-user)(in-package :定義したパッケージ)である。

(in-package :cl-user) ;; ここを忘れがち!!
(defpackage :foobar (:use :cl))
(in-package :foobar) ;; ここを忘れがち!!

前者は、カレントパッケージを:cl-userに動かすもので、後者はカレントパッケージを作成したパッケージに移動させるものだ。

どうしてこのようなことが必要なのだろう?

その答えは、Common Lispはもともとインタラクティブな言語である、ということを踏まえると分かりやすい。その昔Common Lispは、現在のスクリプティング言語のように、直接プログラムを打ち込んで対話的に動作させていた。 LISPは古い時代からあるので、大きな1つのファイルに複数のパッケージを記述する、というスタイルが多かったかもしれない。そんな中Common Lispでもソースコードを分割するのが一般的になり、これを補助する目的でビルドツールが出現した。

Common Lispと同じように同一ファイル内に複数のパッケージを用意できるPerlとの間にある差異は、パッケージ宣言を行っても自動的にカレントパッケージが移動するわけではない、という点である。 Common Lispはインタラクティブな言語なので、REPLを前提として設計されている。REPLを通じて自由にカレントパッケージを移動できるようにするため、パッケージ宣言とパッケージ移動とを区別しているのかもしれない。

package prefixを忘れる

package-inferred-systemでは、システム名をパッケージの頭に付けなければならない。これを忘れると当然呼び出せないので、忘れないようにする。

Common LispのPackage-inferred-systemのサンプルリポジトリを公開した

github.com

作りました。

package-inferred-systemって何

簡単に言うと・・・

  • Common Lispではパッケージ名(言語上での名前空間)とモジュール(コンパイルする単位)とが分離しています
    • 言い換えると,同じファイルにパッケージを複数入れる,といったことができます
    • このため,パッケージ名からモジュールのパスを推測できません
  • このためビルドする際は手動でファイル同士の依存関係を記述するか,全て1つのでかいファイルに収める必要がありました
  • 規約としてパッケージ名とモジュール名とを1対1で対応付けたら依存関係書く必要ないじゃん,という発想で生まれたのがpackage-inferred-systemです

依存しているパッケージ名からビルドするべきファイル名が自明に定まるので,PerlのようにカジュアルにCommon Lispが書ける,というわけです。

package-inferred-systemむずかしい

あまり解説がないので使いこなすのが難しい状態でした。かれこれ2年ほど苦心していました。

なんでむずかしいのか

完全なサンプルがないからです。こう書いたら良いよ,というコードスニペットはあるにはあるのですが,入門者が使うにはかなり敷居が高いものになっていました。自己完結していて動作するサンプルプロジェクトを用意する必要がありました。

そこで今回github projectを作成し,動作するpackage-inferred-systemサンプルを公開しました。初学者の参考になれば幸いです。また,Common Lispプロジェクトの近代化*1の礎になればと思います。

感想

  • 後で困らないように,また応用しやすいように,コメントをちゃんと付けています。
  • デフォルトでは.asdファイルと同階層になってしまうソースファイルのパスをsrc/といった場所に移動させる:pathname機能というのがあるのですが,これがうまく動作したのが一番の喜びでした。

参考文献

t-cool.hateblo.jp

privet-kitty.github.io

*1:今風に扱えるようにする,とも言う

Common Lispでマサワダを生成します

wwsmawaa

こんばんは。id:Windymeltです。「masawada Advent Calendar 2019」22日目です。昨日はid:kazuhi_raさんの『受肉するmasawadaさん - かずひらの日記』でした。ついに3Dモデルの領域を越えて現実にその姿を現わしたid:masawadaさん。彼はこの混沌とした世界を救う救世主となるのでしょうか。それとも、世界に破滅をもたらす悪党なのでしょうか。受肉した身体は二分されているようですが、今後の活躍に期待したいですね。

aaawssmw

冬になりCommon Lispが美味しい季節になってきましたね。今回は新鮮なCommon Lispでmasawadaを作っていこうと思います。

今回はstring-randomでmasawadaをやっていくことにします。PerlにおけるString::Random、 そしてこれを継承したString-random.jsといったライブラリをご存知でしょうか。 ふだん我々が正規表現を利用するとき、文字列がある正規表現にマッチするかどうかを判断したいわけですが、このstring-randomという名前を冠したライブラリは、 正規表現を与えるとそれにマッチするような文字列を返してくれるというハイ・テックなツールなのです。 今回これでmasawadaという文字列を生成していきたいと思います!

sadmamaw

えーではライブラリを探してみましょう。ありませんね。というわけでライブラリを作っていきましょう。

github.com

既につくりおきしておいたライブラリがこちらになります。どのように実装しているのでしょうか?

なんとCommon Lispの正規表現ライブラリであるcl-ppcreは正規表現をパースしてS式のASTを出力することができるのですが、これにパターンマッチを施して実際にマッチするような文字列を動的に挿入していきます。 ある正規表現にマッチする文字列をいきなり生成するのは大変ですが、正規表現の各構成要素は単純なので簡単に出力できます。 例えば*は直前の文字を0回以上繰り返せばよいので、ランダムな数を決めてその分だけ文字を繰り返して結合する、といった具合です。

mmamsmms

こうして完成したmasawadaさんがこちらになります。

(ql-dist:install-dist "http://dist.ultralisp.org/"
                      :prompt nil)
(ql:quickload :cl-string-random)
(use-package :cl-string-random)

(string-random "[masawada]{8}")
asmaaadw

ちょっと迫力に欠けていて物足りないですね。趣向を凝らしてみましょう。

 (string-random "([mswd]a){4}")
"damasama"

ダマ様になりましたね。尊称でもあるdameにも似た響きを持つ、高貴な律動です。ノートルダムのあのダムです。よりシンプルな正規表現ではどうでしょうか。

 (string-random "([a-z]a){4}")
"eaxaqara"

もはや発声不能。動機は仮。存在不明。生存可能。われわれの皮膚に白く光るのは浪飛沫の足跡か、それとも涙の残滓なのか。 平成が壊しながら押し固めてきたものを洗い流しきれないまま令和という新時代を迎えてしまった日本。我々はどこへと向かうのでしょう?

くりかえす時代の中でたゆたう儚い存在である我々。masawadaが生まれるまでループさせてみましょう。

(loop for s =(string-random "([mswd]a){4}")
  do (format t "~C~A" #\return s)
  do (force-output)
  do (sleep 0.01)
  until (string= s "masawada"))

Image from Gyazo

来年も良い年になるといいですね。明日はid:polamjagさんです。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?