川合のプログラミング言語自作のためのアイデア#0002

  • (by K, 2019.03.17)

(10) テーマ#5: ささいな関数群こそがプログラマを救う?

  • 私たちプログラマは、たいていは「すごいライブラリ」を作りたがります。とても高度で高速な数学処理ができるライブラリだったり、めちゃんこに高速なソートだったり、超大規模なグラフィックライブラリだったり。
  • そういうものはもちろん便利です。でもそういうものしか作ってはいけないとか、そういうものが役に立って他のものは役に立たないとか思っていませんか?そんなことはないと思うのです。ここではその話を書きたいと思います。
  • 私が先日作っていたプログラムの一部を例にします。これはHTMLのうちの一部を受け取って、それを適当に加工してコンソールに出力するためのものです。
    char last = 0;
    while (*p != '\0') {
        if (*p == '<') {
            if (strncmp(p, "<br />", 6) == 0) { p += 6; putchar('\n'); continue; }
            if (strncmp(p, "<strong>", 8) == 0) { p += 8; continue; }
            if (strncmp(p, "</strong>", 9) == 0) { p += 9; continue; }
            if (strncmp(p, "<div>", 5) == 0) { p += 5; continue; }
            if (strncmp(p, "</div>", 6) == 0) { p += 6; continue; }
            if (strncmp(p, "<h3>", 4) == 0) { p += 4; putchar('\n'); continue; }
            if (strncmp(p, "</h3>", 5) == 0) { p += 5; putchar('\n'); continue; }
            (似たような処理がまだたくさん続くが面倒なので省略)
        }
        if (*p == ' ' || *p == '\r' || *p == '\n' || *p == '\t') {
            p++;
            if (last != ' ')
                putchar(last = ' ');
        } else
            putchar(last = *p++);
    }
  • まあ普通のプログラムです。でももしstrncmpSkip()という関数を作って以下のように書き換えたらどうでしょうか?
    int strncmpSkip(const char **p, const char *s)
    {
        int l = strlen(s);
        if (strncmp(*p, s, l) == 0) {
            *p += l;
            return 1;
        }
        return 0;
    }
  • 書き換え結果:
    char last = 0;
    while (*p != '\0') {
        if (*p == '<') {
            if (strncmpSkip(&p, "<br />")) { putchar('\n'); continue; }
            if (strncmpSkip(&p, "<strong>")) continue;
            if (strncmpSkip(&p, "</strong>")) continue;
            if (strncmpSkip(&p, "<div>")) continue;
            if (strncmpSkip(&p, "</div>")) continue;
            if (strncmpSkip(&p, "<h3>")) { putchar('\n'); continue; }
            if (strncmpSkip(&p, "</h3>")) { putchar('\n'); continue; }
            (似たような処理がまだたくさん続くが面倒なので省略)
        }
        if (*p == ' ' || *p == '\r' || *p == '\n' || *p == '\t') {
            p++;
            if (last != ' ')
                putchar(last = ' ');
        } else
            putchar(last = *p++);
    }
  • この書き方は以下の点で優れています。・・・strncmpで比較文字列の長さを間違えてしまうというバグの余地がない。pに加算する文字数を間違えてしまうというバグの余地もない。
  • まあこの文脈におけるstrncmpSkipの有用性は疑いないと思いますが、でもじゃあこのstrncmpSkip()をライブラリとして公開するべきでしょうか。こんな誰でも作れそうなヘボい関数を?
  • まずこの関数に一般性はあるでしょうか。私は結構一般性があると思います。コマンドライン引数で、「-L100」とか「--limit100」みたいな指定を可能にするケースは時々あると思いますが、そういうときもstrncmpSkip()は使えると思います。
  • まあそういうときはその都度アプリ側でこの関数を作ればいいじゃないかといわれるとその通りなのですが、でもstrlen()みたいな簡単な関数だってライブラリ化されていますし、もちろん十分に有用なので、簡単すぎるからライブラリ化しちゃダメっていうのも違うと思うのです。
  • 私はつい数か月前までは、こういう関数をライブラリ化するなんて考えられないという立場でした。でも今はすっかり改心して、こういう関数こそライブラリ化していくべきなんだと思いなおしています。こういう小さな積み重ねが、プログラムの見通しを少しずつ良くして、最後には大きな改善につながるのです、きっと。

(11) AutoReleasePoolとtmpPrintf

  • これは些細な関数をライブラリ化してかなりうまく行ったと感じた事例です。
  • まず、以下の関数群を作りました。4つもあるので少し長く見えるかもしれませんが、一つ一つは本当に些細なことしかやっていません。
    typedef strcut AutoReleaseNode_ {
        void *p, *next;
    } AutoReleaseNode;
    
    typedef struct AutoReleasePool_ {
        void *oldPool;
        AutoReleaseNode *node;
    } AutoReleasePool;
    
    AutoReleasePool *AutoReleasePool_defalut = 0;
    
    AutoReleasePool *AutoReleasePool_open()
    {
        AutoReleasePool *w = malloc(sizeof (AutoReleasePool));
        w->oldPool = AutoReleasePool_defalut; // デフォルトの値をバックアップしておく.
        w->node = 0;
        return w;
    }
    
    void AutoReleasePool_add(AutoReleasePool *w, void *p)
    {
        AutoReleaseNode *n = malloc(sizeof (AutoReleaseNode));
        if (w == 0) w = AutoReleasePool_defalut; // 0ならデフォルト指定.
        n->p = p;
        n->next = w->node;
        w->node = n;
    }
    
    void AutoReleasePool_remove(AutoReleasePool *w, void *p)
    {
        AutoReleaseNode *n;
        if (w == 0) w = AutoReleasePool_defalut; // 0ならデフォルト指定.
        for (n = w->node; n != 0; n = n->next) {
            if (n->p == p) {
                n->p = 0;
                return;
            }
        }
        fprintf(stderr, "AutoReleasePool_remove: remove error: p=%x\n", (int) p);
        exit(EXIT_FAILURE);
    }
    
    void AutoReleasePool_close(AutoReleasePool *w)
    {
        AutoReleaseNode *n, *nn;
        if (w != AutoReleasePool_defalut) {
            fprintf(stderr, "AutoReleasePool_close: nesting error\n");
            exit(EXIT_FAILURE);
        }
        for (n = w->node; n != 0; n = nn) {
            nn = n->next;
            if (n->p != 0)
                free(n->p);
            free(n);
        }
        AutoReleasePool_defalut = w->oldPool;
        free(w);
    }
    • AutoReleaseNode: たった8バイトの小さな構造体
    • AutoReleasePool: たった8ナイトの小さな構造体
    • AutoReleasePool_open(): 自動リリースプールを準備する
    • AutoReleasePool_close(): 自動リリースプールを片付ける(=この時に登録された全てのポインタが解放される)
    • AutoReleasePool_add(): 自動リリースプールにポインタを一つ登録する
    • AutoReleasePool_remove(): 自動リリースプールに登録されたポインタを一つキャンセルする
  • その上で以下の関数を作りました。
    char *tmpPrintf(const char *f, ...)
    {
        char buf[1024 * 1024];
        va_list arg;
        int l;
        char *s;
        va_start(arg, f);
        l = vsnprintf(buf, sizeof buf, f, arg);
        if (l < 0 || (int) (sizeof b) <= l) {
            fprintf(stderr, "tmpPrintf: buffer error\n");
            exit(EXIT_FAILURE);
        }
        va_end(arg);
        s = malloc(l + 1);
        memcpy(s, buf, l + 1);
        AutoReleasePool_add(0, s);
        return s;
    }
  • この関数は、sprintf()みたいなものなのですが、格納先のメモリを呼び出し元が準備しなくていいところが違います。
  • この関数tmpPrintf()のミソは、mallocしたポインタをAutoReleasePoolに登録していることです。これにより、このメモリはAutoReleasePool_close()を呼び出しさえすれば、確実に回収されます。
  • もう「確保したメモリは忘れずに解放しなくちゃ!」みたいなことをあまり意識しません。使い捨てです。確保したメモリのポインタとかを使い終わって解放するまで変数にとっておいたりしなくてもいいのです。だからとても気楽です。
  • 忘れちゃいけないのは、関数に入ったらAutoReleasePoolをopenすることと、関数を抜ける前にAutoReleasePoolをcloseすること、その2つだけなのです。
  • 私はこんな風に使っています。
    for (i = 0; i < 100; i++) {
        FILE *fp = fopen(tmpPrintf("file%04d.txt", i), "rt");
        ...
        fclose(fp);
    }
  • まあもちろん
    for (i = 0; i < 100; i++) {
        char tmp[16];
        sprintf(tmp, "file%04d.txt", i);
        FILE *fp = fopen(tmp, "rt");
        ...
        fclose(fp);
    }
  • と書いてもいいのですが、上記の書き方よりも2行も多いですよね?それがなんだかなーと思うわけです。別に速度を追求しなくていい場面なら、簡単な方でいいじゃないかと。
  • 変数名を考えるのって時にはめんどくさいですし、このtmpの最大長がどのくらいになりそうなのかを考えなきゃいけないのもささやかなコストかなーと。

(12) strstr2

  • すごくくだらない名前ですが、我ながらなかなかに有用だったので紹介します。
  • あるフレーズを見つけたら、その先にある別のフレーズまで読み飛ばしてそのポインタを返すというものです。
    char *strstr2(const char *s, const char *t, const char *u)
    {
        char *p = strstr(s, t);
        if (p != 0) {
            p = strstr(p + strlen(t), u);
            if (p != 0)
                p += strlen(u);
        }
        return p;
    }
  • これはどうやって使うのかというと、例えばHTMLのテキストを読み込んだうえで、
    p = HTMLの先頭;
    for (;;) {
        p = strstr2(p, "<a href=", ">");
        if (p == 0) break;
        q = strstr(p, "</a>");
        if (q == 0) break;
        printf("%.*s\n", q - p, p);
    }
  • とやるだけで、リンクしている文言を全部取り出せるわけです。
  • まあ本当にくだらない関数なんですが、でもこれが結構便利だなーと思ったのでした。

次回に続く

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2019-03-22 (金) 21:12:02 (241d)