プログラミングを始めたばかりの中学生の頃、どうにも理解に苦しむ謎の式がありまして、
x = x + 1;
です。「両辺から x を引いたら 0=1 になるじゃん、なんだこれ?!」 と。
プログラミングの本を読みあさると、「これは方程式ではなく、代入 という箱の中身を書き換える命令である」と。
「そういうものか」と飲み込んで数10年、i++ を書きまくってきました。
今更ですがExcelに搭載された LAMBDA関数 であれこれ試していると、「あれってやっぱり変だよな」 と。
第1回のテーマは、 「名前」と「不変性」 についてです。
※全部で5,6回の投稿になりそうです。
1. 「箱」に入れるか?、「名前」を付けるか? それが問題だ。
命令型プログラミングで x = x + 1 の代入を許していたのは、CPUとメモリをバスでつなげる、機械の都合に合わせた命令型プログラミングスタイルをそのまま踏襲して、 「値を出し入れできる箱」 として 変数 を生み出したからと考えています。
-
命令型プログラミングの世界:
javaなどの命令型プログラミング言語で次のよな例です。
x = 10;
x = x + 1;
を読み下すと、
-
xという箱があり、中身は10。 - そこから10を取り出し、1を足して11にし、もう一度箱
xに戻す(再代入)。
もっと機械に近い書き方ならこんな感じ(アセンブラ(CASL II))。
SAMPLE START
; --- x = 10 の処理 ---
LAD GR1, 10 ; レジスタGR1に即値10を代入
ST GR1, X ; GR1の内容をメモリX(変数x)に保存
; --- x = x + 1 の処理 ---
LD GR1, X ; メモリXの値をGR1に読み込む
LAD GR1, 1, GR1 ; GR1の値に1を加算する(※)
ST GR1, X ; 結果をメモリXに書き戻す
RET ; 処理終了
; --- 変数定義 ---
X DS 1 ; 変数xとして1語分の領域を確保
END
命令型プログラミングの世界では、時間経過とともに、x は 変化します 。
一方で、関数型プログラミングでは、変数は「箱」は無いです。 「値につけられた名前」 です。というか、 変数 というのもおかしな話で、 変化できない です。
- 関数型プログラミングの世界:
x = 10;
y = x + 1;
を読み下すと、
-
10という値がある。これにxという 名前を定義 します。 -
11が必要なら、xを 書き換えない 。x + 1という新しい値を用意し、それにyという 新しい名前を付ける 」です。
一度「 x は 10 である」と 定義(束縛:Bind) したら、その世界の中で x が 11 に変わることはないです。これが 「不変性」 です。
2. Excelは「定義」の集まり
この「一度決まったら変わらない」という感覚は、 Excelのシートそのもの です。
セル A1 に 10 と入力し、 B1 に =A1+1 と入力します。B1 の結果は 11 です。
このとき、A1の値は 書き換わりません 。 A1 は A1 のまま、 B1 は新しい値として存在します。
「 A1とは 10 のことである」「 B1 とは A1 に 1 を足したものである」 と、事実を 定義 しているだけです。
Excelで数式を使っている時は、そもそもセルに対して「書き変えろ」と命令したくてもできません。
Excelは、LAMBDA関数が登場する以前から、数式に 名前定義 する機能があり、セルを書き換えることができない 不変性 を持っていたのが、改めて考えさせられます。
3. 何回テストしても同じ結果になるうれしさ!
「名前の中身が絶対に書き換わらない」ので、「入力が同じなら、出力は絶対に同じになる」 というルールが徹底される点がうれしいです。何回テストしても同じ結果になる!!
4. ループ変数 i を捨ててどうするか?
とはいえ、実務ではループ処理が必要で、「変数を書き換えられない(再代入できない)のに、どうやって 1 から 10 まで合計する?」とかどうするの?です。
で、目から鱗だったのは、 「変数を変化させる」のではなく、「新しい値を生成する」 です。
命令型脳の私は、
- ループ変数
iと合計用変数sumValを用意して、for文でiを1から10まで回して...と、どんな命令をどんな順番で動かしてと考えていました。
int sumVal = 0;
for(int i = 1; i <= 10; i++) {
sumVal += i;
}
ここには、時間経過とともに変化する i と sumVal と、i を 繰り返す命令 と 合計する命令 です。(※この文脈なら、 命令 より 文 の方がふさわしいか?!)
関数型脳で考えると、
- 数列を生み出す
SEQUENCE関数で、1から10までの数列を生成する。 - その数列を
SUM関数で合計する。
=SUM(SEQUENCE(10))
ここには、時間経過とともに変化する i はなく、あるのは、数列を生み出す 関数 と数列を合計する 関数 です。
脱線:「名前定義」は「日本語」でいい。ユビキタス言語の実践
一般的なコーディング規約では、 変数名はASCIIであること が常識ですが、Excelでは忘れてもよいと思います。
例えば、今、業務ロジックを記述しているとします。顧客側の現場担当者が「売上総利益」と呼んでいるものを、コードの中で GrossMargin とか UriageSouRieki と翻訳するのは認知負荷(脳のメモリ消費)がかかるので、メンドクサイです。
「現場で使われている言葉(ユビキタス言語)を、そのまま数式の中で使う」 でいいじゃんと。Excelなんだし。
メンドクサイ例:
=LET(x, A1, y, 0.1, x * y)
「認知負荷高っ!」半年後にこのコードを見たら、「 x って何? y って何?」となります。
一般的なコーディング規約例:
=LET(priceVal, A1, taxRate, 0.1, price * taxRate)
=LET(kakakuVal, A1, shouhiZeiRate, 0.1, kakakuVal * shouhiZeiRate)
これでも認知負荷が高いなぁ。。。
最良の例:日本語(ユビキタス言語)
=LET(_定価, A1, _消費税率, 0.1, _定価 * _消費税率)
でいいじゃんと。数式がそのまま「文章」として読めるし。Excelの数式エンジンは、日本語(Unicode)の 名前定義 をサポートしているので、どんどん使います。
ちょい足しコーディング規約:名前の接頭辞に「アンダースコア」を付ける
日本語を使う際、ちょい足しコーディング規約をしたいです。それは、名前の先頭に _(アンダースコア) を付けることです。
_定価_消費税率
これは _ を入力すると、入力支援 が働き、定義した日本語の名前だけが候補に表示されるので、矢印キーで選べるのが便利だからです。
Excelの標準的な関数は英字名なので、関数名を入力した後に、名前を入力する際に、日本語IMEを起動して名前を入力して、また、日本語IMEをOFFにしてと行き来するのがメンドクサイのを防止するためです。
IDEによる支援がもっと強力になれば変わってくるのでしょうが、とりあえず、これをするだけでもだいぶ楽になります。
5. まとめ:代入をやめ、名前を定義しよう
「 x = x + 1 」に感じた変な感じのままプログラミングの道に進めば、関数型脳になれたのになぁと。
Excel LAMBDA関数に親しんでいくと、 「値に名前を付け(定義)、事実を積み上げていく」 という新しいプログラミングスタイルの面白さがでてきました。
- 命令型: 箱の中身を入れ替える(代入)。
- 関数型: 値に名前を付ける(定義)。
ここまでで、「数値」や「文字列」が値であり、それに 名前を付けられる ことが整理できたと思います。
で、関数型プログラミングでよく言われている、「関数そのものも値 なら、当然 関数にも名前を付けられる ので、Excelでもできるのですが、関数型プログラミングがわかっていない、命令型脳の私が、ちょっとしたタイプミスから、これに気づいたのが、これまた目から鱗でしたので、 Excelで「関数型脳」を作る Vol.2:関数は「値」だった —— 偶然のタイプミスとチャーチ数の衝撃 へ続きます。
今日の気づき
-
x = x + 1;(代入)が変な気持ちというのは、関数型プログラミングの世界では当たり前のことでした。 - 関数型プログラミングでは「書き換え」ではなく「定義(名前付け)」の連続。
- 定義する名前には、現場の言葉(日本語)と
_を使うのがよさそう。