概要
モダンFortranにはコーディングルールが見つからないとよく言われます.有名な所では、気象庁のFortran 標準コーディングルールなどがありますが、これも今のFortranには馴染まないところもあります.
個人的な経験に基づく内容ですが,著者自身が気をつけていることをダラダラと書いて,情報整理のきっかけになればと思いました.全てをまとめるには長大な時間がかかりそうですが,勢いで書けるところから書いていきます.
Fortranを使うときに心掛けること
- 自制心を養う
- サポートの強力なエディタを使う
暗黙の型宣言
無効にする以外の選択肢はない.宣言部冒頭にimplicit none
を書く.暗黙の型宣言は使わない方がよい.短いプログラムなら大丈夫と思わせて,後々本当に忘れた頃に問題を引き起こす.プログラムも汚くなる.レガシーコードにimplicit none
を付けると,i, ii, i2
等々30個くらいカウンタ用変数が出てくる.enum
や出力時のunit
番号の指定とも相性が悪い.本当に暗黙の型宣言は使わない方がよい.
プログラムを書いている途中で,変数宣言をするために宣言部に戻るのが面倒なら,block
構文を使ってローカル変数を作るか,ウィンドウを分割できるエディタを使うこと.
本当に暗黙の型宣言は使わない方がよい.
ファイル出力時のユニット番号は,既定がなければnewunitで自動的に割り当てさせる
integer :: unit_write_result
open(newunit=unit_write_result, file="output.txt")
! ユニット番号を使ってwriteする
close(unit_write_result)
output_unit
は標準出力の装置番号として,iso_fortran_env
で定義されている.
標準出力にはprint文を使う
変数名
基本はスネークケースを使う.
Fortranは英字の大文字小文字を区別しないので,スネークケースが妥当だと考えられる.昭和企業でチーム開発する場合,他人が付けた変数のキャピタリゼーションはほぼ確実に守られない.スネークケースならキャピタリゼーションの問題はない.
世の中は英字の大文字小文字を区別する言語が多いので,サンプルコードがキャメルケースの事も多い.そういう言語を経験した人が個人開発で使うことを妨げない.著者は個人開発の場合はキャメルケースを使うが,その際はエディタの補完機能を利用している.
定数にはどうしてもは大文字やパスカルケースを使いたくなるので,自制心をもって使っている.
要所で名前付き構文を使う
do, if, block, select-case
等の構文(construct)には名前を付けることができる.if
構文が長くなるとき(本当はそれ自体がよくない状況だが)や構文内でプリプロセッサディレクティブを使う場合,インデントが崩れて対応関係がわかりにくくなるので,構文終端にコメントでどのif
の終端かを書くことがある.名前付き構文にすればその労力は削減できる.全てに名前を付ける必要はない.
名前を付けるとキーワードが内側に入るので,それを嫌う場合は,継続行を使う.
eachface : do f = 1, num_face
end do eachface
↓
eachface :&
do f = 1, num_face
end do&
eachface
メインルーチン,手続の終わりはきちんと書く
メインルーチンをprogram main
で始めたなら,end program main
で終わる.
意味のないstopやreturnは書かなくてよい
メインルーチン終端のstop
や手続終端のreturn
は意味がないので記述は不要.
1ファイル1手続はアンチパターン
関連ある手続(サブルーチンや関数)は一つのモジュールにまとめる.長くなる場合は,submodule
の機能を使うと1ファイル1手続の利点を引き継ぎつつ問題点をスマートに解決できる.
クラスの定義は1モジュールに1種類とする
クラスを定義するとき,type bound procedureの宣言と実装が分かれており,実装をcontains
以下に書かなければならない.複数のクラスを1モジュールで定義すると,実装が混在するし,名前が衝突することもある.
iteratorパターンを使いたい場合は例外.同じモジュール内でクラスとそれを扱うiteratorを定義しなければならないので,同じモジュール内で定義する.
型宣言の種別(kind)
iso_fortran_env
をuse
して,そこで定義されているkindを使う.整数型ならint8, int16, int32, int64
,浮動小数点数および複素数ならreal32, real64, real128
がある.入力が面倒なら,入力サポート機能を使う.例えば,VSCodeならユーザスニペットが利用できる.
real128
については利用を勧めない流儀もある.コンパイラによってはreal128
が拡張倍精度になっている場合があるためであり,real128
等の定数よりはselected_real_kind
を用いる方が好ましいとの理由である.
しかし,4倍精度実数をサポートしていないコンパイラで,selected_real_kind
によって4倍精度相当の実数を表現するkind
を得ようとしても,4倍精度実数どころか拡張倍精度を表すkind
すら得られない.そのため,real128
で統一した方が好ましい.
処理系の標準サイズの型をkind指定無しで宣言した場合と,kindを明記して宣言した場合は,同じ型と認識される.例えば,整数型の標準が4バイトの場合,integer
とinteger(int32)
はselect type
で同じ型と認識される.
浮動小数点数の精度を切り替える場合は,sp
やdp
というパラメータを定義してコンパイルオプションで切り替えることが行われているが,それが必須でない場合は標準で定められた定数を使う方が好ましい.sp, dp, qp
で統一した方が楽だとも思うが・・・
logical
も整数と同様にlogical8
などのkindが定義されているが,真偽の2値しか扱わないので,logical
でよい.整数型の標準サイズと同じになり,おそらくその処理系が最も高速に処理してくれるサイズになっている.
enumを使う場合は,enumeratorの接頭辞としてその列挙されるものの名前を付ける
iso_c_bindig
をuse
することで,C言語の列挙型を使えるようになるが,Fortranでは名前付きの列挙型は使えない.そのため,列挙子が衝突することがあるので,列挙子の名前に分類名を付ける.
例えば,境界条件を列挙するなら,
enum, bind(c)
enumerator :: BoundaryType_Dirichlet = 1
enumerator :: BoundaryType_Neumann
enumerator :: BoundaryType_Robin
end enum
などとする.
書式は自由形式+column数を拡張
他の多くの言語がそうであるように,自由形式をつかう.column数に制限がかかる場合は,上限をコンパイラ既定の値あるいは無制限に拡張するコンパイルオプションが用意されているので,それを使う.
インデントは半角4文字
個人的な感想としては,2文字は窮屈.2文字になっていたのは上記のcolumn数制限があったからだと思われるが,拡張すれば半角4文字で問題ない.
継続行の&は不要
Fortranは改行が文の終わりになっているが,&を付けることで,次の行に文を続けることができる.
文字列の途中で改行する以外,継続行(改行に続く行)の行頭の&は不要.適切に字下げすればよい.
df = (f(i+1,j ) - f(i-1,j ))/(dx*2)& !行を継続
+(f(i ,j+1) - f(i ,j-1))/(dy*2)
配列式を書くときに,全要素が対象であっても(:)を省略しない
見やすいかどうかではなく,意図しない再割付を防止するため.allocatable
属性を持つ配列は,異なるサイズの配列を代入すると自動的に再割付される.それを行う場合以外は,(:)
を省略しない.
配列の宣言にdimension属性を使わない
配列を参照する時は例えばa(i)
とするので,そのように(integer :: a(100)
と)宣言した方がわかりやすい.
配列を参照するポインタの宣言にはdimension属性を使う
Fortranはポインタの配列を持たない.ポインタの配列を意図したと誤解を生まないように,配列へのポインタの宣言にはdimension
属性を使う.
integer,pointer :: a(:) ! 1次元配列へのポインタ
integer,dimension(:),pointer :: b ! 1次元配列(integer,dimension(:))へのポインタ
物理定数は長い名前を付けて,使うときに別名参照する
定数を英字1文字で宣言するのは好ましくないので,物理定数を宣言するモジュール内で正確な名前を付けて,参照する際によく使う記号と対応付ける.
module Phys_Constant
use,intrinsic :: iso_fortran_env
implicit none
real(real64),parameter :: GravitationalAcceleration(*) = [0d0, 0d0, -9.8d0]
end module Phys_Constant
program main
use :: Phys_Constant, only : g=>GravitationalAcceleration
implicit none
print *,g
end program main
アロー記号=>
をis theと解釈すれば,use
の箇所を g is the gravitational acceleration.と解釈できる.
円周率$\pi$など,よく知られた数学定数は,そのままギリシャ文字をアルファベットで書く(PI
).このとき,日本語読みしてPAI
と書かない.
モジュールの名前は,中に書く内容に応じて接頭辞を付ける
- 数学定数,物理定数,列挙子を書く場合は接頭辞を付けない
- ユーザ定義型を宣言する場合は,
type_
を付ける - クラスを定義する場合は,
cls_
を付ける - abstractクラス(abstract data type)を定義する場合は,
adt_
を付ける - それ以外の場合,複数のユーザ定義型や手続を宣言する場合は,
mod_
を付ける
Fortranは英字の大文字小文字を区別しないので,他の言語のように,型名の先頭を大文字にして,変数(インスタンス)を小文字にするようなことができないため.
Person person = new Person() // 初心者を混乱させるOOPの例.FortranではPersonとpersonは区別されない.
また,use
する際,use :: type_vector2d
というように書くので,素直に読める.
ただし,フォルダ階層でパッケージングする場合はこの限りではない.
フォルダ階層でパッケージングする場合,フォルダ名はキャメルケースを用い,モジュール名には_をセパレータとしてフォルダ階層を反映する
例えば,独自のmath
ライブラリを下記のようなフォルダ構造で作成する場合,
.
├─ app
└─ src
└─ math
├─ constants.f90
└─ functions
├─ special.f90
└─ trigonometric.f90
constatns.f90
のモジュール名はmath_constants
,trigonometric.f90
のモジュール名はmath_functions_trigonometric
とする.
ただし,Fortranには規格上エンティティの名前は63文字と決まっているので,フォルダ階層が深くなるとこの制限に引っかかる場合がある.その場合は略語を適切に決め,プロジェクト内で共有する.例えば,constants
はconst
と省略し,cnt
(countと勘違いしやすい)とはしないなど.
Fortranが対象とする問題領域(物理シミュレーション)では,equation, boundaryConditionなど長い名前名前が現れることが多いため,このパッケージングの方法は相性が悪いことも事実である.このパッケージング方法を採用するかどうかは,プロジェクト開始時によく検討することが必要である.
202Yではエンティティ名は1万文字まで拡張されることが決まっているため,問題は解消される見込みである.
文字型の配列を使わず,文字列を使う
文字型配列を使う理由は,文字列を使いたいからである.それ以外の例はほとんどない.
文字列を宣言する際のlen=は不要
kind
を指定することはないので,character
に続いて括弧+数字があれば,それは文字列の長さと認識すればよい.kindを指定する方が特殊だが,今後kind=4の文字列を使うような場面が多く出てくる場合は,再考する.
外部サブルーチン,外部関数を使わない
外部サブルーチンや外部関数の代わりに内部副プログラムをつかう.複数箇所から参照したい場合は,モジュールを作ってそこに書く.
配列を動的に割り付ける場合は,積極的にsourceおよびmoldを使う
source
指定子を使うと,指定された配列と同じサイズ,同じ値で割り付けられる.mold
指定子は,指定された配列と同じサイズで割り付け,値の初期化はしない.
サイズを指定して割付け,source
指定子にスカラ値を与えると,全要素をその値で初期化できる.
SMAC法の圧力Poisson方程式を解く過程で,速度の予測子の発散 $\theta=\nabla \cdot u$ を用いる.その際,$\theta$を格納する配列のサイズは圧力$p$と同じになる.
program main
use,intrinsic :: iso_fortran_env
implicit none
integer(int32),parameter :: Nx = 1024
integer(int32),parameter :: Ny = 1024
real(real64),allocatable :: pres(:,:) ! 圧力
real(real64),allocatable :: divU(:,:) ! 速度の予測子の発散
allocate(pres(Nx,Ny), source = 0d0) ! 要素数Nx×Nyで割り付け,全て0で初期化
allocate(divU, mold = pres) ! 圧力presと同じ要素数で割り付ける.初期値は速度の発散で計算
end program main
NaNを検出する際はisNaNではなくieee_is_nanを使う
isNaN
は非標準.PGI FortranはisNaN
をもっていない.
引数がNaNの時に.true.
を返す標準の関数は,ieee_is_nan()
.ieee_is_nan()
はスカラ値しか取れないので,配列の要素のいずれかがNaNであるかを検出する場合は,any
関数と併用する.
program main
use,intrinsic :: ieee_arithmetic
implicit none
real(real64),allocatable :: pres(:,:)
! 中略
if(ieee_is_nan(pres(1,1)))then ! スカラ値の判別
end if
! エラー
! if(ieee_is_nan(pres))then
! end if
if(any(ieee_is_nan(pres)))then ! 配列の全要素を検査,いずれかがNaNなら.true.
end if
end program main
変数参照用モジュールは使用しない
特定のモジュールに変数を宣言し,それを複数の手続でuse
して使う流儀があるが,それは実質的なグローバル変数であり,再利用を妨げるので,使用しない.
特定の処理で大きなメモリ空間を共有したい場合は,モジュール変数を使う.このとき,モジュール変数にはprivate
属性を付け,外から見られないようにする.
exitする構文には名前を付ける
Fortranでは,do
構文などの繰り返し処理の途中でもexit
文で脱出できる.exit
文は,構文の名前を指定することで何重ループでも脱出できる.名前がない場合は再内のループから脱出するが,どのループから脱出するかを明記する.
SOR_convergence : do while(error > err_tolerance)
do k=1,Nz
do j=1,Ny
do i=1,Nx
if(hoge) exit SOR_convergence !最外ループのdo while外へ脱出する
end do
end do
end do
end do SOR_convergence
配列構成子には[]を使う
Fortranで配列を作るためには,(/
と/)
(配列構成子)で配列要素を囲む必要があったが,今は[]
が使える.[]
は(/ /)
の完全上位互換なので,[]
を使う.
reshape関数を使わずに多次元配列のリテラルは作れないので,それに時間をかけない
Fortranの配列構成子は必ず1次元に展開されるので,reshape
関数を使わずに多次元配列を作ろうと無駄な努力をしない.自戒.
[[1,2,3], [4,5,6]]
は[1,2,3,4,5,6]
になる.
ファイル拡張子にはf90を使う
Fortranは,その規格でファイル拡張子について何も言及していない.gfortranは,f03, f08などを設けてはいるが,Fortran規格を明確に区別して使って居る人はいないので,固定形式はf
,自由形式はf90
と認識すればよい.どの規格を基準にしているかを指定したい場合は,コンパイルオプションを使えばよい.
F90
など大文字を使うとプリプロセスが行われるようになるが,Fortranのコンパイルの過程でプリプロセスは標準ではなく,特殊な行為を行うという意味で,コンパイルオプションで指定する方がよいと考えている.
手続の引数には必ずintent属性を付ける
手続の引数が読込専用ならintent(in)
,書き込まれる場合はintent(inout)
,引数の全ての要素に値が書き込まれるだけと明確に分かっているときはintent(out)
を付ける.最適化の手助けになると言われているが,意図しない変更を防止する意味もある.
intent(out)は基本的に使わない
intent(out)
属性は,その属性を付けられた引数が未定義であるとして扱う.そのため,
- 引数がスカラ変数で明確に書き込まれるだけ
- 引数が配列で全ての要素を更新するだけ
- ユーザ定義派生型が成分に派生型を持たず,手続き内で全ての成分に値を書き込むだけ
- 特定のクラス向けに代入演算子をオーバーロードするサブルーチンの第1引数
以外の状況では使わない.
allocate/deallocateは1文に1変数
allocate
およびdeallocate
文は,allocatable
属性をもつ複数の変数をまとめて割付・解放できるが,それはやらない.
- 割付結果を
stat
で取得する際,複数の変数をまとめて割り付けていると,どの変数でエラーが起こっているかを判断することができなくなる- (判断できるのかもしれないが,私はその方法を知らない)
- 新たに使う変数を増やしたとき,
allocate
文とdeallocate
文を1行ずつ追加すればよく,また,変数ごとに割付・解放のタイミングを選べる -
source
,mold
指定子で指定する変数との対応関係を1:1で明記するため - 手続き型→オブジェクト指向に切り替えたときに,スカラ量クラスやベクトル量クラスを作り,1物理量1インスタンスとするほうが(物理量群というクラスを作るより)自然であり,それとの対応のため
比較演算子は新しい形式を使う
より直感的に理解できる.
意味 | 旧 | 新 |
---|---|---|
等しい | .eq. |
== |
等しくない | .ne. |
/= |
より小さい | .lt. |
< |
より大きい | .gt. |
> |
以下 | .le. |
<= |
以上 | .ge. |
>= |
比較をするときは左に可変値,右に基準値を書くが,範囲内(外)にあることを評価したい場合だけは例外
if(val < 100)
と書き,if(100 > val)
とは書かない.
ある値val
が範囲($[\mathrm{minimun}, \mathrm{maximum}]$)の内にあるかを判定したい場合は
if(minimun <= val .and. val <= maximun)
と書き,範囲($[\mathrm{range_{min}}, \mathrm{range_{max}}]$)の外にあるかを判定したい場合は
if(val < range_min .or. range_max < val)
と書く.
変数宣言の際に書く属性は,意味のまとまり,使用頻度順
Fortranは,変数宣言の際に型を最初に指定し,それに属性を追記していく.属性は,意味のまとまりが取れること(そして他言語の変数宣言から大きく乖離しないこと)を優先し,次に使用頻度を優先する.
integer(int32),allocatable,save :: hoge(:)
save
属性は使用頻度がallocatable
より少ないので,allocatable
を先に書く.
整数型の1次元配列を指すポインタ型のモジュール変数を外に公開しない場合は,
integer(int32),dimension(:),private,pointer :: ptr
Fortranには,配列を指すポインタは存在するが,ポインタの配列はない.そのため,integer,dimension(:)
を一つのまとまりとして(他言語におけるint[]
として)扱う.また,次にアクセス指定子を付けるのは,他の言語の型宣言(例えばprivate int[]
など)と合わせるため.
仮引数の属性は
integer(int32),intent(in),optional :: hoge1
integer(int32),value,optional :: hoge2
integer(int32),allocatable,intent(inout) :: hoge3
等とする.intent
とvalue
は同じ重要性を持っており,optional
はそれより使用頻度が少ないので,後ろに付ける.しかし,intent
もvalue
もoptional
も仮引数用の属性であるため,integer(int32),allocatable
のように,通常の変数宣言にも用いる属性をまとめて記述する.
以下の様に長々と書くこともできる.(確認していないので,もしかしたらコンパイルが通らないかも知れない)
integer(int32),public,allocatable,save,volatile,target :: hoge(:)
これは,
- “整数型の配列を外部に公開”という意味を重要視し,
- その配列は動的に割り付けられ,
- その状態は手続を抜けても保持される.
- また,手続外部から変更される可能性があり,
- pointeeでもある.
という順番で読む.使用頻度的にはtarget,volatile
とするべきだとも思うが,target,volatile
だとpointeeであることがvolatileなのかと誤解を与えかねない.また,Fortranにおけるpointerの使い方において,target
属性は,pointerと結合しようとしてコンパイルエラーが発生し,target
属性が必要である事に気付いて追記されるので,最後が相応しいと考える.
変数の属性の順序
- 型名
integer
,real
,character
,type
等 -
dimension
(ポインタ変数の場合) - アクセサ
private
,public
- アクセサ
protected
(public
の場合) -
parameter
(定数の場合) -
allocatable
,pointer
-
contiguous
(pointer
の場合) save
-
intent
,value
(仮引数の場合) -
optional
(仮引数の場合) volatile
target
ファイルの有無を確認するには,access関数ではなくinquire文を用いる
Fortranでファイルの存在の有無を確認する方法を調べると,access
関数の使用例が出てくるが,既に利用できないコンパイラもあるので,inquire
文を用いる.
これの意味するところは,(方言,コンパイラ依存も含む)古い情報に引きずられるのではなく,Fortran標準で用意された新しい機能に積極的に置き換えようということである.
文字列リテラルはダブルクオート,formatを表す文字列にはシングルクオートを利用する
Fortranで文字列リテラルを作成する場合,数字・アルファベット・記号をダブルクオート"
もしくはシングルクオート'
で囲む.シングルクオートとダブルクオートのどちらを用いても違いはない.文字列の中にダブルクオート"あるいはシングルクオート'を置く場合には,文字の囲みにシングルクオート(' " '
)あるいはダブルクオート(" ' "
)を使用する必要がある.
英文を記述する場合,シングルクオートはアポストロフィとして利用されるが,ダブルクオートはあまり利用されないので,ダブルクオートを用いた方が手間は少ない.
print *,"I'm implicit_none. I'm writing modern Fortran code."
一方,Fortranのwrite/print
の書式指定は文字列で行うが,C言語におけるprintf
のように,書式指定に文字列を含めることができる.
character :: space = " "
print '(3A)', "_", space , "_" ! _ _ と表示
print '("_",A,"_")',space ! _ _ と表示
他にも,ユーザ定義派生型の出力用サブルーチンを作成した場合,書式指定の際に,DT"型名"
として,型名を文字列で指定することになる.
type(String) :: str
print '(DT"String"(3))', str ! ユーザ定義派生型(User Defined Type)Stringの変数の値を3桁表示するという指定
このとき,文字列にダブルクオート,書式指定にシングルクオートを用いるようにしておけば,どちらを使えばよいか迷うことも,場所によって書き方が異なることも避けられる.
write/print
文では,割り当て済みの変数,実体化済みのインスタンス,pure
な関数(の戻り値)しか表示しない
特にgfotranを用いる場合,write/print
文で関数の戻り値を直接表示しようとすると,実行が停止する場合がある.これは,当該の関数の中でさらにwrite/print
文を用いている場合に必ず生じる1.
したがって,write/print
文で表示するのは,pure
である(ファイル入出力,画面表示,実行停止,引数の更新が存在しない)ことが保証されている変数,関数のみと定めることで,この問題を回避する.
Fortranは,例外を投げる仕組みがなく,エラーストップをしようにも,stop
文では文字列リテラルしか表示できないので,エラーハンドリングのために関数内でwrite/print
を用いる場面が生じる.次のFortran規格でstop
文で変数の値を表示できるようにはなるが,利用できるようになるにはまだ時間がかかると思われる.
CSVファイル形式で出力する場合は,書式に*
,:
を用いる
CSVファイルを出力する書式指定は,繰り返し指定を用いると末尾にカンマが残るなどの問題があり,意外とややこしい.
このような場合,データのある限り書式を繰り返す*
(unlimited repeat count)と,データが無くなれば表示を打ち切る:
(Colon descriptor)を用いると楽.(参考)
program main
use, intrinsic :: iso_fortran_env
implicit none
real(real64) :: x(5), y(5)
integer(int32) :: i
x = [(dble(i), i=1, size(x))]
y(:) = 1d0/x**2
do i = 1, size(x)
print '(*(g0:,","))', x(i), y(i)
end do
end program main
出力は
1.0000000000000000,1.0000000000000000
2.0000000000000000,0.25000000000000000
3.0000000000000000,0.11111111111111110
4.0000000000000000,0.62500000000000000E-001
5.0000000000000000,0.40000000000000001E-001
単位付きの値を返す手続きを使うときは変数名に単位を含める
Fortranには,経過時間を得るための手続きが複数あり,異なる単位の値を返す.
system_clock()
を用いると,ある基準時点からの経過がカウントという単位で得られる.カウントを用いて時間を測定する場合は,変数名末尾に単位_c
を付けている.
block
integer(int32) :: time_begin_c, time_end_c
integer(int32) :: CountPerSec ! 1秒あたりのカウント数
call system_clock(time_begin_c)
call sleep(1)
call system_clock(time_end_c, CountPerSec)
print *, real(time_end_c - time_begin_c)/CountPerSec, "sec"
end block
cpu_time()
を用いると,ある基準時点からのCPU時間が秒で得られる.時間の測定に用いる変数は,名前末尾に_s
を付ける.
block
real(real32) :: time_begin_s, time_end_s
call cpu_time(time_begin_s)
call sleep(1)
call cpu_time(time_end_s)
print *, time_end_s - time_begin_s, "sec"
end block
複数のoptional
引数を持つ手続は,複数の手続に分割して総称名で統一的に呼べるようにする
手続が複数のoptional
な引数を持つ場合,どの引数が渡されているかのチェックと,渡されている引数の組合せによる処理の記述が多くなる.
subroutine discretize(length, interval, number_of_grid_points)
implicit none
real(real64), intent(in), optional :: length
real(real64), intent(in), optional :: interval
integer(int32), intent(in), optional :: number_of_grid_points
! 1. 全ての引数が省略されている場合のチェックとエラー処理
! 2. どれか一つの引数しか渡されていない場合のチェックとエラー処理
! 3. lengthとintervalが渡されている場合の,値の正常性チェックと処理
! 4. lengthとnumber_of_grid_pointsが渡されている場合の,値の正常性チェックと処理
! 5. intervalとnumber_of_grid_pointsが渡されている場合の,値の正常性チェックと処理
end subroutine discretize
このような場合は,可能であれば組合せに応じて手続を複数に分割し,総称名を利用して統一的に呼べるようにする.
interface discretize
procedure :: discretize_using_length_and_interval
procedure :: discretize_using_length_and_number_of_grid_points
procedure :: discretize_using_number_of_grid_point_and_interval
end interface
contains
subroutine discretize_using_length_and_interval(length, interval)
implicit none
real(real64), intent(in) :: length
real(real64), intent(in) :: interval
end subroutine discretize_using_length_and_interval
subroutine discretize_using_length_and_number_of_grid_points(length, number_of_grid_points)
implicit none
real(real64), intent(in) :: length
integer(int32), intent(in) :: number_of_grid_points
end subroutine discretize_using_length_and_number_of_grid_points
subroutine discretize_using_number_of_grid_point_and_interval(number_of_grid_points, interval)
implicit none
integer(int32), intent(in) :: number_of_grid_points ! discretize_using_length_and_number_of_grid_pointsとは異なる引数の順序にしないと
real(real64), intent(in) :: interval ! 総称名を使ったときに区別ができないとエラーがでる
end subroutine discretize_using_number_of_grid_point_and_interval
ただし,総称名は,引数の型とその順序で手続を区別するので,同じ引数(例えば全てがinteger(int32)
)の場合には適用できない.
型束縛手続きの定義は,コンストラクタ・デストラクタ,セッター・ゲッター,任意の手続,演算子のオーバーロードの順
Fortranは,クラスの定義に型束縛手続きの名前と属性を列挙し,実装は同一モジュール内のcontains
以下に書く.
module type_vector_2d
type :: vector_2d_type
real(real64) :: x, y
contains
procedure, public, pass :: construct
end type vector_2d_type
contains
subroutine construct(this)
...
end subroutine construct
end module type_vector_2d
このとき,型束縛手続きは,コンストラクタ・デストラクタ,セッター・ゲッター,演算子をオーバーロードするための手続,その他の処理に分けることができる.
クラスの定義時に属性と名前を列挙する順番は,使用順になることが望ましい.つまり,派生型を用いる場合,
- クラスを宣言し,コンストラクタを呼び出すことで実体化する.
- デストラクタはコンストラクタと対になるので,コンストラクタの次に列挙する.
- コンストラクタ内からセッターが呼ばれたり(特に継承している場合),実体化した後にセッターが呼ばれて値が設定される場合がある.
- ゲッターはセッターと対になるので,セッターの次に列挙する.
- クラスに束縛された手続を用いて,所望の処理を実行する.
演算子をオーバーロードするための手続は,その手続名で呼ばれることはなく,演算子(+, -, *, /, =
)あるいはユーザ定義演算子(.orht., .div., .grad.
)を介して内部的に呼ばれる.そのため,手続名はあまり意識せず,どの演算子がオーバーロードされているかが関心の対象となる.
そのため,最後にまとめて列挙することで,演算子を把握しやすくする.
genericは分類ごとにまとめて書く
Fortranの型束縛手続きは,generic
を利用することで総称名のように同じ名前で呼べるようにできる.
複数のコンストラクタをgeneric
でまとめる場合,分類ごとに列挙が終わった後にまとめて書く.
generic
文はコンストラクタの名前と属性を列挙し終わった直後(デストラクタを列挙する前)にまとめて書く.同様に,セッターをgeneric
でまとめる場合,セッターの名前と属性を列挙子終わった後(ゲッターを列挙する前)にまとめて書く.
type :: vector_2d_type
...
contains
procedure, public, pass :: construct_xxx
procedure, public, pass :: construct_yyy
procedure, public, pass :: construct_zzz
generic :: construct => construct_xxx, construct_yyy, construct_zzz
procedure, public, pass :: destruct
final :: finalize
procedure, public, pass :: set_xxx_aaa
procedure, public, pass :: set_xxx_bbb
generic :: set_xxx => set_xxx_aaa, set_xxx,bbb
procedure, public, pass :: get_aaa
procedure, public, pass :: get_bbb
! 演算子とバインドしない
procedure, public, pass :: magnitude
! ユーザ定義演算子
! 演算->判定の順
procedure, public, pass :: compute_dot
procedure, public, pass :: compute_cross
procedure, public, pass :: is_orthogonal
generic :: operator(.dot.) => compute_dot
generic :: operator(.cross.) => compute_corss
generic :: operator(.orth.) => is_orthogonal
! 既存の演算子
! 代入,加減乗除,比較(==, /=, >, >=, <, <=)の順
procedure, public, pass :: assign
procedure, public, pass :: assign_array
...
generic :: assignment(=) => assign, assign_array
generic :: operator(+) => ...
generic :: operator(-) => ...
generic :: operator(*) => ...
...
end type vector_2d_type
既存の演算子をオーバーロードする型束縛手続きは,代入,加減乗除,比較の順に並べる
実装に必要になる順に並べる.
例えば,加減乗除を行う演算子を実装しても,
type(vector_2d_type) :: u, v, w
w = u + v
と書くためには代入演算子のオーバーロードが必要である.
/=
の実装は,==
の結果に.not.
を付けるだけでよい.同様に,==
と>
が実装されていれば,>=
は==
と>
の.or.
を取るだけで実現できる.
-
Intel Fortran等ではこのような問題は発生しない.今後のgfortranのバージョン更新によって,問題が解決されるだろうと期待する. ↩