足し算のプログラム
2月21日木曜日、晴れ
『アンダースタンディングコンピュテーション』を題材にした勉強会に参加している。
今日、数値計算でなく文法にマッチするかが題材なのはなぜでしょう? という話がちらっと出た。文法マッチのほうが単純だからでしょうか。そういうフォローも出て、そこから、数値計算は「数式」という文法にマッチさせることが先にあって続いて数式処理という計算をせねばならないので、数値計算のほうが難しそうだと僕は納得した。(勉強会の参加者にうまく共有できたかはわからない)
* * *
数式処理はあまりに頭に馴染みすぎているので、「計算する」行為に落とし込むときに、つい「当たり前」の知識を使ってしまうという問題もあるかもしれない。
なんといえばいいのか。仮想的な機械を構成するために現実の言語を使うのだけれど、その仮想機械の言語の解釈に現実の言語知識を混ぜてしまう、というのか──。やっぱりうまく言えない。
* * *
まあいい。
プログラム初学者向けに「足し算」のプログラムを作りましょう、みたいな課題がある。足す数ふたつはプログラムの引数として文字列で与えられるとして、結果を表示しましょう、みたいな。
そのコードはこんな感じになるか?
#include <stdlib.h>
#include <stdio.h>
int main(int argn, char **args) {
int a = atoi(args[1]);
int b = atoi(args[2]);
printf("%d", a + b);
return 0;
}
数式処理は a + b の部分なのだけれど、では「足し算」はどう実現されているのか。というので、足し算を自前で実装してみる……。
以下のコード、結果を下の桁から逆順で表示するという荒削りなところがあるんだけれど、足し算は(小学生の頃、筆算を習ったように)こんな具合に計算するものだった。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
enum carry
{
OFF,
ON
};
struct result
{
enum carry carry;
char value;
} results[20] = {
{OFF, '0'},
{OFF, '1'},
{OFF, '2'},
{OFF, '3'},
{OFF, '4'},
{OFF, '5'},
{OFF, '6'},
{OFF, '7'},
{OFF, '8'},
{OFF, '9'},
{ON, '0'},
{ON, '1'},
{ON, '2'},
{ON, '3'},
{ON, '4'},
{ON, '5'},
{ON, '6'},
{ON, '7'},
{ON, '8'},
{ON, '9'},
};
struct result half_adder(char a, char b, enum carry c);
int main(int argn, char **args)
{
const char *pa = args[1];
const char *pb = args[2];
const char *ita = pa + strlen(pa);
const char *itb = pb + strlen(pb);
enum carry carry = OFF;
while (!(ita == pa && itb == pb))
{
char a = ita != pa ? *--ita : '0';
char b = itb != pb ? *--itb : '0';
struct result r = half_adder(a, b, carry);
putchar(r.value);
carry = r.carry;
}
if (carry) putchar('1');
return 0;
}
struct result half_adder(char a, char b, enum carry c)
{
switch (a)
{
case '0':
switch (b)
{
case '0':
return results[0 + c];
case '1':
return results[1 + c];
case '2':
return results[2 + c];
case '3':
return results[3 + c];
case '4':
return results[4 + c];
case '5':
return results[5 + c];
case '6':
return results[6 + c];
case '7':
return results[7 + c];
case '8':
return results[8 + c];
case '9':
return results[9 + c];
default:
exit(1);
}
case '1':
switch (b)
{
case '0':
return results[1 + c];
case '1':
return results[2 + c];
case '2':
return results[3 + c];
case '3':
return results[4 + c];
case '4':
return results[5 + c];
case '5':
return results[6 + c];
case '6':
return results[7 + c];
case '7':
return results[8 + c];
case '8':
return results[9 + c];
case '9':
return results[10 + c];
default:
exit(1);
}
case '2':
switch (b)
{
case '0':
return results[2 + c];
case '1':
return results[3 + c];
case '2':
return results[4 + c];
case '3':
return results[5 + c];
case '4':
return results[6 + c];
case '5':
return results[7 + c];
case '6':
return results[8 + c];
case '7':
return results[9 + c];
case '8':
return results[10 + c];
case '9':
return results[11 + c];
default:
exit(1);
}
case '3':
switch (b)
{
case '0':
return results[3 + c];
case '1':
return results[4 + c];
case '2':
return results[5 + c];
case '3':
return results[6 + c];
case '4':
return results[7 + c];
case '5':
return results[8 + c];
case '6':
return results[9 + c];
case '7':
return results[10 + c];
case '8':
return results[11 + c];
case '9':
return results[12 + c];
default:
exit(1);
}
case '4':
switch (b)
{
case '0':
return results[4 + c];
case '1':
return results[5 + c];
case '2':
return results[6 + c];
case '3':
return results[7 + c];
case '4':
return results[8 + c];
case '5':
return results[9 + c];
case '6':
return results[10 + c];
case '7':
return results[11 + c];
case '8':
return results[12 + c];
case '9':
return results[13 + c];
default:
exit(1);
}
case '5':
switch (b)
{
case '0':
return results[5 + c];
case '1':
return results[6 + c];
case '2':
return results[7 + c];
case '3':
return results[8 + c];
case '4':
return results[9 + c];
case '5':
return results[10 + c];
case '6':
return results[11 + c];
case '7':
return results[12 + c];
case '8':
return results[13 + c];
case '9':
return results[14 + c];
default:
exit(1);
}
case '6':
switch (b)
{
case '0':
return results[6 + c];
case '1':
return results[7 + c];
case '2':
return results[8 + c];
case '3':
return results[9 + c];
case '4':
return results[10 + c];
case '5':
return results[11 + c];
case '6':
return results[12 + c];
case '7':
return results[13 + c];
case '8':
return results[14 + c];
case '9':
return results[15 + c];
default:
exit(1);
}
case '7':
switch (b)
{
case '0':
return results[7 + c];
case '1':
return results[8 + c];
case '2':
return results[9 + c];
case '3':
return results[10 + c];
case '4':
return results[11 + c];
case '5':
return results[12 + c];
case '6':
return results[13 + c];
case '7':
return results[14 + c];
case '8':
return results[15 + c];
case '9':
return results[16 + c];
default:
exit(1);
}
case '8':
switch (b)
{
case '0':
return results[8 + c];
case '1':
return results[9 + c];
case '2':
return results[10 + c];
case '3':
return results[11 + c];
case '4':
return results[12 + c];
case '5':
return results[13 + c];
case '6':
return results[14 + c];
case '7':
return results[15 + c];
case '8':
return results[16 + c];
case '9':
return results[17 + c];
default:
exit(1);
}
case '9':
switch (b)
{
case '0':
return results[9 + c];
case '1':
return results[10 + c];
case '2':
return results[11 + c];
case '3':
return results[12 + c];
case '4':
return results[13 + c];
case '5':
return results[14 + c];
case '6':
return results[15 + c];
case '7':
return results[16 + c];
case '8':
return results[17 + c];
case '9':
return results[18 + c];
default:
exit(1);
}
default:
exit(1);
}
}
そして、超絶長い(コードの大半を占める) half_adder は、言語組み込みの算術演算を使うと、あっさりこんなにコンパクトになる。
struct result half_adder(char a, char b, enum carry c) {
return results[(a - '0') + (b - '0') + c];
}
switch - case の分岐処理はルックアップテーブルジャンプと可換、みたいな話。
* * *
どうでもいいけれど、「動く」プログラムを書けるという話と、「わかりやすい」コードを書けるという話、そして「できるプログラマーの生産性は10倍、100倍の差を生む」という話と、なんだか色々考えてしまう。
たぶん世の中には「わかりやすい」前者の switch - case 的なプログラムが溢れているんだとおもう。僕も多分、そういうのを量産している。
数学の証明が読めない、わからない、というのと似た感じ。頭のいい人が書いたコードは理解するのが難しく、結果、凡百のプログラマーでは保守できないものになる。
一方で凡百のプログラマーが書いたプログラムはその部分部分が「わかりやすい」ため場当たり的に改変されることが多くなり、結果、全体でどう動くのかわからないものに成り果てる。(そして次々と場当たり的なパッチ・バグ修正が続けられ──)
この記事が気に入ったらサポートをしてみませんか?