C Preprocessorを侵略するゲソ!

この記事は Functional Ikamusume Advent Calendar jp 2010 http://partake.in/events/5784afd8-d43b-4cbe-9256-430d5ababa2b のために書かれたでゲソ!

今日はおまえたちにC Preprocessorを使ったプログラミングを紹介するでゲソ!
私はC Preprocessorは初心者なので、簡単な機能だけしか使えないでゲソ!

手始めにフィボナッチ数を実装してみようじゃなイカ!
イカOCamlでの実装を示すでゲソ。

let rec fib_iter a b c = 
  if c <= 0
  then
    a
  else
    fib_iter b (a+b) (c-1)
in
let fib d = 
  fib_iter 1 1 d
in
let rec iter e f =
  if e <= f
  then begin
    print_int (fib e);
    print_newline ();
    iter (e+1) f
  end else
    ()
in
iter 0 10

実行結果はこんな感じになるでゲソ。

1
1
2
3
5
8
13
21
34
55
89
- : unit = ()

実装しなイカ?

これをC Preprocessorで実装するでゲソ。

ただ、直打ちだとすごく面倒になるので、メタプログラミングするでゲソ。
もっと早く思いついていればメタプログラミングの会で話せたんじゃなイカ?
サンプルコードは http://xhl.kitsunemimi.org/ika1219.zip にあるでゲソ。

1つの関数は1つのファイルで定義するでゲソ。ファイル名は 関数名.ika にするでゲソ。
まず、fib_iter の実装を示すでゲソ。

/* file: fib_iter.ika */
#include "ika.h"

ARGS(A,B,C)

IF( C <= 0 )
THEN
  RETURN( A )
ELSE
  CALL(Z1, fib_iter, B, A+B, C-1)
  RETURN( Z1 )
ENDIF

順番に解説していくでゲソ!

/* file: fib_iter.ika */
#include "ika.h"

ika.h はメタプログラミング用のヘッダでゲソ。詳細は後で話すでゲソ。

ARGS(A,B,C)

ファイルの頭で、関数の引数宣言を行うでゲソ。
この場合は、3つの引数 A, B, C を取るという宣言でゲソ。
今の所全ての変数・引数などの型は整数でゲソ。

IF( C <= 0 )
THEN
  RETURN( A )
ELSE
  CALL(Z1, fib_iter, B, A+B, C-1)
  RETURN( Z1 )
ENDIF

IF, THEN, ELSE, ENDIF を使って条件分岐ができるでゲソ。
返す値の指定はRETURNを使うでゲソ。

  CALL(Z1, fib_iter, B, A+B, C-1)

関数呼び出しでゲソ。
fib_iter が関数名、それ以降が引数、Z1が返値を格納する変数でゲソ。

OCamlソースコードと対応しているじゃなイカ!

この調子で fib と iter も実装するでゲソ。

/* file: fib.ika */
#include "ika.h"

ARGS(D)
CALL(Z2,fib_iter,1,1,D)
RETURN(Z2)
/* file: iter.ika */
#include "ika.h"

ARGS(E,F)

IF(E <= F)
THEN
  CALL(Z3,fib,E)
  printf( "%d\n", Z3 );
  CALL(Z4,iter,E+1,F)
ENDIF

トップレベルはとりあえずmain関数を定義しておくでゲソ。

/* file: main.ika */
#include "ika.h"

INCLUDE <stdio.h>

int main()
{
  CALL(Z4,iter,0,10)
  return 0;
}

これで実装完了でゲソ!

注意点としては、自力でα変換する必要があるでゲソ。
つまり、各ファイルで変数名がかぶらないようにする必要があるでゲソ。
もう一段メタプログラミングすれば解消できると思うでゲソが、そこまではやらなかったでゲソ。

あと、再帰は末尾再帰しかできないでゲソ。

コンパイルしなイカ?

実装ができたら cpp (C Preprocessor)で処理するでゲソ。
hoge.ika を cpp に通して hoge.ika.c に変換するでゲソ。

$ for file in *.ika; do cpp -P $file > $file.c; done
  • P は行番号情報を出力しない gcc cpp のオプションでゲソ。なくても動くでゲソ。

hoge.ika.c が、メタプログラミングをしない場合の hoge の C Preprocessor での実装でゲソ。

トップレベルの関数の.ika.cをもう一度gccコンパイルすると実行できるようになるでゲソ。

$ gcc -o main main.ika.c
$ ./main
1
1
2
3
5
8
13
21
34
55
89

できたじゃなイカ!

ちなみに、C Preprocessorを通すとこんな感じの出力が出るでゲソ。
stdio.h に対応する部分と大量に出る空行、pragmaは省略したでゲソ。

$ cpp main.ika.c
int main()
{
  printf( "%d\n", ( (1<<0)+ 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( 0 + (1<<1)+ 0 + 0 + 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ (1<<1)+ 0 + 0 + 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ 0 + (1<<2)+ 0 + 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( 0 + 0 + 0 + (1<<3)+ 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ 0 + (1<<2)+ (1<<3)+ 0 + 0 + 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ 0 + (1<<2)+ 0 + (1<<4)+ 0 + 0 + 0 + 0) );
  printf( "%d\n", ( 0 + (1<<1)+ 0 + 0 + 0 + (1<<5)+ 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ (1<<1)+ (1<<2)+ 0 + (1<<4)+ (1<<5)+ 0 + 0 + 0) );
  printf( "%d\n", ( (1<<0)+ 0 + 0 + (1<<3)+ (1<<4)+ 0 + (1<<6)+ 0 + 0) );
  return 0;
}

CPP時に値が計算されていることが分かるんじゃなイカ?

解説しなイカ?

要するに、条件分岐を #if / #else / #endif で、変数束縛を #define で、関数呼び出しを #include で実装しているでゲソ。
つまり、こんな感じじゃなイカ?

/* file: fib_iter.ika.c */
                          /*  let rec fib_iter a b c =   */
#if A <= 0                /*    if c <= 0 then           */
#define RETURN_VALUE A    /*      a                      */
#else                     /*    else                     */
#define A B               /*      (* set 1st argument *) */
#define B A+B             /*      (* set 2nd argument *) */
#define C C-1             /*      (* set 3rd argument *) */
#include "fib_iter.ika.c" /*      fib_iter b (a+b) (c-1) */
#endif                  

ただ、このままでは動かないでゲソ。
これが動かないのは当然でゲソ。
#define A した時点で古いAが上書きされてしまうからでゲソ。
たとえば、

#define B 1
#define A B
#define B A+B
B

と書いても、B → A+B → B+B と置換されるだけで終わってしまうでゲソ。
#defineの右辺の式は#define時点では評価されず、実際に展開された時にはじめて評価されるでゲソ。

ここで、各変数が8ビット変数であると仮定すると、イカのようなコードで expr のその時点での値に X を束縛できるでゲソ。

#if (expr) & 1
#define X0 1
#else
#define X0 0
#endif
#if (expr) & 2
#define X1 2
#else
#define X1 0
#endif
#if (expr) & 4
#define X2 4
#else
#define X2 0
#endif
#if (expr) & 8
#define X3 8
#else
#define X3 0
#endif
#if (expr) & 16
#define X4 16
#else
#define X4 0
#endif
#if (expr) & 32
#define X5 32
#else
#define X5 0
#endif
#if (expr) & 64
#define X6 64
#else
#define X6 0
#endif
#if (expr) & 128
#define X7 128
#else
#define X7 0
#endif
#undef X
#define X (X0+X1+X2+X3+X4+X5+X6+X7)

こうすれば、exprの中で使われた変数が#undefされたりされても、X0〜X7が生きていればXの値は有効でゲソ!
X0〜X7の変数名は必要に応じてα変換すればいいでゲソ。
これを X <=: expr と表現することにするでゲソ。
関数の頭とかで適当な別変数に引数の値をセットして、それ以外の場合は #define でできるでゲソ。

/* file: fib_iter.ika.c */
a <=: A
b <=: B
c <=: C
                          /*  let rec fib_iter a b c =   */
#if a <= 0                /*    if c <= 0 then           */
#define RETURN_VALUE a    /*      a                      */
#else                     /*    else                     */
#define A b               /*      (* set 1st argument *) */
#define B a+b             /*      (* set 2nd argument *) */
#define C c-1             /*      (* set 3rd argument *) */
#include "fib_iter.ika.c" /*      fib_iter b (a+b) (c-1) */
#endif                  

但し、これだけではまだ不十分でゲソ。
#includeを展開するとこんな風になるでゲソ。

/* file: fib_iter.ika.c */
a <=: A
b <=: B
c <=: C
                          /*  let rec fib_iter a b c =   */
#if a <= 0                /*    if c <= 0 then           */
#define RETURN_VALUE a    /*      a                      */
#else                     /*    else                     */
#define A b               /*      (* set 1st argument *) */
#define B a+b             /*      (* set 2nd argument *) */
#define C c-1             /*      (* set 3rd argument *) */
// #include "fib_iter.ika.c" /*      fib_iter b (a+b) (c-1) */
/* file: fib_iter.ika.c */
a <=: A
b <=: B
c <=: C

#define を展開すると

/* file: fib_iter.ika.c */
a <=: A
b <=: B
c <=: C
                          /*  let rec fib_iter a b c =   */
#if a <= 0                /*    if c <= 0 then           */
#define RETURN_VALUE a    /*      a                      */
#else                     /*    else                     */
#define A b               /*      (* set 1st argument *) */
#define B a+b             /*      (* set 2nd argument *) */
#define C c-1             /*      (* set 3rd argument *) */
// #include "fib_iter.ika.c" /*      fib_iter b (a+b) (c-1) */
/* file: fib_iter.ika.c */
a <=: b
b <=: a+b
c <=: c-1

c <=: c-1 が問題でゲソ。
これを展開すると

#if (c0+c1+c2+c3+c4+c5+c6+c7-1) & 1
#define c0 1
#else
#define c0 0
#endif
#if (c0+c1+c2+c3+c4+c5+c6+c7-1) & 2 /* まちがい! c0は既に新しい値で更新されてしまっている! */
#define c1 2
#else
#define c1 0
#endif
...

となるでゲソ。

/* file: fib_iter.ika.c */
a1 <=: A
b1 <=: B
c1 <=: C
a  <=: a1
b  <=: b1
c  <=: c1
                          /*  let rec fib_iter a b c =   */
#if a <= 0                /*    if c <= 0 then           */
#define RETURN_VALUE a    /*      a                      */
#else                     /*    else                     */
#define A b               /*      (* set 1st argument *) */
#define B a+b             /*      (* set 2nd argument *) */
#define C c-1             /*      (* set 3rd argument *) */
#include "fib_iter.ika.c" /*      fib_iter b (a+b) (c-1) */
#endif                  

こんな感じに二重にすると、問題なくなるでゲソ。

ただ、これはすごく面倒なので、メタプログラミングしたでゲソ。
ついでだからC Preprocessorでメタプログラミングしたでゲソ!