#FORMAT(書式)
Fortran の I/O は昔から遅いことで有名ですが 、その理由の一つに FORMAT の解釈が煩瑣なことがあるようです。1
古典 FORTRAN の FORMAT は 1H や 1H0 などの(プリンタ)制御文字が必ず先頭に来るなど、謎なところがあって昔から鬼門として知られていましたが、Fortran90 以降整備が進んで、望んだ書式が実現しやすくなった気がします。
とはいえ書式の記号の説明は、文法書でも読む気のしない部分ですので、ここでは書式を使った芸をやって、書式の部分を読むことへの心理的抵抗を減らすことを目的としてみます。
以下でタブ指定子を用いて、'Hello World!' という文字列を逆順に表示してみます。
##タブ指定子 t, tl, tr
Fortran は行指向の言語なので、出力では出力桁位置を指定できます。指定は行頭を1とする絶対位置でも、現在の出力位置からの相対位置でも指定できます。それぞれは絶対位置 T n、左にむかった相対位置 TL n、右にむかった相対位置 TR n(nは整数値)と指定します。
ここでいうタブは、桁位置(カラム位置)と同じだと思えばいいです。
###出力結果
Hello World!
Hello World!
['Hello World!']
['H','e','l','l','o',' ','W','o','r','l','d','!']
Hello World!
!dlroW olleH
###プログラム
program fmt
implicit none
character(len = *), parameter :: text = 'Hello World!'
character :: letters(len(text)) = transfer(text, ' ', size = len(text))
!
! letters = transfer(text, ' ', len(text)) !intel Fortran bug
!
print *, text
print *, letters
!
print *
print '( "[", *("''", a, "'']":, tl1, ",") )', text
print '( "[", *("''", a, "'']":, tl1, ",") )', letters
!
print *
print '( t14, *(tl2, a) )', text
print '( t14, *(tl2, a) )', letters
!
end program fmt
###解説
character(len = *), parameter :: text = 'Hello World!'
まず 'Hello World!' という文字列を text という parameter 定数として定義します。ここで文字列の長さを与える必要がありますが、parameter 定数の場合長さとして * を与えておくと、コンパイラが自動で数えてくれるので人間が文字数を数えて与える必要はありません。
character :: letters(len(text)) = transfer(text, ' ', size = len(text))
!
! letters = transfer(text, ' ', len(text)) !intel Fortran bug
文字列を1文字毎に分解して1文字長の文字配列変数に初期値として与えます。文字列は定数として与えてあるので、コンパイル時にその文字列長を関数で得る事が可能になっています。これによって配列の要素数を確保しています。また transfer 関数を用いて文字列を文字配列に変換しています。(ここで intel fortran v.19.0.1 には bug があって、transfer 関数での初期化がうまくゆかないので、実行時に値を与えるようにします。)
参考記事:https://qiita.com/implicit_none/items/f582f2d4e10f2655e208
print *, text
print *, letters
Hello World!
Hello World!
まずは 並び入出力 (list-directed I/O) で 'Hello World!' の文字列と文字配列を書き出します。全く同じように出力されます。
print '( "[", *("''", a, "'']":, tl1, ",") )', text
print '( "[", *("''", a, "'']":, tl1, ",") )', letters
['Hello World!']
['H','e','l','l','o',' ','W','o','r','l','d','!']
次に1要素毎に分解して鉤括弧中に引用符でくくって出力します。文字列の場合は 'Hello Wolrd!' が一つの単位なので、これが1個だけ出力されます。一方、文字配列の場合は、1文字毎に分けられて ['H','e','l','l','o',' ','W','o','r','l','d','!'] のように出力されます。
ここで、タブ指定子を使って要素区切りの ',' と ']' の処理を行っています。まず "[" で先頭に鉤括弧開くを書き出し、* によって以降の書式を無限反復します、"''" はアポストロフィーを書き出しています。書式を ' ' でくくっているので2個連続させることで文字としてのアポストロフィーであることを示しています。a は文字列の出力指定子で文字幅を省略すると計算機側で必要な文字幅を忖度してくれます。 "'']" で引用符を閉じ、必要なくとも鉤括弧も閉じてしまいます。
":" は、出力するものが無かったらここで終了することを指定します。上の例では text の場合 'Hello World!'、letters の場合 '!' を出力したあとは、もう出力するものが無いので終わりになります。まだ出力するものがある場合は tl1 によって左に1桁出力位置を戻します、つまり ']' を書いた位置に出力位置が移動し、次の出力で "," が上書きされることになります。そうして無限反復により FORMAT は * の次の '(' 位置まで戻り出力要素が無くなるまで繰り返します。
"]" を1要素毎に毎回出力しながら tl1 で出力位置をバックさせて、上書きで消して行くことで、各要素を ',' で区切りつつ、最後に "]" があるような出力を出力要素の個数に依らず共通の書式で実現しています。
print '( t14, *(tl2, a) )', text
print '( t14, *(tl2, a) )', letters
Hello World!
!dlroW olleH
上の行では、まず第14桁(カラム)に出力位置を移動させます、次に * で無限反復を指定し、TL2 で左に2桁戻って第12桁(カラム)に出力位置を移動させ文字列 'Hello World!' を出力しています。
下の行では、第12桁に出力位置を移動させたあと、1文字出力するごとに TL2 で出力位置を2桁分左側に戻しています。こうして文字配列の逆順に出力させることを実現しています。
回文も簡単に書けるのではないかと思われます。
参考:FizzBuzz への応用
以前、このタブ指定子を FizzBuzz に応用しています。
https://qiita.com/cure_honey/items/42547bbaf22f7e14716f
#雑感
自分でこんな記事を書いておきながら何なんですが、このようなテクニックに走るよりも、やや冗長でも明々白々な素直なプログラムの方がロバストというかつぶしも効いて最適化も効いて御利益が多い気がします。
とはいえ、奇行プログラムも小手先のピンチ切り抜け術として使えることがあるのではなかろうかとも思いますです。
-
非同期(asynchronous) I/O が Fortran2003 で導入されるなど、違った方向で対策もとられています。 ↩