Programming::Lisp::Clojure

Clojure+Leiningenで楽々Hadoop!

Hadoopの簡単なサンプルをClojureで書いてみました。leiningenでビルドするとらくちんでした!

動作環境

  • clojure 1.1.0
  • hadoop-core 0.20.1
  • leiningen 1.0.1 (最新の1.1.0だとNG!)→インストール方法はこちら

leiningen 1.1.0対応しました(2010/03/10)

サンプルコードはオライリーのHadoop本、例2-6。Java版はこちら

project.clj

(defproject clj-max-temperature "0.1.0-SNAPSHOT" 
  :description "Sample of Hadoop on Clojure" 
  :dependencies [[org.clojure/clojure "1.1.0"]
                 [org.clojure/clojure-contrib "1.1.0"]
                 [org.apache.mahout.hadoop/hadoop-core "0.20.1"]
                 ;; ここから先は全部必要か分からない
                 [commons-cli/commons-cli "1.2"]
                 [commons-codec/commons-codec "1.3"]
                 [commons-el/commons-el "1.0"]
                 [commons-httpclient/commons-httpclient "3.0.1"]
                 [commons-logging/commons-logging "1.0.4"]
                 [commons-logging/commons-logging-api "1.0.4"]
                 [commons-net/commons-net "1.4.1"]]
  :dev-dependencies [[leiningen/lein-swank "1.1.0"]]
  :main clj_max_temperature)

clj_max_temperature.clj

(ns clj_max_temperature
  (:gen-class)
  (:import (org.apache.hadoop.fs Path)
           (org.apache.hadoop.io IntWritable LongWritable Text)
           (org.apache.hadoop.mapreduce Job Mapper Mapper$Context Reducer Reducer$Context)
           (org.apache.hadoop.mapreduce.lib.input FileInputFormat)
           (org.apache.hadoop.mapreduce.lib.output FileOutputFormat)))

(gen-class 
 :name clj_max_temperature.mapper
 :extends org.apache.hadoop.mapreduce.Mapper
 :prefix "mapper-")

(defn mapper-map [this key value #^Mapper$Context context]
  (let [line (str value)
        year (.substring line 15 19)
        quality (.substring line 92 93)
        air-temperature (Integer/valueOf 
                         (.substring line 
                                     (if (= (.charAt line 87) \+) 88 87)
                                     92))]
    (if (and (not (= air-temperature 9999))
             (.matches quality "[01459]"))
      (.write context (Text. year) (IntWritable. air-temperature)))))

(gen-class
 :name clj_max_temperature.reducer
 :extends org.apache.hadoop.mapreduce.Reducer
 :prefix "reducer-")

(defn reducer-reduce [this key #^Iterable values #^Reducer$Context context]  
  (.write context key (IntWritable. (reduce max (map #(.get %) values)))))

(defn -main [& args]
  (when (not (= (count args) 2))
    (.println System/err "args error!")
    (System/exit -1))
  (let [job (Job.)]
    (FileInputFormat/addInputPath job (Path. (nth args 0)))    
    (FileOutputFormat/setOutputPath job (Path. (nth args 1)))
    (doto job      
      ;; リテラルで現在のクラスの取り方が分からない。。。
      (.setJarByClass (Class/forName "clj_max_temperature"))
      (.setMapperClass (Class/forName "clj_max_temperature.mapper"))
      (.setReducerClass (Class/forName "clj_max_temperature.reducer"))
      (.setOutputKeyClass Text)
      (.setOutputValueClass IntWritable))
    (System/exit (if (.waitForCompletion job true) 0 1))))

Reducerの中でmap/reduce使ってるのがいけてるでしょ!

サンプルコードDL

プロジェクトはこちら

$ git clone git://github.com/making/clj-max-temperature.git
$ cd clj-max-temperature

コンパイル&実行

$ lein compile # uberjarだけでcompileも行われるが、コンパイルエラーが発生してもjar作成に進むので開発時は別に実行した方が良い
$ lein uberjar
$ hadoop jar clj-max-temperature-standalone.jar input/sample.txt output

でおk。(スタンドアローンモードでのみ確認)

スタンドアローンなら、Hadoopをインストールしなくても

$ java -Xmx1000m -jar clj-max-temperature-standalone.jar input/sample.txt output

だけでもおk。簡単な動作確認に良さそう。

もっとMapReduceを使いやすくなるマクロをつくりたいね。
こんなのももうあるけど。

Created at : 2010-02-22 03:21:30   Updated at : 2010-03-20 04:54:16
Category : Programming::Lisp::Clojure::Leiningen

Clojureのdefmacroで強制的にシンボル捕捉させる

Clojureはマクロ定義内のシンボルを名前空間で修飾する。シンボル束縛を回避するためであり、バグを防いでくれるが、次のような書き方をしたいときはNG。

(defmacro foo [x]
  `(defn ~x [x]
     x))
user> (macroexpand-1 '(foo aaa))
(clojure.core/defn aaa [user/x] user/x)                                                                                              
user> (foo aaa)

Can't use qualified name as parameter: user/x
  [Thrown class java.lang.Exception]

シンボル束縛を強制的に行うためには~'をつける。

(defmacro foo [x]
  `(defn ~x [~'x]
     ~'x))
user> (macroexpand-1 '(foo aaa))
(clojure.core/defn aaa [x] x)                                                                                                        
user> (foo aaa)
#'user/aaa                                                                                                                           
user> (aaa 100)
100

使うときは要注意。
尤も、このケースでは(gensym)を使うべきなのだが。。。

→ (追記:2010/03/03)Clojureはマクロ内で使うシンボルの末尾に「#」をつければ勝手にgensymしてくれます。

user> (defmacro foo [x] `(defn ~x [x#] x#))
#'user/foo
user> (macroexpand-1 '(foo aaa))
(clojure.core/defn aaa [x__4956__auto__] x__4956__auto__)

Created at : 2010-02-26 03:22:45   Updated at : 2010-03-20 03:09:05
Category : Programming::Lisp::Clojure

clojure-maven-pluginを使ってMavenでClojure+Javaビルド

これまでさんざんClojureのビルドツールとしてLeiningenを推してきましたが。Leiningenの気に入らない点として

  • Javaをビルドできない(プラグイン作ればすぐ出来そうな気もするけど)
  • 毎回jarがlibディレクトリにコピーされる

を感じていました。
mavenなら↑のようなことはないので、mavenでClojureがコンパイルできてrepl起動して、swank-server立ち上げられるといいなと思っていたら、とっくにありますね、clojure-maven-plugin
このプラグインを使ってすぐにMavenでClojureを使えるようにblankプロジェクトを作ってGithubにコミットしました。 maven-clojure-blank

blank project取得

$ git clone git://github.com/making/maven-clojure-blank.git

.gitディレクトリを削除して、project名(ディレクトリ名、pom.xmlの設定)を変えて使ってください

コンパイル

mvn compileでClojureコードもJavaコードもコンパイルできます。ソースフォルダに

src/main/clojure/am/ik/blank/hello.clj
src/main/java/am/ik/blank/Hello.java

があったとき、

$ mvn compile
$ find target/classes
target/classes
target/classes/am
target/classes/am/ik
target/classes/am/ik/blank
target/classes/am/ik/blank/hello$foo__3.class
target/classes/am/ik/blank/hello$loading__6309__auto____1.class
target/classes/am/ik/blank/Hello.class
target/classes/am/ik/blank/hello__init.class

と両方classファイルできてますね。OK。
Clojureのコンパイルだけしたい場合はmvn clojure:compile

テスト

src/test/clojureにテストコード書いておけばmvn testでjavaのテストと一緒にclojureのテストも行われます。
Clojureのテストだけしたい場合はmvn clojure:test

REPL

replもできます。特徴はクラスパスでしょう。

$ mvn clojure:repl
Clojure 1.1.0
user=> (use 'clojure.contrib.classpath)
nil
user=> (doseq [cp (classpath)] (println cp))
#<File /Users/maki/work/maven-clojure-blank/target/classes/../generated-sources>                                              
#<File /Users/maki/work/maven-clojure-blank/src/main/clojure>                                                                 
#<File /Users/maki/work/maven-clojure-blank/src/test/java>                                                                    
#<File /Users/maki/work/maven-clojure-blank/src/test/clojure>                                                                 
#<File /Users/maki/work/maven-clojure-blank/target/classes>                                                                   
#<File >                                                                                                                      
#<File /Users/maki/work/maven-clojure-blank/target/classes>                                                                   
#<File /Users/maki/.m2/repository/org/clojure/clojure/1.1.0/clojure-1.1.0.jar>                                                
#<File /Users/maki/.m2/repository/org/clojure/clojure-contrib/1.1.0/clojure-contrib-1.1.0.jar>                                
#<File /Users/maki/.m2/repository/jline/jline/0.9.94/jline-0.9.94.jar>                                                        
#<File /Users/maki/.m2/repository/junit/junit/3.8.1/junit-3.8.1.jar>                                                          
#<File /Users/maki/.m2/repository/swank-clojure/swank-clojure/1.1.0/swank-clojure-1.1.0.jar>   
nil

clojureのソースディレクトリ、ビルド出力ディレクトリ、pom.xmlで指定した依存ライブラリが含まれています。これでJavaをコンパイルしてもすぐにreplで使えますね。

swank

Lisp開発と言えばやっぱりSlimeを使いたいですよね。mavenでswank-serverを立ち上げることでrepl同様のクラスパス込の状態で使えます。mvn clojure:swankでswank立ち上げたあとemacsでM-x slime-connectでOK。

スクリプト実行

以下のようにconfigurationscriptに実行したいclojureスクリプトのパスを書いておけばmvn clojure:runでロードされます。もちろんクラスパスは同上。

<plugin>                                                                                                                      
  <groupId>com.theoryinpractise</groupId>                                                                               
  <artifactId>clojure-maven-plugin</artifactId>                                                                         
  <version>1.3.1</version>                                                                                              
  <configuration>                                                                                                             
    <script>src/main/script/run.clj</script>                                                                            
  </configuration>                                                                                                            
  ...
</plugin>

これだけ書くとleiningenいらないじゃんって思いそうですが、 mavenは立ち上がりが遅いのでちょっとイラッとするかもです。(Leinigenも結構遅いですがw)
Javaでライブラリを開発してClojureで呼び出すっていう使い方をしたい場合はこの方式が良さそうです。 ClojureのみのプロジェクトであればLeiningenで十分かなと思います。


Mavenについては
Maven: The Definitive Guide
が詳しいです。
実はここからPDFでまるごとDLできますw

Created at : 2010-03-14 01:50:58   Updated at : 2010-03-14 02:48:18
Category : Programming::Lisp::Clojure