octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

Common Lispでyesコマンドを実装した

この記事 Haskell で yes コマンドを実装した に触発されて、yesコマンドをCommon Lispでモダンに書いてみました。

速度があまり速くなく、識者のご指摘をいただきたいところです。

2017/12/01追記 ご指摘いただけました! 後ろの方に追記しました。

コード

package-inferred-systemで定義されていてroswellスクリプトをもつ、こんな感じの構成です。トリビアル

github.com

cl-yes$ tree
.
├── README.md
├── cl-yes.asd
├── main.lisp
└── roswell
    ├── yes
    └── yes.ros

1 directory, 5 files

速度測定

では、速度を測定します。

GNU Coreutilsのyes

リファレンスとして、ふつうのyesの速度を測ります。

$ yes | pv -r > /dev/null
[9.28GiB/s]

あらまー。10秒走らせたら100GBですって。はやいですねー。

yes.ros

Common Lispの処理系はこんな感じでございます。

$ ros config
externals.clasp.version=5.0
setup.time=3718530120
sbcl-bin.version=1.4.1
default.lisp=sbcl-bin

文字列を出力しつづけるyes関数はこんな感じでございます。シンプル。

(defun yes (out &optional (s "y"))
  (loop
     (write-line s out)))

とりあえず実行してみると、

$ ./roswell/yes.ros | pv -r > /dev/null
[1.86MiB/s]

ええ、遅い…。ビルドしてみるとどうなるでしょうか。

$ ./yes | pv -r > /dev/null
[3.04MiB/s]

出だしの速度が改善されただけでした(ビルド前も5秒待つと、これくらいの数値)。なにが原因なのでしょう。試しにprinc関数に書き換えてみます。

(defun yes (out &optional (s "y"))
  (loop
     (princ s out)
     (terpri out)))
$ ./yes | pv -r > /dev/null
[4.62MiB/s]

ちょっとだけ速くなりましたが、なんでprincのほうが速いんだ。formatでも同じくらい。

(defun yes (out &optional (s "y"))
  (loop
     (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null
[3.59MiB/s]

ちなみに最適化宣言をしても、とくに結果は変わりませんでした。

(defun yes (out &optional (s "y"))
  (declare (optimize (speed 3) (debug 0) (safety 0)))
  (loop
     (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null
[3.04MiB/s]

というか、むしろちょっぴり遅くなった…?

そもそもGNU coreutilsのyesが謎の速さであって、そっちがおかしいのだろうか…。

ううむ、識者のご指摘がほしい…。

速い出力への道 (追記)

この記事を公開したところ、佐野さんから「バッファすると速いよ」とご指摘いただきました。そして併わせて、CFFIを通じてCの文字列でバッファする実装のコードが。こちらを手元の環境で実行すると、本物のyes並の速度が得られました…!

$ ./yes | pv -r > /dev/null
[9.33GiB/s]

同じ内容を吐くだけなら一度だけバッファに溜めて、あとはそれをまとめて出力すればいいのです。目的に合わせて柔軟に思考しましょう。GNU Coreutilsの実装もまさにそのようになっています

では、これをCommon Lispだけでやるとどうなるのか。ちょっと気になりますね。なのでやってみました。

(defun make-buffer (s &optional (times 10000))
  (with-output-to-string (out)
    (dotimes (n times)
      (write-line s out))))

(defun yes (out &optional (s "y"))
  (let ((buf (make-buffer s)))
    (loop
       (princ buf out))))
$ ./yes.ros | pv -r > /dev/null
[ 358MiB/s]

速度は、愚直な実装よりはずいぶん速くなりましたが、バッファのサイズに関わらずこの数字で頭打ちになります。GNU Coreutils yesの4%くらい…。Common Lispのストリームを挟むと遅いんでしょうかね。

まとめ (追加)

Common Lispにおいて猛烈な出力速度が求められる場合は、CFFIを使ってCの配列に出力内容を自前でバッファするのがベストプラクティスであるようです。

どうしてぼくはCommon Lispを書くのか

 この記事は技術記事ではありません。自己分析のための独白のようなものです。

この記事を書いた経緯

 雲が空高く流れる季節となったこのころ、二〇一七年十月末日をもって、ぼくは無職になりました。こうなると平日のほぼ全ての時間、独りで過ごすことになり、さまざまなことが頭をよぎります──人生のことや、うず高く積まれた積ん読のこと(読め)、懐具合のこと、身体のこと、年齢のこと……。
 そんな中でもとくに頭を悩ませるのは、やっぱり仕事のことです。

 次の職場でうまくやっていけるかしら──
 ぼくは何を仕事としたいんだろう──
 仕事とは──

 そういう悩みを書き出すことで整理し納得や気付きなんかを得るために、この記事を書いています。

 ここでは、なんでぼくはCommon Lisp固執しつづけ、時に他の言語をディスってしまうほどCommon Lispが大好きなのか、将来もCommon Lispをやりたいのか、を考えていきます。
 まずは、なんでCommon Lispはじめたんだっけという回想からやってみます。

なんでCommon Lispを始めたか

 中学生のころから、プログラミングをすることについて、興味津々でした。
 きっかけは「自分でゲームをつくれるなんて楽しそう!」程度のものだったと思います。あるいは、鈴木光司の『ループ』を読んで、人工生命の分野に興味が芽生えたということもあったと思います。が、当時できたのはせいぜい、技術家庭の教科書にBASICが載っているのを見、家のパソコンに「N88互換BASIC」というソフトを導入して、ちんまりとした描画プログラムをつくって遊ぶことくらいでした。
 まだその当時はLISPのLの字も知りませんでした。

 また、Wikipediaを知り、いろいろな情報が簡単に手に入ることを学びます。コンピュータと人工生命への興味はとても高まっていましたから、それらについて記事から記事へと関連を辿って貪るように読んでいた記憶があります。その中で、確実にLISPについて触れたはずですが、まだLISPを特別に意識はしていませんでした。せいぜい「チューリング機械というものがあるのか、なんかよくわからんけどカッコイイ!」くらいのものでした。

 そうしてC言語は読み書きできないまま、大学に入ります。
 大学では、RoboCupに関するサークルに入り、AIBOを動かすC++ソースコードGNU/Linuxに触れました。ちなみにC++は未だによくわかりません。
 そこでアクチュエータの制御やカメラの画像認識、行動のタスクスケジューリングを行うコードに触れ、また講義で人工知能系の基礎を学ぶ中で、人工知能からは興味が離れていきました。当時のぼくにはどれもレベルが高すぎ、泥臭い数学が手に負えないという、強い実感があったのかもしれません。あるいは、大好きなSFに出てくる「強いAI」には程遠いと思ったからかも。

 そんなぼくがLISPとファーストコンタクトするのは、大学の文化祭でした。
 文化祭で、図書館が破棄する本の無料開放をやっていました。当時から本を所有することが大好きで、もちろんお金がなかったぼくは、タダで貰えるのならと並べられている古い本を物色していました。その中にあったのがこの本です。

 当時、LISPPascalFORTRANなどと同じくいわゆる古代の言語であり、コンピュータの歴史の中で重要な役割を演じてきた言語である、と認識していました。歴史的に重要な言語であるなら、学ぶ価値があるだろう、というふうに考えたわけです。(その考え方でCOBOLに手を出さなくてよかった!)
 この『これがLISPだ!』、LISPの基礎からエキスパートシステムの構築までを解説しています。いま思うとちょうどいい本なのですが、マクロの応用あたりからだんだん理解が難しくなっていったのを覚えており、読破はできませんでした。処理系で動かさず、紙の上だけで読んでいたからかもしれません。
 とはいえ、ここでLISPの基礎概念に慣れ親しみ、Common Lispの世界に突っ込む準備はできていたわけです😏

 そして大学院へ入ります。
 大学と大学院が違いあまり知り合いがいなかったため、講義の合間は図書館で本を物色していました。コンパイラのドラゴンブックとかラムダ計算の本とかパラパラと眺めていた覚えがあります。同じ区域のコンピュータ関連書籍を眺めていると、そこにはこの本が。

 LISPの本自体はそこに何冊かあったのですが、この本の目次を見て驚きました。他の本はエキスパートシステム等現実感のないものを終わりのほうで書くのですが、音楽配信サーバの作成は本当に「実用」でした。
 で、始めのほうを読んでみるわけです。

1億マイルも離れた100万ドルもするハードウェアで走っている動作中のプログラムのデバッグは非常に面白い経験だった。問題の発見と修正には、宇宙船で走っていたread-eval-printループがなくてはならない貴重なものだった。

 当時、LISPを始めるにあたってSchemeCommon Lispか、どちらにしようか迷っていました。が、この文句と、バイナリファイルをパースする章を読んで、Common Lispを始める決意を固めたのを覚えています。

 はじめた決め手は『実践Common Lisp』に違いありません。
 しかしそれを手に取ったのは、『これがLISPだ!』を読んでいてLISPにちっぽけながら知見があったからだと思っています。そして、人工知能・人工生命に興味を持っていたから、LISPについて知ろうと考えました。
 あのころ土日の昼間を外にも行かずWikipediaばかり眺めて過ごした日々よ、ありがとう。

どうしてCommon Lispなのか

 とはいえ、その遍歴の中でさまざまな言語の名を聞き、実際に触れてみたりもしています。JavaとかPythonとかHaskellとかJavaScriptとかRubyとか。でもなんで未だにCommon Lispを使い続けているのでしょう。
 ぼく自身ふしぎなので、ぼくがCommon Lispを愛して止まない理由について、考えてみようと思います。

規格で仕様が定められている

 ご存知のとおり、Common LispANSIで仕様が定められています。このことが意味するのは、どんな処理系のどんなバージョンでも、Common Lispを名乗る限りにおいて一定の動作をすることが保証されていることです。
 つまり、言語仕様が進化を遂げていき、過去に書いたコードが(ライブラリバージョンのせいではなく)言語仕様の意味で不正なコードになって動かなくなるということがない、ということです。

 たとえば、Common Lispにおける実験的な遅延シーケンス(ジェネレータなどが提案された)のライブラリcl-series は2000年ごろに書かれたライブラリのようですが、いまでもばっちり動きます。同じく規格のあるC言語でも古いけど現役のコードがありそうですが、RubyPythonで同じようにいくでしょうか?

 過去の資産がいつまでも有効であることは、Common Lispの大きな魅力です。

構文について覚えることが少ない

 ご存じのとおり、Common Lispは──というかLISPは──S式でコードを記述します。LISPの構文について覚えておくべきことは少しだけで、それは括弧に始まり括弧に終わり、先頭にやりたいことを書く、ただそれだけです。

 Common Lispでは、LISP特有の伝統的マクロによって新たな構文が導入されるとき、新しい構文要素についていちいち気を払う必要はありません。新しい構文要素に目を慣らすことなく、その構文が何をするのかにだけ気を向けることができます。他人のコードを読んでいるときにかつて出会ったことのない構文を見て(たとえば$などの半角記号なので)調べかたに困る、というようなことがありません。

 もちろん、リーダマクロが導入されたときは例外です。

動的に環境にアクセスできるため開発がしやすい

 ご存じのとおり、Common Lispでは実行中のプログラムが参照する関数の定義を、実行時に差し替えることが可能です。たとえば、実行中のグラフィックスプログラムが参照している描画関数の定義を変更し、REPLから流し込んでやることで、実行中のプログラムの描画内容を即座に変更する、ということが可能です。また、GNU Emacs用のプラグイン、SLIMEによってREPLの機能が大幅に増強され、開いているソースコードに書かれた関数の定義をCommon Lispのプロセスに送る、ということもできます。

 そのためプログラムを書くとき、まずは一番小さな部品をREPLで組み立て、次に別の小さな部品をREPLで組み立て、ということを繰り返していき、最後に組み上げるというボトムアッププログラミングの方法でつくり上げていくことができます。トップダウン的にコードを書いていくと、どうしても初めて動かしたときにてんこ盛りのバグへ対処せねばならず、気持ちを保つのにぼくは苦労します。

 単に対話的につくり上げていくスタイルのほうがぼく自身に合っているだけかもしれませんが、それを究極に推し進めたCommon Lispは、そんなぼくにとって最高の道具です。

将来もCommon Lispをやっていきます

 好きな点が思ったよりもたくさん出てきました。
 新しい風を感じたくて、あるいは知らない概念を学びとるために、別な言語を学び始めることは何度かありましたし、これからもあると思いますが、ぼくは上記の理由からいつかCommon Lispに戻ってきてしまうのだろうなあ、というのが整理してみての実感です。もしぼくが別な言語に完全に移行するときがくるとしたら、その言語はきっとCommon Lispを遥かに越えた言語であるに違いなく、それはそれでいいのかもしれません。

 なので/ですが、今ぼくはCommon Lispが一番好きです。

Oji --- バイト列の文字境界を識別する

 Ojiは文字符号化の方法が分かっているバイト列における「一文字」の範囲を識別するライブラリです。

github.com

 どうしよう、これ。

いちおう動機

flexi-streamsにASCII範囲外の文字を渡してstream-read-charすると、正しく一文字を認識してくれない現象を発見しました。が、これは誤りでした

以下がまず、CL標準のreadでテキストストリームから一文字を取ってきた場合です。ひらがなの「こ」が取得されているのがわかります。

;; 文字列の一文字目「こ」が読まれる
CL-USER> (defparameter str "これはペンです")
STR
CL-USER> (with-input-from-string (in str)
           (format t "read from stream: ~s~%" (read-char in)))
read from stream: #\HIRAGANA_LETTER_KO
NIL

次に、flexi-streamsのほうでstream-read-charした場合です。こちらは、flexi-streamsのin-memory stream (バイナリストリーム) をbivalentなストリーム (バイナリ・テキスト両ストリームとして扱える) に変換した場合に発生します。

;; 文字列の一文字目「こ」ではない文字が読まれる
CL-USER> (defparameter octets (flex:string-to-octets str :external-format :utf-8))
OCTETS
CL-USER> (flex:with-input-from-sequence (in octets)
           (format t "read from flexi stream: ~s~%"
                   (trivial-gray-streams:stream-read-char
                    (flex:make-flexi-stream in))))
read from flexi stream: #\LATIN_SMALL_LETTER_A_WITH_TILDE

flexi streamに対してのstream-read-charでは~が読み取られていますが、これは「こんにちは」をUTF-8で符号化したときの最初のバイトの値です。

;; UTF-8の文字としてではなく、ASCIIバイト列として一番目を見てみる
CL-USER> (format t "first byte as a character: ~s~%"
                   (code-char (aref octets 0)))
first byte as a character: #\LATIN_SMALL_LETTER_A_WITH_TILDE
NIL

と、ここまで書いてて気づいたんですが、実際には、flexi streamを作るときにexternal-formatを指定すれば、ちゃんとデコードしてくれます。ほんといま気づいた……😢

;; なんとexternal-format指定漏れ!
CL-USER> (flex:with-input-from-sequence (in octets)
           (format t "read from flexi stream: ~s~%"
                   (trivial-gray-streams:stream-read-char
                    (flex:make-flexi-stream in :external-format :utf-8))))
read from flexi stream: #\HIRAGANA_LETTER_KO

 flexi-streamsは古くからあるライブラリだし、そういう部分は枯れてて当然ですね。

 はずかしい……。消えたい……。

使い方

CL-USER> (setf moji (oji:load-bytes (babel:string-to-octets "これはペンです" :encoding :utf-8) :utf-8))
CL-USER> (oji:encoding moji)
:utf-8
;; read-charは未実装
CL-USER> (oji:read-char moji)
#\HIRAGANA_LETTER_KO  ;; it's not #\LATIN_SMALL_LETTER_A_WITH_TILDE
CL-USER> (oji:boundary moji)
((0 . 2) (3 . 5) (6 . 8) (9 . 11) ...)

感想とか今後とか

 どうしよう、これ。

 当初の目的としてはふたつあって

  1. flexi-streamsに妙な点があるので直したい (そんな点なかった)
  2. バイト列から文字境界認識や区点番号へのデコードをすることで、文字符号化方式への理解を深める

だったんですが、そのうち1が潰えてしまい、えええーーー……😩

 そうすると、このライブラリ、何に使えるのだろう。とりあえずUTF-8の文字境界認識と区点番号デコードができたので、るんるん気分で「これからinquisitorサポートの文字コードについて実装していきます!!」とか書こうと思ってたのに……。

 動揺していますが、まあ遊びで使うことはできそうだし、調べるの楽しかったので、開発がんばろうと思います!

Lisp GNU Lesser General Public Licenseを和訳してみた

LLGPLを和訳してみた

github.com

 T/Oなんですが、それだけでは寂しいので経緯と概要を記しておこうと思います。

LLGPLとは

 Lisp (特にCommon Lispか)の事情に対応するための前文を付け足したLGPLです。LGPLについては概要、経緯ともにWikipediaの当該記事が詳しいです。

 Common Lispはイメージ指向の言語であり、ひとつの大きなイメージの中に標準関数群、ライブラリ(パッケージ)を全て読み込んでコードを実行するような形態を取っています。LGPLにおいて、以下に引用するのように、静的リンク・動的リンクを区別する文言がありますが、

『ライブラリ』のいかなる部分の派生物も含まないが、それとコンパイルされ るかリンクされることにより『ライブラリ』と共に動作するようデザインされ ているプログラムは、「『ライブラリ』を利用する著作物」と呼ばれる。その ような著作物は、単体では『ライブラリ』の派生著作物ではないので、この契 約書の範囲外に置かれる。

しかし、「『ライブラリ』を利用する著作物」に『ライブラリ』をリンクして 実行形式を作成すると、それは「『ライブラリ』を利用する著作物」ではなく、 『ライブラリ』の派生物となる(なぜならそれは『ライブラリ』の一部を含ん でいるから)。そこで、実行形式はこのライセンスで保護される。

--- GNU 劣等一般公衆利用許諾契約書, http://www.opensource.jp/lesser/lgpl.ja.html

Common Lispでは、プログラムの目的のためにLGPLでライセンスされたライブラリを利用すると、実行時にプログラムがランタイム(イメージ)上にロードされます。そのため、その状態でCommon Lispのランタイムは上記LGPLの条文「『ライブラリ』を利用する著作物」に『ライブラリ』をリンクして 実行形式を作成するとに該当してしまい、ランタイムがライブラリの派生物となってしまいます

2017/9/12 16:52訂正

ランタイムはライブラリの派生物にはなりませんご指摘のとおりなのですが、実行可能形式の作成時にリンクすると、ライブラリの派生物になります。

訂正ここまで

 例として、ゲームなどのアプリケーションが挙げられます。
 LGPLでライセンスされたあるゲームエンジンを利用するゲームアプリケーションがあったとすると、そのゲームは直感的には『ライブラリ』を利用する著作物ですが、LGPLの規定によってゲームエンジンの派生物となります。

 これはLGPLが、動的リンクが可能であるC言語で作成されたプログラムを前提として作成されているために発生する問題です(っていうかWikipediaLGPL記事の「プログラミング言語による特異性」にこのこと書いてあった……)。

 そこで、Common Lispのイメージ指向な事情に対応した条項を追加する必要がある、ということで作成されたのがLisp GNU Lesser General Public Licenseです。
 ちなみにLLGPLは、LGPL v2.1を元にして作られたライセンスです。

翻訳するに至った経緯

 このごろ(やっと)オープンソースプロジェクトのライセンスを気にするようになりました。
 RMSFSFの主張する「自由なソフトウェア」という考えかたに共感を覚え、それを明示するライセンスの内容や抱える問題、ソフトウェアとの関係に興味をもったからです。そして、自分が書いたコードはどのライセンスであるべきなのか、など。

 んでも、ライセンスって原文が英語なので、読むのが苦しい。非常に苦しい。

 そこで谷口さんにこのドキュメントを教えてもらいました。

www.ipa.go.jp

 ふう。これでLGPLまではよしとします。

 ところでLispの上述の事情を考慮したLLGPLなるものがあるようで、存在は以前から知っていましたが、英語なので辞書を引き引き苦労してなんとか読むのですが、これを毎回やるのがなかなかつらい。くるしい。
 どうやらLGPLと違いマイナーなせいか、和訳はないようでした。

 ならば自分で和訳すれば、理解も深まるしいいのではないか。

……というのが、翻訳に至った経緯です。

LLGPL.ja

 というわけでひととおり翻訳しました。
 リポジトリの目的は理解を助けるためであり、厳密さが要求されるときにはちゃんと原文読んでね! というスタンスでいこうと思います。

 そういえば、和訳自体のライセンス、どうすればいいんだろう。GNUのドキュメント用ライセンスがあったと思うけど……。

2017/9/11 の夜に追記

 以下のご指摘を頂き、上記のライセンスどうしようの部分は不適切だと思われたため、削除します。翻訳者はぼくですが(リポジトリに明記していない)、原著作者はFranz Inc.であり許諾はないため、ぼくにライセンスを検討する理由はないことが理由です。

 これはぼくの不勉強のせいでした。ご指摘いただいたこと、お礼申し上げます。

2017/9/12 01:01 追記

そもそもFranz Inc.にコピーライトがあるのに、勝手に翻訳を公開していいのか怪しいため、一旦同社にお伺いのメールを送りました。返答次第では翻訳をWeb上から削除します。

2017/10/04 19:04 追記

Franz Inc. にメールにて確認し、http://opensource.franz.com/preamble.html にリンクを張ってあれば翻訳を公開してよい旨の返答を頂きました。

One --- 手短に入出力を扱うフレームワーク

One — 手短に入出力を扱うフレームワーク

 Oneはファイルや標準入力に対する操作を、bashのパイプ処理のような感じで手短に記述するためのライブラリ・フレームワークです。

github.com

イチの逆襲(あらまし)

 時は宇宙世紀2X17年。ぼくはまだ、CSVの処理をするのにシェルコマンドを使っていた。

$ cat nums.csv
name1,1
name2,3
name3,5
$ cat nums.csv | awk -F , '{sum=+$2}END{print sum}'
9
$ 

 シェルコマンドとパイプラインは便利だ。GNU coreutilssedawkがあれば、宇宙海賊も倒せそうな気がする。

 しかし待て。
 Common Lisp使いたるこのぼくが、シェルコマンドに甘んじていてよいものか。そもそもsedawkはそれ自体が独立した言語になっていて、いちいちmanを引かねばならないくらい複雑だ。

 ならば、とりあえず合計する部分だけでも、Common Lispでやってみよう。

$ cut -d ',' -f 2 nums.csv | ros run -e '(print (loop for line
= (read *standard-input* nil :eof) until (eq :eof line) sum line))' -q

9
$

 おいおい、これはなんだ? この伸びたヌードルみたいなコードは!?

 整理しよう。問題は二つある。
 ひとつは、標準入力を表すだけの変数の名前が*standard-input*と、長すぎる。
 もうひとつは、ファイル、ストリームなどに対する処理が抽象化されていないため、長ったらしいループをすべて書いてやらければならないこと。

 つまり、こういうライブラリを作れば、問題は解決するんじゃあないか:

  1. 標準出力に短いタイプ数でアクセスできる
  2. ファイル、ストリーム(と、あとシーケンス)に対する繰り返し処理を抽象化する
  3. 複数の処理を、パイプのように(あるいは関数合成のように)連結できる

──そして、それをぼくは書いた。
 前のシェルコマンドの例はこのようになる。

$ cut -d ',' -f 2 nums.csv | ros run -s one -e '(one:for* - < one:read* +> + 0)' -q
9

 なんてこった! あのヌードルみたいなクソが見る影もねぇや!!

 こうして、入出力を気軽に、手短に扱えるようになったぼくは、これをつかってログファイルの集計、データの加工をperlrubypython(めんどくさいことをやらせるにはちょっと長いような気がする)を覚えることもなく、手に馴染んだCommon Lispでやってみようと、銀河の荒野へと歩き出したのだった。
 Common Lispがいつか宇宙を照らす光になると信じて……。


っていう。

これは何ぞのものか

 以前つくったこのライブラリ

octahedron.hatenablog.jp

をより良くしたものです。以前のバージョンは繰り返し処理や読み込み関数が決め打ちになっていましたが、もうちょっと柔軟なしくみにしました。Common Lispでシェル芸をやりたかったため、以下のものを参考にしたフィーリングになっています。

sed/awk等の独自言語つらいと言ったものの、これもわりと独自言語になってしまいました。なので「フレームワーク」と大仰ですが呼称しました。

 ちなみに、あらましの例を全部Common Lispでやると、パッケージ名などの影響で、どれよりも長くなります。かなしみ。

$ ros run -s one -s split-sequence -e '(one:for* #P"nums.csv" < one:read-line* $ (split-sequence:split-sequence #\, _) $ (nth 1 _) $ read-from-string +> + 0)' -q
9
$

使用方法

インストール

 roswellを使って簡単に導入できます。

$ ros install t-sin/one

 ワンライナーとして実行するなら以下のテンプレートが有用です。

$ ros run -s one -e '(one:for ...)' -q

REPLで叩くならquicklisp(roswellの導入時にインストールされている)を使って

> (ql:quickload :one)

で導入できます。

基本構文

 one:forマクロを使います。また、入出力にはone/ioパッケージの関数を使うとよいです。もし、とりあえず値を出力したかったらone:for*マクロを使うのも手です。

 基本的な形は以下です:

# ここに出てくる`<>`や`[]`、`*`は説明用の記号
> (one:for <input> [<connective> <operation>]*)

<connective>(結合子とよぶことにします)は、シェルでいうところのパイプ、処理(たとえば、末尾に文字!を追加する、など)を繋ぐものです。<operation>の部分に処理がきます。<operation>の結果を左から右に<connective>で繋いでいく、というのが基本的な考え方です。

結合子のふるまい

 結合子には五種類あり、それは$<>+>?です。+>だけ二文字です。それぞれ次のようなふるまいを表します。

$: 処理の合成

 処理を合成します。具体的には$の直前までの処理に、$の直後の処理を合成します。例として、文字列にaを追加する処理、bを追加する処理の合成を示します。

> (one:for "もじれつ" $ (format nil "~aa" _) $ (format nil "~ab" _) $ one:print*)
もじれつab

ここで初出の_は、直前までの処理の結果を表すプレースホルダーです。詳細は後述します。

<: pathname、ストリーム、シーケンス上の走査

 pathnameやストリーム、シーケンスの各要素について、後続の処理を適用します。各要素は<へのパラメータで指定します。例として、典型的なcatコマンドの動作を示します。

> (one:for #P"text.txt" < one:read-line* $ print)
"一行目"
"二行目"
...

 ここではread-line関数を使ってファイルを走査しています。なお、ストリームにはテキストストリームを仮定し、EOF時に:eofを返すことを想定します。one:read-line*は、そのように改造したread-lineです。

>: 処理結果の蓄積、操作

 直前までの処理の結果を内部に蓄積し、リストにして次の処理へ渡します。次の処理に渡すときの変換方法をパラメータで指定します。例として、文字列の各文字をリストに溜めて、印字します。

> (one:for "のび太さん!" < one:read-char* > identity $ one:print*)
(の び 太 さ ん !)

上記では溜めたあと何もしていませんが、たとえばソートすることができます。

> (one:for "のび太さん!" < one:read-char* > (sort _ #'char<) $ one:print*)
(さ の び ん 太 !)
_人人人人人人人人人人人人人_
> さ の び ん 太 ! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

 ただしこの蓄積処理は全入力をメモリに読み込むため、ソートするときぐらいにしてください。商用環境でwebサーバのログをソートするのに使ったらダメだぜ。

+>: 処理結果の畳み込み

 こちらはメモリに全データを残しておく必要がないときに使います。いわゆるreduceHaskellでいうfold的なふるまいをします。例は、文字列のリストを結合するなど。

> (one:for '("のび太さん" "のび太さ" "の" "のびたさぁーん!") < cdr +> (lambda (x y) (format nil "~a~a " x y)) "")
"のび太さん のび太さ の のびたさぁーん! "

reduce処理なので、畳み込み処理の後ろに初期値を指定できます。nilでよい場合は省略できます。

?: 処理結果の選別

 処理結果を述語でフィルタします。

(one:for '("のび太" "スネ夫" "ドラえもん" "野比のび太") < cdr ? (search "のび太" _) $ print)

"のび太" 
"野比のび太" 

簡易ラムダ式

 さて、ここまでで何度か出てきていますが、関数名に#'を付けなくてよかったり、(search "のび太" _)など、いくつか省略記法を用意しています。

 一つは、パラメータとして単にシンボルが現われたとき、#'を自動で付与するものです。  もう一つは、簡単なラムダ式です。one:forマクロの中で(search "のび太" _)などと書くと、(lambda (input) (search "のび太" input))などと展開されます。

これで、少しは短かく記述ができるはず……。

2017/8/30追記

リーダマクロ#/に機能を分離してみた。ただし、oneをロードするだけで有効になってしまう。だれか、forマクロの中でだけリーダマクロを有効にする方法をおしえてください……。


以上がOneの説明です。

Oneが抱えている問題とこれから

Oneのテストをひととおり書き終えたため、勢いで書き散らしていますが、現在以下のような問題が残っています。

記号

 $,?,<,>,+>と五つの記号を使っていますが、この記号でいいのかどうか…。パイプっぽくするならリーダマクロを局所的に有効にするという手もあるし、|始まりに統一することは可能。あるいは、基本の関数合成に寄せて、$始まりにしてもよいかもしれない。
 記号は、変更が容易なのであまり気にしなくてもよいかもしれない点です。

リスト以外のシーケンスでscanのパラメータを無視している

 リスト以外のシーケンスについて、scanのパラメータ(input < next-fnnext-fn)が無視され、cdr再帰のような繰り返し処理しかできません。
 リストについてはloopbyで実装していますが、ユースケースを想像できなかったので保留中の問題です。

入出力関係の補助関数があってもよい?

 scan時の読み込みやとりあえずprintするときなど、いちいち(lambda (stream) (read-line stream nil :eof))などと入力したくないため、one:read-line*などの簡易関数を用意しています。しかしながら、それでも長いですよね。read-lineならrlとすごく縮めるとか、パッケージのニックネームに一文字パッケージ(例: o:for)を復活させるとか、したほうがよさそう。

 split-sequence:split-sequence関数のフルネームの長さには、涙を禁じえない……。

ドキュメント皆無

 今の時点では、この記事のみがドキュメントです。さっきやっとテスト書いたのでしゃーない。なので、READMEを書かなければなりません。

2017/8/30追記

とりあえずREADMEを書きました。

SBCLでテスト落つる

 テスト落ちます。手元でも確認済みですが、なにも表示してくれないので、これから調査します。

Travis CI - Test and Deploy Your Code with Confidence

2017/8/30追記

深町さん作のユニットテストフレームワークroveを使っているのですが、そちらにある問題のようでした。記事を見ていただいたようで、すぐに修正していただけたため、こちらの問題は解決済みです。ありがとうございました。

意見を聞きたい

 是非や好みがあるとは思いますが、そこそこのグッジョブだと思っています。なので、作者以外のご意見を伺いたいところです。

roswellで開発中プロジェクトを簡単にloadする

 開発中のプロジェクトを簡単にloadできるようにするスクリプトを書きましたが、改訂の途中です。

  • 2017/05/25 追記: roswellのサブコマンドになりました。詳細はroswellサブコマンドへの道参照

動機

 ぼくはrosaやinquisitor等々ライブラリを改修するときにslimeからql:quickloadするため、~/roswell/local-projects/にライブラリの.asdファイルにシンボリックリンクを張って開発をしています。どうもASDFシンボリックリンクを辿ってくれるようなので、便利です。

 ところで一方、roswellのリリースがあるたびに~/.roswellを消すことにしています。理由はなんとなくです。そうすると、だいたい一ヶ月に一回くらい~/.roswellを消していることになりますが、~/.roswell/local-projectsの中身も消えてしまうので、その度にシンボリックを張り直しています。

 それってめんどくさい。

 そして、それって自動化できるなと今日(ついに!)気付いたので、それを自動化してみました。

最初のアプローチ

……というようなことをシェルスクリプトで書いてみました。どうやっているかは中を覗いてみてください。

dotfiles/ros-local.sh at 747385a4e6900484eb2f583196dcf78cd846481c · t-sin/dotfiles · GitHub

 Usageはこんなかんじ。

    cat <<EOF
localprj.sh COMMAND [PARAMS...]
Maintain roswell local-prpjects. localprj.sh see in ROSWELL_DIR to search roswell's
local projects. Also it see in CODE_DIR to search local codes.
COMMANDS:
    show           show present configuration.
    local          list asd files in local-projects.
    list           list available asd files in code directory.
    put ASD_NAME   make symbolic link from ASD_FILE into roswell local-projects.
    del ASD_NAME   unlink ASD_FILE from roswell local-projects.
EOF

S式への道

 仕事中になにやっとんねんという感じですがお昼休みなのでだいじょうぶです。もしかしたら車輪を再発明したかもしれないのでツイートしてみたところ、roswell作者の佐野さんからご意見が。

 ql:qmergeとかasdf:load-asdとかあるんですね。初めて知りました。というわけで、roswellスクリプトとして書きなおしてみました。シェルスクリプト版が存在したのは正味一時間程度でした。

 さようなら、ros-local.sh
 ありがとう、ros-local.sh

 そして、

_人人人人人人人人人人人人人人_
> ようこそ! Common Lisp! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

dotfiles/ros-tap.ros at 751b5298fed6f1482731d6c81b70c24335184489 · t-sin/dotfiles · GitHub

 え、しごと? ちょっとストレスが溜まってたし、忙しくなかったからゆるして……。

roswellサブコマンドへの道(追記)

 この記事を公開後、佐野さんからこのようなご意見をいただきました。

 なんと、roswellのサブコマンドは自前で追加が可能だったんだよ!!
 な、なんだってー!

 というわけで、プロジェクトに切り出しました。ライブラリ開発してるとけっこう便利そうなので使ってみてください。プルリクやご意見大募集中です。

github.com

おわりに

  • eval必要かしらん
  • 出力ファイルが大文字なの、カッコわるい気がする
  • roswellにぷるぷるリクエストを出して、rosコマンドに入れてもらうのアリでは?
    • roswell入れてもらわなくても拡張可能でした

package-inferred-systemに期待したもの

これまでのあらすじ

 package-inferred-systemは、pakageのinferred-systemである。

 なんだか体がだるい感じのぼくはこころに学びを沁みわたらせるため、ライブラリのパッケージをすっきりと書くことはできないか考えてみようと思った。Common Lispを書いていてディレクトリ構成とパッケージ構造を同じにしたいとき、以下の2点が面倒だ:

  • パッケージ名をわざわざパッケージ定義に書くこと
  • パッケージ名とディレクトリ名を一致させること
    • パッケージ構造を変えたとき、変更が漏れやすい

 そこでぼくは、1ファイル1パッケージのスタイルでプログラミングできるらしいpackage-inferred-systemを試してみようと考えたのだった──

TL;DR

 package-inferred-systemは、ディレクトリ構造からパッケージ名を決定するためのものではない。

package-inferred-system とは

 package-inferred-systemは、Common LispASDF拡張機能です。ASDFCommon Lispにおいてライブラリ定義・読み込みを司る、ほぼ標準のアドオンです。Pythonでいえば、setup.py(定義)とdistutilsに相当します。それにしてもPythonにおいてモジュールパッケージは別のものを指す言葉だったのかー*1、知らなかったぜ 😎 。「ほぼ標準」というのは、Common LispANSIで言語仕様が定められていますが、ASDFはその中には含まれておらず、しかしだいたいの処理系に始めからバンドルされているので準標準的である、という意味です。

 ASDFの公式ドキュメントに拠れば、package-inferred-systemとは

Starting with release 3.1.2, ASDF supports a one-package-per-file style of programming, whereby each file is its own system, and dependencies are deduced from the defpackage form (or its variant uiop:define-package).

ASDF Manual: The package-inferred-system extension

とのことです。この機能に対して、誤った期待をしていたために、いろいろもやもやさせられた、というのがこの記事の要旨です。

期待していたもの

 ぼくがone-package-per-fileという言葉で期待したのは、Pythonのモジュールシステムのようなものでした。つまり、次に述べるようなものです。

 Pythonでは、ソースコードのファイル自体がモジュールです。なので、以下のようなa.pyb.pyがあるディレクトリでインタプリタを立ち上げて

# a.py
s = 's in module a'
# b.py
s = 'string in module b'
>>> import a
>>> a.s
's in module a'
>>> import b
>>> b.s
'string in module b'

ということができるようになる、と思っていました。つまり、Common Lispで次のようなことができるようになるのだ、と。

;;;; a/hoge.lisp
(defun foo ()
  (format t "No defpackage!!~%"))
CL-USER> (use-package :a/hoge)
CL-USER> (a/hoge:foo)
"No defpackage!!

 いや、exportしてないじゃん、という意見はごもっともですが。exportは手でする必要はあるにしろ、ディレクトリ構造と一致したdefpackageを書かなくなるなら、それは楽だなあと思っていました。

実際のpackage-inferred-system

 じゃあ、実際にはどうだったか。

 こうでした:ファイルパスと一致したdefpackageをしておけば、ASDF側でディレクトリ構造からパッケージ名を想像して読み込んでくれる。

 つまり、冒頭に挙げた問題点

  • パッケージ名をわざわざパッケージ定義に書くこと
  • パッケージ名とディレクトリ名を一致させること

は解決してくれません!! 残念!!!

 ただ、便利な点がまったくないかというとそういうことでもありません。以前ならば.asdフィアルに書いていた依存関係を、package-inferred-systemが内挿してくれるために、書かなくてよくなります。たとえばpackage-inferred-systemでない以下のようなsystemがあったときに、

;; hoge.asd
(defsystem hoge
  :depends-on (:alexandria)
  :components ((:module "src"
                :components
                 (:file "fuga")
                 (:file "core")
                 (:file "hoge"
                  :depends-on ("fuga" "core"))))

以下のように書くことで、:components節がまるまる不要になります。

;; hoge.asd (package-inferred-system)
(defsystem foo
  :depends-on ("alexandria"
               "hoge/hoge"))

 ただし、各パッケージに該当するファイルには、それぞれ以下のようなdefpackageをする必要があります(長くなるのでin-packageは省略しました)。

;; hoge/hoge.lisp
(defpackage :hoge/hoge
  (:use :cl
        :hoge/src/core
        :hoge/src/fuga))
(defpackage :hoge/src/fuga
  (:use :cl))
;; foo/src/core.lisp
(defpackage :hoge/src/core
  (:use :cl))

 つまり、ディレクトリ構造と:useなどからパッケージの依存関係を推測してくれるんだよ…!!

な、なんだってー!?

おわりに

 package-inferred-systemはone-pakcage-one-fileのスタイルを支援する機能です。しかしそれは、パッケージ名を自動で内挿してくれる機能ではありませんディレクトリ構造とインポートの記述からパッケージ依存関係を推測してくれる機能です。

 この機能の恩恵を特に得やすいのは、パッケージ数(=ファイル数)が多く、ディレクトリ構造が複雑なプロジェクトです。そういったプロジェクトは.asdファイルとディレクトリ構造を手動で一致させる手間が増えるため、defpackageさえちゃんとしていれば、依存関係を書かなくていいpackage-inferred-systemはかなり有用でしょう。

 こうして、package-inferred-systemへの溜飲が下がり、これからは使ってみようかなあという気になったのであった。