SEAN_BLOG

プログラミング etc.

フロント初心者がReact + Redux でオセロを作ってみた話

普段はRuby をメインで使っているのですが、今年に入ってから少しReact + Redux を触っていました。 と言っても、GW に入るまではReact とRedux の公式Tutorial を写経していたくらいですが、、、

で、GW に入ってから一念発起しまして、完成したのがこの「オセロ」アプリです。 GW 最初の4日間はUdamy のReact + Redux コースをひたすら進めていたので、 実質の実装期間は1.5日です。

取り敢えず、形になりましたので忘れないうちにまとめていこうと思います。

目的

  • React + Redux の初心者を脱する。
    • Tutorial をただ写経するレベルを卒業する。
    • React + Redux の一連のflow を理解する。
  • ECMAScript6 等、新しいJS をキャッチアップする。

と、ここら辺のことを意識して、今回は実装をしました。 本当はAPI 使ってCRUD アプリを作ろうかと思っていたのですが、 色々な葛藤がありまして蓋を開けたら「オセロ」が完成していました。。。

使った技術・ライブラリー等

  • React
    • redux - state 管理をしてくれる人
    • classnames - 複数のclassNames をよしなにまとめてくれるいい奴
    • react-alert - いい感じのalert を表示してくれるいい奴
    • etc...
  • Heroku - 10分ほどでdeploy できるいい奴

versions

$ npm ls react redux classnames react-alert
@0.1.0 /path/to//react/othello
├── classnames@2.2.6
├── react@16.8.6
├── react-alert@5.4.0
└── redux@4.0.1

階層構造

src 以下はこんな感じに仕上がりました。 階層構造に関してはUdamy で写経したアプリの構造を参考にして分けてみました。 CSS は、CSS Module なるものが存在するようなので、見よう見まねで導入してみました。

$ tree othello/src
othello/src
├── actions
│   ├── index.js
│   └── types.js
├── components
│   ├── App.js
│   ├── Board.js
│   ├── Grid.js
│   ├── Header.js
│   └── Label.js
├── index.js
├── reducers
│   ├── boardReducer.js
│   ├── index.js
│   └── playerReducer.js
└── styles
    ├── board.module.css
    ├── grid.module.css
    └── header.module.css

4 directories, 15 files

成果物

DEMO

Image from Gyazo

  • 仕様
    • 新規で石を付与することが出来る。
    • 同色の石に挟まれた石が裏返る。
    • ルール上、石を付与することが出来ないマスをクリックすると、アラートを表示する。
    • Reset ボタンを押下すると、初期状態に戻ることが出来る。

URL

Heroku に上げてありますので、お時間ある方は遊んでみてください。

URL: https://aqueous-mesa-26312.herokuapp.com/

実装内容

オセロ盤の状態とプレーヤー(白 or 黒)の状態をstate として管理しました。 Components はオセロ盤のstate (二次元配列)board[10][10]map し、Grid マス目を表すComponent を表示し描画しました。

詳細は下記ご覧ください。( コード、グチャグチャです、、、m(_ _)m)

github.com

ちなみに、今回の実装の目的は、主にReact + Redux を理解することでした。 その為、オセロ自体のアルゴリズムは下記のサイトを参考に実装しました。 参考というか、コピペです。

大変助かりました、ありがとうございます m(_ _)m

www.cc.kyoto-su.ac.jp

ハマったところ

React + Redux の設計の問題

Udamy で作成したサンプルアプリの見よう見まねで設計してみたのですが、結局正解がわからず。。。 行ったり来たりしてしまいました。 設計に関しては経験が物を言うみたいなのもありそうですが、これくらいの規模のアプリならすんなりと設計できるようにしたいものです。。。

mutable vs. immutable

「state の中身変わってるのにrendering されないやん。」と思ったら、これが原因でした。 勝手にハマって、半日を溶かしました。。。 こちらのブログわかりやすかったです。

blog.logrocket.com

hooks はclass component 内では使えない

対象箇所を別のcomponent に切り出し、そこだけfunction component としました。 いまいち、class component とfunction component のメリット・デメリットがわかりません。 まだまだですね。。。ちゃんと復習します!

まとめ

初めて写経を脱っしReact + Redux でアプリを作成したことで、少しだけ自信がついた気がします。 React のシンタックス、Redux の流れに関しては、まだまだマスターしたとは程遠いものの、理解を深めることが出来ました。 アプリを作成する中で新たなLibrary を発見できたのも、良かったかなぁと。 総じて、当初の目的は達成することが出来たかな、と思います。

React + Redux の初心者を脱する。

「中級者」になれたのかと言われれば、まだまだなれてはおりませんが、、、m(_ _)m

少しだけ前進したと言うことで、お許しください。。。

参考

Note to self: ActiveSupport::TimeZone とString での時間の比較

先日TimeZone 周りでハマったので、備忘用にメモします。

結論

  • Time.zone.parse('2019-01-01')2019-01-01 は異なる。*1
  • ActiveSupport::TimeZoneString での時間の比較は注意が必要。
  • Time.zone.parse は比較対象もTime.zone.parse にするのがbetter。

内容

先日RoR で書かれたとあるコードのリファクタリングを行う機会がありました。 メインロジックの実装を完了し、Rspec でテストコードを実装していました。 その日は珍しく集中力が続き、スラスラとテストコードの実装が進みました。 が、それも束の間

$ bundle exec rspec spec/helpers/test_helper.rb
F

Failures:

  1) TestHelper#on_sale? when the released date is on/past 2019-01-01 should be true
     Failure/Error: expect(on_sale?(album)).to be true

       expected #<TrueClass:20> => true
            got #<FalseClass:0> => false

       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       `expect(actual).to eq(expected)` if you don't care about
       object identity in this example.
     # ./spec/helpers/test_helper.rb:10:in `block (4 levels) in <top (required)>'

Finished in 0.22805 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/helpers/test_helper.rb:9 # TestHelper#on_sale?  when the released date is on/past 2019-01-01 should be true

Rspec が落ちました。 ソースを確認してみると、

module TestHelper
  def on_sale?(album)
    album.released_at >= '2019-01-01'
  end
end
require 'spec_helper'

describe TestHelper do
  describe '#on_sale?' do
    let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) }

    context 'when the released date is on/past 2019-01-01' do
      it 'should be true' do
        expect(on_sale?(album)).to be true
      end
    end
  end
end

*2

Time.zone.parse('2019-01-01') >= 2019-01-01

false ( ´Д`)y━・~~

・・・・・

ペーペーの僕には全く理由が分かりませんでした。

そこで、困った時のbin/rails c です。

$ bin/rails c
pry(main)> t1 = Time.zone.parse('2019-01-01')
=> Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00
pry(main)> string_time = '2019-01-01'
=> "2019-01-01"
pry(main)> t1 >= string_time
=> false

t1ActiveSupport::TimeWithZone クラスで、t2String クラスな訳で、、、

この時の脳内は、

  • Ruby(Rails) でそもそもActiveSupport::TimeWithZoneString は比較できない!?
  • 比較できたとして、t2("2019-01-01") は何時なのか?String の場合00:00:00 じゃない!?

みたいな感じでした。

そこで、引き続きコンソール叩いてみました。

pry(main)> t2 = Time.zone.parse('2019-01-01 23:59:59')
pry(main)> t2 >= string_time
=> true
pry(main)> t3 = Time.zone.parse('2019-01-01 12:00:00')
pry(main)> t3 >= string_time
=> true
pry(main)> t4 = Time.zone.parse('2019-01-01 06:00:00')
pry(main)> t4 >= string_time
=> false
pry(main)> t5 = Time.zone.parse('2019-01-01 09:00:00')
pry(main)> t5 >= string_time
=> true 

この辺りで気がつきました。TimeZone の存在に、、、何やってんだよって感じですよね、本当に。

pry(main)> '2019-01-01'.to_time
=> 2019-01-01 00:00:00 +0000

やっぱり、 +0000 ってなってる。String の場合、UTC(協定世界時)で時間が設定されるようです。 一方、Time.zone.parse('2019-01-01') の方はJST で設定されています。 皆さんご存知のように、JSTUTC に比べて9時間早いので、今回の比較は実際には下記の通りです。

Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00

これでは、true になるはずがありませんね。

そこで、今回は下記の通り変更しました。

module TestHelper
  def on_sale?(album)
-    album.released_at >= '2019-01-01'
+    album.released_at >= Time.zone.parse('2019-01-01')
  end
end
require 'spec_helper'

describe TestHelper do
  describe '#on_sale?' do
    let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) }

    context 'when the released date is on/past 2019-01-01' do
      it 'should be true' do
        expect(on_sale?(album)).to be true
      end
    end
  end
end

この箇所に限らずJST を基本的に使っているので、今回のように急にUTC が出てきて予期せぬバグが出ないようにメインロジックをTime.zone.parse を用いて修正しました。

$ bundle exec rspec spec/helpers/test_helper.rb
.

Finished in 0.35198 seconds
1 example, 0 failures

無事テストもパスしました。( ´∀`)

ちなみに、このTimeZone ですがconfig/application.rb で設定できます。

class Application < Rails::Application
  ...
  config.time_zone = 'Tokyo' # ここ
  ...
end

参考:

api.rubyonrails.org

所感

  • テストを書くことって大事ですね。最近テストコードの重要性をひしひしと感じていますが、今回さらに強く思いました。プログラミング始めたての頃は動けばOK みたいな感じでやってましたが、今思えば恐ろしいことです。
  • Time.zone と何回もタイピングしていながら、TimeZone に気がつかなかったのはとても悔しいです。そして、このエントリーを書きながら自分へっぽこぶりにつくづく嫌になってきました。
  • とっても遠回りをしてしまったけれど、仮説検証のサイクルを回せたのはよかったかなぁと。Lv. 3 からLv. 4 くらいにはなったかなぁと。

*1:Timezone がJST の場合。

*2:大人の事情にてソースコードが実際のものとは異なります。

Ruby kramdown のソースコードリーディング

今回はRuby library のkramdown のソースコードリーディングを行います。 kramdown はMarkdown をparse し、HTML 等任意の形式に変換する事ができるRuby のlibrary です。 普段からMarkdown を書く機会も多く、どの様に実装されているのか気になっておりました。 ということで、今回はMarkdown からどのようにHTML が生成されているのかといった箇所を中心に見ていきたいと思います。

目的

  • Markdown をHTML に変換する箇所の実装

基本情報

Version

コード量

lib 配下のコードは5,830 ですね。前回のactive_hash が800 ちょっとでしたので、単純計算すると7倍ほどあります。。。 最初から最後まですべて読んでいくのは大変そうなので、要点を絞って見ていきたいと思います。

$ cloc ./kramdown/lib
      64 text files.
      64 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.80  T=0.07 s (869.0 files/s, 121110.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Ruby                            64           1171           1919           5830
-------------------------------------------------------------------------------
SUM:                            64           1171           1919           5830
-------------------------------------------------------------------------------

階層構造

$ tree ./kramdown/lib
./kramdown/lib
├── kramdown
│   ├── converter
│   │   ├── base.rb
│   │   ├── hash_ast.rb
│   │   ├── html.rb
│   │   ├── kramdown.rb
│   │   ├── latex.rb
│   │   ├── man.rb
│   │   ├── math_engine
│   │   │   ├── itex2mml.rb
│   │   │   ├── katex.rb
│   │   │   ├── mathjax.rb
│   │   │   ├── mathjaxnode.rb
│   │   │   ├── ritex.rb
│   │   │   └── sskatex.rb
│   │   ├── pdf.rb
│   │   ├── remove_html_tags.rb
│   │   ├── syntax_highlighter
│   │   │   ├── coderay.rb
│   │   │   ├── minted.rb
│   │   │   └── rouge.rb
│   │   ├── syntax_highlighter.rb
│   │   └── toc.rb
│   ├── converter.rb
│   ├── document.rb
│   ├── element.rb
│   ├── error.rb
│   ├── options.rb
│   ├── parser
│   │   ├── base.rb
│   │   ├── gfm.rb
│   │   ├── html.rb
│   │   ├── kramdown
│   │   │   ├── abbreviation.rb
│   │   │   ├── autolink.rb
│   │   │   ├── blank_line.rb
│   │   │   ├── block_boundary.rb
│   │   │   ├── blockquote.rb
│   │   │   ├── codeblock.rb
│   │   │   ├── codespan.rb
│   │   │   ├── emphasis.rb
│   │   │   ├── eob.rb
│   │   │   ├── escaped_chars.rb
│   │   │   ├── extensions.rb
│   │   │   ├── footnote.rb
│   │   │   ├── header.rb
│   │   │   ├── horizontal_rule.rb
│   │   │   ├── html.rb
│   │   │   ├── html_entity.rb
│   │   │   ├── line_break.rb
│   │   │   ├── link.rb
│   │   │   ├── list.rb
│   │   │   ├── math.rb
│   │   │   ├── paragraph.rb
│   │   │   ├── smart_quotes.rb
│   │   │   ├── table.rb
│   │   │   └── typographic_symbol.rb
│   │   ├── kramdown.rb
│   │   └── markdown.rb
│   ├── parser.rb
│   ├── utils
│   │   ├── configurable.rb
│   │   ├── entities.rb
│   │   ├── html.rb
│   │   ├── lru_cache.rb
│   │   ├── ordered_hash.rb
│   │   ├── string_scanner.rb
│   │   └── unidecoder.rb
│   ├── utils.rb
│   └── version.rb
└── kramdown.rb

使い方

使い方は非常にシンプルです。 まずは、gem をインストール。

$ gem install kramdown
require 'kramdown'

doc = Kramdown::Document.new('This *is* some kramdown text')
puts doc.to_html
# Output
#=> <p>This <em>is</em> some kramdown text</p>

Markdown をHTML に変換したい場合は、上記sample のようにKramdown::Documentインスタンスを作成します。 この時、変換したいtext を引数に指定します。 あとは、生成したKramdown::Document に対して、to_html メソッドを呼んであげるだけです。

非常に簡単ですね。

実装内容

Markdown をHTML に変換する箇所の実装

Kramdown::Document class に、to_html メソッドが定義してあって、to_html メソッドの中でMarkdown からHTML への変換処理が行われているものだと思っておりました。

#lib/kramdown/document.rb
def method_missing(id, *attr, &block)
  if id.to_s =~ /^to_(\w+)$/ && (name = Utils.camelize($1)) &&
      try_require('converter', name) && Converter.const_defined?(name)
    output, warnings = Converter.const_get(name).convert(@root, @options)
    @warnings.concat(warnings)
    output
  else
    super
  end
end

しかし、実際にソースを覗いてみるとto_html メソッドは存在せず、BasicObjectmethod_missing メソッドをoverride することで、HTML への変換処理を実現していました。 method_missing は、メソッドが呼び出された時に継承チェーン内に該当するメソッドが定義されていなかった時に呼ばれるメソッドです。 つまり今回のケースでは、Kramdown::Documentインスタンスの継承チェーンでは、to_html メソッドが未定義、その結果method_missing が呼ばれるという流れになっています。 このような実装になっているのは、Kramdown がHTML 以外の出力形式にも対応していることに起因するのかと思います。 出力形式ごとにto_~ メソッドを定義するのではなく、一旦method_missing を呼び出しその後の処理を決定しています。 あまり詳しくありませんが、この辺りはメタプログラミングと呼ばれるような分野なのでしょうか??? 近く、メタプログラミングについても学んでみたいと思います。

具体的にMarkdown をHTML に変換している箇所は、lib/kramdown/converter/html.rbceonver_~ のメソッドでした。 それぞれ要素によって~ の部分を動的に変更し、convert~ を呼び出し、Markdown からHTML に変換しています。 恐らくこれもメタプログラミングってやつなんですかね。。。

気になった箇所・メソッド

まとめ

to_htmlto_~ をそれぞれ定義するのではなく、BasicObjectmethod_missing をoverride することで出力形式別に処理を出し分けていました。 恐らくこれがメタプログラミングというやつですね。これを機にメタプログラミング勉強してみたいと思います。

結果的にMarkdown からHTML への変換箇所の実装というより、出力結果による処理の出しわけ箇所を中心に読み進めてしまいました。。。 次回は脱線しない様に気をつけながらソースコードリーディングを行いたいと思います。

この前誰かが「Commit message に注目した、ソースコードリーディングも面白い。」と、言って気がするので、そのうち挑戦してみたいと思います。 確かにこの方法だと実装した人がどの様な目的で当該の機能を実装したのかがわかるので面白そうです。

それでは、本日はこの辺で!

Ruby ActiveHash のソースコードリーディング

ActiveHash は、Ruby のHash をActiveRecord-like のモデルとして使用するためのbase class です。 ./lib 以下のコード量は、800行程度ですので決して多くはないと思います。 コードリーディングをするのは初めてですので、このぐらいの量がちょうど良いかなと思いました。

目的

  • ActiveRecord-like にHash を扱えるようにしている実装
  • ActiveYaml 読み取りの実装

基本情報

Version

active_hash v2.2.0(18/11/22)

コード量

$ cloc ./active_hash/lib/
      11 text files.
      11 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.80  T=0.03 s (395.3 files/s, 36405.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Ruby                            11            190              5            818
-------------------------------------------------------------------------------
SUM:                            11            190              5            818
-------------------------------------------------------------------------------

階層構造

他の人のブログを読んでいるときに、階層構造見るたびに「描くの大変そうだなー。」と思っておりました。 皆さん恐らくtree コマンド使ってたのですね、、、勉強になりました。。。

$ tree ./lib/
./lib/
├── active_file
│   ├── base.rb
│   ├── hash_and_array_files.rb
│   └── multiple_files.rb
├── active_hash
│   ├── base.rb
│   └── version.rb
├── active_hash.rb
├── active_json
│   └── base.rb
├── active_yaml
│   ├── aliases.rb
│   └── base.rb
├── associations
│   └── associations.rb
└── enum
    └── enum.rb

    6 directories, 11 files

使い方

# active_hash 無し
# ./app/models/person.rb
class Person < ActiveRecord::Base
  COUNTRIES = ['US', 'Canada']
end

# in some view
<%= collection_select :person, :country_id, Person::COUNTRIES, :to_s, :to_s %>
# active_hash 有り
# ./app/models/country.rb
class Country < ActiveHash::Base
  self.data = [
    {id: 1, name: 'US'},
    {id: 2, name: 'Canada'}
  ]
end

# in some view
<%= collection_select :person, :country_id, Country.all, :id, :name %>

このように、Ruby のハッシュで書いたものをActiveRecord-like に利用することができます。

下記のようにすれば、クラスの外側でもデータを設定することもできます。

# ./app/models/country.rb
class Country < ActiveHash::Base
end

# ./config/initializers/data.rb
Rails.application.config.to_prepare do
  Country.data = [
      {id: 1, name: 'US'},
      {id: 2, name: 'Canada'}
  ]
end

データを呼び出したいときは、ActiveRecord を呼び出すときのように呼び出せます。

Country.all                    # => returns all Country objects
Country.count                  # => returns the length of the .data array
Country.first                  # => returns the first country object
Country.last                   # => returns the last country object
Country.find 1                 # => returns the first country object with that id
...

ちなみに、詳細は省きますがHash だけではなく、YAMLJSON 等の形式でもデータを格納できます。

詳細: README.md

実装内容

ActiveRecord-like にHash を扱えるようにしている実装

ActiveRecord-like なので、当然といえば当然なのですがDB 保存の処理は全く有りません。 その代わり、インスタンス変数にHash の内容を格納しているようです。 全くわかりませんが、本家のActiveRecord もDB 処理を除いては同じような実装なのでしょうかね??? また今度、ActiveRecord の方のコードリーディングにも挑戦してみたいと思います。

ActiveYaml 読み取りの実装

require 'yaml'

module ActiveYaml

  class Base < ActiveFile::Base
    extend ActiveFile::HashAndArrayFiles
    class << self
      def load_file
        if (data = raw_data).is_a?(Array)
          data
        else
          data.values
        end
      end

      def extension
        "yml"
      end

      private
      def load_path(path)
        YAML.load(ERB.new(File.read(path)).result)
      end
    end
  end
end

先日、YAML から読み取ったデータの処理を実装したばかりなので気になっていた箇所です。 ActiveYaml::Base でModule ActiveFile::HashAndArrayFilesextend する形でデータの読み取りを行なっています。 Module ActiveFile::HashAndArrayFilesYAMLJSON 共通の処理をまとめたという感じなのでしょうかね??? まだまだ設計的な話は全くわかりません。。。

気になった箇所・メソッド

まとめ

初めてのコードリーディングでどこから始めていいのか全くわかりませんでした。全体的な進め方に関しては、nipe0324さんのコードリーディングを参考にさせていただきました。少しずつ自分なりのコードリーディングのやり方を固めていきたいです。

ソースコードに関してだけでなく、cloc コマンドやtree コマンドに関しても知る機会になり、よかったと思います。

次回はクラス図の作成にも挑戦して見たいと思います。一つ一つできることを増やしてきたいと思います!