LoginSignup
3
1

More than 5 years have passed since last update.

ClojureScriptでReactチュートリアルやってみた (1)

Last updated at Posted at 2018-09-14

はじめに

※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探索条件を列挙して次から次へとネストを潜ることができるので、
- :squaresstate内のsquares要素を探索して、
- isquaresリストの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を探索して参照します。
- :squaresstate内のsquares要素を探索して、
- isquaresリストの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

続き

こちら

3
1
2

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
3
1