読者です 読者をやめる 読者になる 読者になる

Rosaに謎のシリアライズ機能が登場

 先日こんなものが完成しました。

octahedron.hatenablog.jp

 まあ用途や有用性の不明な、なんだかよくわからんプロジェクトだとぼく自身も思うわけですが、なんなんでしょうね(ぼく自身ではわりと使ってます)。

Rosaについて

Rosaとは

 文字列に名前を付けて、それをプレーンテキストで表現できる言語です。そして、それをパースするライブラリ・コマンドでもあります。

 機能の弱いJSONとかYAML、のような感じと見做すこともできるかもしれません。
 「タイトル」「著者名」「更新日付」「本文」「あとがき」などのテキスト情報をひとつのファイルに、人間が書きやすい構文(=インデントや妙な括弧がない)で書き表せる言語です。小説とかブログの一記事とか、そういうのを一ファイルで書くのに使おうと思ってつくりました。

 ちなみに、中のテキストのマークアップは感知しないので、別の言語(Markdown、reStructured text、あおぞら文庫形式)を選択できます。というかしてください。

Rosaでパースしたテキストデータ

 Rosaで定められた形式で、key-value的なデータを表すことができます。たとえば、ポケモンとその登場作品の対応、とか。

CL-USER> (setf pokemon "
:diamond-pearl ブイゼル
:sun-moon モクロー
:sun-moon クワガノン")
CL-USER> (with-input-from-string (in pokemon)
           (rosa:peruse-as-plist in))
(:|diamond-pearl| #("ブイゼル")
 :|sun-moon| #("モクロー" "クワガノン"))

テキストをパースすると、rosa:peruseでハッシュテーブル、rosa:peruse-as-plistでプロパティリストになります。これから所望のデータを抜き出して、wc -mするもよし、grep -n hogeするもよしです。

謎のシリアライズ機能 (new!)

 それでとりあえず何が増えたのかというと、ハッシュテーブルやプロパティリストを与えると、それを表現するrosaの文字列に変換する関数が増えました!

CL-USER> (with-input-from-string (in pokemon)
           (rosa:peruse in))
#<HASH-TABLE :TEST EQL :COUNT 2 {1002956813}>
CL-USER> (with-input-from-string (in pokemon)
           (rosa:indite (rosa:peruse in)))
":diamond-pearl ブイゼル
:sun-moon モクロー
:sun-moon クワガノン
"

 それだけです。やったぜ。

 いったいどんなユースケースあるのかは謎ですが、おもしろかったので実装しました。

おもしろいなと思ったこと

 peruseinditeの対が、マセマティカル・モルフォロジーにおける膨張と侵食の関数みたいに、随伴っぽい関係になっているなあ、とふと気づきました。

 まとまっていないし、検証も証明もされていないけど、おもしろげであると思ったことをざっと列挙

  • peruseinditeを使うと、二つの領域(文字列、ハッシュテーブル)を行き来できる
    • peruseすると、文字列はkey-valueなデータに写される
    • inditeすると、key-valueなデータはrosaの言語に写される
  • (indite (peruse in))という関数(モルフォロジの閉包作用素っぽい)を考えると、これは冪等っぽい
  • どんなテキストファイルでもperuseによりrosaの言語の世界に引き込むことができる
    • あとはinditeperuseで二つの領域をぐるぐるする

 なんとなく類似を感じた概念等はこちらの本の6章を参照のこと: 『非線形画像・信号処理  (モルフォロジの基礎と応用)』。

おわりに

 大学で学んだ抽象的な概念がこのような形で姿を見せるとは、学問とはおもしろいものだなあと思った。

Rosa --- メタデータ付きテキストを表現する言語

Rosa

 Rosaは、プレーンテキストにタイトルや作成者などのメタデータを付与するための、メタなマークアップ言語です。また、そのパーサライブラリであり、パーサコマンドの名前でもあります。

 ちなみにCommon Lispで実装しました!!!

github.com

動機

  • ムサシ「何だかんだと聞かれたら」
  • コジロウ「聞かせてあげよう我らが名を」

ロケット団 (アニメポケットモンスター), 口上, 5代目(SM編)- Wikipedia

 不躾ながらも文章やらなんやらを書くのが趣味なのです。そして書いた文章は、題名や書いた日付や、その他脚注やメモや参考にしたURLなんぞといっしょに同じテキストファイルに残しているわけなんです。そういう付加的な情報はファイルの中に書いておきたい*1

 でも、そんな記法・言語ってあったっけ?

 マークアップ言語の中で候補を考えると、XMLMarkdown、reStructuredText、JSONYAMLなどなどいろいろありますが、以下の理由でそれぞれ却下です:

  • XML … あの構文はプレーンテキストと相性が悪い
  • Markdown … 構文は軽いが、名前を任意に埋め込むのは範疇ではない
  • reStructuredText … 構文は軽いが、マークアップ部分がオーバースペック
  • JSON … 名前とデータを記述するのにはいいが、プレーンテキストにブラケットはちょっと…
  • YAMLJSONよりはいいけど、プレーンテキストにインデントはちょっと…

 そもそも、前述の言語は文書構造を表現するものたちであって、付加的な情報を表現するものではないのです。探した限りで、そういう言語は見当たりませんでした。

──そんな言語がないのなら、つくるしかないじゃない! あなたも! わたしも…!!

<2017-03-24追記>

 このrosaを、つくっておきながらジャンルがよくわからなかったとき、Masaiさんからメタマークアップ言語では?と教えていただきました。ありがとうございます!

で、どんな言語?

 というわけでつくったのがrosaでございます。

 どんな言語か紹介します。まず、見た目でいうとこんな感じ。

:title あのときの王子くん
:author アントワーヌ・ド・サン=テグジュペリ
:source-site あおぞら文庫
:source-url http://www.aozora.gr.jp/cards/001265/files/46817_24670.html

:body

〈星から出るのに、その子はわたり鳥をつかったんだとおもう。〉

[#改ページ]


レオン・ウェルトに

 子どものみなさん、ゆるしてください。ぼくはこの本をひとりのおとなのひとにささげます。でもちゃんとしたわけがあるのです。そのおとなのひとは、ぼくのせかいでいちばんの友だちなんです。それにそのひとはなんでもわかるひとで、子どもの本もわかります。しかも、そのひとはいまフランスにいて、さむいなか、おなかをへらしてくるしんでいます。心のささえがいるのです。まだいいわけがほしいのなら、このひともまえは子どもだったので、ぼくはその子どもにこの本をささげることにします。おとなはだれでも、もとは子どもですよね。(みんな、そのことをわすれますけど。)じゃあ、ささげるひとをこう書きなおしましょう。

...

 Rosaのファイルはテキストデータメタデータから成ります。テキストデータに付けた名前(メタデータ)を、:から始まる行で表現します。メタデータの名前のことをrosaのREADMEの中ではラベルラベルと呼称しているので、以降もラベルということにします。

 一行だけのテキストデータと、改行を含むテキストデータで書き方がすこし違います。

インラインラベル

 一行のほうは、:の次から最初のスペースまでがラベルです(インラインといいます)。例を示すと、

:hoge-inline Common Lispがすきです

こうです。二つ目以降のスペースはテキストデータの一部になります(ちなみにラベルは[a-z][a-z-]+正規表現じゃなければ、ふつうの行だと認識されます)。

ブロックラベル

 複数行のほうは、:からスペースなしの改行までがラベルになります(ブロックといいます)。対応するテキストデータの範囲は、次の行から、その次のラベルかファイル終端までです。例示すると、

:hoge-block

Commmon Lispが最高に好きです。
Emacsも好きです。

:fuga-inline モクローも好きです。

こう。

コメント&エスケープシーケンス

 あと、ブロックラベルには、複数行コメント(行頭の;はじまり)があったり、:;に対するエスケープシーケンスがあったりします:

:body

; この行は無視されます
:; この行はセミコロンから始まります
:: この行はコロンから始まります

 以上がrosaの構文です。

 ちなみに、同名ラベルが複数あったとき、その本体はちゃんと出現順で保持されています。

ライブラリ使用法

 Common Lispのライブラリなので、$ ros install t-sin/rosaで導入でき、REPLで(ql:quickload :rosa)するとすぐに使用できます。

パースする

 パースする関数はperuseです。Rosaの入力はストリームで渡してください。結果はハッシュテーブルで返ってきます。

(with-input-from-string (in ":label hogehoge")
  (rosa:peruse in))
; => #<hash-table ...>

 もしすぐに結果が見たければ、peruse-as-plistを使うとplistで結果が返ってきます。

コマンドライン使用法

 だんだんめんどくさくなってきたので簡単に。

 機能としては以下の三つのことができます:

  1. 入力をパースして、ラベルのリストを出力する
  2. 入力をパースして、指定したラベルのテキストを出力する
  3. 入力をパースして、入力中の全ラベル - テキストを出力する

 それぞれ出力の形式をオプションで選べます。

 詳細はREADMEを見てください。

ラベル一蘭を出す

$ cat hoge.txt | rosa index
ラベルが
ズラーっと
出る

指定したラベルの内容を出す

$ cat hoge.txt | rosa pick title -j
"hogeタイトル"

全構造を出す

$ cat hoge.txt | rosa pick title -y
# YAMLで全構造がずらーっとでる

そのほかのユースケース

 テキストファイルのメタデータを表現する以外にも、ラベル - テキストの構造がkey-valueっぽいので、エスケープシーケンスを駆使すれば、デキスト版key-value storeとして使えるような気がしました。非効率だけど。

 あと、仕事では一度この使い方をしたことがあるのですが、ウェブAPIのフォームの値をテキストファイルにrosaの形式で書いておけば、それを読み込むプログラムを(Common Lispで)つくりやすかったです。ある種key-value storeとして使ったとも言える。

今後の発展

 いまは以下のようなのをぼんやり考えています:

  • quicklispに登録しちゃう!?
  • パフォーマンス測定
    • 巨大ファイルだいじょうぶ?
    • 比較対象あれば(だれか教えて!)
  • ラベルの文字数制限する?
  • テストの実装言語非依存化?
  • 別の言語で実装??

 あと、趣味を改善するプロジェクトとして次は、日本語の文章を表現するためのマークアップ言語hearnを考え中なので、こちらもやらねば。

github.com

*1:タイトルやいつ書いたかなどは、ファイル名やパスやファイル更新日時で表現は可能です。でも、気分で変更することもあるので、それらに依存してほしくない。それがファイルの内容に残しておきたい理由です。

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

TL;DR

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

同日 19:45追記; オリジナル画像がダウンロードできませんでした。笑止。

f:id:t-sin:20160429103312j:plain オリジナルサイズ 壁紙そのいち

f:id:t-sin:20160429103306j:plain オリジナルサイズ 壁紙そのに

動機

Lispを愛するならば、デスクトップの壁紙もLispにしたいと思うのはわりと自然な発想だと思います。そうしてLispの壁紙をググってみるわけです

少ない。

ピンとくるのがない。

ところで、Clojureにはなかなかカッコいい壁紙があるようです。公式でロゴがあるからでしょうか。羨しいですね。

Lisp界には、Lispエイリアンというマスコットがおります。あの愛らしい緑色のアイツです。目がいっぱいの。彼(彼女)の壁紙があれば、オールオッケーなのではないか。そう考えた次第であります。

ググってみる

ないぢゃん!!

なんでだよ!!

Why Japanese people!?

結論

というわけで、壁紙を(半年くらい前に)つくりました。ずっと自分で利用するだけだったんですが、つくったものを公開することは(プログラムでなくても)Lisp界への貢献になるのかな、と思った次第で、それで公開することにしました。先駆けになって、もっといいものが出てくると信じて…。

もっといい壁紙がほしいです。

だれかつくってください。

One --- ワンライナーのための入力処理

One

 Oneはファイルや標準入力の行単位の入力を簡単に扱えるようにする、Common Lispでのワンライナーを支援するライブラリです。

github.com

概要

 時は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が見る影もないぜ!

 こうしてぼくはperlrubyを覚えることもなく、よくわからなくて長いpythonワンライナーに悩まされることもなく、手に馴染んだCommon Lispで入力を処理できるようになったのでした。
 めでたしめでたし。

使い方

 Oneはふたつのマクロforforlによって、ファイルや標準入力を一行ずつ読み込む機能を提供します。Pythonなどで

for line in open('data.dat', 'r'):
    print line

なんて書けたりしますが、oneはこれを実現します。ふたつのマクロforforlは、一点を除いてまったく同じ動きをするため(違いはあとで説明します)、以降では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

forforlの違い

 forforlの違いは、各行の読み込みにどの関数を使うかという点です。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...

 このコードうごかねー、への指摘からのコードレビューアドバイスは非常に助かりました。
 ありがとうございました!


職場でお昼休みに書いたせいか、概要のテンションがヤバい。

Inquisitor --- external-formatラッパー & 文字コード/改行コード自動判定ライブラリ

Inquisitor

 Inquisitorとは、Common Lisp文字コード/改行コードの自動判定ライブラリなのである。

github.com

 これまでに文字コード判定を行うライブラリはあったが、 *1

  • 改行コード判定がない
  • さまざまな処理系(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向けSBCLLinuxで作成した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で改行コードが扱えるので