前回Excelで「関数型脳」を作る Vol.4:VBAのループが無くても大丈夫 —— フィルターして、行(レコード)で回す(FILTER / MAP / BYROW) の最後で、
{{1,2};{2,4};{3,6};{4,8};{5,10}} のように配列を入れ子にすると、

#CALC! エラーが起きました。
テーブルの各行(レコード)を処理するために BYROW や MAP を使ったところまでは良かったのですが、「元の値と、計算結果を横に並べる(複数列を返す)」ような、実務ではありがちな処理を書いた途端、このエラーです。
// 期待する結果:1の横に2、2の横に4...と並べたい
=MAP(
SEQUENCE(5),
LAMBDA(x, HSTACK(x, x * 2)) // {{1,2};{2,4};{3,6};{4,8};{5,10}}を返す
)
// 結果:#CALC! (配列の入れ子ができない)
Excelは、「配列(全体のリスト)」の1マスの中に、さらに「配列(HSTACKで作った複数列)」を詰め込めません。
「Excelの関数型アプローチは、1列のデータしか返せないおもちゃなのか?!」
と、諦めかけたのですが、Excelで「関数型脳」を作る Vol.2:関数は「値」だった —— 偶然のタイプミスとチャーチ数の衝撃 で学んだ 「関数も値として扱える(第一級関数)」 という考えが使えるのでは?と思いました。
1. 配列がダメなら「関数」を返せばいいじゃない(THUNKを使う)
Excelは「1つのマスに配列を入れるな」と怒ってます。
数値の 10 は1つの値です。文字列の "A" も1つの値です。だったら、Vol.2で学んだ 「機能が詰まった関数(LAMBDA)そのもの」 も、 立派な値 です。
「配列」を返そうとするから怒られるのであって、 配列を関数で包んで、それを関数として返せば1つの値になる と。
これを実現するために、引数を取らない「配列を包んで関数を返す」関数を作ります。
-
#CALC!が発生するコード
LAMBDA(x, HSTACK(x, x * 2)) // {{1,2};{2,4};{3,6};{4,8};{5,10}}を返す
^^^^^^^^^^^^^^^^この部分を関数で包む
- 解決策
LAMBDA(x, LAMBDA(HSTACK(x, x * 2)))
^^^^^^^^^^^^^^^^^^^^^^^^この部分のことをTHUNKと呼ぶ
LAMBDA(HSTACK(x, x*2)): 配列を関数で包んで1つの値にしている。
このLAMBDAには引数がありません。ただ「実行されるのを待っている箱(カプセル)」です。
プログラミング用語で、このような「後で計算するためのカプセル」を THUNK(サンク) と呼ぶそうです。
これを MAP に渡せば解決するか?!
=LET(
_カプセルのリスト, MAP(
SEQUENCE(5),
LAMBDA(x, LAMBDA(HSTACK(x, x * 2)))
),
// {LAMBDA;LAMBDA;LAMBDA;LAMBDA;LAMBDA} を返す
_カプセルのリスト
)
エンターキーを押します。ムキーッ!またしても #CALC! エラー。でも、ちょっと今回のエラーは違う。スピル #CALC! エラーがスピルしてる。

-
{LAMBDA;LAMBDA;LAMBDA;LAMBDA;LAMBDA}を返しているつもりですが、結果は、 -
{#CALC!;#CALC!;#CALC!;#CALC!;#CALC!;}がスピルで返ってきました。
「結局ダメじゃないか!」と思ったのですが、状況が変わったのでよく考えてみました。
-
仮説:スピルで返ってきた
#CALC!エラーのリストは、まだ実行していない関数のリストでは?
もしそうなら、なんとかこの実行待ちの関数を実行してあげればよいのでは? と。
2. カプセルを開いて実行せよ:REDUCEの登場
画面にはエラーで表示できないものの、手元には 「実行待ち関数」のリスト があります(仮説ですが)。
これを順番に開けて、中身(配列)を取り出して実行し、その結果を縦に積み上げて1つの表にできればよさそうです。
ここで登場するのが、 REDUCE(畳み込み)関数 です。
VBAで、配列の合計値を出したり、文字列を結合したりする時、私たちは無意識にこんなコードを書いています。
Dim 合計 As Long
合計 = 0 '' 初期値が入った箱を用意
For Each 値 In 配列
合計 = 合計 + 値 '' 箱の中身を、新しい計算結果で【上書き】する
Next
VBAのような命令型脳の世界では、このように「1つの変数(状態)を何度も上書き」して最終的な結果を作ります。
関数型脳では 「データの上書き」はできません。 ここが命令型脳との大きな違いです。
REDUCE(畳み込み)関数 は、「現在の状態」を上書きするのではなく、「現在の状態」と「今回取り出した要素」を関数に渡し、そこから『全く新しい次の状態』を生み出して次へバトンタッチしていく、というリレーのような動きをします。
1つの変数を上書きしなくても新しい状態を生み出して次にバトンタッチで行けそうです。
REDUCE関数は、以下の3つを引数に取ります。
- 初期値 (リレーの最初のバトン)
- 処理するリスト (カプセルの山)
- LAMBDA(現在の状態, 今回取り出した要素) (2つを受け取り、全く新しい状態を作って返す関数)
今回のケースで言えば、「今まで積み上げた表」と「新しく開けたカプセルの中身」を 縦に積み上げて、上書きするのではなく 「前より1段積みあがった全く新しい表」を生み出して次に渡す、という動きになればよいです。
3. 世界を一つにまとめ上げる
それでは、手元にある 「実行待ち関数」のリスト を REDUCE に渡し、順番に箱を開けながら VSTACK 関数を使って縦に積み上げてみます。
=LET(
// 1. Thunk(遅延実行カプセル)のリストを作る
_実行待ち関数のリスト, MAP(SEQUENCE(5), LAMBDA(x, LAMBDA(HSTACK(x, x * 2)))),
// 2. REDUCEでカプセルを順番に開けて、縦に積み上げる
_結果, REDUCE(
{"元の数", "2倍の数"}, // 見出し(初期値=最初から持っている状態)
_実行待ち関数のリスト, // 開ける前のカプセルの山
LAMBDA(_今までの表, _thunk,
VSTACK(
_今までの表,
_thunk() // ★注目:カッコをつけて実行待ちの関数を実行する!
)
)
),
_結果
)
数式の最後にある _thunk() に注目してください。
名前の後ろに () を付けることで、 実行待ち関数 が実行されます(エラーになるHSTACK関数を包んでいた関数のカプセルが取り除かれるイメージ)。
この実行結果を VSTACK 関数で、 _今までの表 の下にくっつけていきます。
期待通り、見出し付きの2列×5行のテーブルとしてスピルしました!

4. まとめ:ループをつかわない関数型脳
Vol.4,5を通して、ループを使わずに複数レコードを処理する方法を学びました。
- FILTER: 対象レコードを絞り込む。
- MAP / BYROW: 各レコードに関数を適用する(必要なら THUNK でカプセル化してエラーを避ける)。
- REDUCE: カプセル化されたリストを順番に開けながら、1つの表に畳み込む(平坦化する)。
これでどんな複雑なテーブル処理もExcelの数式だけで書けそうだと思ったのですが、
- あらかじめループ回数が決まっていないループはどうすればよいのだろうか?
Vol.4,5でやった、MAP や REDUCE は、あらかじめループ回数が決まっていました。
ループ回数が決まっていない場合はどうすればよいのだろうか?と。
例えば SEQUENCE 関数のように、自然数を無限に生成する関数はどうやってつくればよいのだろう?
ループの回数が分からないなら、どうするか?
次回、Excelで「関数型脳」を作る Vol.6(最終回):Do Whileループを捨てよ —— 回数未定の探索と「再帰」のパラダイム です。
今日の気づき
- 配列の中に配列は入れられないが、 「配列を返す関数(THUNK)」 なら配列に入れられる。
- THUNKは、引数なしの
LAMBDA()で処理を包み込み、後で()をつけて実行する(遅延評価)。 -
REDUCEは、今まで積み上げた「状態」を持ち歩きながら、リストを1つずつ処理してまとめる(畳み込む)最強の関数。 - THUNK と REDUCE を組み合わせれば、ネスト配列のエラーを突破して複数列の一括変換が可能になる。