LoginSignup
74

More than 1 year has passed since last update.

[WIP]Fortranをコーディングする際に気をつけていること

Last updated at Posted at 2020-01-22

概要

モダンFortranにはコーディングルールが見つからないとよく言われます.有名な所では、気象庁のFortran 標準コーディングルールなどがありますが、これも今のFortranには馴染まないところもあります.

個人的な経験に基づく内容ですが,著者自身が気をつけていることをダラダラと書いて,情報整理のきっかけになればと思いました.全てをまとめるには長大な時間がかかりそうですが,勢いで書けるところから書いていきます.

Fortranを使うときに心掛けること

  1. 自制心を養う
  2. サポートの強力なエディタを使う

暗黙の型宣言

無効にする以外の選択肢はない.宣言部冒頭に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_envuseして,そこで定義されている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バイトの場合,integerinteger(int32)select typeで同じ型と認識される.

浮動小数点数の精度を切り替える場合は,spdpというパラメータを定義してコンパイルオプションで切り替えることが行われているが,それが必須でない場合は標準で定められた定数を使う方が好ましい.sp, dp, qpで統一した方が楽だとも思うが・・・

logicalも整数と同様にlogical8などのkindが定義されているが,真偽の2値しか扱わないので,logicalでよい.整数型の標準サイズと同じになり,おそらくその処理系が最も高速に処理してくれるサイズになっている.

enumを使う場合は,enumeratorの接頭辞としてその列挙されるものの名前を付ける

iso_c_bindiguseすることで,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文字で宣言するのは好ましくないので,物理定数を宣言するモジュール内で正確な名前を付けて,参照する際によく使う記号と対応付ける.

phys_constant.f90
module Phys_Constant
    use,intrinsic :: iso_fortran_env
    implicit none

    real(real64),parameter :: GravitationalAcceleration(*) = [0d0, 0d0, -9.8d0]
end module Phys_Constant
main.f90
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_constantstrigonometric.f90のモジュール名はmath_functions_trigonometricとする.

ただし,Fortranには規格上エンティティの名前は63文字と決まっているので,フォルダ階層が深くなるとこの制限に引っかかる場合がある.その場合は略語を適切に決め,プロジェクト内で共有する.例えば,constantsconstと省略し,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

等とする.intentvalueは同じ重要性を持っており,optionalはそれより使用頻度が少ないので,後ろに付ける.しかし,intentvalueoptionalも仮引数用の属性であるため,integer(int32),allocatableのように,通常の変数宣言にも用いる属性をまとめて記述する.

以下の様に長々と書くこともできる.(確認していないので,もしかしたらコンパイルが通らないかも知れない)

integer(int32),public,allocatable,save,volatile,target :: hoge(:)

これは,

  1. “整数型の配列を外部に公開”という意味を重要視し,
  2. その配列は動的に割り付けられ,
  3. その状態は手続を抜けても保持される.
  4. また,手続外部から変更される可能性があり,
  5. pointeeでもある.

という順番で読む.使用頻度的にはtarget,volatileとするべきだとも思うが,target,volatileだとpointeeであることがvolatileなのかと誤解を与えかねない.また,Fortranにおけるpointerの使い方において,target属性は,pointerと結合しようとしてコンパイルエラーが発生し,target属性が必要である事に気付いて追記されるので,最後が相応しいと考える.

変数の属性の順序

  1. 型名 integer, real, character, type
  2. dimension(ポインタ変数の場合)
  3. アクセサprivate, public
  4. アクセサprotectedpublicの場合)
  5. parameter(定数の場合)
  6. allocatable, pointer
  7. contiguouspointerの場合)
  8. save
  9. intent, value(仮引数の場合)
  10. optional(仮引数の場合)
  11. volatile
  12. 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.を取るだけで実現できる.

  1. Intel Fortran等ではこのような問題は発生しない.今後のgfortranのバージョン更新によって,問題が解決されるだろうと期待する.

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
74