後戻りのある
手続き型プログラミング
by ナムアニクラウド
後戻りのある処理とは
段階的メニュー
作戦選択
スキル選択
対象選択
次のキャラ
段階的メニュー
作戦選択
スキル選択
対象選択
次のキャラ
キャンセル操作
でもって
後戻りができる
本日のサンプルプログラム
サンプルプログラム
以下のようなプログラムを通じて考えよう:
• ユーザーから数値入力を3回受け付け、それぞれ x, y, z と呼ぶ
• 最後に x + y + z を計算して表示する
• ユーザーは「cancel」と入力することで、ひとつ前の数値入力
をやり直すことができる
>
サンプルプログラムの動作例
キャンセルしない場合
>> 1
>
サンプルプログラムの動作例
キャンセルしない場合
>> 1
>
> 1
> 2
>
サンプルプログラムの動作例
キャンセルしない場合
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
6
サンプルプログラムの動作例
キャンセルしない場合
>
サンプルプログラムの動作例
キャンセルする場合
>> 1
>
サンプルプログラムの動作例
キャンセルする場合
>> 1
>
> 1
> 2
>
サンプルプログラムの動作例
キャンセルする場合
>> 1
>
> 1
> 2
>
> 1
> 2
> cancel
>
サンプルプログラムの動作例
キャンセルする場合
>> 1
>
> 1
> 2
>
> 1
> 2
> cancel
>
サンプルプログラムの動作例
キャンセルする場合
> 1
> 2
> cancel
> 100
>
>> 1
>
> 1
> 2
>
> 1
> 2
> cancel
>
サンプルプログラムの動作例
キャンセルする場合
> 1
> 2
> cancel
> 100
>
> 1
> 2
> cancel
> 100
> 3
104
キャンセルを考慮しない場合
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
キャンセルされたときは、おとなしく終了してみる。
まず InputTargetAsync, FMap, Just について説明します→
キャンセルを考慮しない場合
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
今回のサンプルコードのために用意したメソッドを軽く紹介
InputIntegerAsync メソッド: () → Task<IMaybe<int>>
ユーザーに数値を入力してもらう。
成功すると Just<int>, 失敗すると Nothing<int> を返す
キャンセルを考慮しない場合
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
今回のサンプルコードのために用意した型を軽く紹介
IMaybe<T> インターフェース
Just<T>, Nothing<T> という派生クラスがある。
一言でいうとnullの代わり
キャンセルを考慮しない場合
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
今回のサンプルコードのために用意したメソッドを軽く紹介
FMap メソッド: (this Task<IMaybe<T>>, Func<T, T2>) → Task<IMaybe<T2>>
元となったTask<IMaybe<T>>の中身を射影して返す。
キャンセルの扱い
◆ユーザー入力の結果 Nothing<int> 型の値が得られたら、そ
の数値入力がキャンセルされたということにし、その前のユー
ザー入力をリトライしたい。
◆一方で、ユーザー入力の結果 Just<int> 型の値が得られたら、
次の入力へ進みたい。
自明な要件:
- 1番目の入力がキャンセルされたときは、0番目の入力というのは無いので、1番目の入力をリトラ
イする。
- 最後の入力が成功したときは、メニューにおける操作は全てできたということで処理全体が終了。
つらい!キャンセル処理
実装がつらい
// while で無理やり実装した例
while (true)
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) continue;
while (true)
{
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) break;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) continue;
Console.WriteLine(just3.Value);
return;
}
}
実装がつらい
// while で無理やり実装した例
while (true)
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) continue;
while (true)
{
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) break;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) continue;
Console.WriteLine(just3.Value);
return;
}
}
1回目の入力。
キャンセルされたら
もう一度入力を受け取る
2回目の入力。
キャンセルされたら
1回目の入力に戻りたい
3回目の入力。
キャンセルされたら
2回目の入力に戻りたい
実装がつらい
// while で無理やり実装した例
while (true)
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) continue;
while (true)
{
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) break;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) continue;
Console.WriteLine(just3.Value);
return;
}
}
ループが挟まることで、
何を実装したいのかの意図が
埋もれやすくなってしまった
これは3段階なのでまだマシ
つらさ
実装がつらい
// gotoで無理やり実装した例
First:
var x = await InputIntegerAsync();
if (x is not Just<int> just) goto First;
Second:
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) goto First;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) goto Second;
Console.WriteLine(just3.Value);
実装がつらい
// gotoで無理やり実装した例
First:
var x = await InputIntegerAsync();
if (x is not Just<int> just) goto First;
Second:
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) goto First;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) goto Second;
Console.WriteLine(just3.Value);
1番目は、キャンセルされると
1番目をリトライ
2番目は、キャンセルされると
1番目をリトライ
3番目は、キャンセルされると
2番目をリトライ
実装がつらい
// gotoで無理やり実装した例
First:
var x = await InputIntegerAsync();
if (x is not Just<int> just) goto First;
Second:
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) goto First;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) goto Second;
Console.WriteLine(just3.Value);
gotoを使って後戻りをすると、ちょっと
したミスで分かりづらいバグを呼ぶ
ラベル名があるおかげでwhileを使った
例よりも直感的
つらさ
節3:LINQ to 後戻り
いきなり完成形からいきます
完成形
var result = await PhaseProcess.Entry<Unit>()
.Then(async _ => await InputIntegerAsync())
.Then(async x => await InputIntegerAsync().FMap(p => x + p))
.Then(async y => await InputIntegerAsync().FMap(p => y + p))
.Run(Unit.Id);
Console.WriteLine(result);
今回はLINQを使った手法で実現しました! PhaseProcess な
る新しいクラスが今回の肝
Entryメソッドで書き始めて、後戻りの各段階でやりたい処理
をThenメソッドで繋げていって、最後にRunで実行できる
キャンセル無しの場合の比較
var result = await PhaseProcess.Entry<Unit>()
.Then(async _ => await InputIntegerAsync())
.Then(async x => await InputIntegerAsync().FMap(p => x + p))
.Then(async y => await InputIntegerAsync().FMap(p => y + p))
.Run(Unit.Id);
Console.WriteLine(result);
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
キャンセル無し
キャンセルあり
今回の手法
完成形と、キャンセル無しの場合の比較
var result = await PhaseProcess.Entry<Unit>()
.Then(async _ => await InputIntegerAsync())
.Then(async x => await InputIntegerAsync().FMap(p => x + p))
.Then(async y => await InputIntegerAsync().FMap(p => y + p))
.Run(Unit.Id);
Console.WriteLine(result);
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
キャンセル無し
キャンセルあり
今回の手法
キャンセル無しの場合
と同程度に、処理の流
れが追いやすいはず
完成形と、キャンセル無しの場合の比較
var result = await PhaseProcess.Entry<Unit>()
.Then(async _ => await InputIntegerAsync())
.Then(async x => await InputIntegerAsync().FMap(p => x + p))
.Then(async y => await InputIntegerAsync().FMap(p => y + p))
.Run(Unit.Id);
Console.WriteLine(result);
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
var z = await InputIntegerAsync().FMap(p => just2.Value + p);
if (z is not Just<int> just3) return;
Console.WriteLine(just3.Value);
キャンセル無し
キャンセルあり
今回の手法
IMaybeを外す処理も
ついでにサポート
節4: 後戻りのあるforeach
Foreachでも後戻りがしたい
キャラ1の作戦 キャラ1のスキル キャラ1の攻撃対象
キャラ2の作戦 キャラ2のスキル キャラ2の攻撃対象
キャラ3の作戦 キャラ3のスキル キャラ3の攻撃対象
コマンドRPGとかだと、複数のキャラクターが各々に段階的メニューを
通じて行動を決定する場合がある
サンプルプログラム
以下のようなプログラムを書きたい:
• ユーザーから数値入力を6回受け付ける
• それぞれ p1, p2, p3, p4, p5, p6 と呼ぶ
• 最後に (p1+p2)*(p3+p4)*(p5+p6) を計算して表示する
• ユーザーは「cancel」と入力することで、ひとつ前の数値入力
をやり直すことができる
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> cancel
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> cancel
>
> 1
> 2
> 3
> cancel
> 4
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> cancel
>
> 1
> 2
> 3
> cancel
> 4
>
> 1
> 2
> 3
> cancel
> 4
> 5
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> cancel
>
> 1
> 2
> 3
> cancel
> 4
>
> 1
> 2
> 3
> cancel
> 4
> 5
>
> 1
> 2
> 3
> cancel
> 4
> 5
> 6
>
サンプルプログラムの動作例
4回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> cancel
>
> 1
> 2
> 3
> cancel
> 4
>
> 1
> 2
> 3
> cancel
> 4
> 5
>
> 1
> 2
> 3
> cancel
> 4
> 5
> 6
>
> 1
> 2
> 3
> cancel
> 4
> 5
> 6
> 7
351
(1+2)*(4+5)*(6+7) = 351
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> 4
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> 4
>
> 1
> 2
> 3
> 4
> cancel
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> 4
>
> 1
> 2
> 3
> 4
> cancel
>
> 1
> 2
> 3
> 4
> cancel
> 5
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> 4
>
> 1
> 2
> 3
> 4
> cancel
>
> 1
> 2
> 3
> 4
> cancel
> 5
>
> 1
> 2
> 3
> 4
> cancel
> 5
> 6
>
サンプルプログラムの動作例
5回目の入力でキャンセルをする
>> 1
>
> 1
> 2
>
> 1
> 2
> 3
>
> 1
> 2
> 3
> 4
>
> 1
> 2
> 3
> 4
> cancel
>
> 1
> 2
> 3
> 4
> cancel
> 5
>
> 1
> 2
> 3
> 4
> cancel
> 5
> 6
>
> 1
> 2
> 3
> 4
> cancel
> 5
> 6
> 7
312
(1+2)*(3+5)*(6+7) = 312
foreachでも後戻りがしたい
var list = new List<int>();
foreach (var i in Enumerable.Range(0, 3))
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
list.Add(just2.Value);
}
Console.WriteLine(list.Aggregate((a, b) => a * b));
これがキャンセルを考慮しない実装
2回の入力を1セットとして、
3回繰り返す
foreachでも後戻りがしたい
var list = new List<int>();
foreach (var i in Enumerable.Range(0, 3))
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
list.Add(just2.Value);
}
Console.WriteLine(list.Aggregate((a, b) => a * b));
foreachループを使う場合、繰り返しをまた
いで後戻りできる必要がある
i = 2 のときにここで
キャンセルされたら
戻り先はここ (ただし i = 1)
完成形
var result = await PhaseProcess.ForEach(Enumerable.Range(0, 3),
builder =>
{
return builder.Then(i => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p));
})
.Run(Unit.Id);
Console.WriteLine(result.Aggregate((a, b) => a * b));
あるイテレーションの最初でキャンセルすると、その前のイテ
レーションの最後がリトライされる。 PhaseProcess.ForEach
がそういった処理を担ってくれる
完成形 ラムダ式のこの戻り値は
PhaseProcess<Unit, int> 型
あるイテレーションの最初でキャンセルすると、その前のイテ
レーションの最後がリトライされる。 PhaseProcess.ForEach
がそういった処理を担ってくれる
var result = await PhaseProcess.ForEach(Enumerable.Range(0, 3),
builder =>
{
return builder.Then(i => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p));
})
.Run(Unit.Id);
Console.WriteLine(result.Aggregate((a, b) => a * b));
キャンセルなしとの比較
foreach (var i in Enumerable.Range(0, 3))
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
}
PhaseProcess.ForEach(Enumerable.Range(0, 3), builder =>
{
return builder.Then(i => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p));
};
キャンセル無し
今回の手法
キャンセルあり
キャンセルなしとの比較
foreach (var i in Enumerable.Range(0, 3))
{
var x = await InputIntegerAsync();
if (x is not Just<int> just) return;
var y = await InputIntegerAsync().FMap(p => just.Value + p);
if (y is not Just<int> just2) return;
}
PhaseProcess.ForEach(Enumerable.Range(0, 3), builder =>
{
return builder.Then(i => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p));
});
キャンセル無し
今回の手法
キャンセルあり
まとめ
• キャンセルによる後戻りがあるような段階的な制御を、LINQ
的に扱えるようにしました
• キャンセル処理が必要ない場合と似たノリで書けます
• そのうちGitHubとかに実装を上げます
おわり
ちょっと待って?
モナド
フェーズ
今回のような後戻りできる制御において、後戻り1回ごとに遡っ
ていく単位となるメソッドを「フェーズメソッド」とか「フェー
ズ」と呼んでます
フェーズの合成
// PhaseProcess のことを PP と書くことにします
PP<int, int> phase1 = PP.Entry<int>().Then(x => 12);
PP<int, string> phase2 = PP.Entry<int>().Then(x => x.ToString());
PP<string, bool> phase3 = PP.Entry<string>().Then(x => x == “”);
PP<int, bool> bound = phase1.Then(phase2).Then(phase3);
PhaseProcess型の値に対する合成について考えることができる。
あるフェーズでの処理の戻り値は、次のフェーズでの処理の引数
となる
フェーズの合成:実は利用してる
var result = await PhaseProcess.ForEach(builder =>
{
return builder.Then(_ => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p))
.Then(y => InputIntegerAsync().FMap(p => y + p));
}, Enumerable.Range(0, 3))
.Map(list => list.Aggregate((a, b) => a * b).ToMaybe())
.Run(Unit.Id);
Console.WriteLine(result);
これは先ほどのforeachの例
フェーズの合成:実は利用してる
全体としての型が
PhaseProcess<Unit, int>
全体としての型が
PhaseProcess<int, int>
Thenとは違うラムダ式を取ってる
var result = await PhaseProcess.ForEach(builder =>
{
return builder.Then(_ => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p))
.Then(y => InputIntegerAsync().FMap(p => y + p));
}, Enumerable.Range(0, 3))
.Map(list => list.Aggregate((a, b) => a * b).ToMaybe())
.Run(Unit.Id);
Console.WriteLine(result);
フェーズの合成:実は利用してる
var result = await PhaseProcess.ForEach(builder =>
{
return builder.Then(_ => InputIntegerAsync())
.Then(x => InputIntegerAsync().FMap(p => x + p))
.Then(y => InputIntegerAsync().FMap(p => y + p));
}, Enumerable.Range(0, 3))
.Map(list => list.Aggregate((a, b) => a * b).ToMaybe())
.Run(Unit.Id);
Console.WriteLine(result);
ForEachの機能は、フェーズの
合成を駆使して実現してます
合成に対する単位元がある
var result = await PhaseProcess.Entry<Unit>()
.Then(async _ => await InputIntegerAsync())
.Then(async x => await InputIntegerAsync().FMap(p => x + p))
.Then(async y => await InputIntegerAsync().FMap(p => y + p))
.Run(Unit.Id);
Console.WriteLine(result);
PhaseProcess.Entry<T>() で得られるインスタンスは
「何もしない、キャンセルに関わらない」フェーズを表す。
これを他のフェーズと合成しても何も変わらない。
この”単位元”の型は PhaseProcess<T, T>
恐らく結合法則が成り立つ
// PhaseProcess のことを PP と書くことにします
PP<int, int> p = PP.Entry<int>();
PP<int, string> q = PP.Entry<int>().Then(x => x.ToString());
PP<string, bool> r = PP.Entry<string>().Then(x => x == “”);
PP<int, bool> combined1 = (p.Bind(q)).Bind(r);
PP<int, bool> combined2 = p.Bind(q.Bind(r));
フェーズの連鎖が3つ (p, q, r) があって、更にそれらを全て結
合したい。
ここでpとqを先に結合しても、qとrを先に結合しても、結果
的に得られる処理の流れは同じであることが仕様として期待さ
れるはず
フェーズの性質
• 単位元がある
• フェーズ同士の合成ができる
• フェーズ同士の合成には結合法則が成り立つ
フェーズの性質
それってモナドじゃね?
• 単位元がある
• フェーズ同士の合成ができる
• フェーズ同士の合成には結合法則が成り立つ
• 単位元がある
• フェーズ同士の合成ができる
• フェーズ同士の合成には結合法則が成り立つ
フェーズの性質
それってモナドじゃね?
モナドといえば
皆さんご存知のモナドがあります: Task<T> 型
今回紹介した仕組みって、実は SynchronizationContext ある
いは Task-like を適切に実装することでも実現できるのかもし
れない
本当のおわり
ご清聴ありがとうございました
その他の補助メソッド:Map
// Mapメソッド
// 値を射影する
// 以下の「処理2」がキャンセルされると、「Map処理」ではなく「処理1」が再実行される
PhaseProcess<int, string> x = PhaseProcess.Entry<int>()
.Then(async x => x.ToString().ToMaybe()) // 処理1
.Map(x => (x + 1).ToMaybe()) // Map処理
.Then(async x => (x == “”).ToMaybe()); // 処理2
その他の補助メソッド:Do
// Doメソッド
// ユーザー定義の副作用を起こす
// 以下の「処理2」がキャンセルされると、「Map処理」ではなく「処理1」が再実行される
PhaseProcess<int, string> x = PhaseProcess.Entry<int>()
.Then(async x => x.ToString().ToMaybe()) // 処理1
.Do(x => Console.WriteLine(x)) // Map処理
.Then(async x => (x == “”).ToMaybe()); // 処理2
PhaseProcessのメソッド群
メソッド
キャンセル
を要求
キャンセル
を受入
値を変換
ラムダ式を
実行
Then YES YES YES YES
Map YES YES YES
Do YES
今後の展望
• PhaseProcessって名前あまり気に入ってないので考えたい
• PhaseProcess.Entry<int>().Then() が長いので省略できるやつ欲し
い
• モナドとして見た場合の実装方法を考えてみたい
• 各フェーズのメソッドの戻り値が Task<IMaybe<T>> でなければな
らないが、Task<T> も許したい
// やりたいことに対して冗長
var intToString1 = PhaseProcess.Entry<int>()
.Then(async x => x.ToString().ToMaybe());
// 必要十分
var intToString2 = PhaseProcess.Entry<int>(async x => x.ToString());

後戻りのある手続き型プログラミング