C言語で無限リストを実装して、100万の素数リストを作ってみた

リスト構造について

カーニハン著「プログラミング作法」で、C言語によるリスト構造の実装がありました。他のテキストやサイトに載っているやり方なのですが、副作用があります。そこで、ちょっと改善したコードを思いついたので試してみました。
無限リストっぽいこともできるはずです。

コードの説明

まずは、インクルードで標準ライブラリを読み込んでおきます。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

assert.hとtime.hはチェックのために使っています。また、stdbool.hはC11(やC99)対応のコンパイラが必要となります。
続けて、構造体を作っておきます。ここでは、順序対を参考にしながら実装しています。詳しくは後出のpair関数で述べることとしましょう。

typedef struct OrderedPair Ord_t;

/*OrderdPair 構造体
 * 順序対を作るための構造体
 * ただし、属性fだけは操作として、next関数で使われる*/
struct OrderedPair {
    int32_t first;     /*対となる一番目*/
    Ord_t *second;     /*対となる二番目*/
    int32_t lifepoint; /*参照カウンタ*/

    int32_t (*f)(Ord_t*); /*無限リストの新しい項目を作るための関数*/
};

普通は空リストを作るのに、NULLを使うのですが、今回は新しく構造体のEMPTYを作ります。

int32_t dummy (Ord_t*);

/*定数EMPTY
 * 空リストを表現する構造体*/
const Ord_t EMPTY = {
    0,
    NULL,
    0,
    dummy
};

こうしておけば、バグを増やす要因となるNULLを排除できます。

関数の実装

関数のプロトタイプ宣言は以下の通りです。

bool isEmpty (Ord_t*);

int32_t first (Ord_t*);

Ord_t *second (Ord_t*);

void eprintf (char*, ...);

void *emalloc (size_t);

void freelist (Ord_t*);

Ord_t *pair (int32_t, Ord_t*);

Ord_t *next (Ord_t*);

typedef bool(*Ord2bool_t)(Ord_t*);

typedef Ord_t *(*Ord2Ord_t)(Ord_t*); /*Ord2Ord_t = Ord_t* -> Ord_t**/

Ord_t *setChurchNumber (int32_t, Ord2Ord_t, Ord_t*);

int32_t getPrimeNumber (Ord_t*);

bool isPrimeNumber (int32_t);

リストのインタフェースとして、isEmptyとpairとfirstとsecondがあれば、理屈の上では十分です。
ただし、C言語では、そのほかにも動的にメモリを管理する必要があるので、eprintf関数とemalloc関数と、freelist関数を実装しておきます。
isEmpty関数は、引数にEMPTYのアドレスが渡された時、つまり、isEmpty(&EMPTY)のときだけ、trueを返します。

/*isEmpty 関数
 * 引数のlistがEMPTYかどうかをチェックするときに使う。EMPTYならばtrue( = 1)
 * NULLは不具合が起きるので、入力すればエラー*/
bool isEmpty (Ord_t *list) {
    if (list == NULL) {
        eprintf("NULL argument error on the function isEmpty");
        return false;
    } else if (list == &EMPTY) {
        return true;
    } else {
        return false;
    }
}

pair関数に必要なfirst関数やいろいろな関数を実装していきます。今は、詳しく説明しないでおきましょう。

/*first 関数
 * 順序対の一番目を返す
 * pair関数で指定された第一引数(例えば、pair(a,b)ならばa)を返す)*/
int32_t first (Ord_t *list) {
    if (isEmpty(list)) {
        return 0;
    } else {
        return list->first;
    }
}

/*second 関数
 * 順序対の二番目を返す
 * pair関数で指定された第二引数(例えば、pair(a,b)ならばb)を返す)*/
Ord_t *second (Ord_t *list) {
    if ( isEmpty(list) || (list->second == NULL) ){
        /*ペアの二番目を取得できない場合は、自分自身を返す*/
        return list;
    } else {
        return list->second;
    }
}

/*eprintf 関数
 * プログラムを中止させるためのエラー処理を担当。入力値はエラーを報告する文章*/
void eprintf (char *fmt, ...) {
    va_list args;
    fflush(stdout);
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    fprintf(stderr, "\n");
    exit(EXIT_FAILURE);
}

void *emalloc(size_t n) {
    void *p;
    p = malloc(n);
    if (p == NULL) {
        eprintf("%u bytes failed on the function emalloc", n);
    }
    return p;
}

/*freelist関数
 * 引数のlistを解放させる
 * 他から参照されていない限り、EMPTY以外のリストすべてが対象*/
void freelist (Ord_t *list) {
    Ord_t *next;
    for (;!isEmpty(list) || (list->lifepoint >= 1);list = next) {
        next = second(list);
        if (list == next) {
            break;
        } else {
            free(list);
            list = NULL;
        }
        if (!isEmpty(next)) {
            /*すでに死んだため参照カウンタを減らす*/
            next->lifepoint--;
        }
    }
}

さて、pair関数でリストを作っていきます。まずは、pair関数のコードを見てください。

/*pair関数
 * 第一引数と第二引数とで順序対を作る*/
Ord_t *pair (int32_t n, Ord_t *p) {
    Ord_t *s;
    s = (Ord_t *) emalloc(sizeof(Ord_t));
    s->first = n;
    s->second = p;
    s->lifepoint = 0;
    if (!isEmpty(p)) {
        p->lifepoint++;
    }
    return s;
}

pair関数の引数に、二つの値を入力すると、二つを組とするデータ構造を返します。これらをつなげていくと、連結リストとなります。
使い方としては、前出のfirst関数とsecond関数を使います。

   Ord_t *p = pair(1, &EMPTY);
   first(p);  // 1を返す
   second(p); // &EMPTYを返す
   freelist(p);

freelist関数の解放忘れに注意してください。通常のリスト構造もこんな感じで使えます。

    Ord_t *p = pair(3,
                   pair(2,
                     pair(1, &EMPTY)
                   ) 
                 );
    first(p); // 3を返す
    first(second(p)) // 2を返す
    first(second(second(p))) // 1を返す
    freelist(p);

それで、次のnext関数とsetChurchNumber関数を使うと、無限リストのようなこともできます。

/*next 関数
 * 無限リストの次の項目を生成して返す*/
Ord_t *next (Ord_t *list) {
    int32_t n = list->f(list);
    Ord_t *s = pair(n, list);
    s->f = list->f;
    return s;
}


/*setChurchNumber 関数
 * C言語でラムダ計算のチャーチ数を部分的に再現するための関数*/
Ord_t *setChurchNumber (int32_t length, Ord2Ord_t f, Ord_t *list) {
    int32_t i;
    for (i = 0; i < length; i++) {
        /*lengthが4の場合、list = f(f(f(f(list))));*/
        list = (*f)(list);
    }
    return list;
}


int32_t dummy (Ord_t *p) {
    return first(p) + 2;
}

OrderedPair構造のf属性に、dummy関数を入れると、初期値が2の場合は偶数リストを作ることができます。

    Ord_t *s = pair(2, &EMPTY);
    s->f = dummy;
    s = next(s);
    first(s); // == 4
    s = next(s);
    first(s); // == 6
    freelist(s);

pair(2, &EMPTY)をpair(1, &EMPTY)に書き換えると、奇数リストとなります。今度はこれを応用して素数リストを作ってみます。

素数リストの実装

素数を得るためのgetPrimeNumber関数と、素数かどうかをチェックするisPrimeNumber関数を実装します。このうち、getPrimeNumber関数をさっきのdummy関数と同じようにします。

/*getPrimeNumber 関数
 * 素数リストであるlistから次の素数の算出する*/
int32_t getPrimeNumber (Ord_t *list) {
    int32_t i = first(list);
    if (i == 2) {
        return 3;
    }
    if ( (i % 2) == 0) {
        i++;
    }
    /*iは2以外の素数と考えると、奇数でもある。そこで、偶数を避けるために2ずつ足していけばよい*/
    for (i+=2;i

s->fにgetPrimeNumber関数を入れてみましょう。素数リストとなっているはずです。

    Ord_t *s = pair(2, &EMPTY);
    s->f = getPrimeNumber;
    first(next(s)); // == 3
    first(next(next(s)); // == 5
    first(next(next(next(next(s))))) // == 11
    freelist(s);

メモリやデータ型の制約に引っかからない限りは、無限にリストを作っていけます。
それでは、今度は、setChurchNumber関数を使って、百万の素数リストを作っていきます。

   Ord_t *s = pair(2, &EMPTY);
   s->f = getPrimeNumber;
   s = setChurchNumber(1000000, next, s);
   printf("%d\n", first(s));
   freelist(s);

これをgcc5.1.0 (TDM-GCC-64)で実行すると、約十三秒後に、

15485867

がコンソールに出力されました。isPrimeNumber関数でチェックすると、素数なのですが、念のため、検索エンジンでこの数値を調べてみますと、http://www.bigprimes.net/cruncher/15485867/ では、「It is the 1000001st prime number」となっています。
ということは、正確には、「百万飛んで一個の素数リスト」です。

追記(2017年5月9日)

確認した動作環境は、gcc (tdm64-1) 5.1.0です。TDM GCCでオプションに-std=c11を使っています