川合のプログラミング言語自作のためのテキスト第四版#0
(1) はじめに
- プログラミング言語の自作のテキストを書いてからもう5年が経ちました。→a21_txt01
- 今の私なら、また全然違った切り口でプログラミング言語自作入門が書けるだろうと思って、試しに書いてみることにしました。
- 今回は acl4 みたいなやり方で、つまり「ライブラリ自作駆動開発」でやってみようと思います。これは「わらしべ長者な開発」ともいえるもので、うまくいくととても楽しいので、ぜひこれを紹介しつつ、プログラミング言語づくりもしたいと思います。
- [目次]
- a26_txt03(acl4v2_000): はじめに, a_static, a_Version, a_class
- (ここに目次を入れていく予定)
(2) プログラミング言語の作り方として正しいのはどちらなのか?
- 前述の通り、私は以前「10日くらいでできる!プログラミング言語自作入門」を書いていました。今回は全く異なるやり方で言語を作っていきます。・・・そうなると、「じゃあどっちの作り方が正しいのか?」という疑問が出てくるのは当然だと思います。
- でもこれは「東京から大阪に行くときに、新幹線で行くのが正しいのか、飛行機で行くのが正しいのか、それともリニア新幹線の完成を待ってそれで行くのが正しいのか」みたいなもので、別にどれが正しいとか間違っているとかではないのです。
- 私自身も、この2026年版のほうが後発だからこっちが優れている!と言いたい気持ちはあるものの、いやでもやっぱり客観的に見たら、一長一短というか、新幹線vs飛行機みたいなものでしょう。・・・ただまあ、そうですね、この2026年版のほうが意外性はあると思います。
- もし余裕があれば、両方の作り方の違いを眺めてみてください。なるほどねー、こっちではそうやるのかー、みたいな発見がきっとあります。
(3) 「ライブラリ自作駆動開発」とは何ですか?
- 最初に断っておきますが、この言葉は私が適当に考えた造語で、一般的に使われている言葉ではありません。だから辞書とかで調べてもきっとどこにも載ってないです(もし将来載るようになったら、びっくりです)。
- C言語は便利な関数を書き足していけば、どこまででも便利になる可能性を持っていると、私はずーっと前から思っていました。printf()やstrcmp()、sqrt()など標準関数は便利な機能を提供してくれています。もしそれらがなくて、そういうものも全部自作しなければいけないとしたら、それはとてもめんどくさいです(まあそれはそれでちょっと楽しいんですけどね)。
- C言語はC++やほかの言語に比べて機能が少なくて不便だと言われることが多いですが、それは第一には便利な関数が少ないからです。便利な関数をもっと増やせば、見違えるほど便利になるはずなんです。標準関数程度で満足せず、もっと便利になりそうな機能を追加していけば、すごいことになるんです。つまり言語が悪いわけではなく、ライブラリが悪いのです。・・・C言語は最初からライブラリによってパワーアップしていけるようになっていて、そういう拡張性のある言語だったはずなのに、あまりライブラリを使って言語の能力を底上げしていこう!みたいな流れにはならず、「はい、じゃあ新しい言語を作ったのでそちら行きましょう」が強調されて、ちょっと見捨てられ気味なのです。それはかわいそうじゃないですか。
- もちろんオブジェクト指向言語ではないので、クラスライブラリがきれいに書けないという問題はあります。でもそういうきれいさをあきらめれば、とにかくなんだってできるのです。ということで、これからどんどん作って、自分だけのスーパーC言語にしていきましょう。
- 私の聞いたところによると、競技プログラミングが得意な人たちの多くはすでに自分のライブラリを持っていて、問題を早く解くために活用しているようです。そういう話を聞くとうまいなーと思います。
- みなさんは「わらしべ長者」という昔話をご存じでしょうか。・・・最初は一本のわらを持っていただけだったのに、物々交換を繰り返していくうちに、最後は家を手に入れてしまうというすごいお話です。
- Wikipediaにより詳しい説明があります → https://ja.wikipedia.org/wiki/%E3%82%8F%E3%82%89%E3%81%97%E3%81%B9%E9%95%B7%E8%80%85
- 私はライブラリ自作駆動開発をわらしべ長者開発だと説明することもあります。なぜなら、最初は取るに足らない些細な関数を数個作るところから始まります。それができたら、その関数を使ってより高度な関数群を作ります。そしてそれもできたら、それらを使ってさらに高度な関数群を作っていくからです。・・・これを繰り返して、最後には(いや、それが最後というわけでもないのですが)、結構高度な関数をうまいこと作ってしまうのです。当初はまさかここまでできるなんて思いもしなかったのに。
- ここで作った関数群は、もちろん別の開発にも使えます。自作ライブラリは、自分の財産です。次の開発をするときには、わらしべ開発をやり直す必要はないので、最初から高度なことができます。こうして私は、以前の私よりもなんでもさっさと作れるようになっているわけです。私自身が成長したというよりは、ライブラリが成長したようなものですが、とにかく開発速度は上がっているのです。・・・ということで、私も読者の皆さんも、いっしょにスーパーなプログラマになろうじゃありませんか!
(4) 著者について
- 私は「30日でできる!OS自作入門」の著者で、主にそこで有名になった感じです。
- 学生の頃は機械語とかアセンブラが大好きという性格でした。・・・コンピュータは機械語で動いていて、だからこれを使いこなせばコンピュータのすべてが制御できると信じていたからです(まあ原理的には間違ってはいないです)。だからレジスタが何本あるとか、命令セットがどうなっているかとか、やたらと気にしていました。・・・でも実際は、コンパイラの最適化を超えるコードを手で書くのは結構大変で、だからまあ基本的にはコンパイラにお任せしてしまえばよく、そうなってしまうとレジスタとか命令セットとかは実はどうでもよかったりするわけなんですが、学生の頃の私は高価なコンパイラを買うとかはできなくて、16進数が大好きな青年でした(かつては無料で手に入るコンパイラなんてなかったですし、コンパイラの最適化能力も大したことはなく、手書きアセンブラは最強だったのです)。
- 著者の性格としては、周囲の人が新しい技術に注目してどんどんと進んでいく中で、「いやでも、古い技術でもこうやればこれくらいはできるよ?」みたいなことを考えるのが好きで、だからこそ「なるほど、ここに関しては古い技術だけではマネできないな、きっとここが新しい技術の本質なんだな」なんて一人で納得したりしています。
- OS自作の人がなぜ言語自作をすることになったのか、たまに聞かれます。まあ確かに自明ではないですよね。・・・いくつか観点がありますが、ひとつには「そもそもなぜOS自作するのか」というところから考えるとわかりやすいと思います。まず私にとってOS自作は目的ではなく手段なのです。OSから全部作り直すことにすれば、私はソフトウェア世界のすべてを理想通りにできるはずなのです。そもそもOSを作るのってかなり大変なことです。それをわざわざやろうというのは、既存のOSに不満があるからです。それを全部直したいわけです。だけど、ある時気づいたんです。「プログラマ視点で見たら、別にOSの違いなんてあまり大きくなくて、言語の違いがすごく大きいのではないか?」と。だって同じ言語を使っていたら、OSがちょっと違っても、あまり差を感じずに済んだりしませんか?だったらまずは言語を作ったらいいじゃないか!と。
- それに、もし運よくとびっきり便利な言語ができてしまって生産性10倍とかになったら、それでOSを作ればいいじゃないですか。そうすれば10倍速くOSが完成するわけです。おお、それはいい。ぜひやろう。・・・この考え方は別に珍しいものではなく、そもそもC言語だってUNIXの開発効率を上げるために生まれてきた言語なのです。だからOS開発の人が言語開発に興味を持つのは、実は結構普通のことなのです。
- 私はOS自作でちょっと名を上げたときに、先生方から「君がOSを作っていることは本当に素晴らしいと思うが、その技術を後輩たちに伝えることはできないだろうか?もしできなければ、君が死んでしまったらそれで終わりになってしまう」という指摘を受けました。それまでの私はとにかくいいものを自分が作ればいいとしか思っていなくて、この指摘はとても意外でした。しかし同時にとても納得しました。
- それ以降の私は、何かうまくできたと感じるたびに、その作品の成功だけではなく、やり方の継承について考えるようになりました。やり方を伝えることができれば、私一人でがんばるよりもずっと速く良いものが生まれてくるかもしれません。またこれからは、いろんな人のいろんなやり方が混ざっていいとこどりをして、「やり方」も大きく進歩していくかもしれません。そうなったらすごくすてきじゃないですか!・・・そうなったら私はなんとなく不老不死になった気分です(笑)。
- このテキストでは、何をどう作ったかという説明ももちろんしますが、それと同時に、何を考えてそうしているのかもできるだけ説明するようにしています。・・・実はそういう書き方には抵抗がありました。なぜなら私が自分の試行錯誤を書いてしまうと、読者はそれで満足してしまって試行錯誤を自分ではやらないようになってしまうかもしれないからです。また話も長くなってしまいます。しかし一方で、そういうことがきちんと書いてあるほうが読み物としては面白いです。それでまあ迷ったわけですが、とにかく今回はこのスタイルでやってみることにします。試さないであれこれ考えていてもしょうがないですよね。
- そもそもこんな「著者について」っていうのがなくてもライブラリ自作駆動開発は説明できるはずなんです。でも背景が分かったほうがより伝わるかもしれないじゃないですか。ということで、さっそくこのスタイルを実践しているというわけです。
(5) ライブラリの名称
- 「acl4v2」とします。
- まず acl っていうのが何なのか、それを説明します。「a c-lang library」です。ある一つのC言語ライブラリ。theですらないです。その辺にあるライブラリのうちの一つです。取るに足らないものです。・・・これくらい控えめな名前にしておけば、名前負けすることはまずないですよね。
- 4なのは私の中では acl シリーズの第四世代なので、それがそのままついています。・・・v2なのはマイナーチェンジを繰り返して、v1の次に作ったからv2です。
- まあつまり、全然かっこよくない名前です。いやだって、もう考えてもわからないので、ここに時間を使わないで中身で頑張ることにします。
- ちなみにイメージカラーは紅茶色です。・・・だったらこのテキストの色もそんな感じにしたらよさそうですが、wikiの設定を変えると今まで書いたやつも影響されそうですし、個別に色指定するほどのこだわりでもないので、まあこのままでいきます。
(6) ヘッダファイルを作るかどうか
- 従来のC言語の一般的なライブラリの作り方として「~.c」のほかに「~.h」というファイルを作って、インクルードは「~.h」だけやって、関数の実体を「~.c」において、ライブラリはライブラリだけでコンパイルして、アプリはアプリだけでコンパイルして、リンカでくっつける、という方法がとられてきました(分割コンパイル方式)。
- これの良いところはコンパイル時間が短くなることです。だから私もかつてはこの方法でライブラリを作ってきました。
- しかし今はPCの性能が非常に高くなっていて、わざわざ分割コンパイルでコンパイル速度を上げなくても、体感速度はほとんど変わらなくなってきました。・・・一方でコンパイラの視点に立てば、インクルードするのがヘッダファイルではなく実体であれば、関数呼び出し部分をインライン展開することだってできますし、どうやら3番目の引数jはいつも1で呼ばれるらしい、みたいなことに気づけばその性質を使って最適化をすることだってできます(現存するコンパイラでそこまでやってくれるかどうかは未確認ですが)。つまり大域的最適化ができるようになるのです。分割コンパイルではそういうことはやりたくてもやりようがないです。
- もちろん巨大なアプリとかを作る場合はそんなことを言っている余裕はないので、私も素直に分割コンパイルすると思いますが、私は普段30KBとか50KBとかのプログラムしか作らないので、分割しないコンパイルばかりしているわけです。
- 分割コンパイルをしないならヘッダファイルは必要ありません。「~.c」の実体だけあれば十分です。
- 一方でコンパイラはある関数定義を見たときに「この関数はどうやらこのプログラム内では一切呼ばれていないようだけど、これは分割コンパイル用のソースコードで他のソースコードから呼ばれるかもしれない」と考えます。だから全く使わない関数もご丁寧に実行ファイルに含めて出力してしまいます。・・・コード内で一度も使わない関数が居残るなんて、これは非常に邪魔です。アプリの実行ファイルが大きくなるだけです。だから使わない関数は消えてほしいです。
- そのためには関数の属性に static を付けます。そうすると、コンパイラは「この関数はこのソースコード内でしか使われない」と認識して、もし一度も使われなければ実行ファイルに含まないようにコード生成してくれます。すばらしいです。
- ということなので、関数宣言の時はどんどん static を付けましょう!・・・と言いたいところなのですが、そうすると今度は困ったことが起きます。もし巨大なアプリを作ることになって分割コンパイルをやりたくなった時に、ライブラリを分割コンパイルできなくなってしまうのです。がびーん。 static 指定が邪魔なんです。
- ということで、何ができれば理想的かというと、 static を一斉につけたり消したりできればいいわけです。
- それでこんな書き方になりました。
#if (!defined(MyStatic))
#define MyStatic static
#endif
...
MyStatic int libFunc1(int a, int b, int c)
{
....
}
- これで MyStatic というシンボルを空にしてdefineしておけば分割コンパイル対応ソースコードになりますし、未定義のままなら勝手に static 属性がつくようになります。やったね!
(7) 関数名をどうするか
- C言語の標準ライブラリは、すごく自己中心的で、sinとかsqrtとかmallocとかfreeとか、好き勝手に関数名を使っている印象があります。これは標準関数ではないですが、あるときmaxとかminまでライブラリ側で定義していて使っていて、とても迷惑したことがありました。
- 私の考えではわかりやすい名前・使いやすい名前はライブラリ側は避けるようにして、アプリプログラマに使わせてあげるべきです。というかアプリ開発者の思い通りに変数名や関数名を使わせてくれないライブラリは、ちっとも便利ではありません!
- そういう意味では、先の(6)に書いた「MyStatic」はいい名前ではありません。これくらいの名前なら、アプリ側で無意識に使う可能性が十分にあります。
- では関数名はどうしたらいいでしょうか。私はかつてはライブラリ名を頭につけるルールを採用していました。このルールだと「acl4v2_static」になります。・・・この命名規則はアプリ開発時に衝突事故が起きる可能性がほぼゼロなので、その点は最高にすばらしいですが、関数名がとにかく長くて、うっとうしいのです。便利な関数はとてもよく使います。よく使う関数に毎回 acl4v2_ ってつけると、けっこう目障りなのです。
#if (!defined(a_Version))
#define a_Version 9999
#endif
#if (a_Version > 0)
#define static_ a_static
...
#define VecChr a_VecChr
#define Set0 a_Set0
...
#endif
....
#if (!defined(a_static))
#define a_static static
#endif
...
- まず最初の a_Version に関する記述は全部無視してください。それはあとで説明します。
- すると私は MyStatic でも acl4v2_static でもなく、 a_static と書いていることが分かります。そう、私はacl4v2ライブラリの関数は「a_」で書き始めることにしたのです。短いので衝突リスクは多少残りますが、まあ覚えやすいのでこれで許してください、という感じです。
- そして a_Version です。これが何のためにあるかですが、デフォルトではこのマクロは未定義なので9999がdefineされて、その後の条件コンパイルが有効になります。そうすると、a_を付けずに書き始めても勝手にa_を補ってくれるようになります。・・・つまりa_は省略できるのです。これなら全くうっとうしくないです!
- そしてもし衝突が起きて困るなら、 a_Version に 0 をdefineしてからインクルードすればいいわけです。そうすれば省略形は使えなくなるので衝突は起きません。
(8) acl4v2.c
#include "acl4v2_000.c"
#include "acl4v2_001.c"
#include "acl4v2_002.c"
#include "acl4v2_003.c"
#include "acl4v2_004.c"
#include "acl4v2_005.c"
...
(9) acl4v2_000.c
#if (!defined(a_Version))
#define a_Version 9999
#endif
#if (a_Version >= 1)
#define static_ a_static
#define class_ a_class
#endif
#if (!defined(a_static))
#define a_static static
#endif
#include <ctype.h>
#include <errno.h>
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <locale.h>
#include <math.h>
#include <setjmp.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define a_class(c) typedef struct c ## _ c; struct c ## _
(10) 追加の説明
- [Q]なぜincludeがたくさんあるの?
- [A]それはacl4v2.cをインクルードすれば標準関数が一通り使えるという状態にしておきたかったからです。そのほうが便利ですよね?
- [Q]a_class()というマクロは何ですか?
- [A]普通にstructを使って構造体宣言をすると、Abcと書くだけでは型を意味すると思ってもらえずエラーになります。
struct Abc {
int a, b, c;
};
Abc abc; // エラーになる.
struct Abc abc; // これなら許してくれる.
- でもそれは不便です。C++のようにstructを省略できるようになりたいです。それにはtypedefを使うといいのですが、それを全部まとめてやってくれるマクロです。だから以下のように書けます。
class_(Abc) {
int a, b, c;
};
Abc abc; // エラーにならない.
- ちなみになぜエラーにならなくなるのかというと、結果的に以下のように展開されるからです。
typdef struct Abc_ Abc;
struct Abc_ {
int a, b, c;
};
(11) あとがき?
- 今回はたくさん書いたのに、コード本体(9)は30行しか進みませんでした。
- まあそれだけ丁寧に説明したってことでいいのかな?
こめんと欄