Web上での「文芸的プログラミング」

増井俊之 @ pitecan.com

文芸的プログラミングとは

文芸的プログラミング」とは、 スタンフォード大学のDonald Knuthが提唱している、 プログラムの作成とその文書化を同時に行なう手法で、 プログラムとその解説文書をひとつのファイルに混在させながら 追加したり修正したりして同時に開発していくことにより、 完全に整合性のとれた文書とプログラムを開発しようというものである。 プログラムをまず作ってからその解説文書を作成する(またはその逆)という 一般的な手法では、後で修正などを加える場合などは注意しないと 両者の整合性がとれなくなってしまうことがよくあるが、 文芸的プログラミングの手法では 両者がひとつのファイルになっているので そのようなことが起こりにくくなる。 また正しい文書を書くという作業と正しいプログラムを書くという作業の 相互作用によってより適切なプログラムと文書ができあがるという 効果がある。

WEBシステムは 文芸的プログラミングを支援するためにKnuthが開発したシステムである。 WEBシステムはもともとKnuthのTeXシステムを記述するために開発 されたものなのでPascalとTeXが対象になっているが、 Cが使えるようにしたCWEBシステム Cとtroffとの組み合わせが使えるcwebシステム、 任意のプログラミング言語に対応させることのできる Spiderといったシステムも開発されている。

WEBの処理

WEBの処理は図1のように行なわれる。


図1: WEBの処理

WEB文書は複数のプログラムモジュールの集合という形になっており、 各モジュールはその解説部分とプログラム部分とで構成される。 WEB文書をtangleというプログラムに通すと プログラム部だけが抽出されてひとつのPascalプログラムになり、 weaveというプログラムに通すと解説部とプログラム部の 両方が融合したTeXの文書になる。 ここではプログラム部は美しくフォーマットされ、 モジュール毎に章立てが行なわれ、 目次や索引も完備した完全なドキュメントが生成される。 このように、ひとつのWEBファイルからプログラムと そのドキュメントの両方を生成することができるのが特長である。

WEB on Web

整形されたWEB文書を見るためには、図1のように weaveによってTeX文書に変換してから TeXでフォーマットを行ない、dvi2psなどで 印刷形式にする必要ががある。この場合、 weave/TeX/dvi2ps/ghostscript を順に適用する必要ががあり非常にわずらわしいので、 作業を頻繁に行ないにくいという問題がある。

WEB文書のかわりに普通のHTMLファイルを使うようにすれば このような問題は解決すると考えられる。 プログラムをHTMLファイルの中に記述するという形式で 文芸的プログラミングを行なうことにすれば、 tangleに相当するプログラムは必要であるが、 weave関連プログラムはすべて省略して 直接HTMLファイルをブラウザで眺めることができる。 HTMLファイルに対して tangleに相当する処理を行なう wtangleプログラムを開発した。

wtangleプログラム

wtangleプログラムは、 HTMLファイルとして記述された、簡易文芸的プログラミング文書から プログラム部分だけを抽出するPerlプログラムである。 本HTML文書に対してwtangleを適用すると wtangleプログラムが得られるので、 コンパイラのようにブートストラップ式に開発していくことが できるはずである。 以下にwtangleプログラムの全ソースとその解説を記述する。

プログラム

prologue()でプログラムの初期化を行ない、 main()でHTMLファイルからの プログラム抽出処理を行ない、 epilogue()でファイル書き出しを行なう。

#!/usr/bin/env perl
#
# wtangle - Web Tangle
#
&prologue;
&main;
&epilogue;

初期化処理

prologue()では変数の初期化などを行なう。 %SIGに関数名を指定しておくと、 signalが発生したときに$SIG{シグナル名}の関数が呼ばれる。 ここではSIGINTなどを受け取った時に finish()が呼ばれるようにしている。
sub prologue {
  require 'cacheout.pl'; # 複数ファイル出力ライブラリ
  if($#ARGV < 0){
    print STDERR "wtangle --- Web Tangle\n";
    print STDERR "Usage: wtangle documentfiles\n";
    exit 0;
  }
  $SIG{'INT'} = $SIG{'TERM'} =
  $SIG{'QUIT'} = $SIG{'HUP'} = 'finish';
}

メインルーチン

HTMLファイルの中の <pre>, </pre> で囲まれた部分をファイルに書き出す。 ファイル名は <pre>の中で "file=ファイル名{,ファイル名...}"と記述する。 HTMLファイルが更新されたり場合でも プログラム自体は変更されていない場合があるので、 直接プログラムを書き出さずに tmpname()で計算される テンポラリファイルに一旦書き出した後で diff()でもとのファイルと 比較を行ない、異なっている場合のみファイルを更新するようにする。
sub main {
  while($file = shift(@ARGV)){
    unless(open(f,$file)){
      print STDERR "Can't open <$file>\n";
      &finish;
    }
    while(){
      if(m#^${lt}/pre>$#){
        @tmpfiles = ();
      }
      elsif(m#^${lt}pre\s*file=(.*)>$#){
        @files = split(/,/,$1);
        @tmpfiles = ();
        foreach $file (@files){
          $allfiles{$file} = 1;
          push(@tmpfiles,&tmpname($file));
        }
      }
      else {  # cachout()を使って現在行を各ファイルに書き出す
        foreach $tmpfile (@tmpfiles){
          &cacheout($tmpfile);
          $taghead = pack(c,60);
          s/&.{0}lt;/$taghead/g;
          print $tmpfile $_;
        }
      }
    }
    close(f);
  }
}

終了処理

実際に更新があったプログラムのみ書き出して実行可能ファイルとする。
sub epilogue {
  foreach $file (keys %allfiles){
    $tmpfile = &tmpname($file);
    close($tmpfile);
    if(! -e $file || &diff($tmpfile,$file)){
      push(@newfiles,$file);
      &move($tmpfile,$file);
      chmod 0555,$file; # 書込み禁止/実行可能にする
    }
  }
  sort @newfiles;
  $n = $#newfiles + 1;
  print STDERR 
    $n == 0 ? "No file is updated.\n" :
    $n == 1 ? "File '$newfiles[0]' is updated.\n" :
      "Files '".join("', '",@newfiles[0..$n-2]).
      "' and '".$newfiles[$n-1]."' are updated.\n";
  &finish;
}

サブルーチン

使用している小さなサブルーチンのリストを示す。 ここでは<table>タグを用いて表形式のリストにしている。 HTMLを用いているのでこのように表示形式を柔軟に選ぶことができる。

sub tmpname {
  local($_) = @_;
  s#/#_#g;
  "/tmp/stangle$_$$";
}
テンポラリに使用されるファイル名を計算する。
sub finish {
  foreach $file (keys %allfiles){
    $tmpfile = &tmpname($file);
#    unlink($tmpfile) if -e $tmpfile;
  }
  exit;
}
テンポラリに使用されたファイルを全て削除してから プログラムを終了する。
sub move {
  local($from,$to) = @_;
  return if $from eq $to;
  system "/bin/mv -f $from $to";
  if($?){  # mv実行エラー
    print STDERR "Can't move $from to $to\n";
    &finish;
  }
}
ファイル移動を行なう。 簡単のためmvコマンドを使用している。
sub diff {
  local($file1,$file2) = @_;
  system "/usr/bin/diff $file1 $file2 > /dev/null";
  $? >> 8; # コマンドステータス
}
ファイル比較を行なう。 簡単のためdiffコマンドを使用している。

感想など

実際のところ、 プログラムを書くたびにWEB文書やHTMLを真面目に記述するのは とても面倒なので、 普通のプログラム開発に文芸的プログラミングの手法を適用 するのはちょっと無理があるように思う。 しかしオープンソースとしてプログラミングを公開しようという場合や、 プログラミングの解説をしたり教科書を書いたりするような場合には この方式は便利なのではないかと思っている。 真面目に図や解説を書くのは面倒かもしれないが、 プログラムに手書きのGIFを添付するだけでも 後で思い出したりするのに便利だろうし、 説明や関連情報へのリンクを添付するには便利であろう。 人に見せる機会があるプログラム/ 将来再利用する可能性があるプログラム/ 複雑なアルゴリズムをつかったプログラム などはなるべくこの手法で整備してみようかと思っている。

更新履歴

  • 2001ごろ作成
  • 最終更新: $Date: 2006-12-18 13:08:50 -0800 (Mon, 18 Dec 2006) $