omohayui blog

おも‐はゆ・い【面映ゆい】[形][文]おもはゆ・し[ク]《顔を合わせるとまばゆく感じられる意》きまりが悪い。てれくさい。

Life in Christchurch

早いもので、私がクライストチャーチの学校へ通うのもラスト1日となってしまいました。まだまだ勉強足りてないし、もっとここにいたいなぁという思いを綴ります…

Weekends

the crater rim walk

クライストチャーチの南に位置する巨大な火口跡の縁がクレイターリム。
個人でツアーガイドをしている Nicole さんに連れて行って貰いました。
幸いなことにものすごく良いお天気で、ハイキングする人、トレイルランニングする人、マウンテンバイクで登る人たちで賑わってました。
普段はそんなに混んでないらしいです。あと羊も人間以上にたくさんおりました。 f:id:omohayui:20191024175042j:plain 途中でランチ食べたり休憩も挟みながら約3時間ほど海辺から頂上までぐるっと歩き周りました。
素晴らしい景色だったのですが、多分写真だけでは伝わらないです…

C1 Espresso

チューブスライダーでフライドポテトやバーガーが運ばれてくるので有名な何とも不思議なカフェです。 f:id:omohayui:20191024180425j:plain f:id:omohayui:20191024180223j:plain クライストチャーチにはたくさんカフェがあり、地元の人たちもコーヒー好きが多いのですが、スタバは1店舗しかありません。
皆それぞれお気に入りのカフェがあってシアトル系にはあまり興味がないのかもしれないです。
私がたまたまスタバに通りかかった時は謎の Happy Hour でフラペチーノが半額でした。

The Botanic Gardens

クライストチャーチには広大な公園がたくさんあるのですが、中でも Hagley Park は巨大で、その中心に Christchurch Botanic Gardens があります。
f:id:omohayui:20191024181447j:plain 昼間表に出ているのは珍しいという野生のハリネズミを見かけました。めっちゃ可愛かったです。 あとニュージーランドきてから予想以上にジーランスが捕まらないのですが、ここには水辺があるので1匹だけ捕獲しました。

Christchurch Art Gallery

ニュージランドで活躍する現代アーティストの作品が多かったのですが、中でもマウイのアートが面白かったですね。
独特の文化とスピリチュアルな表現に惹かれました。 christchurchartgallery.org.nz

Weekdays

School

New Zealand への短期留学 - omohayui blog の「授業について」に大体のことは書いたのですが、毎週のように生徒や先生が変わっていくので結構目まぐるしい学校生活だなぁと感じる今日この頃です。
先週まではコロンビアやブラジルからきたクラスメイトがいたのですが、授業が自分と合わないとか希望を出すと次の週から他のクラスに移籍できたりして、今週はチリやアルゼンチンから来たクラスメイトが増え、といった感じでなかなか面白いです。
最初は先生の話が聞き取れなくて辛いこともあったのですが、慣れでそこは大分改善しました。
が、相変わらず話すのは苦手です(汗) 私がいつも一緒にランチしたりしている友達は台湾からきた姉妹で、私より英語レベルが上なんですが、拙い英語でも暖かい眼差しで聞いてくれます。
明日は車を出してくれて、ずっと私が行きたかった New Brighton Library に連れてってくれる予定です(涙)

Pokemon Go

びっくりするほどポケゴーやってる人がいなくて全然レイドできてないです。
って話を日本人のクラスメイトにしたら、Facebookのグループに Pokemon Go Canterbury なるものがあってそこに入るとたまにレイドの集まりとかあるらしいです。
もっと早く知りたかったと思いながら参加申請しました…

Next

今週末には日本に帰らなければなりません・・・新たな会社で仕事が待っております。
ですが本当にクライストチャーチが良いところすぎて、また戻ってくることを真剣に考えてしまう今日この頃です。
No Snake, No Mosquito が嬉しいし、町中が綺麗で環境意識高いし、治安もいいし、地元の人たちもフレンドリーだし。
日本で働きながら英語の勉強はもちろん続けるつもりだし、また成長したところを見せに戻りたいなぁ。

New Zealand への短期留学

何しにニュージーランドへ?

今年の4月ぐらいから英語話せる&聞き取れるようになりたい…!
と思ってオンライン英会話を(1日25分のやつ)を毎日続けてはいたのですが、
本当に簡単な会話ぐらいしかできていなくて、どうしたらもっと上達するだろうかということを考えた結果、 英語を使う会社に転職して強制的に話す環境に身を置くことだ!という結論に至り、
5年弱務めた某D社を退社することにしました。
で、11月からは半分以上が foreigners という環境で働くことになったのですが、
有休消化1カ月というロングバケーションを手に入れた私は真っ先にニュージーランドへの短期留学を決意しました。

なぜニュージランド?

私がポケモントレーナーだと言うことをご存知の方はもうお気づきだろう。
ご存知ない方は、 フロリダでヘラクロスとケンタロスとサニーゴを捕まえたときの話 - omohayui blog を一読してほしい。(※しなくていいです)
選定時まだ私が手に入れていない地域限定はパチリスジーランス
カナダという選択肢もありましたが、良さげな学校があるバンクーバートロントモントリオールは緯度的にパチリスが出ないという独自の調査結果が出たので、ジーランス狙いで候補地はニュージランドになりました。
(ニュージランドで働きたいが為に職を捨てて同じ学校に来てる人たちもいるので、この話は学校では絶対にできません。)

f:id:omohayui:20191011192102j:plain

現在10月ですが、ニュージーランドは丁度桜の季節です。
朝と夜は結構肌寒いです。

なぜクライストチャーチ

ニュージランドと言えばオークランドの方が都会でたくさん語学学校があるのですが、
クライストチャーチは自然に溢れたとても綺麗な街で、
私が通っている語学スクールは広大な敷地を持つカンタベリー大学の構内にあり、 大学内の図書館やカフェテリアが使えちゃうんです。

f:id:omohayui:20191011192306j:plain

カンタベリー大学の敷地内には広大なラグビー場があります。
(筑波大にいたときと雰囲気似てるんだけど、なんか一緒にしてはいけないぐらい洗練された感じ…笑)

授業について

まず初日にオリエンテーションとペーパーテストと interview を受けます。
それで、その人のレベルに応じてクラス分けがされて、翌日から10~14人程度のクラスに入って授業を受けます。
私のクラスメイトは日本・中国・タイ・韓国といったほぼアジア系で和気あいあいとした雰囲気。
グループワークかペア作業が多めで、先生の人柄のおかげだと思うけどとても楽しいし面白いです。
明らかに私が最年長な訳ですが、案外若者たちと一緒に混じってやれてしまうものです。
クラス替えのテストは5週間に一度行われます。

また、授業とは別に毎週3時間分の e-Learning が課せられます。
これは Google Docs で writing したものや Canvas Student というアプリを使って録音したものを提出するホームワークです。
期日内に提出すれば先生からレビューが貰えます。

放課後はクラスとは別に Conversation Club や Movie 鑑賞、スポーツのアクティビティに任意参加します。
ちなみに今日は Movie の日で、みんなでスナックつまみながら Bridget Jones を英語字幕付きで観ました。

ホームステイについて

homestay か flatting (シェアハウスの寮版みたいな?) を選びました。
家でも英語を使うようにする為に homestay がおすすめです。
私のホストファミリーはめちゃくちゃ優しくて、すぐスーパーマーケットに行くのに車を出してくれたり、
kiwi が見たいと話してたら、Willowbank という野生保護区の動物園連れて行ってくれました。
f:id:omohayui:20191011200330j:plain host mother はタイ出身で彼女の作ってくれたマッサマンカレーパッタイはめちゃくちゃ美味しかった…
host father はNZの方でビーフハンバーガーとかスパゲティを作ってくれるんだけどそれもとても美味しいのです…
しかも家は学校から徒歩10分。School Assistant には You're very lucky! って言われました。

週末の予定

明日はまだ行ってないクライストチャーチの City Central (市街地の方) に行ってみる予定です。
オススメのカフェをクラスメイトに聞いたら C1 って言ってたので、そこと、修復中の大聖堂とカードボード・カセドラルかな。
続きはまた来週…

Code Jam to I/O for Women 2019

なにそれ?

codingcompetitions.withgoogle.com

Google が毎年開催している、女性向けの online coding contest です。
スコアがTOP150位内にランクインすると Google I/O へのチケットと旅費が賞品として授与されます。

2019 は?

今年は2月に開催されたのですが、Google I/O の話題で盛り上がりつつある昨今、
ふと思い出して今更ながらまとめておこうと思います。
去年はスケジュールの都合で参加できず、今年初めて参加したのですが、 結果としては散々で、世界の壁の厚さを思い知ることに・・・
ランキングを見るとロシアや中国の方が強かったですね、日本人の方も1人ぐらい150以内に入っていた気がするけど先進国の中ではワーストなのでは...ってぐらい少なかったです。

参加方法は?

毎年、開催の1ヶ月程度前から公式サイトで事前登録が始まります。
Google アカウントがあれば誰でも登録できます。
(特に女性であることの証明みたいのはなかった)

事前準備は?

過去問を解く

とりあえず、チュートリアルだけでも絶対やっておいたほうがよいです。
Code Jam のサイトでは、自分で書いたコードがテストケースをクリアするかどうかオンライン上で即時に判定されます。
毎回問題の内容自体は異なりますが、テストケースの入力形式とテスト結果の出力形式は一緒です。
このテストケースを読み込み、テスト結果を出力する部分は予め準備しておけるわけです。

テストケースとテスト結果の input / output 形式

テストケースは標準入力(STDIN)としてテストコードに渡ってきます。

  • 1行目がテストケースの数(1 ≤ T ≤ 100.)
  • 2行目以降が検証する要素の数と検証する要素の値

(例)

3                # テストケースの数
5                # Case1 の要素数
0 2 1 1 2        # Case1 の値
1                # Case2 の要素数
0                # Case2 の値
6                # Case3 の要素数
2 2 2 2 2 2      # Case3 の値

テスト結果は標準出力(STDOUT)としてアウトプットする必要があります。
そしてこれも出力形式が決まっています。

(例)

Case #1: 2              # Case1 のテスト結果
Case #2: 0              # Case2 のテスト結果
Case #3: 10              # Case3 のテスト結果

実際に解いてみる

2018年のハンバーガーの具材を最適化しようという課題を試しに解いてみましょう。
Burger Optimization

プログラミング言語はプルダウンから自分の好きな言語を選べます。

f:id:omohayui:20190503183330p:plain
select programming language at Code Jam

※ 参加した時は、プルダウンに Perl がなかったのですが、今見ると対応言語が増えているようで...

Go で書いてみる

つっこみどころ満載のコードですが晒します。
ローカルで実装しながら検証するときはテストケースを外部ファイルとして作っておくと便利です。
あとは go run コマンドで実行するだけ。
(限られた時間で競うときこそコンパイルが速い go にすべきじゃない?)

$ go run main.go < input_file.txt
package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "sort"
    "strconv"
)

var s = bufio.NewScanner(os.Stdin)

func main() {
    s.Split(bufio.ScanWords)
    t := nextInt() // test case 数

    for i := 1; i <= t; i++ {
        k := nextInt()       // burger の中の要素数
        ds := make([]int, k) // バンズまでの距離のslice
        bs := make([]int, k) // 最適なバンズまでの距離のslice
        for j := 0; j < k; j++ {
            ds[j] = distance(k, j) // バンズまでの距離
            bs[j] = nextInt()      // 最適なバンズまでの距離
        }

        // 最小誤差を出すために並び替える
        sort.Sort(sort.IntSlice(ds))
        sort.Sort(sort.IntSlice(bs))

        es := 0 // 誤差合計
        for j := 0; j < k; j++ {
            d := square(ds[j] - bs[j]) // 誤差
            log.Print(d)
            es += d
        }
        fmt.Printf("Case #%d: %d\n", i, es)
    }

    if err := s.Err(); err != nil {
        log.Fatal(err)
    }
}

func nextInt() int {
    s.Scan()
    i, e := strconv.Atoi(s.Text())
    if e != nil {
        panic(e)
    }
    return i
}

// 次乗
func square(x int) int {
    return x * x
}

// バンズからの距離
func distance(k, j int) int {
    d := k - 1
    if (d - j) < j {
        return d - j
    }
    return j
}

標準入力の取得は os パッケージ(当たり前だけど)、 os.Stdin から io.Reader を一行ずつ読み込むのには、bufio.NewScanner を使いました。
※ bufio.NewScanner はスキャンできる一行のサイズに上限があるので、テストケースによっては使えないことがあるかもしれませんが、
Code Jam の課題ではここを超えてくるテストケースには出会いませんでした。

あとよく使うであろう、nextInt() や square() も予め用意しておいたほうがよさげです。

Perl で書いてみる

この記事を書こうとして Perl がサポートされていることに気づき Perl版も作ってみました。
(書いて気づいたけど、Perl の方がコード量半分で済むし、そもそもコンパイル要らないよ...って)

use strict;
use warnings;
use utf8;

my $t = <STDIN>; # test case 数

for (my $i = 1; $i <= $t; $i++) {
    my $k = <STDIN>; # burger の中の要素数
    my @bs = split(/ /, <STDIN>); # 最適なバンズまでの距離の配列
    my @ds;                        # バンズまでの距離の配列
    for (my $j = 0; $j < $k; $j++) {
        push @ds, distance($k, $j); # バンズまでの距離
    }

    # 最小誤差を出すために並び替える
    @ds = sort { $a <=> $b } @ds;
    @bs = sort { $a <=> $b } @bs;

    my $es = 0; # 誤差合計
    for (my $j = 0; $j < $k; $j++) {
        my $d = ($ds[$j] - $bs[$j]) ** 2; # 誤差
        $es += $d;
    }
    printf("Case #%d: %d\n", $i, $es);
}

# バンズからの距離
sub distance {
    my ($k, $j) = @_;
    my $d = $k - 1;
    return $d - $j if ($d - $j) < $j;
    return $j;
}

当日のハプニング

2019年の競技開始は何やらトラブルがあったようで何度も遅れました・・・ 日本時間 AM0:00 開始予定だったのが直前に 30min 遅れになり、さらにまた 30min 延長、さらに... といった感じで眠気との戦いが大変でした。

感想

来年こそはチケットと旅費を手に入れるぞ!(※会社に出してもらうのではなく)

How to upload a module to CPAN

Why?

とある既存の CPAN module に修正を加える必要あり、PullReq を作成して送ったところ、
自分でアップロードしてみたらと言われ、アップロードを試み、結果たくさんの反省点があったので、
一から自分で作った module をアップロードすることにしました。

Process

Step1: What should I make as a module?

perlnewmod - 新しいモジュールを配布するには - perldoc.jp

とりあえず、CPANアップロードのリベンジをしたいという思いで、以前実装したコードを module に切り出してしまったので、
ここに記載あるように「必要性の議論」はできていないです。(ごめんなさい)
作成したのは、FCM の API v1 向け Client です。
必要だったのは FCM の API Client というより OAuth 2.0 アクセストークンを使った Google API Client でした。
ServiceAccount の json ファイルをそのまま使ってやりたいことをできるものが当時見つからず。
Go で書かれている google api client のソースコードを読んで perl に置き換えることにしたのです。

github.com

Step2: Create a module

今回、 Minilla というオーサリングツールを使いました。

% minil new WWW-FCM-HTTP-V1

するだけで、アップロードまでに必要なものは勝手に作ってくれます。

% ls -l WWW-FCM-HTTP-V1
 Build.PL
 Changes
 LICENSE
 META.json
 README.md
 cpanfile
 lib/
 minil.toml
 t/

lib/ と t/ ディレクトリにそれぞれソースコードを置いて minil test を行うと、
META.json や README.md も更新してくれます。

Step3: Create PAUSE ID

まず、 module ができても、 PAUSE のアカウントがないと CPAN にはアップロードできません。

PAUSE: The CPAN back stage entrance から登録を行います。

そしてここから申請してもすぐアカウントが自動作成されるわけではなく、
手動で作成される (!) ので、1週間ぐらいはかかりました。

おそらく、 "A short description of why you would like a PAUSE ID:" の内容を人の手でちゃんとチェックしているのかなと・・・

アカウントが作成されると「Welcome new user OMOHAYUI」というメールが届きます。
別のメールで仮パスワードが送られてくるので、そちらでログインし、パスワードを変更します。

ここで、 metacpan.org のアカウントと PAUSE ID の連携を試みるのですが、
現在ここで問題が起きていて、連携用コードが PAUSE アカウントに設定しているメールに届きません。。。
これができなくても CPAN にはアップロードできるのですが、
アイコンが設定できなかったり、いいねができなかったりちょっと寂しいものがあります。

Can't associate with PAUSE · Issue #2048 · metacpan/metacpan-web · GitHub この issue が resolve されれば・・・

Step4: Setting pause_config

Minilla でリリースする際には cpan-upload を利用する為、
.pause ファイルを home directory に配置しておく必要があります。
pause にログインする際の user名とパスワードを記載します。

user OMOHAYUI
password *********

Step5: Release

Minilla でリリースする場合は、

minil release

とコマンドを打つだけです。

test と Changes の更新、 Github への commit & push CPAN アップロード までやってくれます。 アップロード後、しばらくすると upload と indexer report のメールが届いて完了です。

補足として、Changeログの更新時は、version と更新日時は自動で追加されるので追記不要です。

{{$NEXT}}
    - next version

0.01 2019-03-31T16:07:40Z

    - original version

Step6: Update modules

自分の場合は、翌日ぐらいに CPAN Testers から Daily Summary Report が届きました。
テスト結果としては、 MSWin32-x86 環境で send test が失敗しているよ!という内容でした。

win環境でテストするのめん..困難なので、 MSWin32 はサポート外としてテストを修正し、再度アップロードを試みました。

初回移行も minil release だけで、いけます。
version もデフォルト 0.02 で更新してくれます。

Next

CPANアップロードの方法は覚えたけれども、
やっぱり Go の方で何か package 作りたいな...

Tutorial: Intro to React

なぜ

今更ながら React を触る必要が発生し、何も知らない状態はまずいということで、 チュートリアルを一通りやることにした。
その昔、 backbone + marionette とか、riot とか触ってた時代はある...

Tutorial: Intro to React

https://reactjs.org/tutorial/tutorial.html

公式サイトでチュートリアルが用意されている。

Before We Start the Tutorial

チュートリアルでは 三目並べゲーム を作りながら、React アプリを作るための基礎を習得できるらしい。
1つずつ concept を知った上で習得して行きたい人は、 step-by-step guide がおすすめとのこと。
とりあえず基礎だけ習得したいので、チュートリアルの方を進めていく。

Setup for the Tutorial

codepen.io を使ってブラウザ上で開発するか、localで開発するかを選ぶ。 今回は local で開発することにする。

1. 最新バージョンの node を入れる。

自分は nodenv を使っているので

% nodenv install 10.15.3
% nodenv rehash
% nodenv global 10.15.3
% node -v
v10.15.3

2. create react app

npx コマンドを使って React アプリのプロジェクトを新規作成する。

% npx create-react-app my-app

3. Delete all files in the src/ folder

新規プロジェクト下の src/ ディレクトリを削除する。 消すのはファイルのみ、ディレクトリ毎消してはいけない。

% cd my-app/src
% rm -f *
% cd ..

4. Add a file named index.css in the src/ folder with this CSS code.

src/ ディレクトリ下に index.css を作成

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

5. Add a file named index.js in the src/ folder with this JS code.

src/ ディレクトリ下に index.js を作成

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

6. Add these three lines to the top of index.js in the src/ folder:

index.js に import文を追記。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

7. npm start

npm start すると、ブラウザが立ち上がって http://localhost:3000 が開かれ、 三目並べゲームのベースが表示される。

Overview

What Is React?

React はなんぞやという話から、 Component の概念、JSXの説明まで。

Inspecting the Starter Code

Setup で作成した index.js の内容を確認。 下記、3つの React Component を扱う。

  • Square
  • Board
  • Game

Game > Board > Square > button というような構成になっている。 現時点では、インタラクティブな Component はない。

Passing Data Through Props

props は Properties の略。 Board Component から Square Component に props を通してデータを渡す。

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }

Board Component の renderSquare method に value を渡せるようにする。

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

Square Component の {/* TODO */} の部分を props から渡される value に置き換える。

この状態で npm start すると1つ1つの Square (マス目)に数字が入るようになる。

f:id:omohayui:20190327104441p:plain

Making an Interactive Component

クリックイベントの処理を追加していく。

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

Square Component に onClick 属性で function() { alert('click'); }} を追記するとポップアップが表示されるようになる。

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

そしてこれは ES6 の allow関数で書いてもOK。

次に、マスを X に塗り替えた状態を保持する為に state を使う。 Square Component に constructor method を追加し、state を初期社する処理を追加。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

subclass の constructor を定義するときには常に super を呼び出す必要がある。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})}
      >
        {this.state.value}
      </button>
    );
  }
}

Square Component で button をクリックしたときに state の value を X に書き換える処理と state の value を button に表示させる処理を追加。

この時点で npm start するとマス目をクリックしたときに「X」がマス目上に表示し続けるようになる。

Developer Tools

ChromeFirefox に React Devtools extension を追加してあげると React Component が tree 上に Devtool で表示され、
props や state の値が確認できるから便利だよという話。

Completing the Game

次はゲームを完成させる為に、ボード上に「X」と「O」を交互に配置させ、勝者を決定する処理を作っていく。

Lifting State Up

リファクタのしやすさまで考えて、ゲームの状態を各 Square Component ではなく親の Board Component に持たせるようにする。

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  renderSquare(i) {
    return <Square value={i} />;
  }

Board Component に子 Square Component の状態を表す state を持たせる為に constructor を追加。 9つの null を持った Array となる。

▼ this.state.squares に値が入ったときのイメージ

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

Square Component で state の value を表示するように修正。

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }

次に、 Square Component の button をクリックしたときに state を更新するように処理を追加したいが、
Square Component が Board Component の state を直接更新することはできない為、
Board Component から state を更新する method を Square Component に渡すようにする。

※ Square Component が Board Component の state を直接更新することはできないのは、state が Component に対して private だからである。

Board Component の renderSquare に下記のように追記して、Square Component に onClick の props を渡すようにする。

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

次に Square Component で、渡された props.onClick と props.value を使うようにする。

class Square extends React.Component {
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

ここで npm start してマス目をクリックすると、まだ handleClick を定義していないので「handleClick is not a function」というエラーになる。

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

handleClick method を Board Component に生やす。 ここで直接、 this.state.squares を編集せずに一度 squares に値をコピーしてから編集していることが重要。

Why Immutability Is Important

先のコードで一度 squares を別の変数にコピーしてから編集している点について、 immutability (不変性)について説明している。

Complex Features Become Simple

immutability で複雑な機能の実装がシンプルに。

Detecting Changes

mutable (可変)な object の変更は検知するのが難しい。

Function Components

Function Component は自身の state を持たず render method のみを持つ Component
React.Component Class の継承も不要なのでただの Function になる

ということで、Square Component を Function Component に書き換える

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

同時に this.props を props に修正する

Taking Turns

三目並べゲームにするために、「X」だけでなく「O」もマークできるようにする。

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

Board Component の state に xIsNext を追加。

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

handleClick で squares にセットする値を xIsNext で判定するようにする。

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // the rest has not changed

Board Component の render で Next player も出し分けするように修正。

Declaring a Winner

どちらのプレイヤーが勝ったのか判定する。
下記の helper function を追加。

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  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;
}

calculateWinner は渡された squares 配列から勝者を判定し、 'X', 'O', or null を返す。

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

Board Component の render() で calculateWinner を呼び出し winner を表示するように修正。

  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

handleClick で既に winner が確定している場合 or 既に変更済みのマス目の場合何もしないという処理を追加。

ここまで実装して npm start すると三目並べゲームができるようになる

f:id:omohayui:20190327105430p:plain

Adding Time Travel

最後にゲームの動作を「戻す」機能を追加する。

Storing a History of Moves

squares 配列を mutable な配列として扱うと戻る機能の実装は難しいが、
各動作の後に immutable な配列として都度 copy しているので、過去の状態に戻すことができる。

過去の squares 配列のデータは history という名前の配列に格納するようにしていく。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

Lifting State Up, Again

次に Top Level の Game Component に history を持たせるようにする。
また、 Game Component で squares の state も触る必要があるので、
再度 state を Board Component から Game Component に渡す処理を追加する。

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

Game Component にも constructor を追加。

class Board extends React.Component {
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

次に Game Component から Board Component へ props として渡された squares と onClick() を Square Component に渡すようにする。 Board Component から要らなくなった constructor を削除。

そして Game Component の render function では history を使ってゲームステータスを表示する。

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />

        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }

Game Component がゲームステータスをレンダリングしているので、
Board Component の render から対応するコードを削除することができる。

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }

最後に、handleClick() を Board Component から Game Component に移動させる。

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

Game Component に移動させたことによって、 handleClick() 内でも 新しい history 配列を set する必要がある。 補足情報:push は破壊的なので concat の方がおすすめ

Showing the Past Moves

map() method を使って Game Component の render で履歴情報を表示するようにする。

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

この時点では、まだ jumpTo() が実装されていないので、devtool では warning が出る。

Picking a Key

ゲームの履歴毎にユニークな key を連番で持たせることができるので、
Game Component の render の <li><li key={move}> を追加する。

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

jumpTo() method を実装する前に、stepNumber を state に追加。

  handleClick(i) {
    // this method has not changed
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // this method has not changed
  }

Game Component 内に jumpTo() method を生やす。

次に handleClick() 内で stepNumber をセットする処理を追加する。 history も過去の分だけを保持するように、 stepNumber + 1 までに slice する。

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

最後に Game Component の render で表示するものを常に最新のゲーム状態ではなく、
選択された履歴を表示するようにする。

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

これで、履歴をクリックをするとその状態がマス目に表示されるようになった。

f:id:omohayui:20190327105355p:plain

Wrapping Up

三目並べゲームが完成したところで、最終チェック。
問題なく動いている。

さらにゲームに機能を追加するためのアイデアも。
※ 今回はそこまではやらず。

所管

  • Component の思想 (private化) は他のフレームワークと同様理解しやすい
  • props は Riot でいう opts だと思った(Riot v4 で opts が props , state になるという噂)
  • 一つ一つ読み込んでいくと immutable の話や React 関係なく重要な tips もあって面白かった