LoginSignup
11
7

More than 1 year has passed since last update.

Fortran用メタプログラミングツールfyppの使い方

Last updated at Posted at 2021-10-21

概要

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ディレクティブではないでしょうか.

ifidef.f90
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で書くには,#:ifdefined()を用います.

ifdef.fypp
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ディレクティブを用います.

ifdef.fy90
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行で書くこともできます.その際は,#{}#で囲まれたインラインディレクティブを用います.

ifdef.fy90
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

定数の値

定数の定義以外に,コンパイル時にパラメータを指定することもよく行われます.

val.f90
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が用いられています.

val.fy90
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で定数を参照するには,参照用のディレクティブ${}$を用います.

val.fy90
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ディレクティブを利用する事で削除できます.

val.fypp
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ディレクティブを利用した方がよいでしょう.

val.fy90
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ディレクティブを利用して,変数を利用すると,記述が簡略化できます.ただし,定数を削除した場合は,変数の値も適宜変更するようにしてください.

val.fy90
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等のマクロの定義があります.

macro.f90
#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の原則が崩れてしまうのは残念です.

macro.fy90
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_には,生成されたソースファイルの名前や行番号に置き換えてほしかったのですが,どうもそういう仕様ではないようです.

macro.fy90
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のマクロと併用するのも一つの手段だと考えています.

macro.fy90
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バイト整数型の定数を定義するプログラムです.

for.fy90
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.fy90
#: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
inc/common.fypp
#: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は次のように簡略化して記述できます.

is_positive.fy90
#: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
is_positive.f90
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_int320_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と書き換えます.

最終的に,ソースファイルは次のようになりました.

is_positive.fy90
#: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
inc/common.fypp
#: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で処理すると,想定通りのリテラルが生成されています.

is_positive.f90
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オプションは,役に立つと思います.

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7