omohayui blog

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

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 もあって面白かった

Go Meta Linter から GolangCI-Lint に乗り換える

GoのLinter使ってる?

自分は Local で開発するときに intelliJ を使ってるんですが(ultimate版)、
FileWatcher という plugin を使って、ファイルを編集保存したタイミングで、
gofmt や gometalinter を実行するようにしてます。
PullReqでレビュー投げる前にここもっとsimpleに書けるよーとかコメント間違ってるよーとか教えてくれるので大変便利です。

GoMetaLinter が重い遅い件

ファイルを編集する度に gometalinter が実行されるとたまに結果が返ってくるのやたら遅かったり、
PCのファンが猛烈に回りだしたりします。
そこで、 golangci-lint に乗り換えてみました。

比較

下記はとあるアプリケーションの一部のソースコードに同じ linter設定で gometalinter と golangci-lint それぞれ実行結果を取ってみたものです。

% /usr/bin/time gometalinter --disable-all -E errcheck -E ineffassign -E interfacer -E unconvert -E misspell -E unparam -E nakedret -E dupl -E goconst -E gocyclo --cyclo-over=15 src/hoge/model
       19.64 real        35.53 user        10.96 sys

% /usr/bin/time golangci-lint run --disable-all -E errcheck -E ineffassign -E interfacer -E unconvert -E misspell -E unparam -E nakedret -E dupl -E goconst -E gocyclo --gocyclo.min-complexity 15 src/hoge/model
        6.24 real        11.33 user         2.99 sys

golangci-lint の実行時間が gometalinter の 1/3 以下に。

結論

github.com

GolangCI-Lint is a linters aggregator. It's fast: on average 5 times faster than gometalinter.

そもそも golangci-lint の README に gometalinter より5倍速いぜ!って書いてあるの見ずに使ってました・・・
時間あるときに中のソースまで見たい。

Cloud KMS で暗号化

Cloud KMS とは

cloud.google.com

Cloud KMS は GCP で提供されている鍵管理サービスです。(Cloud Key Management Service )
GAE上で秘匿情報を管理したいときに Datastoreにそのまま入れるのも...
secret.yaml を作ってリポジトリ管理外にして app.yaml で include してというのも...
と思っていたら、Cloud KMS がとっくにSLA適用されていて結構使っている人が多いので触ってみることにしました。

Cloud KMS のオブジェクト階層

アクセス制御を管理するために設計された階層構造に、CryptKey(暗号鍵)を格納します。

  • Project - プロジェクト
  • Location - ロケーション
  • KeyRing - キーリング
  • Key - 鍵
  • Key Version - 鍵バージョン

この構造のリソース (KeyRing, CryptKey) へのアクセスは、Cloud IAM で制御できます。

手順

※ 注意点

  • KeyRing と Key は無効化はできますが削除ができないので、自分みたいに適当な KeyRing を作成してしまうとゴミが残ってしまうので気をつけましょう...

Cloud KMS API を有効にする

KeyRing を作成する

  • GCP Console > Security > Cryptographic Keys を開く
  • [Create Key Ring] をクリック
  • Key ring name を入力し Key ring location を選択して [Create]
    • KeyRing は、Key をグルーピングする粒度の名前
    • KeyRing Location は基本 global で良いらしいが、公式ドキュメント を見るとパフォーマンスを考慮するなら特定region(サービスを提供する地域)を選択せよとのこと

f:id:omohayui:20181021200508p:plain:w500

Key を作成

  • KeyRing を作成したら [Create Key] クリック
  • Key Name、Purpose、Rotation 等を入力して [Create]
    • Rotation は、新しく Key Version が作成されるスケジュールの設定
    • Rotation を設定しても古い Version の Key が使えなくなるわけではない
    • 古い Key Version を無効化するには、 破棄のスケジュール登録 が別途必要になる

作成フォーム

f:id:omohayui:20181021211257p:plain:w500

作成後

f:id:omohayui:20181021200157p:plain:w600

Key を使って暗号化/復号化

Cloud KMS のクラインアントライブラリ には C#/Go/Java/Node.js/PHP/Python/Ruby が用意されてます。

Goで書くとこんなイメージ

base64エンコード/デコードする必要があるくらいです。

import (
    "encoding/base64"
    "fmt"

    "golang.org/x/net/context"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/cloudkms/v1"
)

// KMSClient is a client to access Cloud KMS
type KMSClient struct {
    keypath string
    service *cloudkms.ProjectsLocationsKeyRingsCryptoKeysService
}

// KMSOptions means the options for constructor
type KMSOptions struct {
    ProjectID, Location, KeyRing, CryptoKey string
}

// NewKMSClient makes KMS client
func NewKMSClient(ctx context.Context, opt *KMSOptions) (*KMSClient, error) {
    client, err := google.DefaultClient(ctx, cloudkms.CloudPlatformScope)
    if err != nil {
        return nil, fmt.Errorf("failed DefaultClient. error: %v", err)
    }
    cloudkmsService, err := cloudkms.New(client)
    if err != nil {
        return nil, fmt.Errorf("failed cloudkms.New. error: %v", err)
    }

    keyPath := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s",
        opt.ProjectID, opt.Location, opt.KeyRing, opt.CryptoKey)

    return &KMSClient{
        keypath: keyPath,
        service: cloudkmsService.Projects.Locations.KeyRings.CryptoKeys,
    }, nil
}

// Encrypt encrypts plaintext
func (k *KMSClient) Encrypt(plaintext string) (string, error) {
    resp, err := k.service.Encrypt(k.keypath, &cloudkms.EncryptRequest{
        Plaintext: base64.StdEncoding.EncodeToString([]byte(plaintext)),
    }).Do()
    if err != nil {
        return "", fmt.Errorf("failed to encrypt. error: %v", err)
    }
    return resp.Ciphertext, nil
}

// Decrypt decrypts ciphertext
func (k *KMSClient) Decrypt(ciphertext string) (string, error) {
    resp, err := k.service.Decrypt(k.keypath, &cloudkms.DecryptRequest{
        Ciphertext: ciphertext,
    }).Do()
    if err != nil {
        return "", fmt.Errorf("failed to ecrypt. error: %v", err)
    }
    text, err := base64.StdEncoding.DecodeString(resp.Plaintext)
    if err != nil {
        return "", fmt.Errorf("failed base64 decode. error: %v", err)
    }
    return string(text), nil
}