背景
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通りあります.
- DO文の終端
- GOTOの飛び先
- 出力の書式指定
- ファイル読み取り時の制御
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
文の前に付いているConvergence
がdo
文のラベルです.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以外であれば何らかの対処をする必要がありますが,上記プログラムでは負の場合にだけexit
とdo
のラベルを利用してループを抜けるようにしています.
可変長配列
もう一つ,上記のプログラムでは可変長の配列を使っています.現代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=
に変更.