Fortran は、Fortran 77 (古くはFortran 66) と Fortran 90 とに大別される。Fortran 90 は、さらに、Fortran 90, 95, 2000, 2003, 2008 などに分類され、後者になればなるほど、新しい仕様や文法が加わったり、古い文法が非推奨になったりしている。とはいえ、77と90の間の違いが、他のどのバージョン間の違いよりも圧倒的に大きい。したがって、Fortran90以降を総称してFortran90と呼ばれることが少なくない。
加えて、(Fortranコンパイラーの)ベンダーごとで、微妙な違いはある。有名なのは、Fortran 77 の仕様「DO 〜 (行番号必須)CONTINUE」で、これについては相当な早期に大半のベンダーが「DO 〜 (行番号なし)END DO」という拡張書式を認めたようだ。あるいは、ORACLE Fortranでは、unsigned integerが定められているが、これはFortran 2008の仕様にも定められていない、ORACLEの独自拡張だ。別のポイントとして、UNIXシステムとのやり取りにおいて、Fortranプロセスからシステムに返り値をわたして終了するcall EXIT(1)が使えるFortranコンパイラーが多い様子だが、これはFortranの仕様書には定められていないらしい(参考: gfortran:EXIT)。
このベンダーごとの相違は時に問題になり得る。昔動いていたコードが20年後には動かなくなったりする。Fortranの仕様では、私の知る限りbackward compatibilityは整っているので、昔動いていたコードが動かなくなったのであれば、それは、
- 昔、ある特定環境では動いていたコードかも知れないが、ローカルな(ベンダー固有の)仕様を実は使っていた
- 昔のFortran仕様では、厳密には定められていなくて、必然的にベンダーごとに微妙に異なる仕様になっていた部分もあるだろう。ただし、その場合でも、「非推奨」の書き方であった可能性は高い。
- 計算機環境依存。例えば 32-bit機と64-bit機(特に浮動小数点数の精度)、あるいは little/big-endians。
が主因だと推測する。今後は、そういうことはなるべく避けるよう、プログラミングしたいものである。とは言え、前述の call EXIT(1) のように、事実上、使わざるを得ない場合も少なくなかろうが!
Fortran 77と90との違いは、(当時レスター大学の天文学者Clive Pageによる) https://www.star.le.ac.uk/~cgp/f90course/f90.html がよくまとまっている。
Fortran 77からFortran 90 への進化において、最も嬉しい点を列挙する(私見に過ぎないが!)。
- 新しい制御構造 (END DO, CYCLE/EXIT, SELECT CASE →
GOTOがさらにobsoleteになった) - Type 文と配列演算の強化 (→ 前者は人間に易しいプログラミング、後者は強力な演算ができる。プログラマーが、すべてを無機質な数字で管理して覚える苦行から少し開放される)
- 個人的経験として、昔、友人が、パンチカードで腕一抱えあるFortranコードを大型計算機で計算したさせたその同じ計算を、現代的言語によりほぼone-liner(さすがに長い一行ではなるが)で再現したことがある。どちらが簡単でミスなくできるかは自明だろう。暗号にならない限り、短く記述できることは正確(ミスの少ない)でかつ作業時間短縮プログラミングにつながる。Fortran90の配列演算とTypeの組合わせは福音と言えると思う。
- Interface文の導入 (→ オブジェクト指向に少し近いことができるようになり、変数の型制限が強い言語に特徴的な欠点を緩和する。ちなみに、Fortran 77でも、組込関数でだけは存在した機構: (例)
INT()の引数は、REALでもDOUBLE PRECISSIONでもよい) - Allocatable (→ 可変サイズの配列)
- Moduleの導入 (→ プログラム単位の分割化、抽象化を容易にする。
COMMONが事実上obsoleteになった) - recursive functions/subroutines が宣言できる (Fortran 77 では禁止)
なお、見た目としては、自由形式(⇔ 各行最初7文字部分には普通のコードを書けないFortran 77の固定形式)は大きな違いになるし、実際、嬉しい点ではある。ただし、プログラミングの実用的な面から見れば、優先順位度的に、上に挙げた点の方がより重要と個人的には判断する。
逆に言えば、これらの機構をフルに使えることこそが、すなわち「使えるならば」、Fortran 90の強みと言えるだろう。
gfortranは、無料で使えるFortran compilerとして、2021年現在、事実上の世界標準になっている。
ビジネスなどの現場で昔から使われてきたものとしては、NAG FortranやORACLE Fortranなどが有名だろうが、いずれも有料。
2021年現在のgfortranは、Fortran 2008に準拠しているようだ。よくは知らないが、他のベンダーも似たような状況ではないだろうか? ちなみに、少なくとも21世紀の初め頃、NAG Fortranは、Fortran 95には準拠していて、私は90/95の違いは意識せずに使っていた。一方、当時だと、Fortran 2000の機能はまだ使用が微妙な雰囲気だった。私見ながら、2021年現在、少なくとも、95と90の仕様の違いは考える必要がないことがほとんどだろうし、2008も大抵は大丈夫ではないだろうか(注: 実は Redhat の少し古いバージョンだとそうでもないようだった)。
Fortranを使う時に、C言語と比べて最もありがたいことは、(私見では)メモリー管理にそれほど気を遣わなくても良いことだ。無論、速度を重要視する時は、頭に入れておく必要があるのは当然だ。たとえば、Fortranの二次元配列では、ARY(:, 2) のようにアクセスする方が、ARY(2, :)よりも効率が良いことはよく知られている。
しかし、最低限、Fortranを使えば、malloc関係の極めて厄介なエラーからは解放されると期待できる。
なお、Fortran 90では、配列を指定する時に、
- 固定長:
INTEGER, dimension(5,6) :: ARY - allocatable:
INTEGER, dimension(:,:), allocatable :: ARY - pointer:
INTEGER, dimension(:,:), pointer :: ARY
の3種類があり、(3)の場合は、C言語と同じく、プログラマーがメモリー管理に責任を負うことになる(malloc()など)。
事実上は、(2)の方法で困ることはない。(3)が嬉しいのは、究極の高速演算を目指す時だけと言っていいだろう。ただし、Fortran90とC言語などの外部ライブラリと直接リンクしてやり取りする必要がある場合には、(3)しか選択肢がないことはあり得る。
さて、gfortran自体はC言語(もしくはC++?)で書かれている、と理解している。つまり、Fortranのコンパイラーの皮をはがせば、内部では、malloc()が使われている。
おそらくそのせいなのだろうが……、gfortranによるFortran 90プログラミングでは、malloc()の不正使用によるというメッセージ付きのSegmentation FaultやSIGABRTが頻出して、往生した。具体的には、malloc(): memory corruption や Program received signal SIGABRT: Process abort signal.。malloc()なんて、そしていかなるpointerさえも、自分のFortranコードでは、一行も使っていないのに……。
Segmentation Faultの場合、(メモリー関係のエラーで頻出することだが)本来、不正なことが行われている箇所と全く別の箇所で、プログラムが異常終了することになる。すなわち、どこで問題が起きたか、掴むのが困難をきわめて、非常に厄介だ。
たとえば、以下のようなケースがあった。
WRITE()文が一切使えなくなったことがあった。変数の内容を(デバッグのために)標準出力に出すことさえできないのだ。しかし、その場合でもPRINT文は使えた——すなわち、WRITE()文を単にPRINT文に置き換えると、標準出力に出すことはできることがわかった(PRINT文だから、標準出力以外、特に変数への出力は不可能なので、常に代わりになるわけでは全くないが)。最初にそのケースに遭遇した時は全く意味不明だったのだが……、プログラムを一行一行追っていくと、全然別の箇所で、配列の添字が間違っていることがわかった。
このようなSegmentation Faultの大半は、allocatable配列関係のトラブルが主因だった。単純化すると、
allocate(ary(3))
val = ary(5)のような文が、Segmentation Faultを起こす。現実には、(allocatableである以上)配列の添字は動的に決められるから、これはコンパイル時には検出不能で、実行時にしかわからないことはよく分かる。NAG Fortranの場合は、上で言えば、二行目を実行しようとした時に、そこで素直にエラーメッセージとともに普通に落ちる(ように記憶している––私の記憶が正しければ!)。だから、コード中、どの行が問題だったか直ちに分かる。実際、Fortranである以上、その程度はコンパイラに期待したいものだ。
しかし……gfortranの場合、Sementation Faultとなって、全く異なる箇所で、有効なエラーメッセージもなく落ちるため、極めて不親切だ。複数のmodulesをリンクしている場合(すなわち、ごく短いコードを除いてすべての場合)、どのファイルのコードの中に問題が潜んでいるのかさえ直接にはわからない。
TDD (Test-Driven Development)の概念に従って、一つ一つ小さい単位から丁寧にテストを繰り返しながら積み上げてプログラミングするしかないのかも知れない。
別の例として、以下のパターンが SIGSEGV あるいは malloc 関係エラーを出すことがあった(以下のコードそのままではない)。
character(len=256) function func1()
func1 = ''
end function func1
subroutine sub2(ch)
chracter(len=*), intent(in) :: ch
end subroutine sub2
call sub2(func1())この解決法は、この最後の行に trim() を加えるだけだった。
call sub2(trim(func1()))この trim() が、Fortranの仕様上、必須という話は聞いたことがない! したがって、これは gfortran のバグだろう(2021年3月31日、gfortran-10.2.0 ((Homebrew GCC 10.2.0_4)))。
Fortran の場合、Makefile にて、mod と f90 との関係を整理する必要がある様子だ。 これが極めて複雑で、バグの温床になる……。
理想は、以下だと考える。
- ソースファイル中にて、
useを使って、必要な module を読み込む - Makefile にて、
.f90.o省略記法で、全object filesをコンパイル。 - Makefile にて、主プログラムの最後のリンクだけは、依存関係を記述する必要があるかも知れない(その必要もないのが理想)
しかし現実には、以下の条件が必要な様子だ(全く自信はないが)。
- Makefile
- それぞれの object file について、依存関係を記述する必要がある。すなわち、Makefile の特徴である
.f90.oなどの省略記法がほとんど使いものにならない(依存関係の存在しない、完全に独立な object files は除く)。 - 主プログラムも通常、まず object file にコンパイルしてからリンクする。ということは、その object fileについて、依存関係を一つ一つ Makefile 中に記述が必要。
- 主プログラムの場合は、それに加えて、リンク時の依存関係を一つ一つ Makefile 中に記述が必要。
- それぞれの object file について、依存関係を記述する必要がある。すなわち、Makefile の特徴である
- ソースファイル中
useを使って、必要な module を読み込む必要がある。
このどれか一つでも忘れると、エラーが出る。そして、そのエラーは必ずしもクリアでないために、何が原因でエラーがでるのかわからずに頭をかかえることが少なくない。最悪、make のときではなく、実行時にエラーがでることもあったような気がする……。
allocate(ary(n))を書いた時は、nが負で無いことをチェックする。- この文は、
if (allocated(ary)) deallocate(ary)とセットで書く(Fortranの仕様的には、コンパイラーがメモリー管理を面倒見ているはずではあるが)。 - ポイント(2)については、関数やsubroutineから返ってきた配列がallocatedの場合には特に注意する。関数がallocated配列を返す場合は、原則として、呼び出し側では、allocatableとして定義しておいて、使用が終わった後に呼び出し元でdeallocateしておく。
- type内にallocated配列を保持する場合は、deallocateするためのサブルーチンを用意しておくのが良いかも知れない。
- 以上、本来は、pointer を使うときには徹底的に注意するも、allocated配列の場合はそれほど気にしなくても良いもののはずだが……、少なくともgfortranの場合は、細心の注意を払いたい。
ary(n)を書く時には、if (n .le. size(ary))とペアにする。無論、aryの添字の上限下限を定めている場合は、それに従う(個人的にはそもそもFortran配列の添字の上限下限を変更するのは避け、添字は必ず1から始めると決めているが)。- 変数の宣言時には、PARAMETERを除いて、初期値を与えない。端的には、
INTEGER :: i = 5の記法は使わない。- Fortran仕様として、変数が初期化されるのは、最初にそのルーチンを呼んだ時だけらしい(SAVE?)。つまり、複数回呼べば、挙動が予想外になり得る。
- CHARACTER やその配列を返り値とする場合は、FUNCTIONではなく、SUBROUTINEとして記述する方が良いことが多い。
- 理由1: SUBROUTINEだと、呼び出し元で長さを完全にコントロールできる。
- 理由2: gfortran は、CHARACTER周りの挙動が不安定な印象がある。
- ただし、SUBROUTINEだと、関数のように直接
TRIM()にfeedすることは当然できないので、一長一短ではある!
- CHARACTER を使う時は、仮に不要だと思っても、徹底的に
trim()を入れておくと、無難だろう。
Masa Sakano, Wise Babel Ltd