octahedron

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

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への溜飲が下がり、これからは使ってみようかなあという気になったのであった。

Lispエイリアン壁紙をつくりました2

 Lisp界のマスコット、Lispエイリアンの壁紙をまたつくりました。

http://f.hatena.ne.jp/t-sin/20170513011545f:id:t-sin:20170513011545p:plain オリジナルサイズ

背景

 以前Lispエイリアンの壁紙をつくりました。

octahedron.hatenablog.jp

 でも、黒いのがほしかったので、またつくりました。

 もっとクールなやつを、だれかつくってください!! だれか!!!

祝! Rosaバージョン1.0!

 最近つくったrosaというライブラリ・ツールのバージョンを1.0にしました。

github.com

 テキストデータに、書きやすく見た目を邪魔しない形式で、メタデータを埋め込むための言語です。実際にどんなものかというのは、以下の記事に書きました。

 名前は「ろさ」「ろーざ」のどちらかで読んでください。元ネタ的には「ろーざ」のようですが、ぼくは「ろさ」って読んでます。ラテン語

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の実行可能バイナリのビルト方法がよくわかっていなかったので、その時偶然できたバイナリをわりと最近まで使い続けてました。

 マークアップ言語部分は、あおぞら文庫の構文や組版本を参考にしつつ、シュッとした言語になればいいなーと妄想しています。

github.com

 苦節もうすぐ七、八年か。考えた当時(大学生!)はCommon Lispでこんなにコード書いてるとは思いませんでした。ゲームでもつくんだろうなって思ってました。どうしてこうなったのか。

 これからも誰の役に立つのかよくわからないプログラムをCommon Lispで書いていけたらいいなあー。

Seriesで響け!ユーフォニアム!

 Series触る触ると言ってたのに触ってなかったので、手始めに響けユーフォニアムをseriesでやってみました。

2017/4/29 追記

「妙なwarningが出てしまうこと」について、以下の記事の反応をいただきました。

Seriesの色々 — #:g1

この記事ではapplyでseriesをつくっているために、seriesの最適化のしくみにこの記事のコードを載せることができなかったのが原因なのでしょう。

Seriesとは

 Seriesは、Common Lispに導入されかけたけど時間がないので却下されたという、繰り返し構造を宣言的に表現するためのしくみです。Seriesを使えばHaskellClojureのような、関数型言語っぽいデータ処理ができそうな感じです。

 仕様は"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 Lispformat関数でやってみたりしたので、とりあえずseriesの入門としてやってみるのにいいのではと思った次第です。

 redditlisp_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