isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

正規化した状態変数を展開する関数を作ろう

 

blog.isyumi.net

 

前回、状態を管理する変数は正規化しようという話をした。これは状態を管理するうえで役に立つ。しかし、その変数を使うときにめんどくさくなる。そこで、正規化した変数を非正規化する専用の関数を一つ作ろう。状態変数を正規化することと、正規化した変数をもとに戻す関数をセットで作ることによって安全かつ簡単なプログラミングができるようになる。以後この関数を展開関数と呼ぶ。これはRDBのcreate viewに相当する。まず、展開関数の役割について具体的に考えよう。

  • 展開関数は正規化した状態変数を使いやすい型に変換する関数
  • 戻り値は「後続の処理で使いやすいか?」が絶対正義
  • 値の重複を削除した型から値の重複する型へ変換する
  • 「いえること」もここで計算する
  • 戻り値はサイクルの中で使い捨てにする

そして、展開関数でやってはいけないこともはっきりさせておこう。

  • 状態変数以外の変数の読み取り(定数なら可)
  • 展開関数の外にある変数の書き込み

次に、この関数を何に使うかだ。

  • 新しいサイクルが始まったら、まず状態変数を読み取って展開関数にかける
  • その後の処理は展開関数の戻り値を使う(状態変数に触らない)
  • 何かをシミュレーションするような処理に使ってもいい
  • テスト

シミュレーションというのは、例えば展開関数の中に現在の所持金をもとにある商品を購入可能か判定する式があったとする。あるユーザーが現在100円を持っており、商品Aはお金が足らず買えなかったとする。もし、ユーザーが150円の給料を得たとき、商品を購入可能か判定したくなった。その場合も、展開関数自体は副作用を一切持たないので適当にダミーの状態変数をこしらえて展開関数にかければ、どこにも影響を出さず結果を判明させられる。このように、もし○○な状態だったらどうなるか簡単に確かめるのも展開関数の使途の一つだ。

では実例を見よう。コツだが、引数と戻り値の数が増えすぎたらめんどくさいので、とりあえず登場する状態変数と展開後の変数(ここではModelと命名しました)を一つのオブジェクトツリーにまとめよう。

//ここから状態変数
class State {
DateTime now;
List<StateVideo> videos = [];
List<StateUser> users = [];
List<StateHistory> historyItems = [];
}

class StateUser {
String userID;
DateTime birthday;
}

class StateVideo {
String videoID;
}

class StateHistory {
String videoID;
String userID;
}

//ここから展開後の変数
class Model {
List<ModelVideo> videos = [];
List<ModelUser> users = [];
}

class ModelVideo {
String videoID;
List<String>playedUsers = [];
}

class ModelUser {
String userID;
List<String> playedVideos = [];
}

後は順番に計算していくだけだ。

 

Model mapToModel(State state) {
return new Model()
..videos = mapToVideos(state.videos, state.historyItems)
..users = mapToUsers(state.users, state.historyItems, state.now);
}

// ビデオ一覧を作成
// ビデオインスタンスに視聴ユーザー一覧も入れる
List<ModelVideo> mapToVideos(List<StateVideo> videos,
List<StateHistory> historyItems) {
List<ModelVideo> results = [];

for (var v in videos) {
var mv = new ModelVideo()
..videoID = v.videoID
..playedUsers = filterPlayedUserIDs(historyItems, v.videoID);
results.add(mv);
}
return results;
}

// ユーザー一覧を作成
// ユーザーインスタンスに視聴ビデオ一覧も入れる
// 誕生日と現在時刻から年齢を計算する
List<ModelUser> mapToUsers(List<StateUser> users,
List<StateHistory> historyItems, DateTime now) {
List<ModelUser> results = [];
for (var u in users) {
var mu = new ModelUser()
..userID = u.userID
..age = culcAge(u.birthday, now)
..playedVideos = filterPlayedVideos(historyItems, u.userID);
results.add(mu);
}
return results;
}


 計算する処理を書いたら、

  1. 現在の状態の取り出し
  2. 展開関数で変換
  3. 使用
  4. 最新の状態に更新

を順番にやっていこう。

void onPlay(String userID, String videoID) {
// とりあえず変換する
var model = mapToModel(state);

// 使いたいように使う
print("ユーザーごとの視聴履歴は ${model.users}");
print("動画ごとの視聴ユーザーは ${model.videos}");

// 最後にstateを更新する
var newHistoryItem = new StateHistoryItem()
..userID = userID
..videoID = videoID;
var newHistoryItems = new List.from(state.historyItems)
..add(newHistoryItem);
state = new State()
..users = state.users
..videos = state.videos
..historyItems = newHistoryItems
..now = new DateTime.now();
}