時代遅れひとりFizzBuzz祭り COBOL編

時代遅れひとり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情報処理技術者テキスト』がある*4COBOLとは縁がないし、学んだこともないのに、こんな所にまで入り込んでいる。恐ろしい話だ。

ともかく、この本やネット上の情報を元に、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.

しかしここまでくると、処理を分けていない最初のコードの方が分かりやすい気がする。

COBOLDSLっぽいという意見がネット上に(それほど多くはないと思うが)見られたりするが、実はここまでのソースの範囲でも個人的に納得できるところがある。

例えば数値をゼロサプレスして表示したら右詰めになったところ。その昔、伝票を扱う業務システムを触っていたので分かるのだが、伝票の金額欄にドットインパクトプリンタで印字する時に右詰めになってくれないと困るのだ。左詰めだと桁位置が変わってしまう。もっともDISPLAY文はプリンタに出力する訳ではないし、数値が右詰め表示される挙動自体が標準的なものなのかどうか知らない(ので間違った認識をしているかもしれない)のだが。

まあ元々COBOLは事務処理用のプログラミング言語を統一しようという所から誕生した言語で、且つ1950〜1960年代に「コンピュータで事務処理」といえば会計/給与計算/在庫管理/伝票処理といった金勘定や帳簿絡みの基幹業務だった訳だ。その手の処理に向いているのは確かだろう。

*1:前回はmakeでFizzBuzzした。

*2:例えばVisual StudioXcodeを使っていると基本的にMakefileを書く必要がない。

*3:最初はmakeで始まり、そのまま終わるものもあれば大規模化や移植性云々でAutotools等の別のツールに移行するものもある、というパターン。まあ最初から規模が大きいことが分かっているならmake以外のツールを採用するのだろうけど。

*4:ちなみに基本情報技術者(旧試験)を受験した時はCASL IIとJavaの2本立てで挑んだ。