概要
Fortran向けのメタプログラミングツールfyppの使い方を紹介します.
従来のプリプロセッサの代替としての利用方法に加えて,Fortranを使っていて「これを書くのが面倒だ」と感じるところに利用する方法を紹介します.
背景
Fortranには,規格で定義されているプリプロセッサはありません.ほとんどのコンパイラはCのプリプロセッサディレクティブを処理できるので,それが実質的な標準になっています.しかし,コンパイラによって処理できる内容が異なっており,可搬性という観点では不十分です.また,複雑な処理の実装は難しく,Fortranの表現力を補うには,力不足です.Fortran 2008において,Intelligent Macrosとよばれるプリプロセッサを規定しようという動きがあり,確かfinal draftまでは入っていたのですが,結局削除されてしまいました.
いくつかの第三者製のプリプロセッサが開発されており,その中でもfyppが比較的なじみやすく(特にPythonに慣れている人にとっては)強力です.近年開発が進められているFortran-stdlibにも採用されています.
Fortranでは,異なる引数を取る同じような手続を,総称名を利用してまとめることができます.例えば,引数が正であることを判定するis_positive(val)
関数を作成した際,val
が1バイト整数であろうと,8バイト整数であろうと,is_posivite
として呼ぶことができます.しかし,個別の引数の型に対する手続は書く必要があり,それらをinterface
文によってまとめることによって達成しています.
logical function is_positive_int8(val)
use, intrinsic :: iso_fortran_env
implicit none
integer(int8), intent(in) :: val
if (val >= 0) then
is_positive_int8 = .true.
else
is_positive_int8 = .false.
end if
end function is_positive_int8
logical function is_positive_int64(val)
use, intrinsic :: iso_fortran_env
implicit none
integer(int64), intent(in) :: val
if (val >= 0) then
is_positive_int64 = .true.
else
is_positive_int64 = .false.
end if
end function is_positive_int64
interface is_positive
procedure :: is_positive_int8
procedure :: is_positive_int64
end interface
これくらいならまだマシかと思いますが,配列に対する処理を書く場合,配列のランク(次元)に応じて,同じ処理を書く必要があります.例えば,実数型の配列array
の中にNaN
があるかを判定する手続を定義したいとします.Fortranは,配列に対する処理は得意なので,配列の次元にかかわらずに
any(ieee_is_nan(array))
と書くことができます.一方で,先述の通りに関数として定義する場合に,取り得る配列のランクだけ手続を定義する必要があります.Fortranの現在の規格におけるrankの上限は15なので,同じ処理の手続を,引数の配列のrankだけを変えて15個定義する必要があります.
こういったFortranの表現力の不一致を補ってくれるのが,fyppです.
環境
- Windows 10
- fypp 3.1
- Python 3.8.5 (conda 4.10.1)
- gfortran 10.3.0
Windowsで動作確認をしているので,コマンドを実行する際のプロンプトには,>
を用いています.
fypp
fyppは,Pythonによるプリプロセッサであり,(他の言語でも使えるものの,主に)Fortranのプリプロセッサとして開発されました.fyppはプリプロセッサというには処理できることが多く,どちらかというとメタプログラミングツール,あるいはトランスパイラとよぶ方が適切だと思います.
プログラミングの世界には,DRY原則があります.正確な意味は調べていただくとして,コピペによるコードの重複を指摘するために用いられることがあります(狭義のDRY原則と呼ぶことにします).背景で述べた,たった1行の処理を行う手続を15個コピペして作るのは,狭義のDRY原則には引っかからないと思っています.しかし,1行の処理を修正しようとすると,15個の手続に対して同じ修正が必要になります.狭義のDRY原則がよく参照されるのは,まさにこの事象を抑制するためです.同じ関数をコピペで複数箇所に設けると,修正があった際に,同じ修正を複数箇所で漏れなく行う必要があります.Fortranでは,この問題に対処するためにテンプレート機能を導入しようと議論を進めていますが,いつ頃になるかは判りません.
ところで,DRYはDon't Repeat Yourselfの略です.自身で重複させるなと言う意味です.つまり,Yourselfではなく,重複を適切に管理できるSomeone elseであれば,重複をさせてもよいと判断できます.fyppは,そのSomeone elseとして用いる事ができます.
インストール
fyppはPythonで作成されているので,pipあるいはcondaが利用できます.
> conda install -c conda-forge fypp
あるいは
> pip install fypp
fyppの実行
fyppでソースを前処理するには,fyppのオプションに入力ファイルと出力ファイルを指定します.
> fypp source.fy90 source.f90
ファイルの拡張子は何でもよいのですが,Fortranのソースであること,.f90ではなく何らかの前処理を行う必要のあるファイルであることを明示するために,.fy90としています.これは任意に決めてよく,Fortran-stdlibでは.fyppを拡張子として用いています.
入力ファイル,出力ファイルを指定しない場合は,標準入出力が使われます.標準入力や標準出力の利用を明示したい場合は,-
をオプションに与えます.
標準入力は,複数行入力できます.Windowsのコマンドプロンプトの場合は,入力し終わった後にCtrl+z
を入力するとfyppがその入力内容を処理します.Unix系ではCtrl+d
です.
入力ファイルの処理結果を標準出力に出したい場合
> fypp source.fy90
標準入力を処理して標準出力に出したい場合
> fypp
あるいは
> fypp - -
標準入力を処理してファイルに出力したい場合
> fypp - source.f90
fyppの機能
まずは,従来のプリプロセッサディレクティブを置き換える機能を紹介します.
fyppのディレクティブ(便宜上そうよびます)は,#:
から始まります.Cのプリプロセッサディレクティブと同様に#から始まりますが,少し異なっています.
後で説明しますが,定義した内容を呼び出すには,$
を用います.
fyppを使う場合は,#
,$
,@
が関係する記号と覚えてください.
定義と分岐
プリプロセッサで最もよく使うのは,#if
や#ifdef
ディレクティブではないでしょうか.
program main
implicit none
#ifdef DEBUG
print *, "debug print"
#endif
end program main
gfortranで前処理結果を表示すると,次のようになります.コマンドラインオプションで定数DEBUG
が定義されているので,print *, "debug print"
が表示されています.
> gfortran -cpp -E ifdef.f90 -DDEBUG
# 1 "ifdef.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "ifdef.f90"
program main
print *, "debug print"
end program
それらに相当する処理をfyppで書くには,#:if
とdefined()
を用います.
program ifdef
implicit none
#! 定数DEBUGは文字列としてdefined()に渡す.
#:if defined('DEBUG')
print *, "debug print"
#:endif
end program ifdef
#!
は,fyppによってコメントとして処理されます.定数DEBUG
は文字列としてdefined()
に渡します.
これをfyppで処理すると,次のような結果が出力されます.
> fypp ifdef.fy90
program ifdef
implicit none
end program ifdef
定数DEBUG
が定義されていないので,print *, "debug print"
が消去されています.
fyppで処理時に定数を定義するには,-D定数
もしくは--define=定数
オプションを用います.
> fypp ifdef.fy90 --define=DEBUG
program ifdef
implicit none
print *, "debug print"
end program ifdef
定数が定義されていなかったときに別の処理を埋め込みたい場合は,#:elif
や#:else
ディレクティブを用います.
program ifdef
implicit none
#! 定数DEBUGは文字列としてdefined()に渡す.
#:if defined('DEBUG')
print *, "debug print"
#:else
print *,"release"
#:endif
end program ifdef
--define
オプションの有無で,生成されるソースファイルの内容が変化していることがわかります.
>fypp ifdef.fy90 --define=DEBUG
program ifdef
implicit none
print *, "debug print"
end program ifdef
>fypp ifdef.fy90
program ifdef
implicit none
print *,"release"
end program ifdef
インラインディレクティブ
複数行にわたって処理を書くのが億劫な場合は,1行で書くこともできます.その際は,#{}#
で囲まれたインラインディレクティブを用います.
program ifdef
implicit none
#{if defined('DEBUG')}# print *,"debug" #{else}# print *,"release" #{endif}#
end program ifdef
先の例と同様に,--define
オプションの有無で,生成されるソースファイルの内容が変化します.
> fypp ifdef.fy90 --define=DEBUG
program ifdef
implicit none
print *,"debug"
end program ifdef
> fypp ifdef.fy90
program ifdef
implicit none
print *,"release"
end program ifdef
定数の値
定数の定義以外に,コンパイル時にパラメータを指定することもよく行われます.
program main
use, intrinsic :: iso_fortran_env
implicit none
#if defined(_Nx) && _Nx > 1
integer(int32), parameter :: Nx = _Nx
#else
integer(int32), parameter :: Nx = 100
#endif
end program main
前処理する際に,-D_Nx=値
オプションで定数_Nx
が定義されており,かつその値が1
(値を設定しなかった場合の標準値)より大きい場合に,その値が設定されます.
> gfortran -cpp -E val.f90
# 1 "val.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "val.f90"
program main
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = 100
end program main
> gfortran -cpp -E val.f90 -D_Nx
# 1 "val.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "val.f90"
program main
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = 100
end program main
> gfortran -cpp -E val.f90 -D_Nx=10
# 1 "val.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "val.f90"
program main
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = 10
end program main
fyppでも,同様に定数の値を評価できます.条件式の書き方は,Pythonと同じです.論理演算子&&
の代わりにand
が用いられています.
program val
use, intrinsic :: iso_fortran_env
implicit none
#:if defined('NUM_GRID_POINTS') and NUM_GRID_POINTS > 1
integer(int32), parameter :: Nx = NUM_GRID_POINTS
#:else
integer(int32), parameter :: Nx = 100
#:endif
end program val
-D定数
もしくは--define=定数
オプションには値を設定できるので,オプション--define=定数=値
を付けて処理を実行します.
> fypp val.fy90
program val
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = 100
end program val
> fypp val.fy90 --define=NUM_GRID_POINTS=10
program val
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = NUM_GRID_POINTS
end program val
確かに処理は切り替わっていますが,予想に反して,定数の名前がそのまま表示されています.
定数の参照
fyppで定数を参照するには,参照用のディレクティブ${}$
を用います.
program val
use, intrinsic :: iso_fortran_env
implicit none
#:if defined('NUM_GRID_POINTS') and NUM_GRID_POINTS > 1
integer(int32), parameter :: Nx = ${NUM_GRID_POINTS}$
#:else
integer(int32), parameter :: Nx = 100
#:endif
end program val
定数の値が埋め込まれているのが判ります.
> fypp val.fy90 --define=NUM_GRID_POINTS=10
program val
use, intrinsic :: iso_fortran_env
implicit none
integer(int32), parameter :: Nx = 10
end program val
fyppの場合,原則として#
で定義,$
で参照と覚えてください.
値を設定しない場合の定数の取り扱い
このように,Cのプリプロセッサと同様に,定数の値をソースの中に埋め込む事ができました.ただし,Cのプリプロセッサとは異なるところが1点あります.定数の値を設定しない場合,定数はPythonのNoneType
として扱われます.そのため,値を比較するような条件式を実行すると,エラーが出ます.
> fypp val.fy90 --define=NUM_GRID_POINTS
val.fy90:5: error: exception occurred when evaluating 'defined('NUM_GRID_POINTS') and NUM_GRID_POINTS > 1' [FyppFatalError]
error: '>' not supported between instances of 'NoneType' and 'int' [TypeError]
定数の削除
定数は,#:del
ディレクティブを利用する事で削除できます.
program val
use, intrinsic :: iso_fortran_env
implicit none
#:if defined('NUM_GRID_POINTS') and NUM_GRID_POINTS > 0
integer(int32), parameter :: Nx = ${NUM_GRID_POINTS}$
#:else
integer(int32), parameter :: Nx = 100
#:endif
#:del NUM_GRID_POINTS
#! これ以降はNUM_GRID_POINTSは参照できない.
end program val
しかし,単純に#:del 定数
だけを書くと,その定数が定義されていない場合に,fyppがエラーを出します.
> fypp val.fy90
val.fy90:11: error: exception occurred when deleting variable(s) 'NUM_GRID_POINTS' [FyppFatalError]
error: lookup for an erasable instance of 'NUM_GRID_POINTS' failed [FyppFatalError]
そのため,#:del
を実行する際も,#:if
ディレクティブを利用した方がよいでしょう.
program val
use, intrinsic :: iso_fortran_env
implicit none
#:if defined('NUM_GRID_POINTS') and NUM_GRID_POINTS > 0
integer(int32), parameter :: Nx = ${NUM_GRID_POINTS}$
#:else
integer(int32), parameter :: Nx = 100
#:endif
#:if defined('NUM_GRID_POINTS')
#:del NUM_GRID_POINTS
#! これ以降はNUM_GRID_POINTSは参照できない.
#:endif
end program val
変数の利用
そうすると,defined('定数')
を何回も書くことになり,煩わしくなります.そのような場合には,#:set
ディレクティブを利用して,変数を利用すると,記述が簡略化できます.ただし,定数を削除した場合は,変数の値も適宜変更するようにしてください.
program val
use, intrinsic :: iso_fortran_env
implicit none
#:set def_num = defined('NUM_GRID_POINTS')
#:if def_num and NUM_GRID_POINTS > 0
integer(int32), parameter :: Nx = ${NUM_GRID_POINTS}$
#:else
integer(int32), parameter :: Nx = 100
#:endif
#:if def_num
#:del NUM_GRID_POINTS
#! これ以降はNUM_GRID_POINTSは参照できない.
#:set def_num = False
#! NUM_GRID_POINTSが削除されたので,def_numも値をFalseに変えておく.
#:endif
#:if def_num
print *, ${NUM_GRID_POINTS}$
#! NUM_GRID_POINTSを削除した段階でdef_numをFalseにしておかないと,NUM_GRID_POINTSの値を埋め込もうとしてエラーが生じる.
#:endif
end program val
マクロ
プリプロセッサの機能が使われる他の場面としては,assertion等のマクロの定義があります.
#define assert(x) \
if (.not. (x)) then; \
write (error_unit, '(A,I0)') "assertion failed: "//__FILE__//", line ", __LINE__; \
error stop; end if
program main
use, intrinsic :: iso_fortran_env
implicit none
integer(int32) :: result
result = 1 + 1
assert(result == 3)
end program main
前処理によってマクロが展開されます.この場合は,result
を予測値(3)と比較し,それが偽であればメッセージを表示して終了します.
> gfortran -cpp -E macro.f90
# 1 "macro.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "macro.f90"
program main
use, intrinsic :: iso_fortran_env
implicit none
integer(int32) :: result
result = 1 + 1
if (.not. (result == 3)) then; write (error_unit, '(A,I0)') "assertion failed: "//"macro.f90"//", line ", 12; error stop; end if
end program main
マクロは,複数行に置き換えられないので,マクロの作成もかなり苦しいように感じています.fyppのマクロは,複数行からなる処理を自然に記述できます.
#:def ASSERT(cond)
if (.not. ${cond}$) then
write (error_unit, '(A)') "assertion failed: "
error stop
end if
#:enddef ASSERT
fyppのマクロの呼出しには2通りあります.@:
ディレクティブを使う方法と,$:
ディレクティブを使う方法です.参照用ディレクティブを使う方法もありますが,それは最後の方で紹介します.
@:ASSERT(result == 3)
$:ASSERT('result == 3')
@:
ディレクティブを使った場合,引数は式として書けますが,$:
ディレクティブを使うと,引数は文字列にしなければなりません.$:
を用いる方法は,公式のドキュメントではPython expressionと書かれているのですが,ただ単に条件式を''で囲む必要があるだけで,条件をPythonのように(例えば'result == 3 and result > 0'
)は書けません.
@:
の方が圧倒的に使用感に優れていますが,#
で定義,$
で参照というfyppの原則が崩れてしまうのは残念です.
program macro
use, intrinsic :: iso_fortran_env
implicit none
#:def ASSERT(cond)
if (.not. ${cond}$) then
write (error_unit, '(A)') "assertion failed: "
error stop
end if
#:enddef ASSERT
integer(int32) :: result
result = 1 + 1
@:ASSERT(result == 3)
end program macro
fyppで前処理を実行すると,複数行に展開されていることが判ります.
> fypp macro.fy90
program macro
use, intrinsic :: iso_fortran_env
implicit none
integer(int32) :: result
result = 1 + 1
if (.not. result == 3) then
write (error_unit, '(A)') "assertion failed: "
error stop
end if
end program macro
既定の変数
Cのプリプロセッサで利用できる__FILE__
や__LINE__
のような変数も用意されています.
-
_FILE_
: 前処理される入力ファイルの名前.標準入力の場合は<stdlin>
になる. -
_LINE_
: 入力ファイルにおける,プリプロセスされた後の行番号.
__FILE__
や__LINE__
は,プリプロセス前のファイルの名前や行番号に置き換えられます.Cのプリプロセッサは,前処理されたソースを確認すること自体が希なのでそれでもよいのですが,fyppはソースファイルを前処理して明示的に別のソースファイルを生成し,それをコンパイルします.そのため,_FILE_
や_LINE_
には,生成されたソースファイルの名前や行番号に置き換えてほしかったのですが,どうもそういう仕様ではないようです.
program macro
use, intrinsic :: iso_fortran_env
implicit none
#:def ASSERT(cond)
if (.not. ${cond}$) then
write (error_unit, '(A)') "assertion failed: ${_FILE_}$, line ${_LINE_}$"
error stop
end if
#:enddef ASSERT
integer(int32) :: result
result = 1 + 1
@:ASSERT(result == 3)
end program macro
処理すると,次のような結果が得られます.
> fypp macro.fy90
program macro
use, intrinsic :: iso_fortran_env
implicit none
integer(int32) :: result
result = 1 + 1
if (.not. result == 3) then
write (error_unit, '(A)') "assertion failed: macro.fy90, line 15"
error stop
end if
end program macro
確かに入力ファイルの名前macro.fy90と,マクロを呼び出している行数15に置き換えられています.しかし,fyppをメタプログラミングツールとしてみると,この前処理後の生成されたソース(例えばmacro.f90とする)をコンパイル・実行したときに,肝心のmacro.f90での行数が判らないと,デバッグも面倒になりそうな気がします.そのため,Cのマクロと併用するのも一つの手段だと考えています.
program macro
use, intrinsic :: iso_fortran_env
implicit none
#:def ASSERT(cond)
if (.not. ${cond}$) then
write (error_unit, '(3A,I0,A)') "assertion failed: ", __FILE__, ", line ", __LINE__, "."
error stop
end if
#:enddef ASSERT
integer(int32) :: result
result = 1 + 1
@:ASSERT(result == 3)
end program macro
> fypp macro.fy90 macro.f90
> gfortran -cpp -E macro.f90
# 1 "macro.f90"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "macro.f90"
program macro
use, intrinsic :: iso_fortran_env
implicit none
integer(int32) :: result
result = 1 + 1
if (.not. result == 3) then
write (error_unit, '(3A,I0,A)') "assertion failed: ", "macro.f90", ", line ", 10, "."
error stop
end if
end program macro
> gfortran -cpp macro.f90
> a
assertion failed: macro.f90, line 10.
ERROR STOP
_FILE_
や_LINE_
以外の既定の変数としては,下記の6個が用意されています.
-
_THIS_LINE_
:_THIS_LINE_
が書かれている行番号(前処理され,マクロが展開された後の行番号ではなく,入力ファイルにおいて_THIS_LINE_
が書かれている位置. -
_THIS_FILE_
:_THIS_FILE_
が書かれているファイル名(_FILE_
も結局入力ファイルの名前なので,違いが不明). -
_DATE_
: 前処理した日 -
_TIME_
: 前処理した時間 -
_SYSTEM_
: fyppを実行しているOSの名前 -
_MACHINE_
: fyppを実行している計算機アーキテクチャの名前
マクロの${_FILE_}$, ${_LINE_}$
を呼び出している行に,${_THIS_FILE_}$, ${_THIS_LINE_}$
を追記して実行してみます.
write (error_unit, '(A)') "assertion failed: ${_FILE_}$, line ${_LINE_}$. ${_THIS_FILE_}$, line ${_THIS_LINE_}$"
当該のwrite
文は,次のように展開されました.
write (error_unit, '(A)') "assertion failed: macro.fy90, line 15. macro.fy90, line 7"
${_LINE_}$
が15,${_THIS_LINE_}$
が7に展開されています.15は@:ASSERT(result == 3)
が書かれている行なので,マクロを呼び出している行です.7は,マクロの定義内で,${_THIS_LINE_}$
が書かれている行です.
テンプレート
いよいよ,Fortranにとって恩恵の大きい機能の紹介に入ります.
背景でも述べたように,Fortranは異なる型に対する手続を定義するのが面倒でした.fyppの#:for
ディレクティブを用いると,そのめんどくささが解消されます.
#:for
ディレクティブの文法はPythonのfor
と同じで,#:for in コンテナ
と書きます.Pythonに慣れている人には簡単ですが,慣れていない人は,この考え方に慣れる必要があると思います.しかし,コンテナとは何であるかにまで踏み込んで考える必要はなく,「何でも入れられる配列の1要素ずつ取り出して,for
の中で参照する」程度の理解で問題ありません.
とりあえず例を示します.これは,1, 2, 4, 8バイト整数型の定数を定義するプログラムです.
program for
use, intrinsic :: iso_fortran_env
implicit none
#! integer kinds
#:set INTEGER_KINDS = ['int8', 'int16', 'int32', 'int64']
#:for int_kind in INTEGER_KINDS
integer(${int_kind}$), parameter :: zero_${int_kind}$ = 0_${int_kind}$
#:endfor
end program for
INTEGER_KINDS = ['int8', 'int16', 'int32', 'int64']
で整数型変数のKINDをコンテナに詰め込んでいます.
#:for int_kind in INTEGER_KINDS
において,INTEGER_KINDS
の中身である'int8'
を取り出してint_kind
に代入し,integer(${int_kind}$), parameter :: zero_${int_kind}$ = 0_${int_kind}$
の${int_kind}$
が展開されてint8
に置き換えられます.
次に,コンテナINTEGER_KINDS
から'int16'
を取り出して同じ処理が行われます.コンテナの全ての中身に対して同様の処理が行われます.
fyppで処理をすると,次のように展開されます.
> fypp for.fy90
program for
use, intrinsic :: iso_fortran_env
implicit none
integer(int8), parameter :: zero_int8 = 0_int8
integer(int16), parameter :: zero_int16 = 0_int16
integer(int32), parameter :: zero_int32 = 0_int32
integer(int64), parameter :: zero_int64 = 0_int64
end program for
Fortranでいうと次のような処理をしている感覚です.下記の処理で,ループ用カウンタc
を動かして配列の値を取得し,int_kind
に代入するのが二度手間に感じるので,do c = 1, 4; int_kind = trim(int_kinds(c))
をまとめてdo int_kind in int_kinds
と書けるようにしていると理解してください.
character(5) :: int_kinds(4) = ["int8 ", "int16", "int32", "int64"]
integer(int32) :: c
character(:), allocatable :: int_kind
do c = 1, 4
int_kind = trim(int_kinds(c))
print *, "integer("//int_kind//"), parameter :: zero_"//int_kind//" = 0_"//int_kind
end do
改行の出力を抑制する
変数を定義するとき,読みやすさのために改行を入れますが,その改行はfyppで処理しても残ってしまいます.
> fypp
program main
use, intrinsic :: iso_fortran_env
implicit none
#! integer kinds
#:set INTEGER_KINDS = ['int8', 'int16', 'int32', 'int64']
#! real kinds
#:set REAL_KINDS = ['real32', 'real64']
! do something
end program main
^Z
program main
use, intrinsic :: iso_fortran_env
implicit none
! do something
end program main
#:mute
ディレクティブを用いると,#:mute
から#:endmute
までの間の改行が出力されなくなります.
> fypp
program main
use, intrinsic :: iso_fortran_env
implicit none
#:mute
#! integer kinds
#:set INTEGER_KINDS = ['int8', 'int16', 'int32', 'int64']
#! real kinds
#:set REAL_KINDS = ['real32', 'real64']
#:endmute
! do something
end program main
^Z
program main
use, intrinsic :: iso_fortran_env
implicit none
! do something
end program main
インクルードファイル
さて,上の例では整数型変数や実数型変数のKINDを取り扱いました.これらは,複数箇所で参照する可能性があるため,一つのファイルにまとめておき,複数のファイルから参照したいと考えることは自然です.
その場合は,#:include
ディレクティブが利用できます.共通の設定をcommon.fypp
に書き,それをプログラムのどこかでインクルードします.program文の外でも中でも問題ありませんが,外に置いた方が区別がしやすくてよいと思います.
#:include "common.fypp"
program for
use, intrinsic :: iso_fortran_env
implicit none
#:for int_kind in INTEGER_KINDS
integer(${int_kind}$), parameter :: zero_${int_kind}$ = 0_${int_kind}$
#:endfor
end program for
#:mute
#! integer kinds
#:set INTEGER_KINDS = ['int8', 'int16', 'int32', 'int64']
#! real kinds
#:set REAL_KINDS = ['real32', 'real64']
#:endmute
インクルードファイルの中身は,前処理をした際に余計な改行を入れないように#:mute
ディレクティブで囲むのがお約束になっています.このとき,#:endmute
の後に改行が必要です.改行を入れないと,インクルードした際に#:endmuteprogram
と展開され,正しく処理ができずにエラーが生じます.(Windowsで実行しているために,改行コードの違いを吸収できていないのかもしれません)
インクルードファイルをソースとは別の場所に置きたい場合は,fyppのオプション-I インクルードディレクトリ
もしくは--include=インクルードディレクトリ
でインクルードディレクトリを指定します.
例えば,include.fy90をsrcディレクトリに置き,srcディレクトリの下にincディレクトリを作ってcommon.fyppを置いた場合は,fypp include.fy90 --include=inc
と指定します.#:include "inc/common.fypp"
などと相対パスを指定することもできます.
関数テンプレート
ここまでfyppの使い方を見てきて,ようやく関数テンプレートを作成できる知識が揃いました.
背景で挙げたis_positive
関数について,整数型および実数型を引数にとれるテンプレートを作成します.
まずは,対応する型に付いてのコンテナを作ります.関数名はis_positive_KIND
とします.今回は型が整数型と実数型があるため,上で例に出したようにinteger(${int_kind}$)
では不十分です.そのため,整数型と実数型の型を持ったコンテナを作ります.
#! number of bits of integer type
#:set INTEGER_BITS = [8, 16, 32, 64]
#! integer kinds
#:set INTEGER_KINDS = ["int{}".format(bits) for bits in INTEGER_BITS]
#! integer types
#:set INTEGER_TYPES = ["integer({})".format(kind) for kind in INTEGER_KINDS]
#! nubmer of bits of real type
#:set REAL_BITS = [32, 64]
#! real kinds
#:set REAL_KINDS = ["real{}".format(bits) for bits in REAL_BITS]
#! real types
#:set REAL_TYPES = ["real({})".format(kind) for kind in REAL_KINDS]
整数型変数のビット数をもつ変数を作り,その変数を参照して整数型変数のKIND('int8', 'int16', 'int32', 'int64'
)を作り,さらにそれを参照して整数型の型宣言にもちいる型名(integer(int8)', 'integer(int16)', 'integer(int32)', 'integer(int64)'
)を作っています.実数型変数についても,同じようにしています.これらの処理については,Pythonの知識が必要です.
その後,さらに整数型変数と実数型変数のKINDおよび型名をまとめた変数を作ります.IR
は,IntegerとRealの頭文字を取っています.IR_KINDS_TYPES = list(zip(IR_KINDS, IR_TYPES))
で,KINDと型名をまとめて取得できる変数を定義しました.
#! integer and real types
#:set IR_TYPES = INTEGER_TYPES + REAL_TYPES
#:set IR_KINDS = INTEGER_KINDS + REAL_KINDS
#:set IR_KINDS_TYPES = list(zip(IR_KINDS, IR_TYPES))
このように変数を定義すると,is_positive
は次のように簡略化して記述できます.
#:include "common.fypp"
module mod_is_positive
use, intrinsic :: iso_fortran_env
implicit none
private
public :: is_positive
interface is_positive
#:for k in IR_KINDS
procedure :: is_positive_${k}$
#:endfor
end interface
contains
#:for k, t in IR_KINDS_TYPES
logical function is_positive_${k}$ (val)
implicit none
${t}$, intent(in) :: val
if (val >= 0) then
is_positive_${k}$ = .true.
else
is_positive_${k}$ = .false.
end if
end function is_positive_${k}$
#:endfor
end module mod_is_positive
総称名の定義は,上で説明した#:for
ディレクティブの使い方と同じです.IR_KINDS
に'int8', 'int16', 'int32', 'int64', 'real32', 'real64'
が入っているので,それをk
に代入しながら,全ての要素について繰り返します.
#:for k, t in IR_KINDS_TYPES
については,IR_KINDS_TYPES
に['int8', 'integer(int8)'], ['int16', 'integer(int16)'], ...
とKINDと型名がセットで格納されているので,その中の([]
でくくられた)一つの要素を取り出し,KINDをk
,型名をt
に代入して,#:for~#:endfor
で囲まれた範囲内で,${k}$
にKINDを,${t}$
に型名を埋め込んでいます.
これを処理すると,次のソースファイルが生成されます.
> fypp is_positive.fy90 is_positive.f90 --include=inc
module mod_is_positive
use, intrinsic :: iso_fortran_env
implicit none
private
public :: is_positive
interface is_positive
procedure :: is_positive_int8
procedure :: is_positive_int16
procedure :: is_positive_int32
procedure :: is_positive_int64
procedure :: is_positive_real32
procedure :: is_positive_real64
end interface
contains
logical function is_positive_int8 (val)
implicit none
integer(int8), intent(in) :: val
is_positive_int8 = .false.
if (val >= 0) then
is_positive_int8 = .true.
end if
end function is_positive_int8
logical function is_positive_int16 (val)
implicit none
integer(int16), intent(in) :: val
is_positive_int16 = .false.
if (val >= 0) then
is_positive_int16 = .true.
end if
end function is_positive_int16
logical function is_positive_int32 (val)
implicit none
integer(int32), intent(in) :: val
is_positive_int32 = .false.
if (val >= 0) then
is_positive_int32 = .true.
end if
end function is_positive_int32
logical function is_positive_int64 (val)
implicit none
integer(int64), intent(in) :: val
is_positive_int64 = .false.
if (val >= 0) then
is_positive_int64 = .true.
end if
end function is_positive_int64
logical function is_positive_real32 (val)
implicit none
real(real32), intent(in) :: val
is_positive_real32 = .false.
if (val >= 0) then
is_positive_real32 = .true.
end if
end function is_positive_real32
logical function is_positive_real64 (val)
implicit none
real(real64), intent(in) :: val
is_positive_real64 = .false.
if (val >= 0) then
is_positive_real64 = .true.
end if
end function is_positive_real64
end module mod_is_positive
これを自分の手で作るのは根気が要る作業ですが,fyppを使えば,簡単に生成できます.
マクロによるリテラルの生成
is_positive
関数を生成できました,一点だけ懸念事項があります.それは,型によらず条件式が(val >= 0)
になっている点です.今回は比較ですが,これが代入を伴う場合には,型の不一致で警告が出される事もあります.可能であれば,0
もそれぞれの型に合わせた表現にしたいところです.
これを実現するためには,まず0
の後ろにKINDを付ける必要があります.これは,is_positive.fy90の中で,0_${k}$
とすればよさそうです.一方で,実数の場合は,0.0_real32
とする必要があります.これは,KINDは単純な定数でしかなく,0_int32
と0_real32
はどちらも4バイト整数型の0を表すためです.つまり,実数の場合だけ,.0
を付ける必要があるわけです.
これは,Pythonの文字列処理の機能を使うことで実現できます.Pythonには,文字列str
の中に特定の文字列があるかを判定する演算子in
があります.if( 'real' in str )
のように使い,str
に'real'
が含まれていればTrue
を返します.
この機能を使うことで,次のようなマクロを定義できます.
#! 型の名前にrealが含まれていたら.0を返し,それ以外は何も返さない.
#:def decimal_suffix(type)
#{if 'real' in type}#.0#{endif}#
#:enddef
本体はややこしいように見えますが,インラインディレクティブを用いており,#{if 'real' in type}#
と#{endif}#
の間に.0
が置かれているだけです.つまり,仮引数type
の中に文字列'real'
があれば,.0
が埋め込まれ,なければ何も埋め込まれません.
マクロを呼び出すときには,@:
や$:
ではなく,${}$
を使って${decimal_suffix(t)}$
と呼び出します.つまり,条件式をif (val >= 0${decimal_suffix(t)}$_${k}$) then
と書き換えます.
最終的に,ソースファイルは次のようになりました.
#:include "common.fypp"
module mod_is_positive
use, intrinsic :: iso_fortran_env
implicit none
private
public :: is_positive
interface is_positive
#:for k in IR_KINDS
procedure :: is_positive_${k}$
#:endfor
end interface
contains
#:for k, t in IR_KINDS_TYPES
logical function is_positive_${k}$ (val)
implicit none
${t}$, intent(in) :: val
is_positive_${k}$ = .false.
if (val >= 0${decimal_suffix(t)}$_${k}$) then
is_positive_${k}$ = .true.
end if
end function is_positive_${k}$
#:endfor
end module mod_is_positive
#:mute
#! number of bits of integer type
#:set INTEGER_BITS = [8, 16, 32, 64]
#! integer kinds
#:set INTEGER_KINDS = ["int{}".format(bits) for bits in INTEGER_BITS]
#! integer types
#:set INTEGER_TYPES = ["integer({})".format(kind) for kind in INTEGER_KINDS]
#! nubmer of bits of real type
#:set REAL_BITS = [32, 64]
#! real kinds
#:set REAL_KINDS = ["real{}".format(bits) for bits in REAL_BITS]
#! real types
#:set REAL_TYPES = ["real({})".format(kind) for kind in REAL_KINDS]
#! integer and real types
#:set IR_TYPES = INTEGER_TYPES + REAL_TYPES
#:set IR_KINDS = INTEGER_KINDS + REAL_KINDS
#:set IR_KINDS_TYPES = list(zip(IR_KINDS, IR_TYPES))
#! 型の名前にrealが含まれていたら.0を返し,それ以外は何も返さない.
#:def decimal_suffix(type)
#{if 'real' in type}#.0#{endif}#
#:enddef
#:endmute
これをfyppで処理すると,想定通りのリテラルが生成されています.
module mod_is_positive
use, intrinsic :: iso_fortran_env
implicit none
private
public :: is_positive
interface is_positive
procedure :: is_positive_int8
procedure :: is_positive_int16
procedure :: is_positive_int32
procedure :: is_positive_int64
procedure :: is_positive_real32
procedure :: is_positive_real64
end interface
contains
logical function is_positive_int8 (val)
implicit none
integer(int8), intent(in) :: val
is_positive_int8 = .false.
if (val >= 0_int8) then
is_positive_int8 = .true.
end if
end function is_positive_int8
logical function is_positive_int16 (val)
implicit none
integer(int16), intent(in) :: val
is_positive_int16 = .false.
if (val >= 0_int16) then
is_positive_int16 = .true.
end if
end function is_positive_int16
logical function is_positive_int32 (val)
implicit none
integer(int32), intent(in) :: val
is_positive_int32 = .false.
if (val >= 0_int32) then
is_positive_int32 = .true.
end if
end function is_positive_int32
logical function is_positive_int64 (val)
implicit none
integer(int64), intent(in) :: val
is_positive_int64 = .false.
if (val >= 0_int64) then
is_positive_int64 = .true.
end if
end function is_positive_int64
logical function is_positive_real32 (val)
implicit none
real(real32), intent(in) :: val
is_positive_real32 = .false.
if (val >= 0.0_real32) then
is_positive_real32 = .true.
end if
end function is_positive_real32
logical function is_positive_real64 (val)
implicit none
real(real64), intent(in) :: val
is_positive_real64 = .false.
if (val >= 0.0_real64) then
is_positive_real64 = .true.
end if
end function is_positive_real64
end module mod_is_positive
ここで導入したdecimal_suffix
マクロと同様の事を行えば,配列のrankを変えて手続を生成するテンプレートも生成できます.
まとめ
Fortran向けのメタプログラミングツールfyppを紹介しました.
Cプリプロセッサの代用としても利用できますし,「これを書くのが面倒だ」と思う代表である,異なる型に対する手続を生成するテンプレートの作り方も紹介しました.ソースを書く労力はかなり簡略化できますが,一方でfyppは複数のファイルをまとめて前処理できないので,一つずつ処理をして行く必要があります.そのため,ビルドの設定が少し難しくなったようにも思いますし,前処理によって生成したソースの管理も,どのようにしようか迷うところです.
ここではfyppの限られた機能しか紹介しませんでした.プリプロセスを中断する#:stop
ディレクティブや,変数の値の取得・設定など,他にもできるはあります.また,コンパイルオプションも色々あるので試してみてください.特に,展開後の行の長さを制御する--line-length
や--folding-mode
オプションは,役に立つと思います.