はじめに
Xcooにソフトウェアエンジニアとして拾ってもらい、もう4か月が経ってしまいました。
前職は機械設計エンジニアで、プライベートでRecursionでコンピュータサイエンス系の学習をしてきましたが、面接を受けるまではClojureという言語の存在すら知らないいわゆる未経験エンジニアでした。
そんな私ですが、やっとClojureに慣れてきたというのもあり、Clojureの特徴や好きなところを紹介していこうと思います。
括弧が多い⁉
まず初めてClojureのコードを見たときの印象は、括弧が多い...でした。例えばHelloWordみたいな簡単な関数だと、
(println "Hello world")
のように、ほかの言語と変わらないですが、関数が増えるごとに括弧が増えていきます。
(defn greeting [name]
(println (str "Hello! " name)))
(greeting "Mike") ;-> "Hello! Mike"
しかし、丸括弧が多いのと最終行に閉じかっこをまとめるの多く見えるだけのようです。JavaSciptもよく見てみると、
function greeting(name) {
console.log(`Hello! ${name}`);
}
みたいに閉じ括弧を改行するので見た目は少なそうですが、括弧数としてはさほど変わらなそうです。
#()が無名関数!?
clojureの無名関数は以下のように#()
を使えば簡潔に記述できます。
(println (map #(* % % %) [1 2 3 4 5]))
初見では四則演算がポーランド記法であることも相まって分かりにくいなと思っていましたが、慣れてしまえばめちゃくちゃ使いやすいです。
この関数はJavaScriptだと、
console.log([1,2,3,4,5].map(x=>x*x*x))
で表されるように、配列の要素をそれぞれ3乗しています。
clojureでも同じように、もちろん関数を別に定義したり
(defn cubed [x] (* x x x))
(println (map cubed [1 2 3 4 5]))
fn
を使って、
(println (map (fn [x] (* x x x)) [1 2 3 4 5]))
のように記述もできます。
しかし、結局無名関数は一回しか呼ばない関数ですし、
(println (map #(* % % %) [1 2 3 4 5]))
みたいに簡潔に書けるのはスッキリして好みです。
関数を合成しまくる!?
Clojureでは部品を組み合わせるように関数を合成でき、ソースコードを見ているとよく使われています。
先ほどの配列の要素をそれぞれ3乗しましたが、今回はその値からさらに5を引く関数を合成してみます。
(println (map (fn [x] (- (* x x x) 5)) [1 2 3 4 5]))
このように、関数内に関数を記述していけば実装可能ですが、ネストが深くなるとかなり読みにくくなります。これはcompという合成関数をつくる関数を使うと可読性が向上します。
(println (map (comp (fn [x] (- x 5))
(fn [x] (* x x x)))
[1 2 3 4 5]))
先ほど紹介した#()
で標準関数を使うとものすごく可読性が上がります。
例えば、"Reynold Hargin" -> "R.H"のように名前からイニシャルを作る関数を以下のように作ってみました。
(def toInitial
(comp #(clojure.string/join "." %) ; 4. "." でつなげる "R.H"
#(map clojure.string/upper-case %) ; 3. upper-caseにして ["R" "H"]
#(map first %) ; 2. 最初の文字をそれぞれ取り出し、["r" "h"]
#(clojure.string/split % #" "))) ; 1." "でsplitして (cf. ["Reynold" "Hargin"]
このように、処理の流れが大変わかりやすくなります。
->, ->>はマクロ⁉
->, ->> は記述した関数を順に実行し、それぞれ結果を次の関数に渡すマクロです。関数の結果を最初の引数に渡すのが->、最後に渡すのが->>です。
関数を組み合わせたり並べたりしてデータを処理していくのが関数型プログラミングの基本的な考え方ですので、まさにそれを体現化したようなマクロです。
文章だけではわかりにくいと思うので、例を見ていただきたいです。例えば、以下のようなデータがあって、20歳以上のMemberのイニシャルを出力してみます。
(def members
[{:id 1 :age 10 :name "Cat Napier"}
{:id 2 :age 20 :name "Jayden Grady"}
{:id 3 :age 29 :name "Jim Dallimore"}
{:id 4 :age 13 :name "Kieren Norrish"}
{:id 5 :age 38 :name "Dionysius Evans"}])
まず、大人かどうか判断する関数adult?
を作り、
(defn adult?
; 受け取ったHashMapを分解しつつageという変数(シンボル)に束縛します。
; ageが20以上であればtrue, それ以外はfalseを返します。
[{:keys [age] :as members}]
(>= age 20))
前の項で関数を合成して作成した、FullNameをInitialにする関数toInitial
を、
(def toInitial
(comp #(clojure.string/join "." %)
#(map clojure.string/upper-case %)
#(map first %)
#(clojure.string/split % #" ")))
マクロ使ってつなげるだけです。
(->> members (filter adult?) ; member listからageが20以上のHashMapを抽出
(map :name) ; keyが:nameのvalueを抽出
(map toInitial) ;それをそれぞれInitialにし、
(println)) ;printする
このように、文章を読むように関数を読むことができます。
これはClojureの中で一番好きな部分です!
おわりに
Clojureは使えば使うほど好きになっていく言語です。まだまだ全然使いこなせていませんが、安全で簡潔なコードが書けるんだろなぁと感じています。とにかくコードを書いて、ほかの技術とも組み合わせながら勉強し続けていきたいと思っています。
Clojureが気になった方はブラウザで実行できるサービスもあるので、ぜひ試してみてください!