id:ayato_pさんのUnderscore.jsがちょっと便利だったので紹介してみる。を読んで思ったのが、Underscore.jsって最近使っていないな、ということでした。
というのも、1年くらい前までは、Underscore + Backboneで素のJavascriptを書いていたのですが、最近はClojureScriptを書くようになったからです。
はじめに
ClojureScriptではJavascriptのオブジェクトは直接は使いません。
JavascriptでコレクションというとArrayになりますが、ClojureScriptではSequence,Vector,ListがありJavascriptのArrayとは別物です。
リテラルで、[1,2,3]
と書くとVectorを表すことになります。そして","は空白と等価なので[1 2 3]
と表現出来ます。
さて、ClojureScript試してみるには、CoffeeScriptと同じくブラウザさえあればよいです。
でREPLが使えます。
では、ayato_pさんのUnderscore.jsのexamplesが、ClojurScriptだとどうなるのか、それぞれ試してみましょう!
Collection Functions
each関数
eachそのものはClojureScriptにはありません。doseqと近しいのですが、eachは引数のArrayがそのままreturnされます。なので、こう定義できます。
cljs.user=> (defn each [li f] (doseq [x li] (f x)) li)
#<function each(li,f){...}>
cljs.user=> (each [1 2 3] println)
1
2
3
[1 2 3]
defnは、関数定義です。function each(lli, f) {...}
を表します。
map関数
ClojureScriptにとって、mapは最頻出関数です。FizzBuzzはこんな感じになります。
cljs.user=> (map #(cond (= (mod % 15) 0) "FizzBuzz" (= (mod % 5) 0) "Buzz" (= (mod % 3) 0) "Fizz" :default n) (range 100))
("FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz" "Buzz" nil "Fizz" nil nil "FizzBuzz" nil nil "Fizz" nil "Buzz" "Fizz" nil nil "Fizz")
reduce
reduceも簡単です。 +も関数なので、第1引数に渡すことで、第2引数の0〜10までの和を求めることができます。
cljs.user=> (reduce + (range 11))
55
filter
コレクションから特定の要素だけ抜き出すfilterも直感的です。
Clojurescriptではデータの塊はレコードとして型定義でき、マップと同様のアクセスができるようになります。
cljs.user=> (defrecord Person [name age])
cljs.user/Person
cljs.user=> (def persons [(Person. "カミル・ストッフ" 26) (Person. "葛西紀明" 41) (Person. "ペテル・プレヴツ" 21) ])
[#Person{:name "カミル・ストッフ", :age 26} #Person{:name "葛西紀明", :age 41} #Person{:name "ペテル・プレヴツ", :age 21}]
cljs.user=> (filter #(> (:age %) 40) persons)
(#Person{:name "葛西紀明", :age 41})
簡単にレジェンドを見つけられます。
Array Functions
first, rest関数
はい。これunderscoreのfirst, restと全く同等で、そのまんまcar, cdrです。
cljs.user=> (defn sum-all [li] (if (empty? li) 0 (+ (first li) (sum-all (rest li)))))
#<function sum_all(li){...}>
cljs.user=> (sum-all (range 11))
55
partition関数
Clojurescriptにも、partition関数(ファミリー)はありますが、ちょっと違う動きをします。コントロールブレイクのようなことをやるために使われます。
underscoreのpartitionのようにコレクションをふるいにかけるようなことやるには、group-byを使ってこんな感じでしょうか。
cljs.user=> (vals (group-by odd? (range 11)))
([0 2 4 6 8 10] [1 3 5 7 9])
zip関数
zip関数そのものは標準の関数にはありませんが、interleaveとpartitionを関数合成することで作り出すことができます。
cljs.user=> (def zip (comp (partial partition 3) interleave))
#<function (x,y,z,var_args){...}
cljs.user=> (zip (range 1 4) ["hoge" "fuga" "piyo"] [10 20 30])
((1 "hoge" 10) (2 "fuga" 20) (3 "piyo" 30))
Function Functions
partial
おっと、zipのところでさりげなく使ってしまってました。ClojureScriptでもpartialで部分適用ができます。
cljs.user=> (defn g [greeting name] (str greeting name))
#<function g(greeting,name){...}
cljs.user=> ((partial g "Hello, ") "kawasima")
Hello, kawasima
プレースホルダーの機能は無いので、先頭の引数をあとで適用することはできません。それをやりたい場合は、関数リテラルを使って、次のように書けば同じことが可能です。
cljs.user=> (def g' #(g % "kawasima"))
#<function g_SINGLEQUOTE_(p115_SHARP_){...}>
cljs.user=> (g' "Hello, ")
"Hello, kawasima"
memoize
メモ化も同じように出来ます。
cljs.user=> (def fib (memoize (fn [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))))
#<function (var_args){...}>
cljs.user=> (fib 100)
354224848179262000000
Object Functions
ClojurescriptはJavascriptオブジェクト専用の関数は用意されていません。しかし、Javascriptオブジェクトをマップに変換して、マップの関数を適用すること簡単にできます。Javascriptオブジェクトをマップに変換する関数がjs->cljです。逆の、clj->jsもありますが、Clojurescript Web REPLではまだ動かないようです。
keys関数, vals関数
cljs.user=> (def table (js/Object.))
#<[object Object]>
cljs.user=> (set! (. table -hoge) 1)
1
cljs.user=> (set! (. table -fuga) 2)
2
cljs.user=> (set! (. table -piyo) 3)
3
cljs.user=> (js->clj table)
{"hoge" 1, "fuga" 2, "piyo" 3}
cljs.user=> (keys (js->clj table))
("hoge" "fuga" "piyo")
cljs.user=> (vals (js->clj table))
(1 2 3)
Javascriptオブジェクトの扱い
以下、ちょっと省略してClojureScriptからJavascriptのオブジェクトを扱う方法です。
cljs.user=> (let [ws (js/WebSocket. "ws://localhost:8080")]
... (doto ws
... (aset "onopen" #(.log js/console "connected!"))
... (aset "onmessage" #(.log js/console (str "received message:" %)))))
#<[object WebSocket]>
このように、Javascriptオブジェクトはネームスペースjsを付けると参照することができます。newするには、ClojureでJavaオブジェクトを作るときと同じく、後ろに"."を付けます。
プロパティを読み書きするには、aset、agetを使います。または(.-prop obj)
のように-付きで呼びだせばプロパティにアクセスできます。
パターンマッチ
パターンマッチと似た機能が、マルチメソッドです。
cljs.user=> (defmulti calc-price (fn [item] (-> item :quantity second)))
#<[object Object]>
cljs.user=> (defmethod calc-price :lbs [item] (* (:price-per-lb item) (-> item :quantity first)))
#<[object Object]>
cljs.user=> (defmethod calc-price :kg [item] (* (:price-per-lb item) (-> item :quantity first) 2.204))
#<[object Object]>
defmultiでディスパッチ関数を書いておき、defmethodでその結果に応じた処理を定義できます。
あとは普通に関数を呼ぶと、マッチしたメソッドが実行されます。
cljs.user=> (calc-price (Item. "apple" 1.23 '(1.1 :lbs)))
1.353
cljs.user=> (calc-price (Item. "orange" 0.68 '(1.4 :lbs)))
0.952
cljs.user=> (calc-price (Item. "cantaloupe" 0.53 '(2.1 :kg)))
2.4530520000000005
Javascriptにパターンマッチを求めて彷徨っている人は、ClojureScriptを使ってみるとよいのではないでしょうか。
まとめ
このように関数型言語の要素を取り入れたunderscore.jsの機能は、ClojureのSyntaxを利用したClojureScriptを使えば、特に必要ではなくなります。
残念なことに日本語文献は入門ClojureScriptがある程度で、基本的には英語、ソースコードを読む必要があり、万人にオススメできるようなシロモノではありませんが、LisperがJavascriptを書くには選択肢として十分アリではないかと思います。