Fortran
ModernFortran
FORTRAN77

Fortranで行番号を排除する方法

背景

Fortran 90以前の古代FORTRAN,代表的にはFORTRAN77では,主に固定形式と呼ばれる形式でプログラムが書かれてきました.7カラム目からプログラムの文を書き,6カラム目には行の継続などの情報が記されます.1から5カラム目には行番号を書きます.

C234567890
      PROGRAM MAIN
      ISUM=0
      DO 100 I=1,100
        ISUM = 
     &    ISUM + I
100   CONTINUE
      END

行番号の役割は,主に処理の飛び先となることです.GOTOの飛び先であったり,DO文(繰り返し)の終端であったりといくつかの使われ方をしますが,基本的には処理が移動するときの目的地を表します.
本記事では,行番号の使われ方を示し,それを現代Fortran,すなわちFortran 90以降の機能で置き換える方法を示します.

なぜ行番号は排除されなければならないか

理由は簡単です.

  • 行番号は完全に任意で,統一された番号付けのルールが存在しない
  • みにくい

我々が見てきた限り,行番号は100刻みで増えていく傾向がありますが,いきなり10刻みで増えたり,処理を追加するとその時点で番号の並びが代わったりするので,思考の妨げになり,処理の追加・削除の際に無駄に頭を働かせることになります.

      DO 100 I=1,10
100   CONTINUE

      DO 200 I=1,100
200   CONTINUE

DO 100のループとDO 200の間に新たに繰り返しを挿入しなければならないとき,行番号として何番を用いるのが適切でしょうか?DO 200のループを多重ループに変更するときはどうでしょうか?

「みにくい」には,番号付けのルールが存在していない事に関連してソースの可読性が悪くなるという意味と,ソースが醜悪になるという二つの意味があります.現代のプログラミング言語では,インデントでスコープを表現します.{}等でスコープを指定しているにも関わらずインデントを行うのは,人間にとっての可読性を上げるためです.怠惰を善とするプログラマが必ずインデントを行うのは,インデントを行わない方が後々苦労することを知っているからです.一方で行番号は1カラム目から始まるため,インデントでソースを整形したとしても,容易にそれをご破算にします.そのような記述が}と同じ頻度で出てくるのは,悪夢としか言いようがありません.

行番号の使われる場面

FORTRANで行番号が使われる場面は,4通りあります.

  1. DO文の終端
  2. GOTOの飛び先
  3. 出力の書式指定
  4. ファイル読み取り時の制御

DO文の終端

これは先ほどから何回も出てきていますが,DO文によって繰り返す範囲の終端を表します.

C DO-CONTINUE
      DO 10 I=1,10
          WRITE(*,*) I
10    CONTINUE

CONTINUEは何もしません.C言語系のcontinueに相当するのはcycleです.

GOTOの飛び先(多重ループからの脱出)

goto文の飛び先を指定します.C言語系のgotoにおけるラベルと同じ役割ですが,行番号だけではだめで,行番号と併せて何らかの処理を書かなければなりません.
goto使用の功罪については広く議論されているのでここで改めて議論はしませんが,使用目的として認知されている多重ループからの脱出を対象にします.下の例文は特に意味のある処理ではありませんが,多重ループの例として考えてください.(数値計算の分野でよく出てくる反復法を模擬しているつもりです)

C GOTO
      E = 1D0
      DO 20 I=1,2
      DO 20 II=1,2
      DO 20 III=1,2
          E = E/DBLE(III)
          WRITE(*,*) E
          IF(E < 1D-9) GOTO 20
20    CONTINUE

なお,E=1D0としているところは,暗黙の型宣言を利用してEという変数を割り付け,倍精度の1.0を代入したつもりですが,Eは単精度変数です.暗黙の型宣言は右辺から型を決めることができません.

出力の書式指定

Fortranでは書式付きの出力として,write文とprint文を利用できます.write文の場合は出力先の装置番号とその書式,print文の場合には書式だけを指定します.FORTRANでは,書式の指定にはFORMAT文を使い,そのFORMAT文が書かれた行番号を指定します.

C FORMAT
      X=10D0
      Y=20D0
      WRITE(*,100) X,Y
100   FORMAT(E10.1,E10.1)

次のように出力されます.

   0.1E+02   0.2E+02

このようにすると,複数の場所から同じFORMAT文を利用し,出力書式を統一できるとされています.

ファイル読み取り時の制御

ファイルに書かれている行数が分からない場合に,ファイルが終端に達した時点でファイルからの読み取りを終了するために利用されます.
下記プログラムでは,1行に一つの実数が書かれたdata.txtというファイルをオープンし,read文を利用して一つずつ読み取ります.ポイントは,read文にあるEND=で,ファイルが終端に達した場合はここで指定した行番号に移動します.

C READ
      REAL*8 ERROR
      DIMENSION ERROR(60)
      OPEN(UNIT=5,FILE="DATA.TXT")
      DO 200 I=1,10000
          READ(5,*,END=210) E
          ERROR(I) = E
200   CONTINUE
210   WRITE(*,*) ERROR
      CLOSE(5)

data.txtにあるデータの数が分からないという前提ですが,FORTRANでは可変長の配列を取れないので,60個と決め打ちしています.

行番号の排除

それでは現代Fortranの機能を利用して行番号を排除しましょう.プログラムは下記の環境で作成・テストしています.

  • Windows 7 64bit
  • Microsoft Visual Studio Community 2015 Ver. 14.0.25431.01 Update 3
  • インテル(R) Parallel Studio XE 2017 Update 4 Composer Edition for Fortran Windows

Intel Fortranのみを利用しており,gfortranやPGI Fortranでの可搬性チェックは行っていませんが,おそらく問題ないと思います.エラーが出るとすると,本記事で着目している機能とは異なる箇所になると予想しています.

DO文の終端

do文の終端にはend doを使ってください

    integer :: i
    do i=1,10
        print *,i
    end do

GOTOの飛び先(多重ループからの脱出)

多重ループからの脱出に限れば,do文にラベルを付けて,ラベルを指定して脱出することができます

    real(8) :: error=1d0
    integer :: i,j,k
    Convergence : do while(error > 1d-9)
        do k=1,2
            do j=1,2
                do i=1,2
                    error = error / dble(i)
                    print *,error
                    if(error < 1d-9) exit Convergence
                end do
            end do
        end do
    end do Convergence

do while文の前に付いているConvergencedo文のラベルです.do文から脱出する命令exitと併せて利用することで,多重ループ内のどこからでも脱出できます.
gotoの他の使用方法であるエラー時の処理の遷移などは,プログラム設計の問題ですのでここでは言及しません.

出力の書式指定

FORMAT文の代わりに書式を代入した文字型変数が利用できます.FORMAT文で指定する書式と同じ内容を文字型変数に代入し,それを書式として利用します.

    character(:),allocatable :: fmt
    real(8) :: x=10d0, y=20d0
    !古典的な書式指定
    fmt = '(A,E10.1,A,E10.1,A)'
    print fmt, "position = (", x, ",", y, ")"
    !printf風の書式指定
    fmt = '( "position = (", E10.1, ",", E10.1, ")" )'
    print fmt, x, y

次のように出力されます.

position = (   0.1E+02,   0.2E+02)
position = (   0.1E+02,   0.2E+02)

自動再割り付け文字列

上記プログラムでは,自動再割り付け文字列を利用し,文字列の長さを陽に扱わなくてもよいようにしています.自動再割り付け文字列は,長さを:allocatableとして宣言します.文字列の長さは当初0ですが,文字列を代入した時点でその分のメモリが割り付けられます.

    character(:),allocatable :: fmt
    print *,"fmtの長さ",len(fmt)
    fmt = '(A,E10.1,A,E10.1,A)'
    print *,"fmtの長さ",len(fmt)
 fmtの長さ           0
 fmtの長さ          19

表示の桁数を動的に変更したい場合には,通常の文字列と同じように編集します.

    character(:),allocatable :: fmt
    fmt = '(A,E10.1,A,E10.1,A)'
    print fmt, "position = (", x, ",", y, ")"
    fmt(8:8) = '2' !小数点以下の桁数を指定している箇所を変更し,第2桁まで表示する
    print fmt, "position = (", x, ",", y, ")"
position = (   0.1E+02,   0.2E+02)
position = (  0.10E+02,   0.2E+02)

複数箇所で書式を使い回したい場合は,参照したい箇所全てからアクセスできる適切な場所に書式情報を代入する文字列を宣言すればよいでしょう.

ファイル読み取り時の制御

ファイルが終端に達しているかどうかは,read文の引数であるiostatの戻り値を確認し,その値に基づいて処理を分岐してください

    integer :: InputFileUnit !装置番号はnewunitで自動的に割り振られる
    integer :: OpenStatus
    character(255) :: OpenMessage

    open(newunit=InputFileUnit, file = "data.txt",action="read",status="old",iostat = OpenStatus,iomsg = OpenMessage)
    if(OpenStatus /= 0) then
        print '(I,A)',OpenStatus,trim(OpenMessage)
        stop
    end if

    block
        real(8),allocatable :: error(:)
        real(8) :: err
        integer :: IOStatus
        character(255) :: IOMessage

        error = [ real(8) :: ]
        DataRead : do
            read(InputFileUnit,*, iostat = IOStatus, iomsg = IOMessage) err
            if(IOStatus < 0) exit DataRead
            error = [ error, err ]
        end do DataRead
        print *,IOStatus,trim(IOMessage)
        print *,error(:)
    end block

    close(InputFileUnit)

ここで注目して欲しいのは,read文の引数です.

read(InputFileUnit,*, iostat = IOStatus, iomsg = IOMessage) err

一つ目で読み取る装置番号を指定し,二つ目で書式を指定します.次のiostatとしてIOStatusという整数型変数を渡しており,ファイルからの読み取り結果がIOStatusに代入されます.どのようなエラーかはiomsgとして渡した文字列に代入されます.
読み取り毎にiostatの値をチェックし,本来は0以外であれば何らかの対処をする必要がありますが,上記プログラムでは負の場合にだけexitdoのラベルを利用してループを抜けるようにしています.

可変長配列

もう一つ,上記のプログラムでは可変長の配列を使っています.現代Fortranでは,allocatableとして宣言した配列は可変長です.

    real(8),allocatable :: error(:)
    !中略
    error = [ real(8) :: ]
    DataRead : do
        read( ... ) err
        !中略
        error = [ error, err ]
   end do DataRead

error = [ real(8) :: ]で長さ0の配列を割り付け,error = [ error, err ]において,errorと新たにファイルから読んだ値errを結合した配列を新たにerrorに代入します.固定長の配列よりは遅いでしょうが,処理によっては便利に利用できるでしょう.

まとめ

本記事ではFORTRANで広く使われていた行番号について,使われている場面とそれを現代Fortranの機能で置き換える方法を述べてきました.ここで言及した機能(end do以外)は行番号の排除に留まらず,広く応用できます.プログラミング言語は道具かも知れませんが,その道具の使い方に習熟することでプログラムの可読性を向上させ,思考を整理することができ,仕事の効率も上昇することでしょう.

更新履歴

  • 12月5日 コメントで頂いた情報を反映し,unit=newunit=に変更.