clojureでcat -n


ちょっと間が空いてしまったが、何をやっていたかと言うとこちらのページで
Haskellのお勉強をしてました。そして、CPANならぬhackageDB :: [Package]に
行ってみて、世の中には好きな人がいるもんだと、感心したりしてました。

そうそう、FreeBSDに、Rubyのgemならぬ、Haskellのcabalを入れようとSDの記事を
頼りに試していた所、cabal-installの所で躓いてしまいました。
曰く、cabalのVersionは、6.02より大きくて7.00以下が必要でっせ、と。
やけに、指定が細かいなあと思っていたら、cabal自身が入っていなかった。
しょうがないので、上記URLより、cabal自身をDL

runghc Setup.hs configure
runghc Setup.hs build
sudo runghc Setup.hs install

しましたよ。で、これが時間のかかる事しきり。大量のメモリーが要求されて、そ
れに耐えられない私のマシンは、スラッシングの嵐に陥ったのでした。
そろそろ、富豪マシンに乗り換えないとだめかな。

余りHaskellにのめり込むと、抜けられなくなるので、Haskellを理解しつつ
それを、他言語に翻訳してみます。ターゲットはclojure。お題は、cat -n
とします。
まず、clojureでファイル読み込みはどうするん? 調べてみたら、

user=> (slurp "c:/sakae/test.txt")
"This is Line 1.\r\n\"line 2.\"\r\n\r\nHere line 4, above line is null.\r\nThis is 'LAST-LINE'"

ファイル全体が、一つの文字列で帰ってきました。ちと扱いにくいなあ。分解は
どうやる? 正規表現が一応あるみたいです。(この場合は、行でsplitですね。)

user=> (re-seq #"[^\r\n]+" x)
("This is Line 1." "\"line 2.\"" "Here line 4, above line is null." "This is 'LAST-LINE'")

黒田さんや山本先生に怒られそうなので、なるべくなら避けたい所。
ああ、調べている途中で、joinに相当するのも見つけたので

user=> (apply str (interpose ":" ["A" "B" "C"]))
"A:B:C"


さて、clojureには、応援部隊が居て、contribにいろいろと公開されている。
それをつらつら眺めていたら、duck_streams.clj と言うのを見つけた。

;; This file defines "duck-typed" I/O utility functions for Clojure.
;; The 'reader' and 'writer' functions will open and return an
;; instance of java.io.BufferedReader and java.io.PrintWriter,
;; respectively, for a variety of argument types -- filenames as
;; strings, URLs, java.io.File's, etc.  'reader' even works on http
;; URLs.
;;
;; Note: this is not really "duck typing" as implemented in languages
;; like Ruby.  A better name would have been "do-what-I-mean-streams"
;; or "just-give-me-a-stream", but ducks are funnier.

へぇ、Rubyなんてのに触れているよ。よし、こやつのお世話になろう。
それには、contribのjar化だな。

sakae@debian:/home/src/clojure-contrib$ ant
Buildfile: build.xml
init:

check_hasclojure:
     [echo] WARNING: You have not defined a path to clojure.jar so I can't compile files.
     [echo]       This will cause some parts of clojure.contrib not to work (e.g., pretty print).
     [echo]       To enable compiling, run "ant -Dclojure.jar=<...path to clojure.jar..>"
     [echo]

compile_clojure:

jar:
      [jar] Building jar: /home/src/clojure-contrib/clojure-contrib.jar
      [jar] Building jar: /home/src/clojure-contrib/clojure-contrib-slim.jar

BUILD SUCCESSFUL
Total time: 8 seconds

BUILD SUCCESSFULって出てるけど、8秒で終わる訳ないっしょ。失敗だね。
では、仰せに従って、

sakae@debian:/home/src/clojure-contrib$ ant -Dclojure.jar=/home/sakae/clj/clojure.jar
Buildfile: build.xml

init:

check_hasclojure:

compile_clojure:
     [java] Compiling clojure.contrib.accumulators to /home/src/clojure-contrib/classes
     [java] java.lang.IncompatibleClassChangeError (types.clj:14)
     [java]     at org.apache.tools.ant.taskdefs.ExecuteJava.execute(ExecuteJava.java:194)

あらら、古いclojureじゃ、だめみたい。svnで先端のclojureを取ってきて、やって
みよう。

sakae@debian:/home/src/clojure-contrib$ ant -Dclojure.jar=/home/src/clojure-dev/clojure.jar
Buildfile: build.xml

init:

check_hasclojure:

compile_clojure:
     [java] Compiling clojure.contrib.accumulators to /home/src/clojure-contrib/classes
     [java] Compiling clojure.contrib.command-line to /home/src/clojure-contrib/classes
     [java] Compiling clojure.contrib.complex-numbers to /home/src/clojure-contrib/classes
     [java] Compiling clojure.contrib.cond to /home/src/clojure-contrib/classes
      :
jar:
      [jar] Building jar: /home/src/clojure-contrib/clojure-contrib.jar

BUILD SUCCESSFUL
Total time: 1 minute 32 seconds

そうだよなあ、これぐらいの時間はかかるよね。DISKもガリガリ動いていたようだ
から、大丈夫だろう。
sakae@debian:/home/src/clojure-contrib$ ls -l *.jar
-rw-r--r-- 1 sakae sakae  262058 Apr 13 11:03 clojure-contrib-slim.jar
-rw-r--r-- 1 sakae sakae 2152373 Apr 13 11:13 clojure-contrib.jar

はて、どちらを使えばいいのだろう? ここは、大きい事はいい事だで行きます。

sakae@debian:/home/src/clojure-contrib$ java -jar /home/src/clojure-dev/clojure.jar clojure-contrib.jar
java.lang.Exception: Unable to resolve symbol: PK in this context (clojure-contrib.jar:0)
        at clojure.lang.Compiler.analyze(Compiler.java:4337)
        at clojure.lang.Compiler.analyze(Compiler.java:4283)
           :

あらら、エラーですよ。先端を追うと、こういう事って、当たり前なんですかね。
ここは、contrib全体を使うんじゃなくて、必要最低限を使うようにして
みます。
duck-streams.clj は、jarファイルにまとめてcontribというnamespaceで使う
事を意図して書かれていますので、ちょいと改変しました。

(ns user ;;clojure.contrib.duck-streams

Haskellで言ったら、preludeみたいに、どこでも使える user-namespaceですね。

user=> (load-file "duck_streams.clj")
#'user/with-in-reader
user=> (def ls (read-lines (reader "test.txt")))
#'user/ls
("This is Line 1." "\"line 2.\"" "" "Here line 4, above line is null." "This is 'LAST-LINE'")

read-lines って、rubyにも有ったような気がしますが、気にしない気にしない。
まあ、これで、ファイルの内容が、メモリーに乗ってくれました。
後は、これと、行番号を合体して、表示するだけですね。
行番号は6桁で右寄せとしましょう。Javaのprintfがそのまま使えちゃたり
しますが、それじゃ、つまんない。Haskellのそれに習って、

user=> (defn rjust [n]
  (str (reduce str "" (replicate (- 6 (count (str n))) "-")) n))
#'user/rjust
user=> (rjust 123)
"---123"

ちゃんと、関数にしました。(今は、debugの途中なので、spaceの変わりに - を
使ってます。)
次は、これらを組み合わせてみます。


user=> (doseq [ [n s] (map vector
                          (iterate inc 1) ls) ]
              (println (str (rjust n) ": " s)))
-----1: This is Line 1.
-----2: "line 2."
-----3:
-----4: Here line 4, above line is null.
-----5: This is 'LAST-LINE'
nil

どうやら、主要部分は出来たっぽい。
遅延評価のおかげで、Haskellぽく書ける事が分かりました。
後は、ファイル名が決めうちになっている所を、コマンドラインから
取り込むようにすれば、出来上がり。


sakae@debian:/home/src/clojure-dev$ time ./catn.sh catn.sh
     1: #^:shebang '[
     2: exec java -cp clojure.jar clojure.lang.Script "$0" -- "$@"
     3: ]
     4:
     5: ;;;; catn.sh target ; unix cat -n
     6:
     7: (load-file "duck_streams.clj")
     8:
     9: (def target (first *command-line-args*))
    10:
    11: (defn rjust [n]
    12:   (str (reduce str "" (replicate (- 6 (count (str n))) " ")) n))
    13:
    14: (doseq [ [n s]
    15:         (map vector
    16:             (iterate inc 1)
    17:             (read-lines (reader target)) )]
    18:     (println (str (rjust n) ": " s)))
    19:

real    0m12.386s
user    0m0.032s
sys     0m0.048s
sakae@debian:/home/src/clojure-dev$

なんと、sleep が一切入っていないにもかかわらず、実行終了まで12秒もかかる
不思議な cat が、出来上がりましたよ。でも、一応名誉のために付け加えておく
と、2回目の実行時は、8秒に短縮されました。