ClojureScript + Reagent で 3目並べ (tic tac toe)

React チュートリアルのお題 3目並べを ClojureScript + Reagent で書いてみた。

Reagent とは

ClojureScript から React を利用しやすくするライブラリで、公式のサンプル をざっとみてもらうと、想像しやすいかと思います。

コンポーネントは React のように jsx ではなく、

[:h2.subtitle "Title"]

のように書きます。

これは Clojure のHTML出力用ライブラリ hiccup の記法で、「HTMLってS式で表現できるじゃん」という Lisper 的な発想から出てきた記法だと思います。Clojure だけではなく、 おそらく Common Lisp/Scheme にも、類似のライブラリがあるはずです。

デモページ

https://f6o.github.io/tick/

React チュートリアルに示されている完成サンプルと異なる点があります。

  • ステータスの表示

    引き分けと勝者を表示する

  • 履歴の機能

    履歴を辿ると、その履歴の前のものが全てが消えてしまう

はまったところ

データが更新されたら、まるっとデータが消される罠

(let [data (new-data)]
    [:div.game-info
        この中で data を使う])

というようにコンポーネントの中身を書いていたら、このコンポーネントが更新されたときに、データも空になってしまうという、自分の間抜けな実装に悩まされました。

(new-data) を引数としてコンポーネントに渡す、もしくは defonce でグローバルな変数として持つという形にすれば解決できます。

データ構造の選択について

初めは Map の中に atom を入れる実装にしていました。

{
  :board [(reagent.core/atom "")
          (reagent.core/atom "") ...]
  :status (reagent/atom :in-play)
  ...
}

ただ、先駆者の実装をみてみると、

(reagent.core/atom
    {
        :board ["" ""...]
        :status :in-play
        ...
    })

のように Map 全体を atom としていて、そうすると、値の参照や更新が書きやすくなりました。

具体的にいえば、参照の時は deref が減り、更新の時は (swap! atomic-data assoc :key "SOME VALUE") というパターンに統合されました。ところどころ、reset! はありますが。

結論は、今回の3目並べだと、全体的に atom としても良いと思います。

ただ、ある関数はこの値だけを参照・更新して、他の関数は別の値だけ参照・更新する、というような場合だと、全体的に atom としてしまうのは無駄な気がします。

また、そういう場合だと「なんで 1つのMap に詰め込んでいるんだ!」とデータ構造警察が訪ねてきそうです。

環境づくり

最後に、環境設定についても触れておきます。

1. とりあえず leiningen をインストール

https://leiningen.org

2. プロジェクトの作成

プロジェクトのスケルトンを作ってくれるので、あまり考えずにスタートできるのが、魅力的です。今回バックエンドは不要だったので、フロントエンドのみのスケルトンを使いました。

https://github.com/reagent-project/reagent-frontend-template

$ lein new reagent-frontend <APPNAME>
$ cd <APPNAME>
$ lein figwheel
...
...
あとはコーディングを頑張る

figwheel は ClojureScript の高機能ビルドツールです。

開発サーバをたててくれたり、デバッガ + オートローダ付きだったり、ブラウザ上のアプリとつながった REPL を利用できたりと、多機能過ぎます。

何が内部で起こっているのか、何を使っているのか、など気になるので、lein/project.clj のお作法についても、知っておく必要があります。

3. わからなかったら、Clojurescript のチートシートを参考に

常時タブに開いておきました: http://cljs.info/cheatsheet/

次は何をしようか。

面白い記事があるので、ClojureScript でブラウザ上で動くdiff を実装してみたいと思います。

SICP と Coursera でプログラミングの独習しはじめて6ヶ月くらい経った感想

独習し始めた動機

大学ではC言語でソートを実装する程度の実習だけしかやらず、コンピュータサイエンスをしっかりと勉強していなかった。 しかし、仕事でウェブサイトのテストをやることになり、JavaJavaScriptでテストコードを書く頻度も増えてきたので、 まじめに勉強しなおしてみようということで、定番の教科書 SICP を読むことにした。

計算機プログラムの構造と解釈

SICP Web Site for the Japanese Edition, Structure and Interpretation of Computer Programs 略して SICP

選んだ理由は、読み始めた当初はあまり意識していなかったが、振り返ってみると、以下の3つ。

  • Lisp をやっていたこと
  • 本が手元にあったが、積ん読状態にあったこと
  • 本の名前で検索してみて、多くのブログエントリがあって、本の内容がぼんやりと把握できていたこと。
独習する上で気をつけておくこと

必ず言葉の定義を写経する

最初はテキストだけを追っていっていたが、わからない単語についてメモしていく。 たとえば 遅延評価 という単語が出てくる。 「評価って何?」「何が遅れるの?」のように自問して、調べて、まとめたら、以降で詰まることは少なくなった。 ただ今でも、演習問題で「これどういう定義だったっけ」と読み返すことは多い。

関係ないけど、 オブジェクト志向の「オブジェクト」と、SICP で使われている「オブジェクト」の違いはなんだろうとも思った。

1ステップずつ進めていく

3章にある遅延評価の問題 3.52 を自分が REPL になって進めた。 紙に書くと、受験勉強のときみたいに手が汚れたので、途中でメモ書きにはタブレットでノートアプリを使うようにした。 編集が手軽にできるので、まとめやすい。

f:id:noorbeh:20171227172558p:plain:w180

こうみてみると、数学の問題を解いているようだ。時間はかかるが、言葉の定義と1行1行展開していく「作業」をすると、わからない部分がどこかわかるようになってきた。

演習問題については、後ろの章で問題に出たことが参照されるので、第3章までの問題については、 少なくとも、目を通しておいたほうがよい。あと、上にあげた日本語公式ページには、模範解答への隠しリンクが貼ってある。

SICPに関するエントリ

4章の途中まででも、抽象的に考えることを、徹底的に叩き込まれるので、 ITに関わる人なら、読んでみるといいです。

古い本なんで、多くの方がまとめていて、もっと探せばあると思いますが、 進めるにあたって見つけた記事の中で気に入ったものをいくつか紹介します。

「たしなみ」ではなく「たのしみ」としてのSICP - 思っているよりもずっとずっと人生は短い。 にある次の文にはハッと気づかされたし、賛同できます。

あらゆるプログラムは、本質として言語処理系なのです。

『計算機プログラムの構造と解釈』について はWebアーカイブですが、まさにこういう感想です。

IQの高い人間じゃないと理解できないに違いない、そもそも実用性がないんじゃないか、と思っていた。 が、やり始めてみると、すぐに病みつきになった。

最後は、弱LisperがMITでSICP(シクピー)を受講した結果 - Qiita です。 講義 6.037 - Structure and Interpretation of Computer Programs のサマリーで、ライブ感が伝わってきます。 YouTube の講義動画と一緒にどうぞ。

www.youtube.com

Coursera

SICP のほうもある程度、軌道にのってきたので、並行しつつ、 無料で受けられる Programming Languages というコースを取ることにした。 単位をもらうには、お金を支払う必要があるようだ。

www.coursera.org

Programming Language の構成について

Part. A, B, C に分かれていて、それぞれ ML, Scheme, Ruby でのプログラミング実習を行う。 そのため、各パートの初めの Week は環境設定について行う。 Part A は Week 5, Part B は Week 3まであって、各 Week ごと以下の流れで進めていく。

  1. 講義動画を見る
  2. レジュメを読む
  3. 課題を解き、提出する
  4. 課題が合格点なら、他の人の解答をレビューする

期限は1週間くらいあるけど、早く進めてもいいし、課題の提出が遅れても、採点してもらえる。 課題の採点は自動採点と他の受講者によるレビューの2段階で採点される。

レビューについては、こういう回答は5ポイントで、これができなかったら -1 ポイントのような 基準が課題ごとにあらかじめ決められているので、コードレビューの仕方についても学べるのは嬉しい。

内容について

まだ Ruby のパートは受けてないので詳細はわからないが、メタプログラミングについてやるようだ。 ML パートでは、関数型言語でのプログラミング作法を学び、Scheme パートでは、Scheme で ML でやったこと復習し、Lisp を実装する。 環境設定についてもしっかりレジュメがあり、ML は Emacs + SML/NJ で、Scheme パートは Racket を使用した。

内容的には、ML パートはリスト操作とパターンマッチが印象的だった。Java でも使えるようになると嬉しい。 一般に使われているプログラミング言語(JavaとかCとか)とMLの機能の比較についても触れているので、知っている人はより深く勉強できる。

いま振り返ると、先に Coursera でこのコースを試してみてから、SICP を進めたほうが進度は早かったかもしれない。

今後の予定

今後は、SICP もまだ4章と5章が残っているし、Coursera で残している Ruby のコースも進めたい。 あとは、せっかく Scheme をやったので、Scheme 手習い をやって、 Littile Prover でテストに役立ちそうなことを学びたい。

最後に

まだ独習は続きますが、仕事でコードを書いているときに Java の Stream を抵抗なく 利用できるようになったのは、良いです。

  • 「プログラミングやってみたいけど、どこから始めればいいんだろう」
  • 「〇〇というプログラミング言語を一通り学んでみたけど、次は何にとりかかろう」

というような状態なら、「定番」と世間で呼ばれているものに、飛びついてみてもいいかなと思います。

AtCoder Beginner Contest 062

テストケース実行マクロを導入して、テストも書きやすくなったのはよかった。
でも、A問題の提出のときに、なんども関数呼出をしないコードを提出してしまい、時間をロスしてしまった。
結局、C問題は解けなかったので、過去問を解いてなれていくしかないかな。

テストケース実行マクロについて

いちいち標準入力に入力するのはもったいないので、 動作確認用のユーティリティを定義した。前の defsolver とあわせて使う。

(defsolver solution-a (n)
  (dotimes (i n)
    (format t "~a~%" (1+ (read)))))

(run-testcases
 solution-a
 ((3 1 2 3) . (2 3 4))
 ((2 4 5) . (5 6)))

問題ページから例をコピペして、リストに整形してあげればよくなったので、楽になったと思う。

(defun %run-test (solver input)
  (with-output-to-string (*standard-output*)
    (with-input-from-string (*standard-input* input)
      (funcall solver))))

(defun run-test (solver input expected)
  (let* ((actual (%run-test solver input))
    (samep (equal actual expected)))
    (format t "~:[FAILS~;pass~]~%" samep)
    samep))

(defmacro run-testcases (solver &rest cases)
  `(progn
     ,@(mapcar
    #'(lambda (c)
        `(run-test
          #',solver
          ,(format nil "~{~a~%~}" (car c))
          ,(format nil "~{~a~%~}" (cdr c))))
    cases)))

意識はしていなかったけれど、つくり終えてみれば、 実践Common Lisp のテストフレームワークのような出来になった。 http://www.gigamonkeys.com/book/practical-building-a-unit-test-framework.html

記事をかいてておもったことは、testng の data provider みたいに

(bind-testcases solultion-a
  ...)

(run-testcases solution-a)

とできると、かっこいいかも。