isyumi_netブログ

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

処理の流れを型で表現しよう

もしかしたらあなたは型をめんどくさいものだと感じているかもしれない。型宣言は動くようにするために仕方なくするものと思っていないだろうか。もしそうなら型を使って頭を整理する習慣について知ってほしい。きっとあなたは型を極力狭く定義したほうが捗ることに気付くだろう。

このコードは二つの数を掛け算するコードだ。掛け算する前の構造体のsumには0を入れてある。このコードは間違いなく動く。

type Sum struct {
Num1 int
Num2 int
Sum int
}

func sum() {
s := Sum{
Num1: 3,
Num2: 4,
Sum: 0,
}

s.Sum = s.Num1 + s.Num2

}

しかし、このように計算前の構造体と計算後の構造体を用意すればどうだろう。コードを読む人は一目で「ああ、ここまでは計算結果が出ていないんだな」ということが分かるし、計算する前の0という値を間違って使ってしまうこともなくなる。

type Operand struct {
Num1 int
Num2 int
}
type Result struct {
Num1 int
Num2 int
Sum int
}

func sum() {
s := Operand{
Num1: 3,
Num2: 4,
}

r = Result{
Num1: s.Num1,
Num2: s.Num2,
Sum: s.Num1 + s.Num2,
}

}

このように、処理の大雑把な流れを型で表現しよう。入ってくる値の型は何で出て行く型の価は何か、その中間はどんなデータ構造か。そして型と型を橋渡しするようにコードを書いていくと疲れにくい。

コマンド化して副作用を追い出そう

多くのプログラムにはこのような外の世界にデータを書き込む処理が登場します。

  • メール送信
  • DBに保存
  • ファイルに書き込み
  • HTTPリクエストにレスポンス

このような処理をここではIOと呼びます。IOが混じるコードのテストは複雑です。多くの場合実機確認することになります。DBに狙った通りにデータが書き込まれたか確認するフレームワークなどありますが、難易度が高いです。また、多くの場合このようなフレームワークは動作が遅いです。手早くシンプルにテストをかけるようにするためにどうすればいいでしょうか。

まず考え方ですが、IOの混じるコードをIO処理の部分とそれ以外の部分に分けて考えます。そして

  • 何を書き込むか計算する処理をテストできるように書く
  • DBやファイルへきちんと書き込めているかは実機確認する

という方針でコードを切り分けていきます。下のコードを見てください。

void main() {
doOperation();
}

void doOperation() {
// なんか複雑な処理
var x = calc(); // なんか複雑な計算
// 複雑な処理
file.put(createData()); // 何かのデータをファイルに保存

db.insert(x);// DBになんか保存
}

calc() {
}

 

 短くてメリットが分かりづらいですが、//なんか複雑な処理 にはすごく複雑な処理が書かれているとしてください。doOperation関数内でファイルへの書き込みとDBへの書き込みが実行されています。この場合、動作確認時にxの値やcreateDataの戻り値を確認するにはDBを見なければいけません。そこで、DBやファイルへの書き込みをコマンド化したオブジェクトを導入し、それを戻り値とします。そして、戻した先で実際のIO処理をします。

void main() {
// 戻り値をコマンドで取得
var commands = doOperation();

// コマンドを一個ずつ実行していく
for (var command in commands) {
if (command is DBInsertCommand) {
db.insert(command.data);
} else if (command is FileWriteCommand) {
file.put(command.data);
}
}
}

// メソッドの中で複雑な計算結果をfileに書き込んだりDBに保存したりする
List<Command> doOperation() {
var commands = [];

// なんか複雑な処理
// 複雑な処理

// 何かのデータをファイルに保存するコマンドを作成
commands.add(new FileWriteCommand()..data = createData());

var x = calc(); // なんか複雑な計算
// DBにInsertするコマンドを作成
commands.add(new DBInsertCommand()..data = calc());

return commands;
}

class Command {}

class DBInsertCommand extends Command {
int data;
}

class FileWriteCommand extends Command {
int data;
}

 

ここではDBInsertCommandとFileWriteCommandをListに入れて戻し、mainメソッド内で実行しています。この一手間によりxやcreateDataの値を簡単にテストできるようになりました。

このようにコマンド化して副作用を追い出していきましょう。

 

補足

command is DBInsertCommand

command is FileWriteCommand

と書いてあるところですがこれはダウンキャストが成功するかどうかでコマンドの方を確認しています。

オプショナル型や代数的データ型がある言語はいい感じに書けると思います。

補足2

一度Listに入れてまとめて返すというのはあまりパフォーマンスが良くないです。その言語のStream型やジェネレータ構文などがないか調べて見ましょう。

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

 

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();
}

 

 

状態を管理する変数は正規化しよう

先日のブログで「あなたの書いたプログラムには状態を保持した変数がある。そこが散らかるとめんどくさいことになるのできちっと管理しよう」という話をした。 

blog.isyumi.net

 

おさらいだが、状態変数というのはサイクルをまたぐ変数だ。博士のメモだ。管理するための色々な技法があるので一つ紹介しよう。RDBと同じように状態変数も正規化しよう。正規化はデータ同士の不整合が起こらないようにするために、重複した値を削除していくことだ。データの矛盾にはシステム外との矛盾とシステム内での矛盾がある。あなたのシステムに20歳と登録されている人が本当は30歳だった、というような矛盾は防ぎづらい。しかし、システムのある場所には20歳と登録されているのに、他の場所には30歳と登録されるような間違いは正しいプログラミングをすれば防げるはずだ。

ここに動画一覧とユーザー一覧がある。ここでuser1がvideo2を再生したとしよう。動画エンティティの視聴ユーザー一覧にuser2を追加した。

正規化していない

しかし、user1の視聴動画一覧にvideo2を追加するのを忘れてしまった。これでは状態変数が矛盾してしまう。そこで動画一覧とユーザー一覧に加えて視聴履歴一覧を追加した。これで視聴履歴一覧に値を挿入するだけでよくなった。

正規化した

小さな点だが勘違いしないようにしたいのは、正規化の目的は手数を減らすことではなく矛盾が発生しないようにすることだ。データ構造を考えるときはどうすればより矛盾が発生しにくいデータ構造になるかを考えよう。

また、重複には「言えること」も含まれる。例えば、四角形の縦と横と面積を持つ変数を考えてほしい。

class Rectangle {
int height;
int width;
int area;
}

四角形の面積は縦 * 横である。つまり、縦・横・面積の三つのうち2個判明していれば最後の1個は不要である。状態として持つのではなく、面積が必要になった時に計算すればいい。

class Rectangle {
int height;
int width;
}

他の例をあげれば

  • 誕生日と現在時刻と年齢
  • Aさんの位置とBさんの位置と距離
  • 税抜き価格と税率と税込価格

などがある。このような変数も重複と考えて削除していこう。

ifの判定式に名前をつけよう

今日紹介するテクニックは実に些細なことだ。しかし、この習慣は有意義だと思う。だから、是非あなたのプログラミングに取り入れてほしい。

 

if(***ここ***){ が長すぎると読みづらいから気をつけよう。

if(aaa == bbb) {

}

このくらいならたいしたことにはならない。

if(aaa % 333 == 1 && iii * 2 == 100){

}

まあ、何とかなる。

if(!(aaa % 333 == 1 && iii * 2 == 100) || !(uuu - eee == ooo)){

}

もう何がなんだか分からん。ただでさえIF文はコードを読む上で神経を使うところだ。そこにこのような分かりづらい式が書いてあると大変に読みづらい。

こういう場合

if( checkAAA(iii) ) {

と別関数にしてしまおう。別関数に別ければ無理に一行で書く必要がなくなる。読みやすいように複数行に別けて書けばいい。しかし、新しい関数をつくるのがめんどくさい場合がある。そんなときはその直前に変数を作るのが簡単だ。

var check = aaa == bbb;

if( check) {

}

 

var hoge = aaa % 333 == 1;

var fuga = iii * 2 == 100;

if ( hoge && fuga) {

}

 

だいぶ読みやすくなった。僕は散らかったコードをこのルールで書き換えただけで見違えるようによくなったケースを見てきた。かなり実践的だと思うのでオススメだ。

状態を管理しよう

プログラムには実行中にどんどん書き換わっていく変数と、使い捨てにされる変数がある。イメージしてほしいのは博士の愛した数式だ。あなたは日中に考えたことはすべて記憶しているが、毎晩寝る度に全部忘れてしまうとする。しかし、机にメモがおいてあり寝る前にそのメモに書き置きをする。そのメモに書いてあることは翌日必ず思い出すことができる。今日の話は、そのメモを整理整頓しようということだ。

例えば、おにぎりを売る会話のプログラムを考えてほしい。

  1. 相手からこんにちはというメッセージが送られてくる
  2. それに対してこんにちはと返事をし、あいさつ済フラグをTrueにする
  3. おにぎり3個くださいとメッセージが届く
  4. それに対して○○円ですと返事をし会話を終了する
  5. 挨拶していない人がおにぎりを要求してきたら無視する
  6. おにぎりの値段は時々変わる

大体のプログラムはこのように

  1. 何かの処理が始まるきっかけを待ち受ける
  2. 何かが起きたら特定の処理をする
  3. 終わったらまたきっかけを待ち受ける

という流れになっている。この時処理を始めるきっかけをイベントと呼ぶ。イベントには

  • ユーザーが画面をクリックした
  • ブラウザがサーバにアクセスした
  • サーバーがクライアントにプッシュ通知をした
  • 指定時刻に仕掛けて置いたタイマーが発動した

等がある。そして、イベンドが起きてから処理が終わり次のイベントの待ち受に入るまでを、ここでは1サイクルと呼ぶことにする(そんな一般用語はないので外で使うときには注意)。今日はサイクルとサイクルをまたぐ変数について話したい。とりあえず下記のコードを見てほしい。

状態変数にいろんなところからアクセスしている

 

aisatuとonigiriPriceはサイクルの度にどんどん書き換わっていく変数だ。ここでは状態変数と呼ぶことにする。おにぎりの合計金額を計算するところで使ったsum変数は使い捨ての変数だ。肌感覚で知っておいてほしいこととして、この状態変数の読み書きが散乱していると高確率でバグる。もしあなたが今までプログラミングをしてきて、なぜかそんなに難しいことをしているつもりはないのにだんだん機能修正が難しくなってくるように感じているなら、おそらくこの状態変数の持ち方が悪い。自分の中で「ソースコード上のどこで状態変数を読み書きするか」についてマイルールを作ることをお勧めする。下のコードを見てほしい。

 

状態変数の読み込みと書き込みを局所化

このコードは

  • onMessageの最初に状態変数を読み取り
  • 途中で新しい状態変数を生成し
  • 最後に上書きする

というルールで書き換えてみた。この程度の長さではあまり恩恵を実感しづらいが、このように「全体に影響する変数をどこで読み込みどこで書き込むか」をしっかり管理することが安全なコードを書くテクニックだ。参考にしてほしい。

 

早期ReturnでIF文の組み合わせを見やすくしよう

IF文の中を整理する簡単な方法を紹介しよう。

こういう仕様を考えてほしい。

お客さんがある商品を購入可能か判定したい。購入可能の条件として

  • ログインしているか
  • 在庫があるか
  • 在庫がないなら再入荷可能か
  • お金は足りているか

があったとする。購入可能ならTrueを、購入不可能ならFalseとReasonを返してほしい。途中でReturnしないならこういうコードになる。

途中でReturnしないIF文

ネストが深く分岐のルートが追い辛いためリーダビリティが低い。さらに、こういうコードは変更に弱い。ある日急に「お金が足りなく、かつ在庫もなければどんなエラーが出るの? その場合お金が足りてないメッセージを優先して出してほしいんだけど」って言われたとして、対応可能だろうか。

こういうコードはとにかく順番にReturnしていこう。

どんどんReturnしていくIF文

見やすくなったであろう。このコードは読みやすいし処理が追いやすい。また仕様とコードの見た目の乖離が少ない。どんどんReturnしよう。

ちなみに、こういうIF文はガードと呼ばれる。ある条件を満たしているか確認してからやりたかった処理をするためのIF文のことだ。