へのへのもへじの描き方 ~How to make the PROGRAM of HE.NO.HE.NO.MO.HE.JI~ 手続向け言語を用いたプログラミングの考え方について
へのへのもへじを描くプログラムの作り方を描いています。古い文章なので言語も古いですね。
「へのへのもへじ」を知っていますか?日本人なら誰もが一度ぐらいはお目にかかったことがあると思いますが、ことばの通りに描いていくと人の顔になるというあれです。
ことばの通りに描いていくと絵が描き上がるものとしては、他に「つるニハ○○ムし(鶴には丸々虫)」などがありますし、「へんなコックさん」に代表される絵書き歌もあります。ポケモンの絵書き歌もありますね。
どんなに絵が苦手な人でも「へのへのもへじ」は上手に描けると思います。それは、絵を構成する要素が普段書き慣れたひらがなであるためです。各要素が簡単なものであれば、それを組み合わせることはさして難しいことではありません。それに「へのへのもへじ」はそれ自体が作業の順番を示しています。描く要素とその順番まで示されているのですから、これはシンプルながら非常に良くできた設計書と言えます。
1.手順書
実際に「へのへのもへじ」を描いてみましょう。用意するものは紙と鉛筆だけです。
手順を書面化するとこのような感じになるでしょうか。
(1)「へ」を書く
(2)その下に「の」を書く
(3)最初の「へ」の右にもうひとつ「へ」を書く
(4)その下に「の」を書く
(5)二つの「の」の下の真ん中に「も」を書く
(6)その下に「へ」を書く
(7)全体を包むように「じ」を書く
(1)から(7)までを順番に実行すると「へのへのもへじ」が完成します。テキスト文字では「じ」をうまく表現できませんが、(1)~(6)までは描く事ができます。
へ へ
の の
も
へ
不思議なもので、こうやって並べただけで文字が人の顔に見えてしまいます。この理由は、脳が人の顔を認識するメカニズムにかかわっているようです。しかし、日本人は見慣れているから顔と分かるのでしょうが、ひらがなの分からないアメリカ人はこれを顔と認識できるのでしょうか?もっとも、パソコン通信等で使われるフェィスマーク(「:-)」や「:-P」といった文字の組み合わせで表情を作って気分を表わすもの)はアメリカが発祥ということなので、きっと分かってもらえるかもしれません。
余談ですが、子供の頃なぜ「へのへのもへし」ではなく「へのへのもへじ」なのか疑問に思っていました。いえ、いまでもその解答を得たわけではありませんが、とりあえず「じ」の濁点は耳ということで納得しています。本当のところはどうなのでしょうか?
2.サブルーチン
「へのへのもへじ」には、「へ」が 3回、「の」が 2回でてきます。手順書に書く際、この同じ部分をどうにかできないでしょうか。
歌の歌詞の書かれたものを見ると、よく「*繰り返し」とか「* repeat」といった書き方を目にします。これは、歌の中に同じ歌詞が何度も出てくる時に使われ、紙面の省略に使われています。
同じようにして手順書を書き直してみましょう。
(1)「へ」を書く(*1)
(2)その下に「の」を書く(*2)
(3)最初の「へ」の右にもうひとつ((*1)と同じ)
(4)((*2)と同じ)
(5)二つの「の」の下の真ん中に「も」を書く
(6)その下に((*1)と同じ)
(7)全体を包むように「じ」を書く
何か変ですね。日本語になっていません。これは、書く文字は同じなのにその位置が異なるため、場所を示すことばがついているからです。
場所を指定する部分と文字を書く部分とを分割して書き直してみましょう。
(1)「へ」を書く(*1)
(2)次の位置を(1)の下とする
(3)「の」を書く(*2)
(4)次の位置を(1)の右とする
(5)((*1)と同じ)
(6)次の位置を(5)の下とする
(7)((*2)と同じ)
(8)次の位置を(3)と(7)の下の真ん中とする
(9)「も」を書く
(10)次の位置を(9)の下とする
(11)((*1)と同じ)
(12)全体を包むように「じ」を書く
今度は、意味は通じますが、内容が分かりにくくなってしまいました。
文章中に「xxと同じ」と書かれると、人はまず今読んでいる位置を記憶して、「xx」の部分を探しに行きます。そして、見つけて内容を確認した後で元の部分へ戻って来ます。この操作を行っている間に、それまでのことを忘れてしまうので、人間には理解しにくくなる訳です。
3.パソコンの場合
今度は、パソコンに「へのへのもへじ」を描かせてみましょう。
前に書いたように、テキスト文字で顔の輪郭の「じ」を書くのは難しいため、「へのへのもへ」まで書いてみます。
10 CLS
20 LOCATE 0,0
30 PRINT "へ"
40 LOCATE 0,1
50 PRINT "の"
60 LOCATE 4,0
70 PRINT "へ"
80 LOCATE 4,1
90 PRINT "の"
100 LOCATE 2,2
110 PRINT "も"
120 LOCATE 2,3
130 PRINT "へ"
140 END
これはBASICですが、さきほどの日本語で書いた手順書の最後の形に似ていることに気付くと思います。人間にとって分かりにくかった記述も、パソコンにとっては分かりやすい形だったのです。
更に、サブルーチン・コールの命令を用いて同じ部分をまとめてみます。
10 CLS
20 LOCATE 0,0
30 GUSUB 200
20 LOCATE 0,1
50 GUSUB 300
60 LOCATE 4,0
70 GUSUB 200
80 LOCATE 4,1
90 GUSUB 300
100 LOCATE 2,2
110 PRINT "も"
120 LOCATE 2,3
130 GUSUB 200
140 END
200 PRINT "へ"
210 RETURN
300 PRINT "の"
310 RETURN
この程度では分かりにくくなるばかりで利点はありませんね。
4.パソコンの場合2
今度はC言語で書いてみます。
なぜ先にBASICで書いたのかというと、C言語よりもBASICの方が言葉に近いためです。C言語の場合、内容とは関係ない「include」や「main」等が出てきてしまい、多少複雑になります。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
main()
{
cls();
locate(0, 0);
printf("へ");
locate(0, 1);
printf("の");
locate(4, 0);
printf("へ");
locate(4, 1);
printf("の");
locate(2, 2);
printf("も");
locate(2, 3);
printf("へ");
return (0);
}
このプログラムは、エスケープシーケンスを使用していうます。PC-9801ではそのまま実行できますが、PC-AT互換機ではANSI.SYSを組み込んでから実行して下さい。
5.C言語でのサブルーチン
BASICの時のようにC言語でもサブルーチン化してみます。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
void print_he(void)
{
printf("へ");
}
void print_no(void)
{
printf("の");
}
main()
{
cls();
locate(0, 0);
print_he();
locate(0, 1);
print_no();
locate(4, 0);
print_he();
locate(4, 1);
print_no();
locate(2, 2);
printf("も");
locate(2, 3);
print_he();
return (0);
}
C言語でサブルーチン(関数)を作る際、main関数の前に記述するか後に記述するかは個人の自由です。ただし、C言語では、使用する関数はあらかじめ宣言されていなければなりません。私の場合、main関数の前に他の関数を記述して、プロトタイプ宣言を省略しています。
ここでは、確かに同じ部分をサブルーチン化していますが、このやり方は利口とは言えません。行数が増え、複雑化し、プログラムの見通しが悪くなっているため、バグが発生し易く、拡張や保守がしにくくなっています。
6.他の書き方
最初の日本語の手順書には「その下」や「右に」といった表現がありましたが、プログラムでは突然数字による位置指定に変わってしまいました。そのほうがパソコンでやりやすいからなのですが、ちょっと考え方をかえると手順書に近いニュアンスのプログラムを書く事もできます。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
main()
{
static char *heno[] = { "へ","の","へ","の","も","へ",NULL };
int addx[] = { 0, 0, 4, 0, -2, 0, 0 };
int addy[] = { 0, 1, -1, 1, 1, 1, 1 };
int x;
int y;
int i;
cls();
x = 0;
y = 0;
for (i=0; heno[i] != NULL; i++) {
x += addx[i];
y += addy[i];
locate(x, y);
printf(heno[i]);
}
return (0);
}
あらかじめ、表示する位置を「その下」や「右に」にあたる相対位置として用意しておき、現在の位置を更新しています。例えば、「その下」はY方向に+1、「右に」はX方向に+2としています。右が+1でなく+2なのは表示する文字が幅の広い全角文字を使用しているためです。
あまりに処理が単純で、サブルーチン化の余地がありませんね。とはいえ、データはもう少し手を加えられそうです。
7.関連データをまとめる
データ同士に関連がある場合、それらはひとまとめにしておいた方が便利です。
文字と移動方向は一対一で対応します。もともと文字を「どこへ」表示するかを決めるためのものなのですから。という訳でまとめてしまいましょう。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
struct heno_t {
char *moji;
int addx;
int addy;
};
main()
{
static struct heno_t heno[] = {
{"へ", 0, 0},
{"の", 0, 1},
{"へ", 4,-1},
{"の", 0, 1},
{"も",-2, 1},
{"へ", 0, 1},
{NULL, 0, 0}
};
int x;
int y;
int i;
cls();
x = 0;
y = 0;
for (i=0; heno[i].moji != NULL; i++) {
x += heno[i].addx;
y += heno[i].addy;
locate(x, y);
printf(heno[i].moji);
}
return (0);
}
構造体を用いてデータをまとめてみました。この程度のプログラムでは構造体を使うありがたみがないのですが、規模が大きくなり、複数のファイルにまたがるデータ等の場合、効果が現れます。
8.データを変えてみる
7.のプログラムは、表示する文字がなくなる(NULLになる)まで、位置指定と表示を繰り返しています。このデータを入れ替えると、同じプログラムで別の表示を行えます。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
struct heno_t {
char *moji;
int addx;
int addy;
};
main()
{
static struct heno_t heno[] = {
{"ヘ", 0, 0},
{"ノ", 0, 1},
{"ヘ", 4,-1},
{"ノ", 0, 1},
{"モ",-2, 1},
{"ヘ", 0, 1},
{NULL, 0, 0}
};
int x;
int y;
int i;
cls();
x = 0;
y = 0;
for (i=0; heno[i].moji != NULL; i++) {
x += heno[i].addx;
y += heno[i].addy;
locate(x, y);
printf(heno[i].moji);
}
return (0);
}
文字をカタカナにしてみました。なんともしまらない顔になってしまいますね。
さらにデータを変えてみます。
static struct heno_t heno[] = {
{"つ", 2, 0},
{"る", 1, 2},
{"ニ",-3,-2},
{"ハ", 0, 1},
{"oo", 0, 1},
{"ム", 0, 1},
{"し", 1, 1},
{NULL, 0, 0}
};
変更するのはデータだけで、他は何もしなくて結構です。
今度は「つるニハ○○ムし」です。文字の大きさを変えられないのでおかしな感じではありますが、それっぽく見えると思います。
このプログラムでは、文字数も文字そのものも移動方向も、すべて「へのへのもへ」の時と異なっていにもかかわらず、データの部分を書き換えるだけで変更が済んでいます。もともと、そうなるようにこの処理を作ったからです。
9.汎用化
せっかくデータの変更で処理内容の変更ができるようにしたのですから、もう少し汎用化してみましょう。
#include <stdio.h>
#define cls() printf("\33[2J");
#define locate(x,y) printf("\33[%d;%dH",(y)+1,(x)+1)
struct data_t {
char *moji;
int addx;
int addy;
};
typedef struct data_t heno_t;
int write_heno(heno_t *heno)
{
static int x = 0;
static int y = 0;
while (heno->moji != NULL) {
x += heno->addx;
y += heno->addy;
locate(x, y);
printf(heno->moji);
heno++;
}
return (0);
}
main()
{
static heno_t heno[] = {
{"へ", 0, 0},
{"の", 0, 1},
{"へ", 4,-1},
{"の", 0, 1},
{"も",-2, 1},
{"へ", 0, 1},
{NULL, 0, 0}
};
static heno_t damy[] = {
{"",-2, 2},
NULL
};
static heno_t turu[] = {
{"つ", 2, 0},
{"る", 1, 2},
{"ニ",-3,-2},
{"ハ", 0, 1},
{"oo", 0, 1},
{"ム", 0, 1},
{"し", 1, 1},
{NULL, 0, 0}
};
cls();
write_heno(heno);
write_heno(damy);
write_heno(turu);
return (0);
}
関数write_henoへデータを渡すと、カーソルの移動と文字の表示を行ってくれます。データとして「へのへのもへ」であるhenoと「つるニハ○○ムし」であるturuを用意しました。また、turuが丁度良い位置から表示されるように、ダミーデータのdamyを使用しています。
10.実は・・・
ここまでに「へのへのもへじ」の書き方をいろいろと考えてきましたが、実はもっと簡単な方法があります。おそらく、誰もが考えつく方法です。
#include <stdio.h>
int write_heno(char *heno[])
{
int i;
for (i=0; heno[i] != NULL; i++) {
printf(heno[i]);
}
return (0);
}
main()
{
static char *heno[] = {
"へ へ\n",
"の の\n",
" も\n",
" へ\n",
NULL
};
static char *turu[] = {
"ニつ\n",
"ハ\n",
"oo る\n",
"ム\n",
" し\n",
NULL
};
write_heno(heno);
printf("\n");
write_heno(turu);
return (0);
}
この方法なら表示にエスケープシーケンスを使う必要もないし、構造体もいりません。データは画面へ表示されるそのままのイメージで変数へ格納するため、その作成に悩むこともありません。
ではなぜあのようなプログラムを作ったのでしょうか?それは、この話が「へのへのもへじ」を人間が描く時の手順から始めたからです。
人が「へのへのもへじ」を描く時、ことばの順番に描き進みますよね。これを「へへののもへじ」と描こうとすると、少し考えてしまいます。これは「へのへのもへじ」が、描く対象物(文字)とその手順を同時に表わしているため、それに逆らう描き方が難しくなるということです。
パソコンの場合、文字の表示は左から右へ、上から下への一方向に行われます。そのため、下へ行って右上へ行ってまた下へ・・・といった動き方は複雑な処理となります。そこで、人間の考え方をパソコンに合わせてやり、描く順番という要素を排除し、最終的な生成物(顔)ができれば良いとした時、ここに示した単純な処理が生まれるわけです。
パソコンは人の考えを数値で表現しようとします。画面に表示される絵も、入力される文字も、内部では数字で扱われます。その数字で表現されたものを、意味のあるものとして受け取るのは、「へのへのもへじ」を人の顔と認識する人間なのです。
ここにプログラムリストを記述するのは無理があったような・・・でも、C#での書き方とか文章だけじゃ無理だしなぁ。




