Lispエイリアン壁紙をつくりました
TL;DR
Lisp界のマスコット、Lispエイリアンの壁紙をつくりました。
同日 19:45追記; オリジナル画像がダウンロードできませんでした。笑止。
動機
Lispを愛するならば、デスクトップの壁紙もLispにしたいと思うのはわりと自然な発想だと思います。そうしてLispの壁紙をググってみるわけです。
少ない。
ピンとくるのがない。
ところで、Clojureにはなかなかカッコいい壁紙があるようです。公式でロゴがあるからでしょうか。羨しいですね。
Lisp界には、Lispエイリアンというマスコットがおります。あの愛らしい緑色のアイツです。目がいっぱいの。彼(彼女)の壁紙があれば、オールオッケーなのではないか。そう考えた次第であります。
ないぢゃん!!
なんでだよ!!
Why Japanese people!?
結論
というわけで、壁紙を(半年くらい前に)つくりました。ずっと自分で利用するだけだったんですが、つくったものを公開することは(プログラムでなくても)Lisp界への貢献になるのかな、と思った次第で、それで公開することにしました。先駆けになって、もっといいものが出てくると信じて…。
もっといい壁紙がほしいです。
だれかつくってください。
One --- ワンライナーのための入力処理
One
Oneはファイルや標準入力の行単位の入力を簡単に扱えるようにする、Common Lispでのワンライナーを支援するライブラリです。
概要
時は20XX年、ぼくはnums.csvの二列目を合計したくなった……。
$ cat nums.csv name1,1 name2,3 name3,5 $ cut -d ',' -f 2 nums.csv | ccl -Q -b -e '(print (loop for line = (read *standard-input* nil :eof) until (eq :eof line) sum line))' 9
いったいどうしたんだ!? この伸びたラーメンみたいなコードは!?
でもぼくは、深呼吸をして、心を落ち着けて、Common Lispの処理系マネージャ(Roswell, CIM, ...)を導入し、あるライブラリを書くことにした。
その結果——
$ cut -d ',' -f 2 nums.csv | ros -s one -e "(print (reduce #'+ (o:for (line :stdin))))" 9
なんてこったい! あのクソloopにクソstandard-inputが見る影もないぜ!
こうしてぼくはperlやrubyを覚えることもなく、よくわからなくて長いpythonのワンライナーに悩まされることもなく、手に馴染んだCommon Lispで入力を処理できるようになったのでした。
めでたしめでたし。
使い方
Oneはふたつのマクロfor
とforl
によって、ファイルや標準入力を一行ずつ読み込む機能を提供します。Pythonなどで
for line in open('data.dat', 'r'): print line
なんて書けたりしますが、oneはこれを実現します。ふたつのマクロfor
とforl
は、一点を除いてまったく同じ動きをするため(違いはあとで説明します)、以降ではfor
を例にして説明します。
基本的な構文はこうです:
(one:for (var input) body*)
たとえば上で述べたpythonのファイル読み込みは、oneでは次のように書けます:
(one:for (line "data.dat") (print line))
標準入力から読み込みたいときは*standard-input*
ではなくキーワードの:stdin
を指定します:
(one:for (line :stdin) (print line))
読み込んだ値を集約したいときは、body部を空にすると各行のリストが得られます。そのリストに対して所望の集約を施してください。
(one:for (line "nums.dat")) ; => (1 3 5) (apply #'+ (one:for (line "nums.dat"))) ; => 9
for
とforl
の違い
for
とforl
の違いは、各行の読み込みにどの関数を使うかという点です。for
ではcl:read
を、forl
ではcl:read-line
を使っています。for
では読み込んだ行が数値だった場合、文字列ではなく数値に変換されます。forl
では文字列のままです。
この違いは重要です。cl:read
を用いるということは、スペースも改行も一緒くたに扱われるということだからです。つまり、スペースなどの入った入力を処理する場合は、forl
を使うのが安全です。for
を使うのは、明らかに数値しか、しかも目的の値しか入っていない入力を扱うときに限るのがよいでしょう(冒頭のcut
したデータを合計するような)。
$ cat data.txt 1 3 5 2 4 6 $ ros -s one -e '(print (one:for (line "data.txt")))' (1 3 5 2 4 6) $ ros -s one -e '(print (one:forl (line "data.txt")))' ("1 3 5" "2 4 6")
より短かく書くために
Oneのパッケージは、より短かく書くためにo
というニックネームをもっています。なのでone
と入力することすら面倒なときはそのニックネームで楽をしましょう。
Oneのこれから
必要に迫られて、とりあえず入力を楽に扱えるようにしました。でも、たかだか合計をするためだけに
$ ros -s one -e '(print (reduce #'+ (o:for (l "nums.dat"))))'
とするのもバカな話で(合計についてはapply #'+
というスマートな方法がありますが)、reduceに渡せる関数をbody部に指定すると、返り値がrecudeされたものになるという機能があると便利かなあと思いました。こんなイメージです:
$ ros -s one -e '(print (o:for (l "nums.dat") #'+))'
いまはこれくらいしか思いつきませんが、使っていけばいろいろと問題点や改善案もでてくることだと思うので、ちょこちょこ自分でつかっていこうと思っています。
おまけ
oneとは別の話ですが、シェルコマンドをシェルスクリプト並にさくっと叩けるといいんじゃないかなあ、と思いました。
Special thanks to...
このコードうごかねー、への指摘からのコードレビュー・アドバイスは非常に助かりました。
ありがとうございました!
- Asataro.Masaiさん
- κeenさん
職場でお昼休みに書いたせいか、概要のテンションがヤバい。
Inquisitor --- external-formatラッパー & 文字コード/改行コード自動判定ライブラリ
Inquisitor
Inquisitorとは、Common Lispの文字コード/改行コードの自動判定ライブラリなのである。
- 改行コード判定がない
- さまざまな処理系(CCL、ECLなど)に対応してない
- external-formatが処理系依存ってヒドい
- Quicklispに載っていてほしい
というもの足りなさがあったため、勉強がてらつくってみることにした。 実際には、文字コード判定部はzqwell氏のものをそのまま流用したうえで
- 各処理系対応
- external-formatのラップ
- 改行コード判定
を追加した。
Quicklispへはまだ登録申請を行なっていない。CircleCIでbuild failedしたままだとカッコがつかないと考えているためで、ただし原因はよくわかっていない(どうしよう……)。
使用方法
以下、特に記さない限り処理系はClozure CLとする*2。
Case1: 文字コード/改行コードを知りたい
関数inquisitor:detect-encoding
にバイトストリームと言語圏(後述)を指定すると、文字コードを知ることができる。
(with-open-file (in "/path/to/utf8-lf.ja" :direction :input :element-type '(unsigned-byte 8)) (inquisitor:detect-encoding in :jp)) =>:UTF-8
改行コードについてはinquisitor:detect-end-of-line
にバイトストリームを渡せばいい。
(with-open-file (in "/path/to/utf8-lf.ja" :direction :input :element-type '(unsigned-byte 8)) (inquisitor:detect-end-of-line in)) =>:LF
もし処理系が改行コードを扱えない場合、二つ目の返り値として:CANNOT-TREAT
が返ってくる。
これは文字コードについても同じで、その処理系で扱えない文字コードと判断されたら:CANNOT-TREAT
が返ってくる。
処理系が改行コードを扱えるかを知るにはinquisitor.eol:eol-available-p
を呼びだせばいい。
inquisitor:detect-encoding
に指定する言語圏について
inquisitor:detect-encoding
には、どの言語圏内で文字コード判定を行うか指定する必要がある。地域によって使われている書記体系はさまざまであって、それぞれに対応した文字コードもたくさんあり大変なので、書記体系を絞ってから判定しましょうということか。あるいは、符号化方式によっては異なる書記体系間で同じ符号があてがわれることがあるからか。なんにせよ、文字コード判定とは大変なものなのだなあと思った次第である。
Inquisitorでは以下の11の言語圏を指定できる。
Case2: external-formatを(処理系に依存することなく)つくりたい
Common Lispの仕様について各人思うところはあるだろうが、ぼくはひとつ、external-formatの仕様が処理系依存すぎるところが気にくわない。SBCLでは改行コードが扱えなかったりして、クロスプラットフォームを目指したコードでテキスト処理をするときに怖気がするのである(Windows向けSBCLにLinuxで作成したUTF8-LFを食わせるとどうなるのだろう、とか)。
そこでinquisitorでは、処理系に依存せずにexternal-formatをつくれるようにした。
(inquisitor:make-external-format (inquisitor.keyword:utf8-keyword) (inquisitor.keyword:lf-keyword)) =>:UTF-8 ; SBCLの場合 =>#<EXTERNAL-FORMAT :CP932/:DOS #xxxxxxxxxxx> ; CCLの場合
そしてinquisitorでは、ベクタやストリームやpathnameを渡すだけでexternal-formatを返す関数を用意した。
ベクタの場合
(let ((str (encode-string-to-octets "公的な捜索係、調査官がいる。わたしは彼らが任務を遂行しているところを見た。"))) (inquisitor:detect-external-format str :jp)) =>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>
ストリームの場合
(with-open-file (in "/path/to/utf8-lf.ja" :direction :input :element-type '(unsigned-byte 8)) (inquisitor:detect-external-format in :jp) =>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>
pathnameの場合
(inquisitor:detect-external-format #P"/path/to/utf8-lf.ja" :jp) =>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>
これでテキストファイルを開くのがいくぶん楽になるはずである。こんなふうに。
(let* ((p #P"/path/to/textfile.ja") (ef (inquisitor:detect-external-format p :jp))) (with-open-file (in p :external-format ef) (read-line in))) =>"一行目の文字列"
現状と今後
とりあえず、各言語圏のテストデータを用意して、一応テストとしての体裁は整えることができた。テストで失敗している箇所がいくつかあってどう対処すべきかを考える必要がある。
- UTF16のサロゲートペアを含むテストデータがなぜかUCS2(サロゲートペアは含まないはず)と判定される
- 各言語圏の誤判定
- 妥当な結果か否か=テストデータが悪い? or 文字コード判定部のバグ?
- 文字コードの境界テストデータがない
- 誤判定との関係(誤判定データには全ての文字コードに含まれる文字しか使われていない、など)
各言語圏の詳しい人に見てもらうのが一番だと思うけど、どうしたものか。 あとは、冒頭でも書いたとおり、CircleCIに登録してバッジ出したはいいがbuild failedのままなので、quicklispに登録したものか悩ましい、などなど。
どうしよう。
文章ふざけすぎかなあ。
*1:onjo氏のものやzqwell氏による左記の多言語対応版
*2:external-formatで改行コードが扱えるので