さめたコーヒー

kbaba1001のブログ

Clojure と頭の中、呼吸、あるいは自由に動けるようになるまで

はじめに () ありき。Clojureでは何かをしようとしたときまず () を書く。ときどき [] だったり {} とか #{} とかだったりもするが、とりあえずカッコから始まる。

次はどうなる? あぁ、関数を定義しようと思ったんだった。

(defn)

名前をつけてやろう。

(defn foo)

ついでに引数も。

(defn foo [{:keys [bar]}])

おや?少し様子がおかしい。

もちろん (defn foo [bar]) でもいい。でも僕は (defn foo [{:keys [bar]}]) のほうが好きだ。

これは Destructuring (分配束縛) というやつで、(foo {:bar 1}) のような呼び出しをするときに便利だ。 Clojure では HashMap {} を使ってデータをまとめることが多いからはじめから HashMap を渡す想定で引数を設計しておくと、あとから引数を増やすことになったときに便利だ。

まぁともかくこんな感じで Clojure を書いている。プログラミング言語は思考に影響する。自分なりのコードの考え方、捉え方がたぶん人それぞれあって、それは呼吸のようなもので自分にあった言語であれば体は自由に動く。逆に合わない言語であれば不調をきたす。具体的には僕の場合Go言語を書くと顎関節症になる。コード、ひいては設計は次に自分がどう動くかを決定する。Clojureはゴムのような不思議な言語だ。伸びたり縮んだりしながら変化を受け入れる。 例えば、

(defn hello-jp [{:keys [user-name]}]
  (prn (str "こんにちは、" user-name "さん")))

(defn lang-en [{:keys [user-name]}]
  (prn (str "Hi, " user-name)))

こういうコードがあるとき別の実装を

(defn lang-ch [{:keys [user-name]}]
  (prn (str "你好、" user-name)))

のように追加することはかんたんだけど、こういう似た者同士はまとめたくなるのが心情なわけで、いくつかやり方はあるけど例えば

(defmulti hello
  (fn [{:keys [lang]}] lang))

(defmethod hello "jp" [{:keys [user-name]}]
  (prn (str "こんにちは、" user-name "さん")))

(defmethod hello "en" [{:keys [user-name]}]
  (prn (str "Hi, " user-name)))

(defmethod hello "ch" [{:keys [user-name]}]
  (prn (str "你好、" user-name)))

こんな風にしておくと

(hello {:lang "jp" :user-name "馬場"})

みたいにして呼び出せて便利。HashMapの値で呼び出す関数をかえるっていうよくあるシチュエーションにおいて、なんとなくメタプログラミングでもしたくなるような気もするんだけど、この defmulti defmethod というやつを使うとメタプログラミングしなくてもスッキリ書ける。しかもこれはオープンクローズドの原則に沿っている。 defmethod を追加するとき他の defmethod やさらには defmulti のことも気にしなくていい。 この考えを推し進めると integrantduct のようなライブラリになっていくのだけれど、その話は今日はしない。 defmulti defmethod は Clojure のもつ不思議な機能の一つにすぎなくて、この言語はもっと色々な風変わりな機能がある。

ところで、 Clojure はいくつか実装があって、一般的に単に Clojure という場合 JVM の上で動く Clojure のことを指す。他にも JavaScript にビルドできる ClojureScript とか C# になるやつとか Erlang になるやつとか色々ある。 あなたは知らないかもしれないけど、この地球にはすべてを Clojure で書きたいと思っている人類が若干名いるということだ。 で、こういう他言語をラップしてしまう言語は他にもあるけど Clojure たちがちょっと他と違うのは Clojure が元々の言語を呼び出すことにあまり抵抗がない点だろうか。

例えば、 JVM の Clojure には Stack がない。基本的なデータ構造にも関わらず提供されてない。アルゴリズムを書くときに時々 Stack は使いたくなるわけで、じゃあどうするかというと Java の Stack をそのまま拝借する。

(let [stack (java.util.Stack.)]
  (.push stack 1)
  (.push stack 2)
  (.pop stack))  ;; 2 が出力される

java.util.Stack. は Java でいうところの new Stack<T>() みたいなもので、 Java のクラス名の最後に . をつけると new の意味になる。 で、そうして作ったオブジェクト (上記のコードで言う stack 変数) を第1引数にして .関数名 を呼び出すとメソッド呼び出しができる。 ちょっと奇妙な構文だけど、Clojureの元々ある関数型言語としての機能を活用するにはこれが実は便利だ。

突然だけど hello world という文字列をすべて大文字にしてシャッフルして重複を取り除いた文字列を作りたくなった。(実は最近これに近いコードを実際に書いたんだ)

(clojure.string/join (distinct (shuffle (map clojure.string/upper-case "hello world"))))
;;=> "HL EWODR"

でもこのコードはなんとなく読みづらい。処理の起点が右の方に偏っているから右から左に読むのはなんとなく気持ち悪い。 だからぼくならこう書く。

(->> "hello world"
     (map clojure.string/upper-case)
     shuffle
     distinct
     clojure.string/join)
;;=> "LDRHW OE"

読みやすくなったね。

え?よくわからない?

->> は関数の実行結果を次の関数の引数の末尾に入れてくれるマクロだ。ちなみに引数の最初に入れてくれる -> もあってこれもよく使う。 詳しくは公式のガイドでも読んでくれ。

clojure.org

->->> のいいところは処理の途中に別の処理を挟み込みやすくなること。例えば途中でプリントデバッグしたくなったら、

(->> "hello world"
     (map clojure.string/upper-case)
     shuffle
     ((fn [a] (prn a) a))
     distinct
     clojure.string/join)
;;["O" "H" "E" "L" "L" "D" "R" " " "L" "O" "W"]
;;"OHELDR W"

こんな感じにしてやればいい。 ((fn [a] (prn a) a)) は個人的によく書くイディオムで、 (fn [a] (prn a) a) は引数 a をとってそれを表示してそのまま a を返すだけの関数なので、それを実行するためにさらに () でくくっている。

(->> "hello world"
     (map clojure.string/upper-case)
     ((fn [a] (prn a) a))
     shuffle
     distinct
     clojure.string/join)

(->> "hello world"
     (map clojure.string/upper-case)
     shuffle
     distinct
     ((fn [a] (prn a) a))
     clojure.string/join)

位置をずらせばどこでもデバッグできる。メソッドチェーンの間にデバッグ処理をかんたんに差し込めるのは便利だ。もちろん他の処理を差し込んでもいい。 他の言語でも似たような機能はあるけど、引数に与える関数を増やすだけで処理を変えられるのってなんとなく気持ちいい。 (->>) のカッコがゴムのように伸びていく感覚がわかるだろうか?

さっきからゴムのように伸び縮みするという変な言い回しを好んで使っている。なんのことだろう? 実はこれはエディタの機能にも関係がある。僕は Clojure を書くとき paredit という機能を使っている。

calva.io

例えば、

(->> "hello world"
     (map clojure.string/upper-case)
     shuffle
     distinct)
clojure.string/join

これを

(->> "hello world"
     (map clojure.string/upper-case)
     shuffle
     distinct
     clojure.string/join)

こうしたい。あるいは

(map clojure.string/upper-case "hello world")

これを

(map clojure.string/upper-case) "hello world"

こうして

"hello world"
(map clojure.string/upper-case) 

こうして

(->>)
"hello world"
(map clojure.string/upper-case) 

こう

(->>
    "hello world"
    (map clojure.string/upper-case))

わかった?

(map clojure.string/upper-case "hello world")

(map clojure.string/upper-case) "hello world"

この動きを Barf Forward といい、

(->>)
"hello world"
(map clojure.string/upper-case) 

(->>
    "hello world"
    (map clojure.string/upper-case))

この動きを Slurp Forward という。

) の位置を式(フォーム)に対して伸ばしたり縮めたりする。引数を式から追い出したり取り込んだりする。これによってコードが出来ていく。 この感覚をぼくはゴムのようだと思っている。

あぁそういえば ( ) の多さが気持ち悪いんだっけ? 大丈夫、すぐに見えなくなる。伸ばしたり縮めたりすればいいだけだから。

さて、だいぶ Clojure を書いているときの頭の中がわかってきたかな?言語で呼吸をする感覚はその言語で次にどう動くかが見えたときにわかる。それは単に ( ) を移動させているときだったり、設計を大きく変えるときだったり、色々だけど次の一手を考えたとき自分が動きたいように言語が動いてくれると気持ちがいい。それが合わないとコードは書けない。