LoginSignup
29
28

More than 5 years have passed since last update.

underscore.jsでやっていたことは、すべてClojureScriptがやってくれる

Posted at

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関数そのものは標準の関数にはありませんが、interleavepartitionを関数合成することで作り出すことができます。

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を書くには選択肢として十分アリではないかと思います。

29
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
28