Clojure-Pyでファイルリード
●テキストファイルを読む
行単位で遅延リードしてテキスト処理。
ファイル終了判定は空文字列との一致判定。pythonのreadlineは行末に改行をくっつけたまま読み込むのでファイル終端でなけれな空行でも空文字列にはならない。
(defn repeatedly [f] (lazy-seq (cons (f) (repeatedly f)))) (defn file-content-seq [f] (->> (repeatedly #(.readline f)) (take-while #(not= "" %)))) (defn proc-line [line] (format "[%s]" (.rstrip line))) (defn main [filename] (with-open [f (py/open filename)] (doseq [line (file-content-seq f)] (println (proc-line line))))) (main (first *command-line-args*))
repeatedlyがなくて「おや?」と思ったけど、untranslated.cljに入ってた。他にもかなり重要そうな基本フォームが入ってるけどそんなんで大丈夫なの?
●CSVファイルを読む
Pythonは標準ライブラリにcsvモジュールが用意されています。
(require 'csv) (defn iterable->seq [iterable] (lazy-seq (try (cons (.next iterable) (iterable->seq iterable)) (catch py/StopIteration _ nil)))) (defn proc-row [row] (.join "<>" row)) (defn main [filename] (with-open [f (py/open filename)] (doseq [row (-> (csv/reader f) iterable->seq)] (println (proc-row row))))) (main (first *command-line-args*))
Pythonのfor-eachループはnextメソッドで値を取得し、StopIterationという例外throwが終了を表す。このループ終了仕様はちょっと厄介かもしれない。Pythonでは構文で例外を隠しているのだろうけど、Clojure-Pyでは明示的に例外処理しなきゃならない。
.nextの度にtry..catchを仕掛けるというのはパフォーマンス的に大丈夫なのかな?
Clojure-Pyではジェネレータ(types.GeneratorType)はSeqableをextendしていてseq関数でシーケンス化できる。でもイテレータクラスは継承無しのダックタイピングなのでジェネリックには扱えないような? ...このへん、もうすこし調べたい。
Clojure-PyでWin32API
Clojureからネイティブライブラリ
Pythonにはctypesというネイティブライブラリをダイレクトに扱うモジュールがあります。ということはClojure-Pyを使うとClojureからネイティブが呼べるということになりますね。
;;-*- coding:utf-8 -*- (ns sample-ctypes (:require ctypes) (:require [clojure.string :as string])) ;; convert to unicode string (defn u$ ([s codec] (py/unicode s codec)) ([s] (u$ s "utf8"))) ;; get attribute from python-object (defmacro pyattr [obj a] `(py/getattr ~obj ~(name a))) ;; get attribute thread macro (defmacro pyattr.. [obj & attrs] `(-> ~obj ~@(map (fn [a] `(pyattr ~a)) attrs))) ;; Sample user32 MessageBoxW (let [title (u$ "【サンプル CLojure-Py】") message (u$ (string/join "\n\n" ["clojure-py からの〜" "ctypes.windll からの〜" "MessageBoxW を表示"])) message-box (pyattr.. ctypes/windll user32 MessageBoxW)] (message-box 0 message title 0x40))
Python2.7で試しました。
Pythonオブジェクトのメンバへのアクセス
(.attrname obj)
でオブジェクトのメンバにアクセスできると書いてありますが何故かうまくいきませんでした 書き方が違いました、最後に追記しておきます。
(py/getattr obj "attrname")
でメンバにアクセスできることが分かりました。*1
したがってMessageBoxを使うには。
(let [user32 (py/getattr ctypes/windll "user32") message-box (py/getattr user32 "MessageBoxW")] (message-box ...))
のようにする必要がありますが、面倒なのでスレッドマクロにしています。
Clojureの新しいメンバ参照方式(追記)
TLで教えてもらいましたが、Clojure族はこれまでメンバ変数もメソッドも . で参照していましたが、メンバ変数については .- という新しい記法が導入されているそうです。というかReadMe.mdに書いてありました。*2
(.user32 ctypes/windll)
はダメでしたが、
(.-user32 ctypes/windll)
なら行けました。これならユーティリティマクロなど書かなくても。
(let [message-box (-> ctypes/windll .-user32 .-MessageBoxW)] (message-box 0 (u$ "本文") (u$ "タイトル") 0x40))
あるいは少し強引に
(-> ctypes/windll .-user32 .-MessageBoxW (apply [0 (u$ "本文") (u$ "タイトル") 0x40]))
と書けますね。スッキリした。
JVM系言語比較にJythonが無いので書いてみた
新規記事:「関数型の考え方: 実にさまざまな変換処理」を公開いたしました ibm.co/P6AxBQ #Java #dWJapan
— developerWorks Japanさん (@dW_Japan) 10月 25, 2012
このお題だと「Jythonならでは」という特色は出ないですね。CPythonでもIronPythonでも同じコードになりそうだけど、まぁそこは他の言語との比較が目的ということで気にしないことにしましょう。
環境は ubuntu12.04 の apt で入れた Jython 2.5.1 です。Python で functional するのに 2.5 はギリギリセーフのラインですね。
まずは元ネタのクラスベースのコードを丸写し。
PythonでClassを作るとなんといっても目につくのはself, self, self ですね。それからインスタンススコープのメンバ変数はコンストラクタ __init__ の中でおもむろに self にくっつけるのが通例になってます。
Python の演算子は関数ではないので reduce を適用するには関数化が必要です。関数内やメソッド内でローカル関数が定義できますので add はローカル定義してみました。メソッド self.isFactor を高階関数にわたすのに小細工がいらないあたりは便利です。
次は遅延評価版。
遅延評価版 filter の ifilter を使うことで、フィルタの結果をジェネレータで返します。ジェネレータは他言語だとストリームとかシーケンスとか呼ばれることもあるあれです。partial は 部分的用を行う関数。
Python にはメソッドチェーンで処理を繋ぐ文化がなく、そのため連続するメソッドや関数の呼び出しを1つの式にまとめようとすると、呼び出しのネスト地獄に陥ります。それを解消するために func_thread を定義しています。これを使うと
foo(bar(hoge(piyo(x))))
という式を
func_thread(x, piyo, hoge, bar, foo)
と書けます。括弧が減るだけでなく評価の順序が左から右になるので読みやすいかと。Clojure に似たような式変形を行うマクロ ->> があり「スレッド」「スレッディング」と呼ばれています。ネーミングはそこからとりました。F# の |> も似てますがこちらは「パイプ」と呼ぶんでしたっけ。
元ネタサイトの TlPrimeNumberClassifier.isPrime でメソッドチェーンを使っている部分をこの関数スレッドディングで書き換えています。
次はクラス定義無しで関数型的に
ちょっとしたバッチ処理やデータ加工等の補助ツールが欲しい時、そういう目的だとクラス定義でかっちりとしたコードを書くのはまどろっこしいと感じます。手早くコードを書くスタイルだとこんなコードになるのでは無いでしょうか。私の普段のコードに一番近いです。(さすがにここまで徹底して lambda 縛りにはしませんけど)。
lambda にするメリットは return が不要になること。lambda と return は同じ6文字ですが、通常の関数定義だとさらに def と 仮引数を囲む括弧がありますのでトータルでは lambda の方が文字数が少ないですね。
ちなみに、全部スレッディングすると
さほど読みにくくは無いですね。
余談
reduce(lambda x,y: x+y, ジェネレータ)
は
sum(ジェネレータ)
でも同じです。ClojureやScalaにも合計関数 sum はあるはずですが、元ネタサイトでは reduce/inject/fold と高階関数に統一されているようなので、今回はあえてそちらに合わせました。