immutableJSのMapをredux-persistで保存できないときの対処法

以前説明したimmutableJSは簡単にimmutableオブジェクトを扱うことができるのでreduxのreducerで大活躍します。 normalizr風味のreducerを作る場合は、下記のようにimmutableJSのMapとListで簡潔に書く事ができます。

import { ADD_THOUGHT } from "../actions/thought";
import { Map, fromJS } from "immutable";

const initialState = fromJS({
  ids: [],
  entities: {}
});

export default function thought(state = initialState, action) {
  switch (action.type) {
    case ADD_THOUGHT:
      return state
        .setIn(["entities", action.draft.tid], action.draft)
        .set("ids", state.get("ids").push(action.draft.tid));
    default:
      return state;
  }
}

一つ問題があります。immutableJSはfromJSを利用するとMapオブジェクトでstateを保存します。これが、redux-persistを使うとMapで保存できないという問題です。redux-persistはtransformsという機構でserialize, deserializeを行うことができ、ここにモジュールを適用することで保存できるようになります。redux-persistのauthorであるr2tzz氏が提供するredux-persist-immutableがありますが、READMEの情報が少なくよくわかりません。他のソースを読むと、トップレベルでMapの置換を適用するようです。私の今回のreducerではnavというReact Navigatorのreducerがあるので、ここに適用する必要はないと思っていました。

Redux Persist Transform Immutable

Redux Persist Transform Immutableというものもr2tzz氏が提供しています。これは、reducerごとにシリアライズ・デシリアライズをできるものです。これはREADMEもしっかり書いてあります:)

では、実際のコードを見てみましょう。

import immutableTransform from "redux-persist-transform-immutable";

...

  componentWillMount() {
    persistStore(
      this.store,
      {
        storage: AsyncStorage,
        transforms: [
          immutableTransform({
            whitelist: ["user", "draft", "thought"],
            blacklist: ["nav"]
          })
        ]
      },
      async () => {
        await this.store.dispatch(initUser());
        await this._loadAsync();
      }
    ); //.purge();
  }

使い方は、transformsのvalueにimmutableTransformを入れます。whitelist/blacklistで追加します。どちらかを指定すれば片方がいらないかもしれませんが、わかりやすくするためにwhitelist/blacklist両方いれています。React Nativeでの利用なので、storage: AsyncStorageはそのままです。 ちなみにpersistStore(..).perge()でstateを消去できるので、開発初期にはよく使うことになると思います。

Redux + immutable.js + Reselectでredux reducerを安全かつ簡潔にする

日本語の技術ブログを読んでいるとImmutable.jsでmodelを作り、そこにロジックを入れる方法が書かれてるものが多いですが、英語の情報をまでみてみると、それはどちらかというと特殊な方法で多くのところでReduxのReducerに導入しています。

今回は、後者の方法を説明していきます。

Immutable.js

Reduxのstateはimmutableで操作すべきであり、そのためObject.assign()slice等を駆使してReducerを書いていきます。問題はその方法が冗長であり、人によってES6を使わないなど書き方も違うのでコードが見にくくなります。これを解決するのがImuutable.jsになります。ドキュメントを見ると(Typescript前提で書かれてるドキュメントなので、知らないと非常に読み難い…)、便利な関数が沢山生えてます。setInなどの関数を使えば、Nestされているオブジェクトに一発でデータを挿入できたりします。そして、そのオブジェクトが常にImmutableであることを保証してくれます。

Immutablejsのreducerへの組み込み方は簡単で、InitialStateをimmutableJSで作れるだけです。

import { ADD_TITLE, ADD_ELEMENT } from "../actions/draft";
import { Map, fromJS } from "immutable";

const initialState = fromJS({
  title: null,
  element: null,
  standards: {}, 
  answers: {}
});

export default function draft(state = initialState, action) {
  switch (action.type) {
    case ADD_TITLE:
      return state.set("title", action.title); // 直感的かつ簡潔!
    case ADD_ELEMENT:
      return state.set("element", action.element);
    default:
      return state;
  }
}

上記のstate.draft.standardsはImmutable.Mapオブジェクトとなります。問題はこれをtoJS,toArray, toObjectをコンポーネント内のRedux stateの呼び出しのmapStateToPropsで使うのがReduxのアンチパターンになっていることです。

This is a particular issue if you use toJS() in a wrapped component’s mapStateToProps function, as React-Redux shallowly compares each value in the returned props object. For example, the value referenced by the todos prop returned from mapStateToProps below will always be a different object, and so will fail a shallow equality check.

で、どうするか。ここで登場するのがReselectになります。

Reselect

Reselectは、キャッシュを持ち、変更があった場合だけ新しいオブジェクトを作成するような挙動を記述できます。これをmapStateToPropsに入れ込むことにより、アンチパターンを避けることができます。 具体的なコードは、

# app/selectors/draft.js
import { createSelectorCreator, defaultMemoize } from 'reselect';
import { isEqual } from 'lodash';

const createDeepEqualSelector = createSelectorCreator(
  defaultMemoize,
  isEqual
);

const draftStandardsSelector = (state) => state.draft.get('standards').toObject();

export const standardsInDraft = createDeepEqualSelector(
  [draftStandardsSelector], // InputSelector(s)
  (standards) => {
    return standards;
  }
);

詳しくはREADMEや他の技術ブログを参考にしていただければと思いますが、標準の比較関数は単純なオブジェクト比較(===)を利用しているため、オブジェクトのkey/valueのペアまで比較できません。ここでlodashisEqualを使って、deep比較をすることにしている(今回の場合、shallowな比較(ネストされたものは比較しない)で十分ではあるがREADMEがそうなっているので))。createDeepEqualSelectorの最初の引数が、Input Selectorと呼ばれるもので、ここが以前のもの(キャッシュ)との比較対象です。isEqualコードを読んでいないが直感的にあまりnestしすぎない方が比較自体の計算量が少なくなって良さそうですね。

これをcomponentのmapStateToPropsで直に呼び出すのは、単純です。

import { standardsInDraft } from '../selectors/draft';
...

function mapStateToProps(state, ownProps) {
  const title = state.draft.get("title");
  const element = state.draft.get("element");
  const standards = standardsInDraft(state); // directly calling
  return { title, element, standards };
}
export default connect(mapStateToProps)(Editor);

これでreducer内でimmutablejsで快適にstateを操作できるようになりました!

[読書] CRITICAL THINKING: A Beginner’s Guide To Critical Thinking, Better Decision Making, And Problem Solving !

CRITICAL THINKING: A Beginner’s Guide To Critical Thinking, Better Decision Making, And Problem Solving ! ( critical thinking, problem solving, strategic thinking, decision making)

という本を読んだ。一般的な概念の説明で終わってしまい実際どうやってCritical Thinkingを使うのかが言及されていない。Critical Thinking実行方法は、下記のようだが、

How to Carry Out Critical Thinking Step By Step

  1. Establish what the problem is
  2. Undertake to analyze the problem
  3. Think up manageable solutions
  4. Choose the best possible solution
  5. Wind up your process

これが全体の流れでどこがそこにあたるのかわからない。

  1. Observation
  2. Analysis
  3. Interpretation
  4. Reflection
  5. Evaluation
  6. Inference
  7. Explanation
  8. Problem solving
  9. Decision making

341円で知らない単語を覚えることができたと思えばよいが、内容はいまいちでした。

2017年Python環境設定 – andaconda/virtualenv/cookiecutter/dotenv

1. Install anaconda / conda

condaによるポータブルなPython環境構築のすすめを参考にすると、(ana)condaが最近の主流とのこと。 下記にアクセスし、各OSのパッケージをダウンロードしてインストール。3系を使います。

https://www.continuum.io/downloads

あとでLinux上で動かす予定なので、なるべく依存関係を作りたくないので、pyenvの導入はしないことにしました。

2. Create virtualenv

Rubyのbundler相当なのが、virtualenvらしいので、仮想環境を作り、その環境に入る。

$ conda create -n kagamibot
$ conda env list
$ source activate kagamibot # deactivate で出る

3. Install packages

基本、conda installでパッケージを入れていくだが、パッケージがないことも多く、そんなときは、conda-forgeを入れるようになるので、最初から追加しておく

$ conda config --add channels conda-forge

とはいえ、virtualenv変更後はpipでも良いので、 conda installでなかったら、pip installみたいな感じ。

$ conda install xxx 
$ pip install xxx

virtualenvでのパッケージはcondaで管理できるので、新しいプロジェクトをコピーするときは、下記のようにexport/importする

conda env export > myenv.yaml  #  conda env create --file myenv.yaml

(参考)http://qiita.com/Hironsan/items/4479bdb13458249347a1

4. Create Project

調べてみるとプロジェクトのディレクトリ構造の自由度が高い。 つまり、人々が各々ディレクトリを作ったり、ファイルを作ったりしている。それがフレームワークを使ってる私みたいな人には、非常に気持ちが悪い。 プロジェクトの雛形が欲しいので、調べるとcookiecutterというのがあるのでそれを使う。

今回はデータサイエンス用のテンプレを使用しているが、テンプレートはpythonだけでなく多言語のもある。 詳しくは、githubのcookiecutterを参照してほしいが、 下記のようなboilerplateができるので自分で考えなくてよいのは非常によい。

├── LICENSE
├── Makefile           <- Makefile with commands like `make data` or `make train`
├── README.md          <- The top-level README for developers using this project.
├── data
│   ├── external       <- Data from third party sources.
│   ├── interim        <- Intermediate data that has been transformed.
│   ├── processed      <- The final, canonical data sets for modeling.
│   └── raw            <- The original, immutable data dump.
│
├── docs               <- A default Sphinx project; see sphinx-doc.org for details
│
├── models             <- Trained and serialized models, model predictions, or model summaries
│
├── notebooks          <- Jupyter notebooks. Naming convention is a number (for ordering),
│                         the creator's initials, and a short `-` delimited description, e.g.
│                         `1.0-jqp-initial-data-exploration`.
│
├── references         <- Data dictionaries, manuals, and all other explanatory materials.
│
├── reports            <- Generated analysis as HTML, PDF, LaTeX, etc.
│   └── figures        <- Generated graphics and figures to be used in reporting
│
├── requirements.txt   <- The requirements file for reproducing the analysis environment, e.g.
│                         generated with `pip freeze > requirements.txt`
│
├── src                <- Source code for use in this project.
│   ├── __init__.py    <- Makes src a Python module
│   │
│   ├── data           <- Scripts to download or generate data
│   │   └── make_dataset.py
│   │
│   ├── features       <- Scripts to turn raw data into features for modeling
│   │   └── build_features.py
│   │
│   ├── models         <- Scripts to train models and then use trained models to make
│   │   │                 predictions
│   │   ├── predict_model.py
│   │   └── train_model.py
│   │
│   └── visualization  <- Scripts to create exploratory and results oriented visualizations
│       └── visualize.py
│
└── tox.ini            <- tox file with settings for running tox; see tox.testrun.org

作成方法は、

$ conda install cookiecutter
$ cookiecutter https://github.com/drivendata/cookiecutter-data-science 
# インタラクティブに質問に答えるかたちでプロジェクト作成

プロジェクト名のディレクトリが作成されるので、そこに入り、パッケージをインストール。

$ cd kagamibot 
# pipのインストール用のrequirements.txtをcondaでインストールする。
$ cat requirements.txt | while read package; do conda install $package; done

5. python-dotenv

Githubに載せたくないAPIのキーなどを管理する cookiecutterのテンプレートにあるのに、requirements.txtにないのでインストール。

$ conda install phython-dotenv

# PROJECT_ROOT/.env
TWITTER_AUTH_TOKEN=xxx

使い方は、下記のような感じ。

import os
from dotenv import load_dotenv, find_dotenv

# find .env automagically by walking up directories until it's found
dotenv_path = find_dotenv()

# load up the entries as environment variables
load_dotenv(dotenv_path)

twitter_auth_token = os.environ.get("TWITTER_AUTH_TOKEN")

以上が、色々なページを参考にして構築した環境です。 こういうのって時間がかかるので、参考になれば幸いです。