[C#] Benefits of Right Fold (FoldRight) and Common Implementation Patterns
[C#] Exploring FoldRight: Benefits and Design Patterns for Implementation
古い記事を掘り起こすシリーズ第2弾。
→ 第1弾:【WPF】DataGridでコンスセルなUndoRedo
元ネタ:イテレーター非同期
実のところ最近、Codeがなかなか頭に入らないです![]()
元ネタの記事も良く分からん💦
この記事は上級者向けで、内容も一般的なC# Codeではないです。
※スライドモードを使用していますが、折り畳みを見る場合は本文を直接読んでください。
目次
それはそれとして
AIによるとFoldRight(右畳み込み)と CPS(継続渡しスタイル)という実装パターンだそうなので、これを基にWPFによる実装例を考えます(CPSは別項で)
どういう実装なのか?
FoldRightNonStrictが最も有用なため、下記にまとめておきます。理解し辛いので。
以下まとめ
FoldRightNonStrictとは?
→ 訳は「非厳密右畳み込み」となります。
他にFoldRightLazy、LookbackFoldなどと言い表すことも出来ます。
FoldRightNonStrictを使うと、
データは最初から最後まで順に流しながら、
判断と意味付けだけを“最後まで遅らせる”設計ができます
具体的に「何が可能になるか」
・最後に起きた事象を基準に、過去を必要な分だけさかのぼれる
・評価タイミングを呼び出し側に完全に委ねられる
・畳み込み関数そのものを Strategy として差し替えられる
→ 通常のCSharp実装ではこのようなストリームを構築するのは困難です。同様のライブラリはあると思いますが。
FoldRightNonStrict(LasyFoldRight)とYieldとの違い
参考:https://qiita.com/Tadahiro_Yamamura/items/26a925d9a670f01bf497
yieldも遅延評価なのでよく似ていますが、以下の違いがあります。
(以下AI)
本質的な制御構造の違い
yield return は 列挙(iteration)を遅延させる仕組みである。
コンパイラはメソッドを状態機械に変換し、MoveNext() が呼ばれるたびに次の要素を生成する。
このモデルでは、処理は常に 前から後ろへ一方向に進み、「次の要素」以外の未来の情報は存在しない。
一方、非厳格 FoldRight は 計算そのものを遅延させる構造である。
右畳み込みでは「現在の要素」と同時に「残りの計算(未評価)」が関数として渡されるため、
後続の結果に応じて 今 の振る舞いを変えることができる。
この違いは、設計上の表現力に直結する。
-
yield- 扱えるのは「次の値」だけ
- ストリーム処理・無限列・IO には強い
- 未来の結果が現在に影響する設計はできない
-
非厳格 FoldRight
- 「残りの計算」を値として保持できる
- 後方依存・条件付き累積・途中停止の判断を前段に反映できる
- CPS や
Func<T>を用いた構造的な設計が可能
言い換えると、yield は 制御フローの遅延であり、
非厳格 FoldRight は 評価戦略そのものの設計である。
C# では yield が実用上の遅延処理の主役になることが多いが、
「後から分かる情報を前に伝播させたい」ケースでは、
非厳格 FoldRight の方が本質的に適している。
Stackにdelegateを積むという観点から見たyieldとの違い
delegate を積んで遅延評価する場合、
yield は「次の delegate を 1 つだけ内部に隠し持つ仕組み」であり、
非厳格 FoldRight は「残りの delegate 群を値として明示的に構築する仕組み」である。
| 観点 | yield | 非厳格 FoldRight |
|---|---|---|
| delegate の所在 | 列挙子内部(不可視) | 値として露出 |
| 積み方 | 常に1ステップ先 | 任意に積める |
| 並び替え | 不可 | 可能 |
| 巻き戻し | 不可 | 可能 |
| 後方依存 | 表現不可 | 表現可能 |
なぜ yield では「後方依存」ができないか
yield は pull 型モデルだ。
Consumer → MoveNext() → Producer
要求が来た瞬間に「1ステップだけ」進む。
だから producer 側は、
未来の計算を値として渡せない
「残り全部」を関数として持たせられない
一方 FoldRight は、
現在 + (残りの計算)
を同時に扱う。
だから、
後でエラーが出るなら今止める
累積結果を条件付きで前に反映
ログの意味付けを後段基準で決める
といった設計が成立する。
ここから、イテレーター非同期で紹介されている Code Pattern を例に挙げていく。
....何度読んでも難解かつ不可解なので。
右畳み込みの実装パターン
元記事内から引用
興味がある方用
※ちなみに、素直に書くと左畳み込みとなります。C#は左畳み込みがデフォです。
- Stack版 FoldRight
public static TResult FoldRight<TSource, TResult>(this IEnumerable<TSource> source, TResult seed, Func<TSource, TResult, TResult> func)
{
////Stack<TSource> に source 全体を入れる
//Stack は後入れ先出し(LIFO)だから、
//シーケンスの順番が逆転する。
Stack<TSource> stack = new Stack<TSource>(source);
//逆転された順序で foreach が回る
つまり本当に「右から左」へ向かって畳む。
foreach (TSource element in stack)
seed = func(element, seed);
//seed とともに func(element, seed)
//を適用しながら畳み込みを進めていく。
return seed;
//seedは初期値
}
逆転された順序で foreach が回る
つまり本当に「右から左」へ向かって畳む。
用途
- 再帰的構造を後方から自然に構築したい時
たとえばリストを右側に追加する形の再構築や、構文木のような「後ろを見ながら構成する」処理。
- 短絡評価を右側からしたい時(NonStrict 系の右畳み込み)
右畳み込みは「まず最後の要素を処理してから、必要な時だけ前へ進む」というスタイルがとりやすい。
具体例
※スライドモードを使用していますが、折り畳みを見る場合は本文を直接読んでください。
折り畳み
1. ログ解析:**最後の異常値を起点に前方追跡**業務ログや計測ログでありがちなパターン。
例:「最後の ERROR 行を起点に、そこまでの直前に起きていた WARNING を集める」
シーケンスを前から読む左畳み込みだと、
“未来に起きる ERROR に反応して過去の WARNING を集める”
という逆向きのロジックになるため、状態を持ち回す必要が出てくる。
右畳み込みなら動作順そのまま。
最後の行を見る ↦ ERROR なら「追跡モード」に入って前にさかのぼる
→ WARNING を拾い続ける → 途中で通常行が挟まったら終了
ログの「後ろから読む」性質に自然に合う。
2. 設定ファイルやルールの“後勝ち”処理
環境設定・スタイル設定・ルールマージなどでよくある
後に書かれたものが優先されるという仕様によくマッチする。
3. SQL 的なクエリパイプラインのビルド
LINQ のクエリ生成や式ツリーを動的に組み立てる場面で、
最後のフィルタや並べ替えを基準に、前段の式をはめ込む
という処理がしばしば出てくる。
式ツリーの生成順は実質「右畳み込み」。
FoldRightStrict (再帰的に末尾からすべてを評価する右畳み込み)
※あまり実用的ではないので参考程度
public static TResult FoldRightStrict<TSource, TResult>(this IEnumerable<TSource> source, TResult seed, Func<TSource, TResult, TResult> func)
{
var iter = source.GetEnumerator();
// シーケンスを走査するための列挙子を取得
Func<TResult> rec = null;
// 再帰的に「残りの要素を右から畳み込む」処理を表す関数
// null 許容の delegate を一度だけ代入することで自己参照
//を実現(Yコンビネータの簡易版)
// 実際の再帰処理本体
rec = () =>
(iter.MoveNext())
// 次の要素へ進められれば
? func(iter.Current, rec())
// 現在の要素を関数に適用し、その結果として
//「残りの部分を再帰的に畳み込んだ値」を渡す
// → これにより「右側の要素が先に畳み込まれる」
//=右結合になる
: seed;
return rec();
}
長いシーケンスに対してはStackoverflowを起こす。
C# の実行モデルと相性が悪い。
概要
-
全要素分の再帰が発生し、深さが増えるほど危ない
再帰が 1 万〜数十万回レベルに達すると、
ほぼ確実に StackOverflowException が飛ぶ。 -
遅延シーケンスと相性が悪い
IEnumerator を使っているため、遅延評価そのものはできる。
ただし「常に MoveNext を進める」必要があるので、
途中で短絡したり“必要な分だけ読む”という芸当は苦手。
FoldRightNonStrict のように “要る時だけ後ろを読む” といった
高度な制御は実質不可能。
-
例外制御やデバッグがとにかくしんどい
-
メモ化や CPS でラップしない限り高速化も難しい
FoldRightNonStrict(遅延評価する右畳み込み)
応用が結構ある。本稿で最も興味深い実装方法。
WPF版もこれを使用している。
public static TResult FoldRightNonStrict<TSource, TResult>(this IEnumerable<TSource> source, TResult seed, Func<Func<TSource>, Func<TResult>, TResult> nonStrictFunc)
{
var iter = source.GetEnumerator();
// シーケンスを走査する列挙子を取得
Func<TResult> rec = null;
// 再帰的に「残りの要素を右畳み込みした結果」を表す遅延関数
// 最初は null にしてから自己代入することで、再帰的なラムダを実現
// 再帰の本体(遅延評価される)
rec = () =>
(iter.MoveNext())
? nonStrictFunc(() => iter.Current, rec)
// - iter.Current を「必要になったときだけ
//取り出す関数」として渡し rec を「必要になっ
//たときだけ右側を評価する再帰」として渡す
// nonStrictFunc がどちらを呼ぶかによって、
//再帰を続けるか途中で止めるかを決める
: seed;
return rec();
}
用途
1. ログ解析:最後の異常から必要な分だけさかのぼる
右畳み込みと似ているが、NonStrict なら、最後のエラー行を見つけた時点でそこから前の行を 必要になった分だけ読むことができる。
特徴:全行を読む必要がない
- 後ろから順に、必要箇所だけというアルゴリズムを素直な形で書ける
監視ログ・計測ログなど大規模データで特に効果的
2. 大量データのストリーム処理での「後方短絡」
CSV、JSONL など行単位の大規模データを扱う場合、
NonStrict であれば 末尾から探索し、条件が満たされた時点で探索を終了 できる。
3.設定ファイルの後勝ち解析の強化版
通常の後勝ち(後の設定が優先する)だけでなく、
“上書きが発生した時点で前の設定はもう読まない”
といった特殊ルールがある現場コードで役に立つ。
- 再帰的な式解析:右側の構造を見てから左を読む
-
式パーサーや計算式チェッカーで
“右側の演算子/値を確認してから左の構文を決める”
というケースは現実に存在する。 -
遅延評価なので、大量データでも必要な部分だけを処理でき、メモリ節約・パフォーマンス向上に寄与する。
Github
ストックありがとうございました。
.net 10, Visual Studio 2026を採用しました。Fieldキーワード使用のため、.net9以前を使用する場合は従来通りにバッキングフィールドを使用してください。
.net5までは使えるはずです。
2022の方がいい方はコメントください。 検討します。
使用したCSVファイル。1万行入ってますが、重い場合はSmallを使ってください。
出処はVisual Studio内のフォルダです(笑)。
https://drive.google.com/drive/folders/1B6ECUp5EHSn-I5luim1bzGKqJOYA6GGB?usp=sharing
Consoleでの実装例
1-10000までの整数を扱うConsoleアプリを作成します。
- デモ
比較のための通常版実装
- FoldLeft・左畳み込み(通常のC#Code的実装)-
折り畳み
using System;
class Program
{
static void Main()
{
var it = FoldLeftFindEnumerator(0, 10000, 9000);
while (it.MoveNext()) // MoveNextを介さないと型名が出てしまう
{
var value = it.Current;
if (value != -1)
{
Console.WriteLine("FoldRightSimple: " + value);
}
}
}
- FoldRigtFindResult
class FoldRigtFindResult
{
// 遅延レイヤーなし版:単に右側から走査する
public static IEnumerator<int> FoldRightSimple(int start, int end, int threshold)
{
for (int i = end; i >= start; i--)
{
if (i <= threshold && i % 100 == 0)
{
yield return i; // 条件に合った値だけ返す
yield break; // 最初に見つけた時点で終了
}
}
yield return -1; // 見つからなかった場合の値
}
FoldRightSimple: 9000
C#で右畳み込みを忠実に再現したもの
Haskell的な遅延レイヤーを作り、構造的に右畳み込みを再現します。
//遅延レイヤーなし版()
var it2 = FoldRigtFindResult.FoldRightSimple(0, 10000, 9000);
while (it2.MoveNext())
{
var value2 = it2.Current;
if (value2 != -1)
{
Console.WriteLine(value2);
}
}
//遅延レイヤーあり版(忠実な右畳み込み)
var it2 = FoldRigtFindResult.FoldRightFind(0, 10000, 9000);
while (it2.MoveNext())
{
var value2 = it2.Current;
if (value2 != -1)
{
Console.WriteLine("FoldRightFind: " + value2);
}
}
- 出力結果
FoldRightFind: 10000
FoldRightFind: 9900
FoldRightFind: 9800
FoldRightFind: 9700
FoldRightFind: 9600
FoldRightFind: 9500
FoldRightFind: 9400
FoldRightFind: 9300
FoldRightFind: 9200
FoldRightFind: 9100
FoldRightFind: 9000
どでゲスかね。
右畳み込み用のクラス
static IEnumerator<int> FoldLeftFindEnumerator(int start, int end, int threshold)
{
int acc = -1; // 見つからなかった場合
for (int i = end; i >= start; i--) // 逆順に走査
{
if (i <= threshold && i % 100 == 0)
{
yield return i;
//yield break; // 最初に見つけた時点で終了
}
}
yield return acc;
}
class FoldRigtFindResult
{
// 遅延レイヤーなし版:単に右側から走査する
public static IEnumerator<int> FoldRightSimple(int start, int end, int threshold)
{
for (int i = end; i >= start; i--)
{
if (i <= threshold && i % 100 == 0)
{
yield return i; // 条件に合った値だけ返す
}
}
yield return -1; // 見つからなかった場合の値
}
// 遅延レイヤーあり版:忠実な右畳み込み
public static IEnumerator<int> FoldRightFind(int start, int end, int threshold)
{
return FoldRight(
start,
end,
-1, // seed
(i, k) => // f(i, rest)
{
if (i <= threshold && i % 100 == 0)
{
return YieldOne(i, k); // 見つけたら返して、あとは続く
}
return k; // 見つからなければ後続へ
}
);
}
/// <summary>
/// 以下、右畳み込みの定義をC#で再現したもの
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="seed"></param>
/// <param name="f"></param>
/// <returns></returns>
static IEnumerator<int> FoldRight(int start, int end, int seed, Func<int, IEnumerator<int>, IEnumerator<int>> f)
{
IEnumerator<int> acc = YieldSeed(seed);
for (int i = start; i <= end; i++)
{
acc = f(i, acc); // foldRight の形: f(i, acc)
}
return acc;
}
// 右畳み込みの “一番右端の層(シード)” を表す。
// この IEnumerator は seed をひとつ返すだけのレイヤー。
// foldRight の末尾に相当する。
static IEnumerator<int> YieldSeed(int seed)
{
yield return seed;
}
// value を返したあと、後続レイヤー next を“続けて実行する”ための層。
// foldRight でいう f(x, rest) の構造を C# で模倣している。
// next はまだ評価されていない後続の処理で、MoveNext によって展開される。
static IEnumerator<int> YieldOne(int value, IEnumerator<int> next)
{
// 現在のレイヤーとして value を吐く
yield return value;
// 後続レイヤーを、そのまま遅延的につなげて返す
while (next.MoveNext())
yield return next.Current;
}
}
mermaid図
※スマホ版など未対応ブラウザでは表示されません
- 右畳み込みの処理フロー(レイヤー生成)
最新C# Codeと比較した場合の右畳み込みの優位性
興味がある人向け
※興味があったら検証して、この記事へのLinkを貼ってください。
- FoldRightNonStrict(遅延右畳み込み)を現代C#で代替する場合
IEnumerable<T> + LINQ
-
TakeWhile、SkipWhile、Select などである程度の短絡評価は可能
-
ただし「右畳み込み的に最後から処理する」「途中で早期終了する右畳み込み」は標準LINQには直接ない
→つまり自作が必要 -
Strategy Patternのように処理を後から柔軟に差し込むことが出来る。
素の FoldRightNonStrict(再帰+遅延評価) vs LINQ / foreach で遅延評価の自作 で比較した場合まとめ
| 実装方法 | 可読性の特徴 |
|---|---|
| 素の FoldRightNonStrict(再帰+遅延評価) | - 再帰の形がそのまま数学的な右畳み込みに対応しているため、右畳み込みの意図が直感的にわかる - 遅延評価の部分も Func で明示されるので、「残りの処理はまだ評価していない」という意味が明確- ただし再帰が深くなると StackOverflow の可能性がある(大規模データで注意) |
| LINQ / foreach で自作 | - 「右から順に処理する」ためには Reverse() などで一度反転する必要がある- 途中打ち切りを実装する場合、 TakeWhile や break を組み合わせる必要があり、ロジックがやや冗長- 遅延評価を完全に再現する場合は IEnumerable の yield return やネストしたラムダを駆使する必要があり、直感的ではない |
IAsyncEnumerableとの比較
IAsyncEnumerable は
「外部から順にやってくるデータを await foreach で受け取る」
という、あくまで 順方向(前)に進むストリーム処理 の枠組みなので、FoldRightNonStrictと同じ実装は困難です。
● 1. IAsyncEnumerable は後ろから読めない
IAsyncEnumerable が提供するのは 時間軸に沿った前方向ストリーム。
FoldRight の「後ろから畳み込む」という本質を実行する手段がそもそも無い。
● 2. IAsyncEnumerable は「計算の遅延開始」を制御できない
FoldRightNonStrict では、nonStrictFunc が Func を受け取り、それを呼ぶまでは再帰が進まない。
これは 利用側が評価タイミングを完全に操作できることを意味する。
● 3. IAsyncEnumerable は“データの入れ物”であって、“構造”ではない
FoldRightNonStrict は処理そのものを「関数として組み替える」ことで
ロジックを後ろから構築する DSL 的な機構になっている。
IAsyncEnumerable は あくまで値の列。
そこで制御構造を組み替えることはできない。
| 観点 | IAsyncEnumerable | FoldRightNonStrict(遅延評価右畳み込み) |
|---|---|---|
| 役割 | 非同期ストリームを段階的に読み出すための標準的手段 | データ列を“後ろから”畳み込みつつ、必要になった瞬間だけ評価するための関数型テクニック |
| 遅延の仕組み |
await foreach が要素到着を待ちながら逐次処理 |
畳み込み関数が「必要なときだけ再帰を進める」ことで遅延が生じる |
| スタイル | 命令的(for/foreach の延長線) | 関数的(再帰 + 関数を渡して制御構造を作る) |
| 主用途 | IO・ネットワーク・時間のかかる逐次処理 | 条件付きの短絡評価・“後勝ちルール”・構文解析・ツリー処理 |
| 実務への適合度 | 極めて高い(正式 API・例外処理・キャンセルなど揃っている) | ニッチ(汎用ではなく、高度な制御構造が欲しい場面向け) |
| 学習コスト | 低い(async/await を知っていれば良い) | 高い(遅延の伝播、再帰、ラムダの戻り値に関数が混ざる) |
| メリット | 読みやすい・保守しやすい・IO に強い | “必要になるまで計算を発生させない”制御が作れる |
| デメリット | 畳み込みを後ろから行うような処理は不得意 | 可読性が下がりやすい・再帰による負荷・デバッグ困難 |
| 向いている例 | CSV の逐次読み込み、API の結果を逐次処理、ログの tail | 設定ファイルの後勝ち処理、条件に応じた短絡畳み込み、式の遅延評価、カスタム DSL |
実のところ自分で検証したいところです。興味あるんで
....っていうか知識があれば検証するまでもないんだろうなという
WPFでの実装例
本稿では FoldRightNonStrict(遅延版) のみ取り上げる。
キリがないので
- デモ
ソツなく動画。
案外見づらいのでYotube版も用意しました。
FoldRightNonStrict(遅延評価折り畳み)でCSVの列挙を行う
ここではCSV解析を例にとろうと思います。
いちばん手軽なんで。
今回は責務を明確に分割して(AIで)作りました。
(自分の作品と言えるものはある程度自力で書くことにしてるんですけどね)
1.CSVファイルで1行ずつ読み込む
一万件のCSVでも問題がありません。
Youtubeデモ動画(スライドモードだと見れない)
‐ ViewModelのコンストラクタ
public LogViewerViewModel()
{
LoadNextCommand = new DelegateCommand(LoadNext);
LoadAllCommand = new DelegateCommand(LoadAll);
FileSelectorCommand = new DelegateCommand(SelectFile);
ComplexibleConditionCommand = new DelegateCommand(CallExecuteStreaming);
// PlotModel = new PlotModel { Title = "Speed Chart" };
_viewstrategy = new CompositeStrategy(
new SpeedStrategy(),
new SpeedCategoryStrategy(),
new SpeedPlotBuilder()
);
}
- コア実装部分:LazyLineEvaluator_FoldRightNonStrict.cs
/// <summary>
/// WPF用 ViewModel向けの文字列版 LazyLineEvaluator
/// FoldRight(右畳み込み)構造を忠実に再現
/// </summary>
public class LazyLineEvaluator_FoldRightNonStrict
{
+ private readonly IEnumerable<string> _lines;
// EvaluateNext 用の列挙子
private readonly IEnumerator<string> _enumerator;
private readonly List<LineStrategy> _strategies = new();
public LazyLineEvaluator_FoldRightNonStrict(IEnumerable<string> lines)
{
_lines = lines ?? throw new ArgumentNullException(nameof(lines));
_enumerator = _lines.GetEnumerator();
}
/// <summary>
/// 条件+Factoryを登録
/// </summary>
public void AddStrategy(Func<string, bool> condition, Func<string, ILogLine> factory)
{
_strategies.Add(new LineStrategy(condition, factory));
}
/// <summary>
/// 最初に条件に一致した行だけ処理して終了
/// 右畳み込み風遅延評価
/// </summary>
// EvaluateFirst を右畳み込み+遅延評価で作り直し
public bool EvaluateFirst(Func<string, bool> condition, Action<string> onMatch)
{
if (_enumerator is null) return false;
while (_enumerator.MoveNext())
{
var line = _enumerator.Current;
if (condition(line))
{
onMatch(line);
return true;
}
}
return false; // 条件に合う行なし
}
/// <summary>
+ /// 再構築版:スタックを一切使わない右畳み込み(完全非再帰)
/// </summary>
+ public IEnumerable<TResult> EvaluateAllRight<TResult>(
+ Func<string, bool> condition,
+ Func<string, TResult> factory)
{
var list = _lines.ToList();
int index = list.Count - 1;
// 継続たちを蓄積していく(StackOverflow のスタックとは別物)
+ var continuations = new Stack<Func<IEnumerable<TResult>>>();
// 右側の構造を先に貯める(本来 foldr がやること)
while (index >= 0)
{
string line = list[index];
if (condition(line))
{
var value = factory(line);
+ continuations.Push(() => Prepend(value));
}
else
{
continuations.Push(() => Enumerable.Empty<TResult>());
}
index--;
}
// ここから副次的に Cons 連鎖を作っていく
IEnumerable<TResult> result = Enumerable.Empty<TResult>();
while (continuations.Count > 0)
{
//IEnumurableのクラスメソッド。
+ var build = continuations.Pop();
+ result = build().Concat(result);
}
return result;
}
private static IEnumerable<T> Prepend<T>(T head)
{
yield return head;
}
/// <summary>
/// foldr 構造を維持するための "Cons" 的構成子
/// </summary>
private static IEnumerable<T> Prepend<T>(T head, IEnumerable<T> tail)
{
yield return head;
foreach (var x in tail)
yield return x;
}
}
}
- LogViewerViewModel.cs(抜粋)
private LazyLineEvaluator_FoldRightNonStrict? _evaluator;
// -------------------------------------------------------------
// 右畳み込み:「最初の一致だけ」
// -------------------------------------------------------------
private void LoadNext()
{
if (_evaluator is null) return;
_evaluator.EvaluateFirst(
line =>
{
var f = line.Split(',');
return f.Length >= 6 &&
int.TryParse(f[5], out int speed) &&
speed >= 20;
},
line =>
{
var classified = new CsvLogLine(line);
Lines.Add(classified);
}
);
}
- CSVLogLine.cs
CSVの条件設定をして出力するクラス。
using RightFoldPattens.Logging;
namespace RightFoldPatterns.LogLines
{
public class CsvLogLine : ILogLine
{
private readonly string _line;
private SpeedCategory _category;
// X軸用
public DateTime Timestamp { get; }
public CsvLogLine(string line)
{
_line = line;
var fields = line.Split(',');
Speed = int.TryParse(fields[5], out var s) ? s : 0;
if (DateTime.TryParse(fields[2], out var ts))
Timestamp = ts;
else
Timestamp = DateTime.MinValue;
// 条件に合わせて SpeedCategory と Severity を設定
+ Category = Speed >= 60 ? SpeedCategory.Fast :
+ Speed <= 20 ? SpeedCategory.ERROR : // 20 以下を Error に
+ Speed <= 40 ? SpeedCategory.Slow :
+ SpeedCategory.Normal;
Severity = Speed >= 60 ? LogSeverity.Warning :
Speed <= 20 ? LogSeverity.Error :
LogSeverity.Normal;
}
public string Message => _line;
public int Speed { get;set; }
public SpeedCategory Category { get; set; }
public LogSeverity Severity { get; set; }
public SpeedCategory SpeedCategory
{
get => _category;
set => _category = value;
}
}
}
- ClassifiedLogLine.cs
特定範囲の数値をカテゴリー分けするクラス。
using RightFoldPattens.Logging;
using System;
using System.Collections.Generic;
using System.Text;
namespace RightFoldPatterns.LogLines
{
public class ClassifiedLogLine : ILogLine
{
public string Message { get; set; }
public LogSeverity Severity { get; set; }
public SpeedCategory SpeedCategory { get; set; }
SpeedCategory ILogLine.SpeedCategory { get => SpeedCategory; set => throw new NotImplementedException(); }
public ClassifiedLogLine(string line)
{
Message = line;
}
}
public enum SpeedCategory { Slow, Normal, Fast,
ERROR
}
}
CSV内の5列目のある数値以上を全部読む
public ObservableCollection<CsvLogLine> Lines { get; }
= new ObservableCollection<CsvLogLine>();
// -------------------------------------------------------------
// 右畳み込み:「条件に合うすべて」
// -------------------------------------------------------------
private void LoadAll()
{
if (_evaluator is null) return;
Lines.Clear();
Func<string, bool> condition = line =>
{
var f = line.Split(',');
return f.Length >= 6 &&
int.TryParse(f[5], out int speed) &&
speed >= 70;
};
Func<string, CsvLogLine> factory = line => new CsvLogLine(line);
foreach (var log in _evaluator.EvaluateAllRight(condition, factory))
{
Lines.Add(new CsvLogLine(log.Message));
}
}
特定範囲の数値出力 + Chart表示
これはRichTextBox使用という制約上、出力件数の制限があるのですが、CancellTokenを利用してフリーズを回避した実装です。
ChartはOxyPlot(https://oxyplot.github.io/) です。
右畳み込みの真骨頂的な。
Strategy Pattenと組み合わせてヰる。
Chart表示という形で右畳み込みの表現を試みました。
正味な話もっと突っ込もうかと思ったんだけど、処理的に限界があるようだったんで
DataGridにすればよかった。
ChartWindow? _chartWindow;
public async void CallExecuteStreaming()
{
try
{
// 既存の処理があればキャンセル
if (_cts != null)
{
_cts.Cancel(); // 既存の処理をキャンセル
}
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (_chartWindow != null)
{
_chartWindow?.SpeedPlotView.Model = null;
_chartWindow?.Close();
}
_chartWindow = new ChartWindow(this);
_chartWindow.Show();
///_chartWindow.Dispose() を呼ぶと Binding が壊れる!
Application.Current.DispatcherUnhandledException += (sender, e) =>
{
e.Handled = true; // true にするとアプリは終了せずに継続
_chartWindow?.Dispatcher.Invoke(() =>
{
MessageBox.Show("エラーが発生しましたわ: " + e.Exception.Message);
Lines.Clear();
return;
});
};
// Series と PlotModel を準備
var series = new LineSeries { Title = "Speed" };
var plotModel = new PlotModel { Title = "Speed Chart" };
plotModel.Series.Add(series);
plotModel.Axes.Add(new DateTimeAxis
{
Position = AxisPosition.Bottom,
StringFormat = "HH:mm:ss",
Title = "Time"
});
plotModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "Speed"
});
Func<string, CsvLogLine> factory = line => new CsvLogLine(line);
plotModel = await ExecuteStreamingWithPlotModel(
line =>
{
var f = line.Split(',');
return f.Length >= 6 && int.TryParse(f[5], out int speed) && speed >= 70;
},
factory,
logLine =>
{
Lines.Add(logLine); // ViewModel 側の ObservableCollection に追加
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(logLine.Timestamp),
logLine.Speed));
plotModel.InvalidatePlot(false); // 描画更新
}, token);
_PlotModel = plotModel;
}
catch (OperationCanceledException)
{
MessageBox.Show("前の処理を女性的にキャンセルしましたわ");
}
finally
{
}
}
別クラスに分離すべきかも。
非同期処理にCancellTokenを上手く利用してフリーズを回避しています。
using RightFoldPattens.Logging;
namespace RightFoldPatterns.InterfaceDomain
{
public interface ILineProcessor
{
/// <summary>
/// 入力を受け取り、必要なら変更して返す
/// </summary>
ILogLine LogProcess(ILogLine line);
}
}
// -------------------------------------------------------------
// Strategy ベースの複雑条件処理(EvaluateAll を使用)
// -------------------------------------------------------------
+ public async Task<PlotModel> ExecuteStreamingWithPlotModel(
Func<string, bool> condition,
Func<string, CsvLogLine> factory,
+ Action<CsvLogLine> onItem, CancellationToken token)
{
var plotModel = new PlotModel { Title = "Speed Chart" };
var series = new LineSeries { Title = "Speed" };
plotModel.Series.Add(series);
plotModel.Axes.Add(new DateTimeAxis { Position = AxisPosition.Bottom, StringFormat = "HH:mm:ss" });
plotModel.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = "Speed" });
if (_evaluator == null || _viewstrategy == null)
return plotModel;
await Task.Run(() =>
{
+ // 入力ストリームを左から消費し、意味付けは右側で確定する
foreach (var log in _evaluator.EvaluateAllRight(condition, factory))
{
+ /* 生データ(string)は順番通り流れてくる
+ condition と factory は 途中で評価されるが
+ 最終的に「CsvLogLineとして意味を持つかどうか」は
+ _viewstrategy.LogProcess(log) に委ねられている */
+ token.ThrowIfCancellationRequested(); //外部から計算全体を切断する。
+ // 2. Strategy が「畳み込み関数」そのものになっている
+ var processed = _viewstrategy.LogProcess(log)
+ as CsvLogLine;
/* このコードでは、
畳まれる対象:CsvLogLine
畳み方:_viewstrategy
が完全に分離されている。
*/
/*
null は「畳み込みの単位元」
+ 何も生まなかっただけで、流れは止めない
*/
+ if (processed is null)
+ continue;
onItem(processed);
series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(processed.Timestamp), processed.Speed));
plotModel.InvalidatePlot(false);
}
});
return plotModel;
}
実装部分のPlantML図
- 処理順に落とし込んだもの
....見た目が無駄に複雑な実装になったなあと。
参考記事
これが一番重要といってもかごんではないです。
過度にAIに頼らないでソースを当たるクセを。
....準備中。
あとがき
実装が終わった時点で放置してました。
ここのところ仕事で無理が祟ってたので勘弁してほしい。
こうして改めて見ると極めて興味深いとともに、理解が難しい実装です。
RichTextBoxではなくDataGridを使えばフリーズはほぼないらしいので、次はDataGrid使いたいですね。このプロジェクトではめんどくさいのでもうやりません。
分かりやすい図解が出せるといいのかもしれませんが、図解って苦手でして。

