LET 関数のスゴイ使い方 – 関数型プログラミングとの関係
LET 関数のスゴイ使い方
Gooel Sheets の LET 関数が本領を発揮するのは、名前付き関数だと思う。例えば LET を使って名前付き関数を次のように定義すれば、複雑なカーネル密度推定も Google Sheets で手軽に実行できてしまう。
= let(
ds, filter(data, isnumber(data)),
n, count(ds),
iqr, quartile(ds, 3) - quartile(ds, 1),
h, 0.9 * min(stdev(ds), iqr / 1.34) * power(n, -0.2),
h_s, h * smoothness,
lo, min(ds) - h * 3,
hi, max(ds) + h * 3,
xs, sequence(rows, 1, lo, (hi - lo) / (rows - 1)),
ker, map(xs, lambda(x, sum(arrayformula(norm.dist(x, ds, h_s, false))) / n)),
{xs, ker}
)
上記の名前付き関数自体の話は、この記事を参照してね。以下で述べるのは、これ自体の話というよりは、この関数内での LET の「使い方」が主眼。
Google Sheets の LET 関数の使い心地は、普通の言語でプログラムを書く体験にとても近い。それは上の名前付き関数を、JavaScript に直してみるとよく分かる。下は何となく書き直してみたもの。
const kde = (data, rows, smoothness) => {
const ds = data.filter(d => typeof d === 'number' && !isNaN(d));
const n = ds.length;
const iqr = quartile(ds, 3) - quartile(ds, 1);
const h = 0.9 * Math.min(stdev(ds), iqr / 1.34) * Math.pow(n, -0.2);
const lo = Math.min(...ds) - h * 3;
const hi = Math.max(...ds) + h * 3;
const step_size = (hi - lo) / (rows - 1);
const xs = Array.from({ length: rows }, (_, i) => lo + i * step_size);
const kernel = xs.map(x => {
const kernel_sum = ds.reduce((acc, d) => {
return acc + norm_dist(x, d, h * smoothness, false);
}, 0);
return kernel_sum / n;
});
return { xs, kernel };
};
見比べると、名前付き関数と JavaScript が、ほぼ 1 対 1 に行を対応付けられそうなほど似通って見える。なお、JavaScript 側で未定義の関数が仮定されているのはご容赦くださいな🙏 quartile() とか stdev() とか norm_dist() とか。論旨には影響ないのでね。
LET の本質は、関数を記述する “構文” の提供?
スプレッドシートの関数で、普通の JavaScript のようなプログラミング体験ができるのは、多くの人にとっては意外ではなかろうか。スプレッドシートのプログラミングは、何重にも括弧を入れ子にして見通しが悪く、普通のプログラミング言語のような保守性が無い (= 保守しにくい) ことが多いからね。
勇み足に言ってみると LET は、名前付き関数を整理して記述する “構文” を提供すると解釈できるかも?奇数番目の引数を変数、偶数番目の引数がその変数に代入される式のように解釈できるもんね。イメージでいうと、こんな感じ↓↓
// Google Sheets の LET 関数
ds, filter(data, isnumber(data)),
n, count(ds),
// 意訳してみると...?
ds = filter(data, isnumber(data));
n = count(ds);
これが LET の本質と思うと、ありがたみがだいぶ変わってくる。多くの場合、LET は自明でトリビアルな使い方で導入されることが多いよね。例えば LET(x, A1, x + 1) みたいな。こうした単純化された導入は、仕組みを理解するには役立つけど、使いこなすためのヒントがゴッソリ抜け落ちちゃってる気がする。
表計算と関数型プログラミング
LET を使った関数定義は、どことなく関数型プログラミングっぽい気がする。僕は関数型プログラミングに詳しくないけど、特に重要な次の 3 つの特徴 (参考) を満たしてる。
- 不変性。一度値を割り当てた変数 (上の例では
dsとかnとか) の値は変わらない- 不変性の制約を課されることで、自然と
reduceやmapなどの関数に役目があり、スタイルの面でも関数型っぽい
- 不変性の制約を課されることで、自然と
- 参照透過性。同じ引数を与えれば、必ず同じ値が返る。
- これは実装によるところもあって、もし引数のみで関数を定義すれば、参照透過性は実現できる。
- 実際には関数内でセルの値を参照できてしまう (らしい) ので、常に参照透過なわけではないんだけど
- 副作用がない。表計算で問題にすべき副作用は、他のセルの値を勝手に書き換えないことでしょう。これは名前付き関数の機能として、確実に担保されている。
表計算と関数型プログラミングは相性が良いのかもしれないね。if() が文ではなく関数になってるのが Lisp っぽいと書いた記事も見かけた。もちろんこれだけが関数型プログラミングの本質ではないと思うけど、もしかして表計算と関数型プログラミングって通じる部分があるのかも?と思ったのでした。
