Lisp GNU Lesser General Public Licenseを和訳してみた
LLGPLを和訳してみた
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言語で作成されたプログラムを前提として作成されているために発生する問題です(っていうかWikipediaのLGPL記事の「プログラミング言語による特異性」にこのこと書いてあった……)。
そこで、Common Lispのイメージ指向な事情に対応した条項を追加する必要がある、ということで作成されたのがLisp GNU Lesser General Public Licenseです。
ちなみにLLGPLは、LGPL v2.1を元にして作られたライセンスです。
翻訳するに至った経緯
このごろ(やっと)オープンソースプロジェクトのライセンスを気にするようになりました。
RMSやFSFの主張する「自由なソフトウェア」という考えかたに共感を覚え、それを明示するライセンスの内容や抱える問題、ソフトウェアとの関係に興味をもったからです。そして、自分が書いたコードはどのライセンスであるべきなのか、など。
んでも、ライセンスって原文が英語なので、読むのが苦しい。非常に苦しい。
IPAが良いドキュメントを公開してましたよ。https://t.co/O4B6yxrA37
— TANIGUCHI (@ta2gch) 2017年9月3日
そこで谷口さんにこのドキュメントを教えてもらいました。
ふう。これでLGPLまではよしとします。
ところでLispの上述の事情を考慮したLLGPLなるものがあるようで、存在は以前から知っていましたが、英語なので辞書を引き引き苦労してなんとか読むのですが、これを毎回やるのがなかなかつらい。くるしい。
どうやらLGPLと違いマイナーなせいか、和訳はないようでした。
ならば自分で和訳すれば、理解も深まるしいいのではないか。
……というのが、翻訳に至った経緯です。
LLGPL.ja
というわけでひととおり翻訳しました。
リポジトリの目的は理解を助けるためであり、厳密さが要求されるときにはちゃんと原文読んでね! というスタンスでいこうと思います。
そういえば、和訳自体のライセンス、どうすればいいんだろう。GNUのドキュメント用ライセンスがあったと思うけど……。
2017/9/11 の夜に追記
以下のご指摘を頂き、上記のライセンスどうしようの部分は不適切だと思われたため、削除します。翻訳者はぼくですが(リポジトリに明記していない)、原著作者はFranz Inc.であり許諾はないため、ぼくにライセンスを検討する理由はないことが理由です。
著作物のライセンスは、(原)著作者や権利者だけが決めることができます。派生物とみなされる翻訳について、preambleのCopyright/翻訳者が誰であるか/何が対象なのかが明示されず、許諾があったように見えないのに、ライセンスの適用を検討されている点には違和感を覚えました。 https://t.co/Cg58OGhTXV
— heno (@_heno) 2017年9月11日
これはぼくの不勉強のせいでした。ご指摘いただいたこと、お礼申し上げます。
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のパイプ処理のような感じで手短に記述するためのライブラリ・フレームワークです。
イチの逆襲(あらまし)
時は宇宙世紀2X17年。ぼくはまだ、CSVの処理をするのにシェルコマンドを使っていた。
$ cat nums.csv name1,1 name2,3 name3,5 $ cat nums.csv | awk -F , '{sum=+$2}END{print sum}' 9 $
シェルコマンドとパイプラインは便利だ。GNU coreutilsとsedとawkがあれば、宇宙海賊も倒せそうな気がする。
しかし待て。
Common Lisp使いたるこのぼくが、シェルコマンドに甘んじていてよいものか。そもそもsedやawkはそれ自体が独立した言語になっていて、いちいち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*
と、長すぎる。
もうひとつは、ファイル、ストリームなどに対する処理が抽象化されていないため、長ったらしいループをすべて書いてやらければならないこと。
つまり、こういうライブラリを作れば、問題は解決するんじゃあないか:
- 標準出力に短いタイプ数でアクセスできる
- ファイル、ストリーム(と、あとシーケンス)に対する繰り返し処理を抽象化する
- 複数の処理を、パイプのように(あるいは関数合成のように)連結できる
──そして、それをぼくは書いた。
前のシェルコマンドの例はこのようになる。
$ cut -d ',' -f 2 nums.csv | ros run -s one -e '(one:for* - < one:read* +> + 0)' -q 9
なんてこった! あのヌードルみたいなクソが見る影もねぇや!!
こうして、入出力を気軽に、手短に扱えるようになったぼくは、これをつかってログファイルの集計、データの加工をperlやrubyやpython(めんどくさいことをやらせるにはちょっと長いような気がする)を覚えることもなく、手に馴染んだCommon Lispでやってみようと、銀河の荒野へと歩き出したのだった。
Common Lispがいつか宇宙を照らす光になると信じて……。
っていう。
これは何ぞのものか
以前つくったこのライブラリ
をより良くしたものです。以前のバージョンは繰り返し処理や読み込み関数が決め打ちになっていましたが、もうちょっと柔軟なしくみにしました。Common Lispでシェル芸をやりたかったため、以下のものを参考にしたフィーリングになっています。
- シェルのパイプライン
- 数学の関数合成 (Haskellの
$
) - Common Lispのseries (関連記事)
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サーバのログをソートするのに使ったらダメだぜ。
+>
: 処理結果の畳み込み
こちらはメモリに全データを残しておく必要がないときに使います。いわゆるreduce
、Haskellでいう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-fn
のnext-fn
)が無視され、cdr
再帰のような繰り返し処理しかできません。
リストについてはloop
のby
で実装していますが、ユースケースを想像できなかったので保留中の問題です。
入出力関係の補助関数があってもよい?
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作者の佐野さんからご意見が。
@sin_clav 便利そう。symlinkがそこそこ使いにくいosもあるので、ros scriptで書きませんか?asdf:load-asdを呼ぶlispのソースを(ql:qmerge "local-init/")以下に生成するのでほぼ等価だと思うのでどうでしょうか?
— Masatoshi SANO (@snmsts) 2017年5月24日
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サブコマンドへの道(追記)
この記事を公開後、佐野さんからこのようなご意見をいただきました。
@sin_clav 本体に取り込まなくても、けっこう場当たり的に拡張できるようにしたので、密な結合が必要そうな物以外は本体に取り込むのをためらいます。 https://t.co/9JrdFFLjUZ
— Masatoshi SANO (@snmsts) 2017年5月24日
なんと、roswellのサブコマンドは自前で追加が可能だったんだよ!!
な、なんだってー!
というわけで、プロジェクトに切り出しました。ライブラリ開発してるとけっこう便利そうなので使ってみてください。プルリクやご意見大募集中です。
おわりに
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 LispのASDFの拡張機能です。ASDFはCommon Lispにおいてライブラリ定義・読み込みを司る、ほぼ標準のアドオンです。Pythonでいえば、setup.py
(定義)とdistutilsに相当します。それにしてもPythonにおいて。「ほぼ標準」というのは、Common LispはANSIで言語仕様が定められていますが、ASDFはその中には含まれておらず、しかしだいたいの処理系に始めからバンドルされているので準標準的である、という意味です。モジュール
とパッケージ
は別のものを指す言葉だったのかー*1、知らなかったぜ 😎
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.py
とb.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への溜飲が下がり、これからは使ってみようかなあという気になったのであった。
Lispエイリアン壁紙をつくりました2
Lisp界のマスコット、Lispエイリアンの壁紙をまたつくりました。
http://f.hatena.ne.jp/t-sin/20170513011545 オリジナルサイズ
背景
以前Lispエイリアンの壁紙をつくりました。
でも、黒いのがほしかったので、またつくりました。
もっとクールなやつを、だれかつくってください!! だれか!!!
祝! Rosaバージョン1.0!
最近つくったrosaというライブラリ・ツールのバージョンを1.0にしました。
テキストデータに、書きやすく見た目を邪魔しない形式で、メタデータを埋め込むための言語です。実際にどんなものかというのは、以下の記事に書きました。
名前は「ろさ」「ろーざ」のどちらかで読んでください。元ネタ的には「ろーざ」のようですが、ぼくは「ろさ」って読んでます。ラテン語。
Ver. 1.0になって増えた機能
以下のことをしました:
- ラベルには半角スペース以外の文字を使えるようになった
- リスト省略表記
リスト省略表記
リスト省略表記は、ラベルを並べてリストを表現するという方法のシンタックスシュガーです。以下の2コードが等しい感じです。
;; 略記じゃないベタ書き CL-USER> (with-input-from-string (in " :tag lisp :tag common-lisp :tag programmnig ") (rosa:peruse-as-plist in)) (:|tag| #("lisp" "common-lisp" "programmnig"))
;; リスト省略表記 CL-USER> (with-input-from-string (in " :tag> - lisp - common-lisp - programmnig ") (rosa:peruse-as-plist in)) (:|tag| #("lisp" "common-lisp" "programmnig"))
これだけです。
以下、回想
アイデア自体は2011年ごろからあり、今でいう静的サイトジェネレータの記事データを表現するために考えたものでした。当時はまだLispは興味があるだけだったので、rosaをC++で実装しようした形跡があり、手元の旧プロジェクトのコミットログにはそれが残っています。就職してからCommon Lispで実装しだして一旦完成、そのあとに謎の軽量マークアップ言語機能が乗ったりしたあと放置。
なんとか実行可能バイナリをつくったこともありました。Common Lispの実行可能バイナリのビルト方法がよくわかっていなかったので、その時偶然できたバイナリをわりと最近まで使い続けてました。
マークアップ言語部分は、あおぞら文庫の構文や組版本を参考にしつつ、シュッとした言語になればいいなーと妄想しています。
苦節もうすぐ七、八年か。考えた当時(大学生!)はCommon Lispでこんなにコード書いてるとは思いませんでした。ゲームでもつくんだろうなって思ってました。どうしてこうなったのか。
これからも誰の役に立つのかよくわからないプログラムをCommon Lispで書いていけたらいいなあー。
Seriesで響け!ユーフォニアム!
Series触る触ると言ってたのに触ってなかったので、手始めに響けユーフォニアムをseriesでやってみました。
2017/4/29 追記
「妙なwarningが出てしまうこと」について、以下の記事の反応をいただきました。
この記事ではapply
でseriesをつくっているために、seriesの最適化のしくみにこの記事のコードを載せることができなかったのが原因なのでしょう。
Seriesとは
Seriesは、Common Lispに導入されかけたけど時間がないので却下されたという、繰り返し構造を宣言的に表現するためのしくみです。Seriesを使えばHaskellやClojureのような、関数型言語っぽいデータ処理ができそうな感じです。
仕様は"Common Lisp the Language 2nd Edition"の付録Aに書かれています。英語ならウェブでも読めます。
また日本語の説明は次のページが詳しいです。
ちなみに使うときはこんな感じです。
CL-USER> (ql:quickload :series) CL-USER> (use-package :series) CL-USER> (collect 'string (subseries (apply #'series (coerce "グソクムシ" 'list)) 0 20)) "グソクムシグソクムシグソクムシグソクムシ"
響け!ユーフォニアムとは
アニメの名前で、ぼくは見ていませんが、ちょっと前にtwitterでこんな遊びがはやっていました。
その時期にぼくもCommon Lispのformat
関数でやってみたりしたので、とりあえずseriesの入門としてやってみるのにいいのではと思った次第です。
redditのlisp_jaが、似た問題でちょっと盛り上がってた時期がありました。
結果…
Seriesのままならこんなかんじ:
CL-USER> (let* ((str "響け!ユーフォニアム") (len (length str))) (mapping ((i (scan-range :from 0 :by 1 :upto len))) (collect 'string (subseries (apply #'series (coerce str 'list)) i (+ i len))))) #Z("響け!ユーフォニアム" "け!ユーフォニアム響" "!ユーフォニアム響け" "ユーフォニアム響け!" "ーフォニアム響け!ユ" "フォニアム響け!ユー" "ォニアム響け!ユーフ" "ニアム響け!ユーフォ" "アム響け!ユーフォニ" "ム響け!ユーフォニア" "響け!ユーフォニアム")
問題に沿って標準出力に出力するならこうか:
CL-USER> (let* ((str "響け!ユーフォニアム") (len (length str))) (iterate ((i (scan-range :from 0 :by 1 :upto len))) (format t "~a~%" (collect 'string (subseries (apply #'series (coerce str 'list)) i (+ i len)))))) 響け!ユーフォニアム け!ユーフォニアム響 !ユーフォニアム響け ユーフォニアム響け! ーフォニアム響け!ユ フォニアム響け!ユー ォニアム響け!ユーフ ニアム響け!ユーフォ アム響け!ユーフォニ ム響け!ユーフォニア 響け!ユーフォニアム NIL
関数型っぽくシーケンス処理を書けるので、カッコいいと感じました。これからも使っていきたいです。
疑問
上のコード、実行するたびに以下のようなwarningが出るんですよね。なんとかならないものか。
; in: LET* ((STR "響け!ユーフォニアム") (LEN (LENGTH STR))) ; (ITERATE ((I (SCAN-RANGE :FROM 0 :BY 1 :UPTO LEN))) ; (FORMAT T "~a~%" ; (COLLECT 'STRING ; (SUBSERIES (APPLY #'SERIES #) I (+ I LEN))))) ; ; caught WARNING: ; Warning 28 in series expression: ; (COLLECT 'STRING (SUBSERIES (APPLY #'SERIES (COERCE STR 'LIST)) I (+ I LEN))) ; Non-series to series data flow from: ; (APPLY #'SERIES (COERCE STR 'LIST)) ; to: ; (SUBSERIES (APPLY #'SERIES (COERCE STR 'LIST)) I (+ I LEN)) ; ; compilation unit finished ; caught 1 WARNING condition