trifle

技術メモ

A Complete React Redux Tutorial for 2019 を読んで Redux に入門する

これまで名前しか聞いたことがなかった Redux をプライベートで知る必要が出てきたかもしれないので, 入門してみることにしました. ちょうど最近,

daveceddia.com

という記事が出たようなので, これを手がかりに, 自分用のメモを残してみます.



Redux を使うと何がうれしいのか

React では, ある親 Component から子 Component に状態を受け継ぐ時, 子 Component はそれを props として引き受けます.
例えば以下のように記述された 3 * 3 のマス目のようなものを考えてみてください.

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

function Board(props) {
  const renderSquare = i => (
    <Square value={i} onClick={() => props.onClick(i)} />
  );

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

function Game() {
  const handleClick = i => {
    console.log(i);
  };

  return (
    <div className="game">
      <div className="game-board">
        <Board onClick={i => handleClick(i)} />
      </div>
    </div>
  );
}

Square を定義する関数の中に props.value というのがあり, これはひと階層上の Board で示された value={i} を受け取っているということです.
また, Square の要素をクリックすると, props.onClick に伝わりますが, これはやはり同様に Board で示された onClick={() => props.onClick(i)} を呼び出します. そしてそれはさらに上の階層の GameonClick={i => handleClick(i)} を呼び出します.
このようにデータの受け渡しが Component の親子関係で行われます.
以上のような単純な例であれば問題無いですが, このデータの受け渡しの親子関係が, 曽祖父からやしゃごまで, と言えるくらい深い階層間で行われるとすると大変ですね. それに, その深い階層間の途中にある Component にとって, その受け渡されていくデータが無関係なものだとしたら, 無意味にそれらの Component の情報を増やすことになるので, ソフトウェアの設計上あまり良くないと言えるでしょう.


Redux はデータを一元的に管理することによってこの問題の解決を図ります.

2019 年現在なら Context API でもいいかも?

私自身は React 初心者なので, 経験に基づいた知見ではないことをご了承ください

Redux は強力なゆえに, 小規模な開発ではオーバーキルになってしまう可能性があります.
単に直接孫にデータを渡すだけであれば React ビルトインの Context API を使うのでも良さそうです.

Redux の導入

npm install redux react-redux

redux が本体, react-redux がそれを React と繋げるもの, と考えればよいでしょう.

Store

Redux の概念です. あらゆる state を管理するただ一つの倉庫です.

Reducer

Redux の概念です. ただの関数ですが, Store に保管された state を更新するという役目を担っています.
(ES6の Array にもありますが)畳み込み演算について知識があれば reduce という名前は聞いたことがあるでしょう.

[1, 2, 3, 4, 5].reduce((i, j) => i * j)  // 120

上記の計算を大仰に解説すると, 初期状態 1 に対して, 受け渡された 2 を掛け合わせて 2 に, 受け渡された 3 を掛け合わせて 6 に, 受け渡された 4 を掛け合わせて 24 に, 受け渡された 5 を掛け合わせて 120 になります. ここで重要なのは, 状態に対して, 受け渡された値を掛け合わせるという関数を適用し, 新たな状態を作ることをひたすら繰り返していることです.
Redux の Reducer にも同じことが言えます. つまり Reducer に状態を渡すことで, 新たな状態が生まれるというわけです.

Reducer について留意すべき点は

  • undefined を返さないようにする
  • 副作用を持たせないようにする

ことです.

Action と Dispatch

Redux の概念です.
まず, Action ですが, 実体としてはただのオブジェクトで, Reducer の中で行われる処理の識別子として考えればよいでしょう.
Action 単体では何も起こりません. これは Dispatch と合わせて使うものです. dispatch() という関数の引数に Action を指定してやることで, その Action にもとづいて store が操作されます.

Redux の例

ここまでの内容を整理するために, 具体的なコードの例を使いましょう.
簡単なカウンターを考えます. 初期値は0で, 1増やす操作や1減らす操作, リセットする操作などができればよさそうです.

import { createStore } from 'redux';

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        count: state.count - 1
      };
    case 'RESET':
      return {
        count: 0
      };
    default:
      return state;
  }
}

これが Reducer の一例です. これをもとに

const store = createStore(reducer)

このように Reducer を引数にとって Store を定義できます.
実際にこの Store に対して Action を Dispatch するには

store.dispatch({ type: 'INCREMENT' }); // count を 1 に
store.dispatch({ type: 'INCREMENT' }); // count を 2 に
store.dispatch({ type: 'DECREMENT' }); // count を 1 に
store.dispatch({ type: 'RESET' }); // count を 0 に

このように記述してやればよいです.
{ type: 'INCREMENT' } という Action が, switch 文の case 'INCREMENT': の部分に対応づけられているのが分かると思います.

state は Read-Only

Reducer に副作用を持たせないようにすることとも関連がありますが, Reducer は Pure な関数で無ければなりません.
つまり Reducer の内部で state.count = 0 だとか state.item.push(newItem) だとか, state を書き換える操作をしてはいけません. 新しい state の値を return すること, これが Reducer の役目です. state の書き換えは Action のレベルで管理しないといけないことなのです.

Provider

さて, ここまで Redux 本体の説明を行ってきました. ここからは, Redux をいかに React に結びつけるか, という react-redux の話が始まります.
Provider は Redux を操作できる React の Component です.

import { Provider } from 'react-redux';

const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

このように Provider で包んでやると, Counter も, その子 Component も, その子 Component も ... 全ての子孫が Redux の (この場合は store という変数名で定義された)Store にアクセスする権限を持ちます.
ただし, これは権限を持っているというだけであり, 実際に操作するにはもうひと段階準備が必要です. それが以下で述べる connect です.

connect 事始め

connect を使っていかに React に Redux を導入するかを説明するために, 先ほど出てきた<Counter /> という Component の実装が具体的に以下のようになっているとしましょう.

class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  decrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  };

  reset = () => {
    this.setState({
      count: 0
    });
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.state.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

export default Counter;

この Counter は, 現時点では, カウンターの値や, カウントを増やしたり減らしたりする関数を, 内部の実装として管理しています. これを Redux を使って外部に委ねたいです.
そのために何をするかというと, あらゆるものをpropsに変換します.
この変換を行うのが mapStateToPropsmapDispatchToProps です.

mapStateToProps

まずは, カウンターの値を Redux に委ねてみましょう.

import { connect } from "react-redux";

class Counter extends React.Component {
  increment = () => {
    /* TODO */
  };

  decrement = () => {
    /* TODO */
  };

  reset = () => {
    /* TODO */
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

export default connect(mapStateToProps)(Counter);

これが connect の第一段階です. 何が変わったのか説明します.

  • this.state.countthis.props.count に変えた. これによりカウンターの値はもはや Counter 内部のものではなくなった.
  • export default Counter;export default connect(mapStateToProps)(Counter); に変えた. つまり, 生の Counter ではなく, connect という関数にラップされた Counter が使われるようになる.
  • connect という関数は第一引数に mapStateToProps という関数をとる. これは Component の state を props に変化させる. 具体的には return されるオブジェクトの key が props の名前に, オブジェクトの value が props の値になる.


上記コード例では /* TODO */ という部分が残っています. まだカウンターの値を変更すること, すなわち Redux の Store を操作することが出来ていないのです. 先に進みましょう.

Action Creator

復習になりますが, Redux の Store の操作の仕方は,

store.dispatch({ type: 'INCREMENT' });

このように Action を Dispatch するのでした.
ところで, .dispatch({ type: 'INCREMENT' }); の部分をそのまま毎回書くのは骨が折れます. そこで, 以下のような Action の定義ファイルを用意してやると良いでしょう. Action を一覧として管理できるというメリットもあります.

actions.js

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const RESET = "RESET";

export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
export const reset = () => ({ type: RESET });

この incrementdecrement など, Action を return する関数を Action Creator と呼びます. これを活用して, 先ほどの /* TODO */ の部分を埋めてみましょう.

import { increment, decrement, reset } from "./actions";

class Counter extends React.Component {
  increment = () => {
    this.props.dispatch(increment());
  };

  decrement = () => {
    this.props.dispatch(decrement());
  };

  reset = () => {
    this.props.dispatch(reset());
  };

これでカウンターの値を操作することまで出来るようになりました!
しかし, まだ出来ることがあります. この dispatch の部分も props に変えてしまおうというのが connect の第二段階 mapDispatchToProps です.

mapDispatchToProps

import { connect } from "react-redux";
import { increment, decrement, reset } from "./actions";

class Counter extends React.Component {
  increment = () => {
    this.props.increment();
  };

  decrement = () => {
    this.props.decrement();
  };

  reset = () => {
    this.props.reset();
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

const mapDispatchToProps = {
  increment,
  decrement,
  reset
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

mapDispatchToProps を使ってみました. 何が変わったでしょうか.

  • this.props.dispatch(increment());this.props.increment(); という風に書けるようになった.
  • その代わりに connect の第二引数に mapDispatchToProps というオブジェクトが渡されるようになった. これは実体としては, Action Creator の集合である.

以上で react-redux の基本概念の説明が完了しました.

Redux でも非同期処理がしたい!〜 Middleware の登場

ここからは全然理解が及んでいないので概略を述べるに留めます

Reducer は副作用を持ってはいけないと述べましたが, 現実のアプリケーションではデータを引っ張ったり通信したり...と非同期処理が噛みます. 汚い処理をどこかで担当しなければいけません.
Redux 自体には Middlleware という非同期処理を噛ませるレイヤーが用意されていますが, その記述に関しては統一的な手法が定まっているわけではなく, 何らかの独自のやり方で書けるようにせねばならないようです. redux-thunk とか redux-saga とかあるみたいです.

redux-thunk では Action Creator でオブジェクトではなく関数を返す, redux-saga では独立した非同期処理のプロセスを作る ... みたいな雰囲気しか私の理解は及んでいません. Redux は難しいですね...

その人が GitHub で普段どんな言語を使って開発しているかをすぐ見れるやつ作った

https://github-language-data.now.sh/

↑これです

例えば自分の今の状況はこうなる.

f:id:HelloRusk:20190309142209p:plain

仕組みといたしましては, GitHub API

GET /users/:username/repos

で, 直近の30件のリポジトリ一覧を取得し, それぞれのリポジトリjson の language をみています.

OAuth認証をしていないのですぐにAPI制限に到達してしまいます. 一文字打つごとにGETする仕組みをやめたほうがいいと思うんですが, なんとなく画面がニョロニョロ変化するのが楽しいので, このままにしておきます.

ちなみに GitHub API には, これとは別に,

GET /repos/:owner/:repo/languages

というものがあり, これは, あるリポジトリに対して, 言語別にバイト数を集計できます.

簡単な例で考える React Hooks のよさ

最近安定版にも導入された React Hooks が自分の中で面白いので, とても簡単な例で試してみたよという話をします.

例1: フォーム

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'ここにポエムを書いてね'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('お前のポエムはこれだ: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          ポエム記入欄:
          <br/>
          <textarea value={this.state.value} onChange={this.handleChange} />
          <br/>
        </label>
        <input type="submit" value="送信" />
      </form>
    );
  }
}

export default Home;

これはフォームになっていて, 書いた内容がそのまま表示されます.

f:id:HelloRusk:20190223232720p:plain

こんな感じ. 実は公式ドキュメントの例をほぼそのまま使っているだけですね



これを React Hooks の useState で書き直してみるとこのくらいに省力化できます.

export default () => {
  const [value, setValue] = useState('ここにポエムを書いてね');

  const handleChange = event => {
    setValue(event.target.value);
  }

  const handleSubmit = event => {
    alert('お前のポエムはこれだ: ' + value);
    event.preventDefault();
  }

  return (
    <form onSubmit={() => handleSubmit(event)}>
      <label>
        ポエム記入欄:
        <br/>
        <textarea value={value} onChange={() => handleChange(event)} />
        <br/>
      </label>
      <input type="submit" value="送信" />
    </form>
  );
}

どうですか?綺麗じゃないですか?
value が状態としての this.state.value から, ただのローカル変数に落ちたことで, めちゃくちゃすっきり書けていますよね.
注意としては, React Hooks を使う場合は(すなわち Classではなく関数で書く場合は)Class の時のように this.handleSubmit = this.handleSubmit.bind(this); というような初期化ができず, 代わりに onSubmit のプロパティの部分で () => handleSubmit(event) とアロー関数を渡します.
これはどういうことかを考えると, https://uhyohyo.net/javascript/11_2.html にあるように, bind の戻り値は新しい関数なんですね. Class を使う場合では onSubmit={this.handleSubmit} だけでは関数は更新されず, コンストラクタの部分で bind で呼び出すことで初めて更新されるんですね.
一方, アロー関数で書けば矢印の先は評価されるので, ここで既に関数の更新が行われます. というわけで, Class を使わない functional component の例では, 必ずこういう関数自体を更新したいケースではアロー関数になっているのだと理解しています.



例2: 時計

React の中での大切な概念として Lifecycle というものがあります. といっても自分も正確に理解しているわけではありませんが...

qiita.com

がいつ見ても分かりやすいなと思います.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

export default Clock;

さてこれは現在の時刻をリアルタイムで表示する例です(これも公式ドキュメントの例から). componentDidMount で1秒後に時刻を更新して, componentWillUnmount でそのリソースを解放しています.
このような「状態の変化に応じて何かをする」ときに役立つ React Hooks が useEffect で, これを使うと

export default () => {
  const [date, setDate] = useState(new Date());

  const tick = () => {
    setDate(new Date());
  }

  useEffect(() => {
    const timerID = setInterval(
      () => tick(), 
      1000
    );
    return () => {
      clearInterval(timerID);
    };
  })

  return (
    <div>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  )
}

これまた非常に簡潔になりました.
useEffect は優れもので, 公式ドキュメントを読むと,

If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.

ということが分かります. Lifecycle を集約できるのです.



時代はどんどん functional component に向かっていくのでは

今まで見てきた内容から分かると思うのですが, React の公式ドキュメントは現状かなり Class で書かれています.
React に触れる人がまず目を通すであろう React Tutorial も, ただ DOM を render するだけのパーツでさえも Class で書かれているのが分かります. これはちょっととっつきづらいでしょう.
自分が初めて React に触れた時も Class がたくさん出てきたのに驚いた記憶があります. 使い方が分かっていくうちに Class を使わない functional component でも事足りる場合が多いことに気づいていきました. そこに追い打ちをかけるのがこの React Hooks の登場だと思います.

一方で, Facebook 社は Class を完全に捨て去るわけでもないようです. https://reactjs.org/docs/hooks-intro.html#gradual-adoption-strategy を読んでみてください.

We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future. At Facebook, we have tens of thousands of components written as classes, and we have absolutely no plans to rewrite them. Instead, we are starting to use Hooks in the new code side by side with classes.

Rustで2048をつくる

2048というゲームのブームはとっくに終わっているとは思いますが, 自分は未だに通学中とかライブ開演前とかの空き時間によくやっています.
そんな2048をパソコンでもやりたいなと考えたので, つくってみました.

Rustのビルドシステム兼パッケージマネージャである Cargo の環境があれば,

git clone https://github.com/7ma7X/2048.git
cd 2048
cargo run

で体験できます. Cargo を入れていない方は公式チュートリアルを参考にどうぞ(布教).
以下は感想戦のようなものです.

全体の設計

当初は一枚のプログラムでもいいかなと思いましたが, 盤面の操作が肥大化していくことに気づいたので, 盤面操作関連を board.rs に, 全体のゲーム進行を main.rs に分離しました.
board.rs にはテストケースも含まれています. なんとなく2048のようだが実は正しくない動作, になることを防ぐために, テストケースはかなり慎重に書きました. とくに盤面を動かす動作は本物の2048をやりながら慎重に色々なパターンを試しました.

2048は4*4の配列とスコアが全て

Rustにはクラスが無い代わりに, 型や構造体にトレイトを注入して機能を追加していきます. 2048の場合はこんな感じです.

pub struct Board {
  score: i32,
  board_data: [[Option<i32>; 4]; 4]
}

impl Board {
  // ここに機能を書いていく
}

盤面の数値は, 値が無い場合も含めてひとつの値として取り扱いたいので, Option 型を使います.

ゲームオーバー判定

Boardを初期化するメソッドと, 現在のBoardの状況を表示するメソッドを作った後は, まずゲームオーバー判定ができることが必要だろうと考えました.
これは「全ての盤面が数で埋まっていて, かつ縦横2連続で同じ数字が並んでいるものがない」かどうかを確認すれば良さそうです.

pub fn is_continuable(&self) -> bool {
  let mut ans = false;

  for i in 0..3 {
    for j in 0..4 {
      match self.board_data[i][j] {
        Some(x) => {
          match self.board_data[i+1][j] {
            Some(y) => if x == y { ans = true; break; },
            None => { ans = true; break; }
          }
        }
        None => { ans = true; break; }
      }

      match self.board_data[j][i] {
        Some(x) => {
          match self.board_data[j][i+1] {
            Some(y) => if x == y { ans = true; break; },
            None => { ans = true; break; }
          }
        }
        None => { ans = true; break; }         
      }
    }
  }

  ans
}

なお, テストケースはこんな感じです. Rustでは #[test] を関数の前につけることでテスト時実行の標識になり, cargo test で確認できます.

#[test]
fn check_continuability() {
  let mut test_board = Board::initialize();
  assert_eq!(test_board.is_continuable(), true);
  test_board.board_data = [
    [Some(4), Some(2), Some(4), Some(2)],
    [Some(8), Some(4), Some(8), Some(4)],
    [Some(4), Some(8), Some(4), Some(8)],
    [Some(2), Some(4), Some(2), Some(4)]
  ];
  assert_eq!(test_board.is_continuable(), false);
  test_board.board_data = [
    [Some(4), Some(2), Some(4), Some(2)],
    [Some(8), Some(4), Some(8), Some(4)],
    [Some(4), Some(8), Some(4), Some(8)],
    [Some(2), Some(4), Some(2), Some(2)]
  ];
  assert_eq!(test_board.is_continuable(), true);
  test_board.board_data = [
    [Some(4), Some(2), Some(4), Some(2)],
    [Some(8), Some(4), Some(8), Some(4)],
    [Some(4), Some(8), Some(4), Some(8)],
    [Some(2), Some(4), Some(2), None]
  ];
  assert_eq!(test_board.is_continuable(), true);
}

動かす動作

肝心の盤面を動かす動作ですが, 以下のように考えました(自分はこういうアルゴリズムを考えるのがとても苦手なので, もっと効率の良い方法があるような気がします. あくまで一例程度に).
基本的にはバブルソートならぬ「バブル数値潰し」をやります.
例えば

[16, -, 8, 8]

を左にスライドしたいとします.
1番目の要素を0番目の要素へ潰す. ところが1番目の要素は無いのでこのまま.
2番目の要素を1番目の要素へ潰す. すると, [16, 8, -, 8].
1番目の要素を0番目の要素へ潰す. 値が異なるので潰れずこのまま.
3番目の要素を2番目の要素へ潰す. すると, [16, 8, 8, -].
2番目の要素を1番目の要素へ潰す. すると, [16, 16, -, -].
1番目の要素を0番目の要素へ潰す. しかしここで [32, -, -, -] とすると実際の2048の挙動とは違うものになってしまう(つまり, 一度2倍された値をさらに2倍するようなことが禁止されている). なので適切にboolを設定して [16, 16, -, -] のままであるようにする.

基本的にはこれでOKですが, 例外があって, [a, a, b, b] のパターンは [2a, 2b, -, -] になり, 2回潰すことが必要です(本物の2048をプレイして確認してみてください). このケースだけ例外として先に弾きました.

スコア体系についても注意が必要です. 本物の2048では「新しくできた後の値」がスコアに追加されます.

実装してみたのが以下の通り. 左以外の方向についてもインデックスを変えるだけです.

pub fn move_left(&mut self) {
  for i in 0..4 {
    if self.board_data[i][0] == self.board_data[i][1] 
    && self.board_data[i][2] == self.board_data[i][3] 
    && self.board_data[i][0] != None
    && self.board_data[i][2] != None
    {
      self.board_data[i][0] = self.board_data[i][0].map(|v| v*2);
      self.board_data[i][1] = self.board_data[i][2].map(|v| v*2);
      self.board_data[i][2] = None;
      self.board_data[i][3] = None;

      self.score += self.board_data[i][0].unwrap() + self.board_data[i][1].unwrap();
    }
    else
    {
      let mut is_renewed = false;
      for j in 0..4 {
        for k in (0..j).rev() {
          match self.board_data[i][k+1] {
            None => {}
            Some(y) => {
              match self.board_data[i][k] {
                None => {
                  self.board_data[i][k] = Some(y);
                  self.board_data[i][k+1] = None;
                }
                Some(x) => {
                  if x == y && !is_renewed {
                    self.board_data[i][k] = Some(2 * y);
                    self.board_data[i][k+1] = None;
                    self.score += 2 * y;
                    is_renewed = true;
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

テストケースは以下のようなものを各方向にそれぞれ3つずつ書いています(全部実際にプレイしながらの実例).

#[test]
fn check_move_left() {
  let mut test_board = Board::initialize();
  test_board.score = 276;
  test_board.board_data = [
    [Some(16), Some(4), Some(2), Some(4)],
    [Some(2), Some(32), Some(8), Some(8)],
    [Some(8), Some(8), Some(16), Some(2)],
    [None, Some(4), Some(2), None]
  ];
  test_board.move_left();
  assert_eq!(test_board.score, 308);
  assert_eq!(test_board.board_data, [
    [Some(16), Some(4), Some(2), Some(4)], 
    [Some(2), Some(32), Some(16), None], 
    [Some(16), Some(16), Some(2), None], 
    [Some(4), Some(2), None, None]
  ]);
}

新しく発生する2または4

2048では初回に2つ, それ以降は1つ, 2または4が空いているマスに湧いていきます. 乱数を使ってうまいこと2または4を発生させます.
Rustでは乱数は外部ライブラリなので, extern crate rand; します. Rust ではライブラリ(パッケージ)のことをクレートと呼びます.
Cargo.toml の dependencies にも別途その旨が必要です. Node の package.json と同じような感じです.

本物の2048で遊んでみると, 2と4がだいたい3:1くらいの比率で出ているような気がしたので,

fn generate_two_or_four() -> i32 {
  let choices: [i32; 4] = [2, 2, 2, 4];
  let mut rng = thread_rng();

  *choices.choose(&mut rng).unwrap()
}

が新しい値発生器です.
次に, 現在の盤面から空いている座標一覧を取得する

fn fetch_no_number_area(&self) -> Vec<(usize, usize)> {
  let mut ans = Vec::new();

  for i in 0..4 {
    for j in 0..4 {
      if self.board_data[i][j] == None { ans.push((i, j)); }
    }
  }
  ans
}

を定義します.
これらを使って,

pub fn fill_in(&mut self) {
  let no_number_vec = Board::fetch_no_number_area(self);
  let mut rng = thread_rng();

  match no_number_vec.choose(&mut rng) {
    Some(tuple) => {
      let (x, y) = *tuple;
      self.board_data[x][y] = Some(Board::generate_two_or_four());
    }
    None => {}
  }
}

が定義されます.
余談ですが, 空いているマスをランダムに選択する no_number_vec.choose(&mut rng)Option 型を返すので, 最初の実装では単に .unwrap() していましたが, 実際にこの2048のプログラムで遊んでいたところ, 「全部のマスが数字で埋まっているがゲームオーバーでない状況」で panic! を起こして, そこで初めて,「空いているマスが無いので .choose が効かない」可能性に思い至り, きちんとパターンマッチさせるようにしました.
このように .unwrap() を適当に使っていると痛い目に会いますね.

それぞれの方向に動かせるかどうかを判定する

あとはこれで main.rs にゲーム本体の処理を書いて完成...といきたかったところですが, 実際にこの2048のプログラムで遊んでみると重大な問題があることが判明しました.
それは, 「本物の2048では一つも数字が潰れない方向にスライドしても2または4が湧くことはない」というのを見逃している点でした.
4方向のどの方向にも動かせないか否かを判定する .is_continuable とは別に, それぞれの方向で動かせるか否かを判定しなければならなかったのです.

そこでまず, 動かせる列というのは何者かを考えてみます.

fn is_movable_row(row: [Option<i32>; 4]) -> bool {
  match row {
    [_      , None   , None,    None   ] => false,
    [Some(a), Some(b), None,    None   ] 
      if a != b                          => false,
    [Some(a), Some(b), Some(c), None   ] 
      if a != b && b != c                => false,
    [Some(a), Some(b), Some(c), Some(d)] 
      if a != b && b != c && c != d      => false,
    _                                    => true
  }
}

こんな風にかけました. 型がある言語でよかったですね. あとはこの判定を4つの列に対して行うだけです. 左スライドであれば,

pub fn is_left_movable(&self) -> bool {
  let mut ans = false;

  for i in 0..4 {
    let row = [
      self.board_data[i][0],
      self.board_data[i][1],
      self.board_data[i][2],
      self.board_data[i][3]
    ];
    if Board::is_movable_row(row) {
      ans = true;
      break;
    }
  }
  ans
}

他の方向もインデックスを適宜変えるだけなので簡単です.
ゲーム本体の処理である main.rs の方では

  if input.starts_with("i") || input.starts_with("I") {
    if board.is_up_movable() {
      board.move_up();
      board.fill_in();
      board.display();
    }

というように .is_up_movable() の判定を噛ませることで, その方向に動かせない場合は, 操作の入力自体はできるものの, 盤面操作だけでなく表示も含めて何も起こらないようにしました.
これで, 勝手に2または4が湧かなくなったとともに, その方向に盤面が動かないこともプレイしながら分かりやすくなったので, とてもよかったです.

その他の感想

  • 全体的に Option 型を使って書けたのが安心感がありました.

  • 当初は.highscoreみたいなファイルを作ってハイスコアを保存することも考えたのですが, Rustのファイル I/O 関数の操作がかなり面倒だったので(open, read, writeなどあらゆる処理で Result 型 が帰ってきます. まあわざわざ IO 型まで用意されていてそこから抜けられないような Haskell よりはマシかも...), 諦めました.
    そもそもこの2048で遊んでも保存したくなるようなハイスコアまで全然行かないと思います. 理由として一つ挙げられるのが, 数字の背景に色が付いていないことです. 数字に色を付ける程度ならこのプログラムでも出来ますが, ケバくなりそうなのと, 個々のターミナルの環境依存性が強そうなので, そこまでやる気にはなりませんでした.

  • GitHubを漁ったらRustで書かれた2048でもっとすごいのが色々あった.

まあこれやる位なら本物かjavascriptを使ってブラウザ上で動くやつをやった方ががいいのでは

NFAをDFAに変換して状態数最小化する

大学の試験で「与えられた言語を認識するような状態数最小の有限オートマトンを答える」問題が頻出されているため, 検算用のプログラムを書いてみます.

NFA の入力形式

input.txt として以下のように与えます.

2 0
0,0 1,0,
1,2,2,
2,3,3,
3,,,1

一行目は[入力文字の種類数] [始状態] を空白区切りで与えています.
二行目以降はCSVのようになっていて, カンマ区切りのうち, 最初の要素は状態の番号づけ, 二番目から最後から二番目までの要素は各文字を読み取ったあとの状態遷移, 最後の要素は「その状態が終状態か否か」を示します. 状態遷移に関しては, NFAなので, 状態が複数ある場合は空白区切りで列挙し, 一つもない場合は空文字になります. 終状態か否かに関しては, 終状態の場合のみ何かしら文字を入れることにします.

状態数最小のDFA に変換するプログラム

間違いがあれば指摘してほしいです. 私のテストの成績が上がるかもしれないので... gist: https://gist.github.com/7ma7X/bf42cc4b3f251e88c9bc55b5eb34eb3b

import pprint

nfa = []

with open("input.txt", "r") as f:

    # アルファベットの種類数と始状態を読み取る
    n, nfa_initial_state = f.readline().split()
    n = int(n)
    for i in range(n+1):
        nfa.append(dict())

    for line in f.readlines():
        data = line.replace("\n", "").split(",")

        # 遷移を読み取る
        for i in range(n):
            nfa[i][data[0]] = data[i+1]

        # 終状態か否かを読み取る
        nfa[n][data[0]] = True if data[n+1] else False

# NFAを確認したい場合はここをアンコメント
# pprint.pprint(nfa)


# NFA -> DFA

dfa = []
for i in range(n+1):
    dfa.append(dict())

unchecked_states = [nfa_initial_state]
checked_states = []

while unchecked_states:
    tmp_states = unchecked_states.pop()
    checked_states.append(tmp_states)

    for i in range(n):
        # 終状態か否かを書き込む
        dfa[n][tmp_states] = False
        for el in tmp_states.split():
            if nfa[n][el]:
                dfa[n][tmp_states] = True
                break

        # 遷移を書き込む
        new_states = set()

        for el in tmp_states.split():
            if nfa[i][el]:
                next_list = nfa[i][el].split(" ")
                new_states = new_states.union(next_list)

        new_states = " ".join(sorted(list(new_states)))
        dfa[i][tmp_states] = new_states

        if new_states not in checked_states:
            unchecked_states.append(new_states)

# ラベル貼りかえ前のDFAを確認したい場合はここをアンコメント
# pprint.pprint(dfa)

# 状態のラベルの張り替え
checked_states = sorted(list(set(checked_states)))
m = len(checked_states)

for i in range(n+1):
    for j, state in enumerate(checked_states):
        if i == n:
            dfa[i][j] = dfa[i].pop(state)
        else:
            dfa[i][j] = checked_states.index(dfa[i].pop(state))

dfa_initial_state = checked_states.index(nfa_initial_state)
checked_states = list(range(m))

# ラベル貼りかえ後のDFAを確認したい場合はここをアンコメント
# pprint.pprint(dfa)


# DFAの最小化

min_dfa = []
for i in range(n+1):
    min_dfa.append(dict())

marked_list = []
unmarked_list = []

for i in range(m-1):
    for j in range(i+1, m):
        if (dfa[n][i] and not dfa[n][j]) or (not dfa[n][i] and dfa[n][j]):
            marked_list.append((i, j))
        else:
            unmarked_list.append((i, j))

while True:
    is_renewed = False

    for i in range(m-1):
        for j in range(i+1, m):
            for k in range(n):
                if dfa[k][i] < dfa[k][j]:
                    next_tuple = (dfa[k][i], dfa[k][j])
                else:
                    next_tuple = (dfa[k][j], dfa[k][i])

                if (i, j) in unmarked_list and next_tuple in marked_list:
                    is_renewed = True
                    unmarked_list.remove((i, j))
                    marked_list.append((i, j))

    if not is_renewed:
        break

# MarkedList, UnmarkedListを確認したい場合はここをアンコメント
# print(marked_list)
# print(unmarked_list)

state_num = 0

min_dfa_hash = dict()
for (i, j) in unmarked_list:
    if i in min_dfa_hash and j in min_dfa_hash:
        k = min_dfa_hash[i]
        l = min_dfa_hash[j]
        for state in checked_states:
            if state in min_dfa_hash and min_dfa_hash[state] == l:
                min_dfa_hash[state] = k
    elif i in min_dfa_hash:
        min_dfa_hash[j] = min_dfa_hash[i]
    elif j in min_dfa_hash:
        min_dfa_hash[i] = min_dfa_hash[j]
    else:
        min_dfa_hash[i] = min_dfa_hash[j] = state_num
        state_num += 1

for state in checked_states:
    if state not in min_dfa_hash:
        min_dfa_hash[state] = state_num
        state_num += 1

min_dfa_initial_state = min_dfa_hash[dfa_initial_state]
new_checked_states = []

for state in checked_states:
    tmp_state = min_dfa_hash[state]

    # 終状態か否かを書き込む
    if tmp_state in min_dfa[n] and not min_dfa[n][tmp_state]:
        if dfa[n][state]:
            min_dfa[n][tmp_state] = True
    elif tmp_state not in min_dfa[n]:
        if dfa[n][state]:
            min_dfa[n][tmp_state] = True
        else:
            min_dfa[n][tmp_state] = False

    # 遷移を書き込む
    if tmp_state in new_checked_states:
        continue
    else:
        new_checked_states.append(tmp_state)

        for i in range(n):
            min_dfa[i][tmp_state] = min_dfa_hash[dfa[i][state]]

new_checked_states = sorted(list(set(new_checked_states)))

for i in range(n+1):
    for j, state in enumerate(new_checked_states):
        if i == n:
            min_dfa[i][j] = min_dfa[i].pop(state)
        else:
            min_dfa[i][j] = new_checked_states.index(min_dfa[i].pop(state))

min_dfa_initial_state = checked_states.index(min_dfa_initial_state)

print("始状態: {}".format(min_dfa_initial_state))
pprint.pprint(min_dfa)

にある「10110を部分語として含むような言語」でやってみます. NFA で書くと

なので, input.txt

2 0
0,0,0 1,
1,2,,
2,,3,
3,,4,
4,5,,
5,5,5,1

となります. プログラムを実行してみます.

pprint.pprint(nfa) をアンコメントすると,

[{'0': '0', '1': '2', '2': '', '3': '', '4': '5', '5': '5'},
 {'0': '0 1', '1': '', '2': '3', '3': '4', '4': '', '5': '5'},
 {'0': False, '1': False, '2': False, '3': False, '4': False, '5': True}]

これは, 一つ目の連想配列が0に関して読む前をkey, 読んだ後をvalueとしていて, 二つ目の連想配列が1に関して読む前をkey, 読んだ後をvalueとしていて, 最後が終状態か否か, を表しています.
ラベルを張り替える前の pprint.pprint(dfa) をアンコメントすると,

[{'0': '0',
  '0 1': '0 2',
  '0 1 3': '0 2',
  '0 1 3 5': '0 2 5',
  '0 1 4': '0 2 5',
  '0 1 4 5': '0 2 5',
  '0 1 5': '0 2 5',
  '0 2': '0',
  '0 2 5': '0 5',
  '0 5': '0 5'},
 {'0': '0 1',
  '0 1': '0 1',
  '0 1 3': '0 1 4',
  '0 1 3 5': '0 1 4 5',
  '0 1 4': '0 1',
  '0 1 4 5': '0 1 5',
  '0 1 5': '0 1 5',
  '0 2': '0 1 3',
  '0 2 5': '0 1 3 5',
  '0 5': '0 1 5'},
 {'0': False,
  '0 1': False,
  '0 1 3': False,
  '0 1 3 5': True,
  '0 1 4': False,
  '0 1 4 5': True,
  '0 1 5': True,
  '0 2': False,
  '0 2 5': True,
  '0 5': True}]

状態数が10個にまで広がってしまっていますね. ラベルを貼りかえた後の pprint.pprint(dfa) をアンコメントすると,

[{0: 0, 1: 7, 2: 7, 3: 8, 4: 8, 5: 8, 6: 8, 7: 0, 8: 9, 9: 9},
 {0: 1, 1: 1, 2: 4, 3: 5, 4: 1, 5: 6, 6: 6, 7: 2, 8: 3, 9: 6},
 {0: False,
  1: False,
  2: False,
  3: True,
  4: False,
  5: True,
  6: True,
  7: False,
  8: True,
  9: True}]

と, だいぶ見やすくなりました. そして最終結果は

始状態: 1
[{0: 0, 1: 1, 2: 5, 3: 5, 4: 0, 5: 1},
 {0: 0, 1: 2, 2: 2, 3: 4, 4: 2, 5: 3},
 {0: True, 1: False, 2: False, 3: False, 4: False, 5: False}]

と, 状態数6個になります. これを整理して図示すると,

まあ多分合ってるんじゃないかと思います.


ここおかしいんじゃね?みたいなところがあったら是非是非コメントしてください.

今TeXを始めるならVSCodeが良いかもしれない, という話と, MacOSで最低限の文書を出せるようにするまで

VSCodeLaTeX拡張機能に, 部分的な数式のレンダリングの機能があることを今日知りました.



どうやらこれは比較的最近追加されたようで, ザッと調べてみると, https://github.com/James-Yu/LaTeX-Workshop/pull/867 で最初にHover previewが提案され, その後, https://github.com/James-Yu/LaTeX-Workshop/pull/890 で完成に至ったような感じで, 今年の10月のことでした.
個人的には, TeXは以前からシケプリを作ったりするのに使ってはいましたが, 今まではTeXShopを使っていて, 正直あまり使い勝手が良くないなと思っていましたし, 普段から使い慣れているVSCodeの方が便利そうだな, というわけで, VSCodeに乗り換えることにしました.

以下, MacOSでのセットアップの手順をメモしておきます.



doratex.hatenablog.jp

まず, 私も化学を1年間教わっていた寺田先生による詳細な環境構築の手順に従うと良いでしょう.
MacTeXのインストーラのダウンロードと, TeX Liveディレクトリの更新が非常に時間がかかります.
残念ながら私はヒラギノフォントの埋め込みがどうしてもうまくいかなかったので, 代わりにIPAexフォントの埋め込み

sudo kanji-config-updmap-sys ipaex

をしました. 私はIPAexでも十分綺麗だと思うので問題に感じませんでしたが, いつかきちんと再設定できるといいですね. とにかく,

kanji-config-updmap status

と入力して, CURRENT family for ja: [hogehoge] みたいな感じで表示されればフォントの埋め込みに成功していると思います.
これで,

ptex2pdf -l -ot -kanji=utf8 -synctex=1 [YOUR_TEX_PROGRAM]

コンパイルがうまく通り, pdf が出力されればOK.



次に, VSCode の方です. 拡張機能 LaTeX Workshop を入れましょう. そして, settings.json に 以下を追記します.

    "latex-workshop.latex.tools": [
      {
          "command": "ptex2pdf",
          "args": [
              "-l",
              "-ot",
              "-kanji=utf8 -synctex=1",
              "%DOC%"
          ],
          "name": "ptex2pdf"
      },
    ],
    "latex-workshop.latex.recipes": [
        {
            "name": "ptex2pdf",
            "tools": [
                "ptex2pdf",
            ]
        }
    ]

注意すべきなのは(おそらくVSCodeTeXの環境構築をやろうとしている多くの人が見ているであろう http://elecho.hatenablog.com/entry/2017/04/27/175500 など, 古い記事にはしばしば見られる) latex-workshop.latex.toolchain が現在は Obsolete になっていて, 代わりに latex-workshop.latex.tools でコマンドを定義して, latex-workshop.latex.recipes で(レシピという名の通り)それを組み立てる感じになっている点です. もっとしっかりとした(bibtexなども使った)settings.json の書き方は以下のようなものが参考になります. 確かにレシピっぽい.

gist.github.com



環境構築は以上の通りです. 実際の使い方は,

  1. ある程度TeXファイルを書き上げたら, Command + Shift + P -> LaTeX Workshop: Build with recipe -> ptex2pdf で pdf を生成
  2. VSCode の右上の虫眼鏡みたいなやつをクリックすると右に生成されたpdfが別タブで表示される
  3. その後はTeXファイルを適当にいじって保存すると, Sync TeXの機能によって自動で右側のpdfも修正されていく.

という感じですね. 良きTeX生活を!

Rustの配列に関する雑多なメモ

多重配列の初期化

固定長のarrayの場合は[T; N]という形式なので, 例えば2次元配列の場合は,

let a: [[i32; 3]; 2] = [[0; 3]; 2];
println!("{:?}", a); // [[0, 0, 0], [0, 0, 0]]

let mut b = [[39; 2]; 3]; // 型を省略すると推論される
b[1][1] = 8;
println!("{:?}", b); // [[39, 39], [39, 8], [39, 39]]

で良いと思われる. 初期化せずに値を入れようとするとコンパイルエラーになる.

では, 可変長のvecを二重に包む場合は?と考えてみる. 一つはvec!マクロを使う方法が考えられて,

let mut v1: Vec<Vec<i32>> = vec![vec![99; 3]; 2];
v1[1][1] = 8;
println!("{:?}", v1); // [[99, 99, 99], [99, 8, 99]]

とarrayとほぼ同様の操作ができる.
また, resizeメソッドという, vecを指定の長さに変更する(長さが増える場合は指定された値で足りない要素を埋める)メソッドを2回使えば,

let mut v2: Vec<Vec<i32>> = Vec::new();
v2.resize(4, Vec::new());
for i in 0..4 { v2[i].resize(2, i as i32); }
v2[1][1] = 8;
println!("{:?}", v2); // [[0, 0], [1, 8], [2, 2], [3, 3]]

と言う感じで行けるようだ.
注意すべきなのは, Vec::with_capacity(10)というように初期化したり, v.reserve(10)というように領域を確保しても, 領域が保証されているだけで, そのまま値を入れようとすると実行時にエラーになってしまうので, 気をつけたい.

let mut c: Vec<i32> = Vec::with_capacity(10);
println!("{}", c.capacity()); // 10
println!("{}", c.len()); // 0
// c[2] = 8; // 実行時エラー


配列のループ

https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/loops.html

Rustのループは

for var in expression {
  code
}

という形式で, expressionの部分はイテレータを返す式にしなければいけないようだ. 逆にイテレータにすることでmax, minやfold, map, filter等の配列特有のメソッドを利用できるようになる. https://doc.rust-lang.org/std/iter/trait.Iterator.html https://qiita.com/lo48576/items/34887794c146042aebf1

arrayの場合は, array自体はiterableではないので何かしら付ける. arrayの大きさが32以下であれば, &[T]IntoIteratorと同様になる. それ以上の大きさであれば.iter(), .into_iter() を使う.

let a = [2, 3, 5, 7, 11];

for i in &a {
  print!("{} ", i); // 2 3 5 7 11 
}

for i in a.iter() {
  print!("{} ", i); // 2 3 5 7 11 
}

for i in a.into_iter() {
  print!("{} ", i); // 2 3 5 7 11 
}

元の配列をmutableにし, ループでもmutableな参照にすることで, 規則的に配列を変換することができる.

let mut p = [2, 3, 4];

for i in &mut p {
  *i *= 2;
}

println!("{:?}", p); // [4, 6, 8]

これはmapの方が楽なのでは?と思ったが, 調べるとRustのイテレータのmapは遅延評価なので, 使いにくいことがわかった.

let a = [2, 3, 4];
let b = a.iter().map(|x| x * 2);
println!("{:?}", b); // Map { iter: Iter([2, 3, 4]) }

// collect()で評価させる
let c: Vec<i32> = a.iter().map(|x| x * 2).collect();
println!("{:?}", c); // [4, 6, 8]

IntoIterator はsliceが使え, 既存のデータ構造の一部を安全に取り出して操作することができる.

let mut p = [0; 20];

for i in &mut p[2..10] {
  *i = 9;
}

println!("{:?}", p); // [0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]


vecはiterableなのでそのままループできてしまうのだが,

let mut v = vec![2, 3, 5, 7, 11];

for i in v {
  print!("{} ", i); // 2 3 5 7 11
}

これだと所有権がiに移ってしまってループの後でv.push(13);などが出来なくなってしまう. なので, 参照渡しを徹底するように心がけたい.

let mut v = vec![2, 3, 5, 7, 11];

for i in &v[2..4] {
  print!("{} ", i); // 5 7
}

v.push(13);

println!("{:?}", v); // [2, 3, 5, 7, 11, 13]