octahedron

LemとSKKとCommon 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...

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


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