時代遅れひとりFizzBuzz祭り、今回はCOmmon Business Oriented Language、汎商業目的言語だ。略してCOBOL。金勘定や帳簿絡みの処理にマッチしたその姿は、ともすると「ビルド専用」といった趣のmake*1と相通ずるものがあると思う。DSL的ってことさ。
ついでにmakeもCOBOLも文法や何やらでぶつくさ文句を言いわれつも決定的な代替品がなかなか無くて使い続けられてる、といった面があると思う。
makeは色々と代替のビルドツールがあるし分野によってはmake以外のツールが一般的だったりする*2が、しかしUnix系OSの世界でCやC++でコードを書くと大抵ビルドツールはmakeから始まる*3気がする。一方のCOBOLについては、門外漢なこともあってか代替品の存在を聞いたことがない。いや「今までCOBOLで書いていたものを別の言語で……」というパターンは聞いたことがあるのだけど、大抵は元々COBOLにマッチしないものをCOBOLで書いていたとかその逆にCOBOLに最適な部分まで他言語に置き換えてエライ目に遭ったとかいう裏話があるように思う。帳票を右から左に流すような、COBOLが得意とする土俵においてのCOBOLの代替品は聞いたことがない。
門外漢と書いたように、COBOLとは全くもって縁がない。皆無だ。というか組み込み開発では間違ってもCOBOLという選択肢は無い。分野が異なるのだから当然だ。
もっともプログラマになる前はCOBOLと縁があった。在庫管理・出荷指示のシステムがCOBOLで構築されていたのだ。実際の所、日本の過半数はCOBOLに支配されているのかもしれない :)
COBOL支配説の証拠に、何故か手元には『プログラミング入門COBOL―情報処理技術者テキスト』がある*4。COBOLとは縁がないし、学んだこともないのに、こんな所にまで入り込んでいる。恐ろしい話だ。
ともかく、この本やネット上の情報を元に、COBOL未経験者なりに書いてみた。使用した処理系はOpenCOBOL 1.0(MinGWバイナリ)で、下回りに使っているMinGWは5.0.3(GCC 3.4.2)。
最初のバージョン。COBOL 2002では小文字の使用もフリーフォーマットのコーディングも規格的に可能となったらしいが、折角なので大文字で正書法で書いてみた。多分COBOL 85相当だと思う。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. *AUTHOR. EEL3. *DATE-WRITTEN. 2010-06-29. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 DISPLAY "FizzBuzz" WHEN 0 ALSO ANY DISPLAY "Fizz" WHEN ANY ALSO 0 DISPLAY "Buzz" WHEN OTHER DISPLAY CNT END-EVALUATE END-PERFORM. STOP RUN.
C言語メインの人としては、剰余演算を単独で実行できない(のでDIVIDEで除算する時にREMAINDERで求める)とか、IF - ELSEにelse if的なものがなくて多岐分岐できない(のでELSE節でIF - ELSEの入れ子にするか、EVALUATEを使う)とか、驚きの連続だった。あとコードの長さも。
EVALUATEでALSOを使う書き方は、何となくOCamlのパターンマッチングを思い起こす点があるように感じられて、興味深い。
OpenCOBOLでオプション-Wallを付けてコンパイルした所、AUTHORとDATE-WRITTENについてobsoleteと警告されてしまったので、コメントアウトしている。Micro Focus Server Express 4.0J SP2の言語リファレンスの「5.1 見出し部」によると、AUTHORとDATE-WRITTENはCOBOL 85の時点で廃要素となっていたようだ。確かに「-std=cobol85」でコンパイルすると警告が出るし、「-std=cobol2002」だとエラーになってコンパイルできない。ということは現行のCOBOL 2002では廃止されたのだろう。
ちなみにこのコードをフリーフォーマットで且つ大文字小文字を混ぜ合わせて書くと、こんな感じだろうか。
identification division. program-id. FIZZBUZZ. environment division. configuration section. source-computer. PC. object-computer. PC. data division. working-storage section. 01 CNT pic 9(3). 01 DIV pic 9(2). 01 REM3 pic 9(1). 01 REM5 pic 9(1). procedure division. MAIN. perform varying CNT from 1 by 1 until CNT > 100 divide 3 into CNT giving DIV remainder REM3 divide 5 into CNT giving DIV remainder REM5 evaluate REM3 also REM5 when 0 also 0 display "FizzBuzz" when 0 also any display "Fizz" when any also 0 display "Buzz" when other display CNT end-evaluate end-perform. stop run.
インデントの付け方は適当。とりあえず予約語を小文字に、利用者語を大文字にしてみた。従来の書き方のソースコードとはまた違った趣のある見た目だ。何となくDSLないし独自形式の設定ファイルにありそうに思う。
惜しいことに正書法で書いたほうのコードはYCOBOL 0.25では(マニュアルを見る限り)通らないようなので、YCOBOLでも動くように手を入れてみた。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 DIV PIC 9(2). 01 REM PIC 9(2). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 COMPUTE DIV = CNT / 15 COMPUTE REM = CNT - (DIV * 15) EVALUATE REM WHEN 3 DISPLAY "Fizz" WHEN 5 DISPLAY "Buzz" WHEN 6 DISPLAY "Fizz" WHEN 9 DISPLAY "Fizz" WHEN 10 DISPLAY "Buzz" WHEN 12 DISPLAY "Fizz" WHEN 0 DISPLAY "FizzBuzz" WHEN OTHER DISPLAY CNT END-EVALUATE END-PERFORM. STOP RUN.
こうすると、YCOBOLでも動作する。
さて、ここまでのコードでは数値を表示するときに足りない桁を0でパディングした状態で表示されるので、ゼロサプレスして左詰めで表示したい。最初に思いついたのは次の方法。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). 01 PVAL PIC X(8). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 MOVE "FizzBuzz" TO PVAL WHEN 0 ALSO ANY MOVE "Fizz" TO PVAL WHEN ANY ALSO 0 MOVE "Buzz" TO PVAL WHEN OTHER MOVE CNT TO PVAL END-EVALUATE DISPLAY PVAL END-PERFORM. STOP RUN.
数字項目から英数字項目に転記する際に英数字項目の方の桁が大きい場合、左詰めで転記される。なので(何を誤解したのか)単純に転記したらゼロサプレスされて左詰めになるかもしれないと思って試してみたが、駄目だった。
ここでゼロサプレス用の`Z'の存在に気がついたので、今度はDISPLAYで表示する前に`Z'を使った数字項目に転記してから表示するようにしてみた。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 PCNT PIC Z(2)9. 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 DISPLAY "FizzBuzz" WHEN 0 ALSO ANY DISPLAY "Fizz" WHEN ANY ALSO 0 DISPLAY "Buzz" WHEN OTHER MOVE CNT TO PCNT DISPLAY PCNT END-EVALUATE END-PERFORM. STOP RUN.
最初、CNT自体を`Z'を使って定義したら正しく動かなくて悩んだのは秘密。
この方法だと確かにゼロサプレスされるのだが、3桁で右詰めされてしまう。まあ確かに0を空白文字に置き換えているだけなので、挙動としては正しいのだが……。
最後の足掻きで、ゼロサプレスしてから英数字項目に転記してみた。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 PCNT PIC Z(2)9. 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). 01 PVAL PIC X(8). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 MOVE "FizzBuzz" TO PVAL WHEN 0 ALSO ANY MOVE "Fizz" TO PVAL WHEN ANY ALSO 0 MOVE "Buzz" TO PVAL WHEN OTHER MOVE CNT TO PCNT MOVE PCNT TO PVAL END-EVALUATE DISPLAY PVAL END-PERFORM. STOP RUN.
空白文字を取り除いて(結果として左詰めになって)くれないかとダメ元で試してみたが、やっぱり駄目だった。
どうにも簡単に左詰めにできる方法が発見できなかったので、大人しくゼロサプレスもせずに表示させることにした。多分、何とかする方法があるのだと思うが、そこまで深入りする気力がない。
元に戻って、今度は最初に書いたソースをベースに、処理を分割してみることにした。何となくこのソースでは不要な気もするが、ものは試しだ。
COBOL 2002だと関数を定義できるようだが、やり方がイマイチ分からなかったし、何よりOpenCOBOL 1.0にはまだ実装されていないようだ。そこで伝統的だと思われる方法で書いてみた。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). * PROCEDURE DIVISION. MAIN. PERFORM VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 PERFORM FIZZBUZZ END-PERFORM. STOP RUN. * FIZZBUZZ. DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 DISPLAY "FizzBuzz" WHEN 0 ALSO ANY DISPLAY "Fizz" WHEN ANY ALSO 0 DISPLAY "Buzz" WHEN OTHER DISPLAY CNT END-EVALUATE.
最初のバージョン。PERFORMの中身を単純に外に追い出してみた。一応動くのだが、この書き方の正当性について全く自信を持てない。理解があやふやなままであることの証拠だ。
PERFORMの中でPERFORMを呼んでいるが、これは1つに纏められる。
IDENTIFICATION DIVISION. PROGRAM-ID. FIZZBUZZ. * ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. PC. OBJECT-COMPUTER. PC. * DATA DIVISION. WORKING-STORAGE SECTION. 01 CNT PIC 9(3). 01 DIV PIC 9(2). 01 REM3 PIC 9(1). 01 REM5 PIC 9(1). * PROCEDURE DIVISION. MAIN. PERFORM FIZZBUZZ VARYING CNT FROM 1 BY 1 UNTIL CNT > 100 STOP RUN. * FIZZBUZZ. DIVIDE 3 INTO CNT GIVING DIV REMAINDER REM3 DIVIDE 5 INTO CNT GIVING DIV REMAINDER REM5 EVALUATE REM3 ALSO REM5 WHEN 0 ALSO 0 DISPLAY "FizzBuzz" WHEN 0 ALSO ANY DISPLAY "Fizz" WHEN ANY ALSO 0 DISPLAY "Buzz" WHEN OTHER DISPLAY CNT END-EVALUATE.
しかしここまでくると、処理を分けていない最初のコードの方が分かりやすい気がする。
COBOLがDSLっぽいという意見がネット上に(それほど多くはないと思うが)見られたりするが、実はここまでのソースの範囲でも個人的に納得できるところがある。
例えば数値をゼロサプレスして表示したら右詰めになったところ。その昔、伝票を扱う業務システムを触っていたので分かるのだが、伝票の金額欄にドットインパクトプリンタで印字する時に右詰めになってくれないと困るのだ。左詰めだと桁位置が変わってしまう。もっともDISPLAY文はプリンタに出力する訳ではないし、数値が右詰め表示される挙動自体が標準的なものなのかどうか知らない(ので間違った認識をしているかもしれない)のだが。
まあ元々COBOLは事務処理用のプログラミング言語を統一しようという所から誕生した言語で、且つ1950〜1960年代に「コンピュータで事務処理」といえば会計/給与計算/在庫管理/伝票処理といった金勘定や帳簿絡みの基幹業務だった訳だ。その手の処理に向いているのは確かだろう。