はじめに
※9/14 頂いたコメントをもとに更新
諸事情によりClojureScriptに入門する事になりましたので、こちらの記事
https://qiita.com/lagenorhynque/items/7c049f3c3b967ee777ac
を参考に馴染み深いReactチュートリアルに挑戦してみました。
ほとんど個人的な備忘録なのでかなり適当です。
まずは、公式チュートリアルの「Declaring a Winner」の章まで進めてみました。
全ソースはこちら。
1. クラス型コンポーネントの基本形
- 基本形
(defn コンポーネント名 []
(let [state (reagent.core/atom {:キー 値})]
fn []
(letfn
[(ローカル関数)]
[Hiccup風のDSL]))
- 実装
(defn square [{:keys [on-click value]}]
[:button.square
{:on-click #(on-click)}
value])
(defn calculate-winner [squares]
(let [lines (vec [[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[0, 3, 6]
[1, 4, 7]
[2, 5, 8]
[0, 4, 8]
[2, 4, 6]])]
(reduce (fn [_ [a b c]]
(if (and (not= (squares a) "")
(= (squares a) (squares b))
(= (squares b) (squares c)))
(reduced (squares a))
nil))
nil
lines)))
(defn board []
(let [state (r/atom {:squares (vec (repeat 9 ""))
:x-is-next? true})]
(fn []
(letfn
[(handle-click [i]
(let [x-is-next? (get @state [:x-is-next?])
squares (get @state :squares)]
(when (and (= (calculate-winner squares) nil) (= (squares i) ""))
(swap! state assoc-in [:squares i] (if x-is-next? "X" "O"))
(swap! state assoc [:x-is-next?] (not x-is-next?)))))
(render-square [i]
[square {
:value (get-in @state [:squares i])
:on-click #(handle-click i)
}])]
(let [status (if (= (calculate-winner (get @state :squares)) nil)
(str "Next player: " (if (get @state [:x-is-next?]) "X" "O"))
(str "Winner: " (calculate-winner (get @state :squares))))]
[:div
[:div.status status]
[:div.board-row
(render-square 0)
(render-square 1)
(render-square 2)]
[:div.board-row
(render-square 3)
(render-square 4)
(render-square 5)]
[:div.board-row
(render-square 6)
(render-square 7)
(render-square 8)]])))))
う〜ん、見慣れない記法ばかりで圧倒されますね。
各要素の意味についてひとつひとつ噛み砕いていきましょう。
1.1. HicCup風のDSL
こういう感じ
https://github.com/weavejester/hiccup/wiki/Syntax
でJSX表現をLisp的表現に置き換えていく。Hiccupが何なのか分かってないですが。
慣れるとすごく書きやすそう。
[Hiccup風のDSL]
1.2. ローカル関数
letfn
を用いてローカル関数とそのスコープを定義する事ができます。
ローカル関数が複数ある場合は単純に[]
内に列挙していきます。
(letfn
[(ローカル関数)]
[Hiccup風のDSL])
1.3. ローカル変数
let
を用いてローカル変数とそのスコープを定義する事ができます。
適用先のスコープは関数である必要があるらしいので、無名関数(fn [])
を設定しています。
(let [state (reagent.core/atom {:キー 値})]
fn []
(letfn
[(ローカル関数)]
[Hiccup風のDSL])
ここではローカル変数とはstateの事です。
ClojureScriptは純粋なイミュータブルなので、一回設定した値を変更する事は基本的には認められません。
従って、reagent.core/atom
を使って変更可能なプロパティを設定してあげます。
1.4. グローバル関数
コンポーネントクラスはdefn
を用いて名前空間内のグローバル関数の形で記述します。
(defn コンポーネント名 []
(let [state (reagent.core/atom {:キー 値})]
fn []
(letfn
[(ローカル関数)]
[Hiccup風のDSL])
ふう、基本形ができあがりました。
2. ClojureScriptコーディングあれこれ
単純にClojureScriptとして学んだ事を書いていきます。
2.1. しんどいstate更新
イミュータブルなので「配列のi番目を更新したいな〜arr[i] = 2
」みたいな安易な記述は許されません。
@
でderef
したりreset!
したりswap!
するらしいです。
この記事が参考になりました。
https://gist.github.com/kohyama/6076544#atom
@
or deref
: referenceを参照する
reset!
: 上書き
swap!
: 関数を適用して再代入
reset!
やswap!
で変更できそうですね。実装を読み解いて行きましょう。
(swap! state assoc-in [:squares i] (if x-is-next? "X" "O"))
assoc-in
はオブジェクト(state
)に対して探索条件([:squares i]
)で更新箇所を探索して更新値((if x-is-next? "X" "O")
)を設定します。
assoc-in
探索条件を列挙して次から次へとネストを潜ることができるので、
-
:squares
でstate
内のsquares
要素を探索して、 -
i
でsquares
リストのi
番目の要素を探索しています。
こういったオブジェクト探索は実プロダクトでは頻発する、かつコードが冗長化しがちですが、かなりスマートに書けますね。この手の配列操作(map
とかreduce
とか)はJavaScript ES6にも採用され始めていますが、やっぱりLispが始祖なんですかねえ(徐々に信徒の目に変わっていく)。この辺を使いこなせれば気持ちよくなっていけそうです。
2.2. しんどいstate参照
[square {
:value (get-in @state [:squares i])
:on-click #(handle-click i)}]
- value
value={this.state.squares[i]}
を書きたい
assoc-in
のようにget-in
で探索条件設定してstate
を探索して参照します。
-
:squares
でstate
内のsquares
要素を探索して、 -
i
でsquares
リストのi
番目の要素を探索しています。 -
on-click
onClick={() => this.handleClick(i)}
を書きたい
また、#
は無名関数の表現で、コールバックを渡したいときに使っているみたいです。
2.2. Collectionのデータ型
{:squares (vec (repeat 9 ""))}
repeat
で""
が9個格納された配列のようなもの(遅延シーケンス)を生成しますが、どうやらJavaScriptにおける配列と型が違うというか、associativeなデータ型にしないとassoc-in
などで操作ができないため、vec
でベクターにしています。
データ型についてはこちらを参照。まだ慣れていないので繰り返し見ていこうと思います。
http://freak-da.hatenablog.com/entry/2015/08/05/222224
2.3. console.log
(js/console.log (pr-str value))
やっぱりデバッグではconsole.log
のお世話になるみたいです。JavaScriptの基本ライブラリにアクセスするようにはこのようにすればいいみたいです。
2.4. reduce, reduced
Vanilla JS
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
ClojureScript
(reduce (fn [_ [a b c]]
(if (and (not= (squares a) "")
(= (squares a) (squares b))
(= (squares b) (squares c)))
(reduced (squares a))
nil))
nil
lines)))
cljsでfor
のような安易な配列操作は好まれない(?)ようです。reduce
等を使ってスマートに操作しましょう。
reduce
は第1引数に関数、第2引数に初期値、第3引数にコレクションを設定します。第3引数のコレクションから1つずつ要素を取り出し、第1引数の関数で処理していきます。第1引数の関数は、第1引数に前回の処理結果、第2引数にコレクションから取り出した値を引き受けます。0番目のコレクション要素の処理のとき、前回の処理結果は存在しないため第2引数の初期値を使用します。
この例では、第3引数のlinesをひとつひとつ処理するのですが、第1引数の関数の第1引数は_
となっております。これはUnused Argumentといって、使用しない引数に対して使います。こうする事で、単純にコレクションから要素をひとつひとつ取り出して検証する処理を記述できます。
また、reduce
ですが、第1引数を返却値としてreduce
処理をbreakします。
こうする事で、条件に該当する要素が見つかったら処理を打ち切ることができます。
#3. 知識というか概念的なところ
##3.1. イミュータブルって
すごいいい記事見つけたと思ったらshinpeiさんだった
https://nekogata.hatenablog.com/entry/2013/06/15/013752