川合のプログラミング言語自作(ライブラリ自作)のためのアイデア#0001

  • (by K, 2019.03.04)

(1) はじめに

  • 「プログラミング言語やライブラリを作っていいですよ」と言っても、「現状で特に不満を感じてはいないので、何を作ったらいいのかわかりません」と答える人は少なくありません。・・・欲がないですねえ(笑)。
  • ここではそんな人のために、例えばこんな風に考えたらどうかな?というヒントをいくつか書きたいと思います。
  • 「なんだー、こんな身近なことでいいのかー」と思ってもらえたらうれしいです。気が楽になるといろいろと思い付きやすくなるのではないかと思います。

(2) テーマ#1: 配列の自動拡張機能を付けるべきかどうか?

  • 仮に int a[10]; みたいな配列変数があったとして、a[9] = 3; は全く問題ないけど、 a[10] = 1; は本来はエラーにするべきだと思います。
  • それで、これを見つけた時に、「エラーになって止まる」のがいい言語なのか、それとも「止まらないで無視して(=だから何もしないで)先に進んでくれる」のがいい言語なのか、それとも「aを自動で拡張してa[10]も使えるようにしてくれる」のがいい言語なのか、そしてその時に黙ってやるのがいいのかそれとも警告くらいは出したほうがいいのか、あなたはどう思うでしょうか?
  • まあ一番まずいのは「エラーも何も出さず、配列の境界を越えてアクセスして他の変数の値を1で上書きして続行してしまう」なのは私にもわかります!

  • エラーで止まるのはもちろんいい言語です。ユーザは宣言時のサイズが十分ではなかったことに気付くからです。
  • しかし一方で、もし後からでもサイズが変更できる言語だったら、そんなのいちいち報告しないで、自動で拡張して続けてくれたらいいのに・・・という考え方もできます。もしこれが実現したら、宣言時にサイズを深刻に考える必要はなくなり、気軽にプログラムが書けるようになるでしょう。それはとてもいいことです。心配しなければいけないことが減るのは、他のことに集中する余裕にもつながります。

  • 黙って続行するというのは、一見するとひどいと思うかもしれません。自分のミスに気付けないじゃないかと。・・・しかしもし、この手のエラーがあった時に、「どこでエラーがあったのか」や、そのエラーの番号を何か特別な変数に記録してくれて、それを後から参照できるとしたらどうでしょうか?・・・それを前提にした「黙って続行する」です。
  • 一般にエラー処理はこんな感じでおこなわれています。
    err = 処理1(...); if (err != 0) { エラー処理 }
    err = 処理2(...); if (err != 0) { エラー処理 }
    err = 処理3(...); if (err != 0) { エラー処理 }
  • それとか、例えばポインタを使ってアクセスしたいときはこんな風に書くかもしれません。
    if (p != 0) { a = *p; }
  • これはつまり、ポインタが正常だったらポインタを使ってアクセスする、と書いているわけです。
  • さてこれらを見てどう思いますか?何をするにもif文で、うっとうしいと思わないでしょうか。私は思います。正常な場合よりも、エラーのチェックの記述の方が多くなる可能性すらあります。
  • ・・・おいおい、こいつは今さら何を言っているんだ、エラー処理を書くことこそプログラミングの主たる作業じゃないか、正常系だけ書けばいいとか思っているんだとしたら、それは初心者か、おバカさんのどちらかだ、という人もいそうですが、でもできればエラー処理なんかあまり書きたくないですよね。本来やりたい正常系をビシバシと書いていきたいはずです。
  • そう考えた時、エラーがあってもとりあえず止まらないで無視して前に進んでくれる言語だったらどんなにいいでしょう。エラーなんて最後にちょっと確認すればいいじゃないですか。あー、ここまでに来る途中のどこかで失敗していたのか、そうかそうか、それは残念だったね。じゃあエラー表示しておこう。・・・これでいいじゃないですか。

(3) テーマ#2: 自動でエラー処理してくれたらいいかも?

  • 私はコンソールアプリを書いているときに、ファイルのオープンに失敗したら「fopen error」と表示してexit(1);するのが定番になっています。
  • またmallocしてNULLを返されてしまったら、「out of memory」と表示してexit(1);するのが定番になっています。
  • あらかじめ大きなバッファを用意しておいて、ファイル全体を読み込ませようとして、それでメモリに載りきらなかったときは、「too large file」と表示してexit(1);するのが定番になっています。
  • このように定番のエラー処理があると思うのですが、それを毎回書くのはけっこうめんどくさいです。かといってエラー処理を省略してしまったら、当然ながら誤動作する危険性がありますし、エラーに気付けません。
  • それなら最初からこの定番処理付きのライブラリ関数を用意したらどうでしょうか。・・・そうですね、エラーの場合は関数から戻ってこなくて、戻ってきたらエラーはなかったと決めつけて構わないので、NoErrを略した_neを末尾につけてみましょうか。
    // 普通の書き方.
    a = malloc(n);
    if (a == NULL) {
        fprintf(stderr, "out of memory\n");
        exit(EXIT_FAILURE);
    }
    fp = fopen(argv[1], "rt");
    if (fp == NULL) {
        fprintf(stderr, "fopen error : %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    
    // 便利な関数がある場合の書き方.   
    a = malloc_ne(n);
    fp = fopen_ne(argv[1], "rt");
  • ということで、10行が2行になるわけです。これはすごく見通しがよくなると思いませんか?

(4) とにかく人間は間違うことがある、ということを前提にすべき

  • いくつかの言語(特にC言語やC++言語)は、とにかく人間が間違わないことを前提にしているというか、間違えたらどうなっても知らないよ、みたいな強気な仕様が多い気がします。
  • でも実際は人間はときどき間違えるのです。だから間違えているかもしれないという可能性にも多少は配慮してほしいです。
  • そして間違えたときに間違いに気付けるきっかけがあると最高です。

(5) 最適化は果たしてユーザのためになっているのだろうか?

  • 多くのC言語やC++言語では、高度な最適化を売りにしています。これは素晴らしいことではあるのですが、でも弊害もあります。
  • 最近の最適化はかなり優秀で、こちらが高速化してやろうと思って書き方を工夫しても全く速さが変わらないことがたまにあります。それどころか、遅くなってしまうこともあります。・・・この遅くなってしまうケースというのは、素直ではない書き方を見たコンパイラが「このパターンは知らないぞ」と自前の最適化の適用をあきらめてしまったせいで、元の書き方よりも遅くなってしまうという現象です。
  • こんなことをされるとどうなるかというと、初心者が最適化手法を学ぶ機会がなくなるのです。・・・何もしなくても速いし、良かれと思ってあれこれ努力するとかえって遅くなるのです。こんな経験をしたら、そりゃあ普通は自前で最適化しようとする気はなくなってしまうでしょう。
  • 何の変哲もない素直なプログラムが、超絶にがんばったプログラムと同じ速さで動くなんて、それはもう相当にかっこいいです。コンパイラ作者はさぞかしいい気分でしょう。でも、そのせいで犠牲になっているものもあるということです。
  • 最適化を過剰に重視する考え方の先には、「もはやプログラムは書かれた通りに動く必要はない、書かれた意図を推測して動くべきだ」という思想につながっていると思います。私は基本的にプログラムは書かれた通りに動くべきであって、変に意図を推測して余計なことはしてほしくないと思う人です。こう書けばコンパイラはこう思うだろうから・・・みたいなことに気を使わなければいけないとしたら、それはプログラミングではない別の何かになってしまいそうです。

(6) テーマ#3: やりたいことを素直に書けるべき

  • Windowsでなんか適当にウィンドウを出してそこに線を引いたり丸を描いたりしたいと思った時、win32-APIを使ってやろうとすると、すごくやるせない気持ちになります。・・・なぜかというと、こちらはただウィンドウを出して線を引きたいだけなのに、ウィンドウメッセージに応対するためのコールバック関数を用意させられて、しかもいろんなメッセージにどう対応するか自分で考えないといけないからです。まあデフォルトで構わないものは全部デフォルトに投げられるのが救いではあるのですが、でもなんでこんなに何行も書かないといけないの?とは思います。私が小学生や中学生の時にやっていたBASICならただLINE命令やCIRCLE命令を使えばそれだけで線でも円でもすぐに描けたのに。
  • 結局、私はWindowsから、ほしくもないのにパラダイムを押し付けられているのです。私は絵が描きたかったのであって、Windowsの仕組みを知りたかったわけではないのです。やりたいことをやりたいようにできないというのはダメです。私はアプリが作りたいのであって、ドライバを書きたいわけじゃないのです。ドライバならOS側の都合を押し付けられるのは構いません。OSの下請けとして頑張るのがドライバの仕事なので。でもアプリはそうじゃないと思うのです。これじゃあ見通しが悪くなって、アプリ開発がうまく行かなくなるリスクだけを上げていると私は思います。
  • 上記の(2)や(3)のテーマも、結局はプログラマが自分の本来やりたいことに集中できるようにしているとみることもできます。

(7) 物事を複雑にするべきではない

  • 言語やライブラリを使っていると、なんでこれはこんな仕様なんだろう?って思うことはないですか?単純にこうすればいいだけなんじゃないかな?とか。
  • 世間の多くのソフトウェアは、複雑な問題を複雑に解決しており、それは全然うまくありません。中には単純な問題を複雑にしてしまっているという例もあります。
  • プログラマの腕の見せ所は、複雑そうな問題を単純なやり方で見事に解決するところにあるはずです。私はそう信じています。複雑な問題から逃げずひるまずに複雑なまま解決することが技術力だと思っている人もいるみたいですが、そんなのは他の誰かでもできることです。・・・もちろんいつもうまいやり方を思いつけるとは限りませんが、しかしもし運よくうまいやり方に気付いたら、そのときは是非それを試してみましょう!それができるようになればあなたも一流の仲間入りになるはずです!!

(8) テーマ#4: メモリ管理

  • メモリ管理は、古くからあるシステムプログラミングのテーマだと思います。しかしそれでもまだこれが決定版だというものが決まったわけではありません。
  • メモリをallocしてfreeするというただそれだけのことをきちんとやればいいだけの話なのですが、どうしてもみんな時々は間違えてしまいます。私だって年に何度もミスります。・・・またミスらないにしても煩わしいと思うことはよくあります。
  • このテーマになるとまずはガベージコレクションに触れないわけにはいきません。ガベージコレクションは「プログラマはallocだけしていればよく、freeはシステムが自動で行う(=プログラマはほとんど意識しない)」という究極のスタイルを目指したアルゴリズムです。
  • 古典的なガベージコレクションは「マーク・アンド・スイープ」と呼ばれるアルゴリズムを使って、メモリ上のすべてのオブジェクトを監視してどこからも参照されなくなったメモリ域を自動で見つけてfreeしていきます。しかしこれは決して軽い処理ではなく、相応の性能低下はおきます。この性能低下が10%なのか1%なのか0.1%なのかは、言語やライブラリ作者の腕の見せ所となります。
  • マーク・アンド・スイープを使わないタイプのメモリ管理支援機能もあります。これをガベージコレクションと呼ぶか呼ばないかは、人や文脈によってさまざまなのですが、とにかく中間的な方法であることは間違いないです。例えばオブジェクトに参照カウンタがついて、これが0になったら自動でfreeするような仕組みがこれにあたります。参照カウンタ方式は循環参照が発生するとそこをプログラマが明示的に対処しない限りfreeできない(リークしてしまう)という問題がありますが、しかしマーク・アンド・スイープと比較すれば圧倒的に性能低下が小さくて、よくできたアルゴリズムの一つです。その代わり、参照カウンタの管理をミスしてしまったら、メモリリークや早まったfreeなどを起こす可能性はあります。
  • また「自動リリースプール」というアルゴリズムもあります。これは今すぐにfreeするわけではないのだけど、あとでまとめてfreeするという仕組みで、たとえば関数から抜けるときにそれまでに生成したテンポラリなオブジェクトを一気に片づけます(関数の末尾にプールの破棄が明示されていれば)。allocしたときにデフォルトでこのプールに入れることにしておけば、明示してプールから出さない限りは自動で消えてくれるので、freeし忘れをかなりなくせます。このアルゴリズムもマーク・アンド・スイープと比較すると抜群に性能低下が小さいです。こちらも代償はあり、新たにプールの管理が必要になりますし、プールから取り出して管理するときは、プールの恩恵にあずかれないことになります(とはいえ、プールの管理は参照カウンタの管理よりは楽だと思いますが)。
  • 最近はやっているRustという言語も、独自のメモリ管理の考え方を持っています。とても面白いです。

(9) 少しずつ積み重ねて進んでいきたい

  • 私はプログラミングが大好きです。でもただ毎日作りたいプログラムを書いていればそれでいいのでしょうか。その過程で新しいアルゴリズムや新しいテクニックを覚えることはあるでしょう。だから昨年より今年のほうが早くうまく作れるでしょうし、今年より来年はもっとうまくできるでしょう。でもそれだで満足していいのでしょうか。
  • 私は自分の使う言語やライブラリも少しずつ改良したいです。そうしたらもっともっと書きやすくなりますよね。歩みはのろいかもしれませんが、しかしとにかく前に進んでいれば、前に進まない人たちとの差は徐々に開いて、きっと最後は大きな差になるのです。
  • いや、そんな大それた改良は、別の人に任せておけばいい。自分は新しい言語やライブラリが出たときにそれを追いかけていくだけで十分なんだ。・・・はい、そういう考え方もできます。それで満足できるのならそれでいいと思います。でももしみんなそう思ってしまって、言語やライブラリの開発から逃げてしまったら、もはや進歩はおきません。誰かがやらなければいけないのです。
  • もし「面白そうだ、やってみたい」と思えたら、ぜひ自分であれこれとやってみてください。
  • 自作の言語やライブラリを使うことで、既存言語や既存ライブラリを使うよりも格段にすっきりとプログラムが書けると、ものすごくいい気分になれますよ!

次回に続く

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2019-03-17 (日) 09:36:01 (317d)