IF関数のネストをどうにかしたい
前回(Vol.1 Excel LAMBDA:IF関数を消し去る「真のChurch Encoding」と代数的データ型)は、ExcelのLAMBDA関数と「チャーチ符号化(Church Encoding)」を使って、コンパイラ言語のような Result型(代数的データ型) をExcel上に構築しました。
業務ロジック上、入力値のバリデーションが必要なのでIF関数を消し去るは言い過ぎでしたが、「箱(関数)」にデータを閉じ込めることで、関数の実行順序制御からは、IF関数を消せたのですが、入力値のバリデーションで、IF関数がネストするという残念な結果に。
// Vol.1のステップ3. バリデーション(入り口の関所)
_fx正数チェック, LAMBDA(_v,
IF(NOT(ISNUMBER(_v)),
_ERR("数値を入力してください"),
IF(_v > 0,
_OK(_v),
_ERR("価格は0より大きくしてください")))),
入力値のバリデーションの特性上、IF関数を使うのは止む無しですが、 ネストはどうなのか? と。
この例では、2階層のネストですが、これに加えて「空欄チェック」「文字数チェック」「上限値チェック」などが追加等々が考えられ、その度にIF関数深くなっていくのは、美しくないです。
今回は、この醜いネストを平坦化し、データが美しい直線を流れていく 「パイプライン処理」 を構築します。
1. ネストしているIF関数を小さな関数に分割する
IF関数がネストしてしまう原因は、「1つの関数の中で複数のチェックをしようとしている」からなので、これを、1つにつき1つの小さな関数に分割します。
// 値を受け取り、判定・計算してResult型の箱(_OKか_ERR)を返す
_fx数値チェック, LAMBDA(_v, IF(NOT(ISNUMBER(_v)), _ERR("数値を入力してください"), _OK(_v))),
_fx正数チェック, LAMBDA(_v, IF(_v <= 0, _ERR("価格は0より大きくしてください"), _OK(_v))),
_fx消費税計算, LAMBDA(_v, _OK(_v * 1.1)),
ここで重要なのは、「入力の形と出力の形(関数シグネチャ)をすべて統一すること」 です。
どの関数も「値を受け取って、_OK または _ERR の箱に入れて返す」というルールを守ることです。
2. MATCH関数で繋ぐメンドクサさ
関数を分割できたので、あとは順番に実行していくだけです。
では、Vol.1で作った箱を開ける関数 _MATCH だけを使って、これらをバケツリレーしてみます。
_箱1, _fx数値チェック(A1),
// 箱1を開けて、OKなら正数チェックへ、ERRならそのままERRの箱を返す
_箱2, _MATCH(
_箱1,
LAMBDA(_v, _fx正数チェック(_v)), // OKの時
LAMBDA(_err, _ERR(_err)) // ERRの時(ただエラーを横流しする)
),
// 箱2を開けて、OKなら消費税計算へ、ERRならそのままERRの箱を返す
_箱3, _MATCH(
_箱2,
LAMBDA(_v, _fx消費税計算(_v)), // OKの時
LAMBDA(_err, _ERR(_err)) // ERRの時(またお前か…)
),
...うーん、イケてない...
毎回 _MATCH で箱を開け、エラーだった時のために LAMBDA(_err, _ERR(_err)) という「ただエラーの箱を作り直して次にパスして、、、を繰り返していてメンドクサイです。
3. 鉄道指向プログラミング(Railway Oriented Programming)
箱を開けて箱を作り直してなんて元々やりたかったことじゃないです。
やりたかったことは、 OKの時は処理を続けて、ERRの時は後続の処理を無視 したかったです。
このような処理フローは、関数型プログラミングの世界では 鉄道指向プログラミング(Railway Oriented Programming:ROP) と呼ばれています(F#コミュニティのScott Wlaschin氏が提唱した有名な概念です)。
ROPの考え方は、
- データが流れる線路には 「正常系の線路」 と 「異常系の線路」 の2本が並行して走っています。
- 各関数(チェック処理)は、成功すれば正常系の線路へ、失敗すれば異常系の線路へデータを流す 「ポイント(分岐器)」 の役割を果たします。
- 一度異常系の線路に乗ったデータは、その後の駅(関数)には一切停まらず、終点まで直行します。
では、この「ポイント(分岐器)」をExcel LAMBDAで実装してみます。
4. ポイント(分岐器)の「BIND関数」
この鉄道の分岐器となるのが、関数と関数をつなぐ BIND関数 です。
// 箱を受け取り、OKなら次の関数へ中身を渡し、ERRならERRのまま次へパスする
_BIND, LAMBDA(_箱, _次の関数, _箱(_次の関数, _ERR))
ずいぶんとシンプルな仕組みですが、これでうまく動くのです。
それは、Vol1.で作った、 「箱」 は、 「2つの関数(OK用、ERR用)を受け取って、自分の状態に合わせてどちらかを勝手に実行する関数(クロージャ)」 だからです。
-
箱が
_OK(100)だった場合:
箱は「OK用の関数」を実行しようと待ち構えています。そこに_次の関数を渡すと、そのまま_次の関数(100)が実行され、正常系の線路を進みます。 -
箱が
_ERR("エラー")だった場合:
箱は「ERR用の関数」を実行しようとします。そこに_ERRを渡すので、そのまま_ERR("エラー")が実行され、新しいエラーの箱として異常系の線路を直行します。
データ自身が関数だからこそ、外側から判定して開ける必要はなく、「次にやるべきこと」と「エラー時の対処法」を箱に直接渡せばいい のです。
5. まずは手作業で線路を繋いでみる(IF関数ネスト消滅)
_BIND 関数を使って、コードを書き直してみます。
_ステップ1, _fx数値チェック(A1),
_ステップ2, _BIND(_ステップ1, _fx正数チェック),
_ステップ3, _BIND(_ステップ2, _fx消費税計算),
IF関数のネストが消滅し、処理が上から下へと直線的に流れるようになりました。
もしA1に「あ」という文字が入っていたら、_ステップ1 で _ERR("数値を入力...") という箱が作られ、異常系の線路に乗ります。
続く _ステップ2 や _ステップ3 では、_BIND が中身の計算関数を一切無視して、エラーの箱のまま下へ下へと受け流してくれます。
と、これでもよいのですが、もう少しモダンな言語風にしてみます。
6. REDUCEで回す!Excel向けパイプライン(PIPE)処理
手作業で _BIND を繋ぐだけでも十分に美しいですが、例えば、入力フォームのチェック項目が10個に増えたら?と考えると、メンドクサイです。
モダン言語なら [関数A, 関数B, 関数C].reduce(...) のようにスマートに一気に流し込めますが、Excelの場合、配列の中にLAMBDA関数を直接入れると #CALC! エラーになってしまいます。
そこで、CHOOSE 関数と SEQUENCE 関数を使って、擬似的な「関数の配列(リスト)」を作り出し、REDUCE で一気にパイプライン処理にかけます。
// Excelの配列制限を回避し、CHOOSEで関数のリストを表現する
_関数リスト, LAMBDA(i, CHOOSE(i, _fx数値チェック, _fx正数チェック, _fx消費税計算)),
7. 完成版コード:IF関数ネスト消滅ー関数のパイプライン実行
これまでの要素をすべて結合した、Vol.2の完成版コードです。
以下のコードをA2セルに貼り付け(コメントは除外して)、A1セルに「文字」「マイナスの数値」「正の数値」を入れて遊んでみてください。
=LET(
// --- 1. インフラ層(Vol.1のおさらい) ---
_OK, LAMBDA(_v, LAMBDA(_OK関数, _ERR関数, _OK関数(_v))),
_ERR, LAMBDA(_msg, LAMBDA(_OK関数, _ERR関数, _ERR関数(_msg))),
_MATCH, LAMBDA(_箱, _OK関数, _ERR関数, _箱(_OK関数, _ERR関数)),
// --- 2. 結合層(今回の主役:美しい線路の分岐器 BIND) ---
_BIND, LAMBDA(_箱, _次の関数, _箱(_次の関数, _ERR)),
// --- 3. 業務ロジック層(ネストしていたIFを単機能に分割) ---
_fx数値チェック, LAMBDA(_v, IF(NOT(ISNUMBER(_v)), _ERR("数値を入力してください"), _OK(_v))),
_fx正数チェック, LAMBDA(_v, IF(_v <= 0, _ERR("価格は0より大きくしてください"), _OK(_v))),
_fx消費税計算, LAMBDA(_v, _OK(_v * 1.1)),
// --- 4. パイプライン層(Excelの配列制限を回避するハック) ---
_関数リスト, LAMBDA(i, CHOOSE(i, _fx数値チェック, _fx正数チェック, _fx消費税計算)),
// --- 5. 実行(REDUCEによるPIPE処理) ---
_入力値, A1,
_最終箱, REDUCE(
_OK(_入力値), // スタート:入力値をまずは「OKの箱」に梱包してコンベアに乗せる
SEQUENCE(3), // 3つの処理を順番に回す
LAMBDA(_現在の箱, i,
_BIND(_現在の箱, _関数リスト(i)) // BINDが安全に次の関数へバケツリレーする
)
),
// --- 6. 出力(最終的な箱を開封する) ---
_MATCH(
_最終箱,
LAMBDA(_結果, "計算結果: " & _結果),
LAMBDA(_エラー, "エラー終了: " & _エラー)
)
)
IF関数のネストが消滅して、関数をパイプラインで処理できるようになりました。
もし、「上限100万までのチェック」を追加したくなったら、業務ロジック層に関数を1つ足し、SEQUENCEの数を増やすだけで済みます。ロジックが完全に分離され、安全に拡張できる設計の完成です。
次回書くか?!
今回は「1つの入力値(A1)」に対する連続チェックを完璧にしました。
しかし実際の業務では、 「単価(A1)と数量(B1)の両方が正しい時だけ、掛け算をしたい」 というケースがあります。
今回の BIND (ROP)は「エラーが出たら即終了」の仕組みなので、「単価も数量も間違っていた場合に、両方のエラーメッセージをまとめて出す」ということができません。
次回、複数のResult型を同時に扱う話を書くか?!