9日目「関数・サブルーチン・モジュールを作ろう」

能書き

プログラムが長くなれば長くなるほど保守作業は困難になります。
プログラムの一部を「サブルーチン」や「関数」として 本体から分離することで、より見やすく、手の加えやすい「応用の利く」プログラムになります。
program test
 !------------------------
 ! Fortran template
 !------------------------
 use bacs
 implicit none
 !--- main ---
  ...
 
 !--- internal procedures ---
 contains
 
 function f1(...)
  ...
 end function f1

 subroutine sub1(...)
  ...
 end subroutine sub1

end program
使い方は、こちらを復習して下さい。
Lesson 25. ユーザー定義関数

定義:関数の定義

 contains
以下で、
 function 関数名([仮引数1],[仮引数2],...)
  型 :: 関数名
  [型 :: 仮引数名]
   関数名 = {式}
 end function

学習

『sin(x)』のような「関数」を自分で作ることができます。
実際の用途としては、
何度も出てくる式をまとめる
複雑な処理を本体から分離する
組み込み関数にない特殊関数を作る
などがあります。 まずは簡単な例。
 real(kind=double) :: a,b
 
 a = 1.0d0
 b = 2.0d0
 print *, "a+b^2 =", myFunc(b,a)
 

 contains !--- internal procedures ---
 
  function myFunc(a,b)
   implicit none
   real(kind=double) :: a, b, myFunc
     myFunc = a*a +b
  end function
 
 end program
このように、
containsとend programの間にもう一つプログラムを書く
(ただしprogramの代わりにfunction) というのが基本的な考え方です。
functionに特有の性質として、
『関数名』をfunction内では変数として扱う(要:型宣言)
end functionの時点で『関数名』に入っている値が結果として返ってくる
ことを覚えておきましょう。

学習:仮引数と局所変数

関数『function myFunc(a,b)』のかっこ中の変数『a,b』のことを、 「仮引数」と言います。
「引数」は「ひきすう」と読む。
ここには、本体で使われたときにかっこの中に入っている値、 すなわち『b,a』の値(こちらを「実引数」と言う)が、「対応する順番で」入ります。
「対応する名前の変数」ではありません!混乱しないように。
すなわち上のプログラムの実行結果は「a*a+b=3」ではなく
 a+b^2 =     5
となります。
ここで、
関数中で宣言された変数と、本文中の変数とは無関係(独立)
というのがポイントです。 このように、外部から直接見ることができず、 そのプログラム単位内だけで使われる変数のことを「局所(ローカル)変数」と言います。
変数名の競合に気を遣わなくて良いので、 そのままコピーペーストで他のプログラムに使い回すことができます。
ただし『関数名』がかぶるとダメです。 カッコが配列とも紛らわしいので、「関数っぽい」名前を心がけましょう。
逆に
本文の変数は、関数中で宣言し直さなければそのまま使える
のですが、用途が限定される(普通の意味での「関数」ではなくなる)ので
関数内で使う変数は引数として受け渡す
のが基本だと思って下さい。

問題:階乗の関数を作りなさい

そういうわけで、 「整数 n を受け取り、n! を実数で返す」関数を作ってみましょう。 >>解答
Lesson 26. サブルーチン

定義:サブルーチンの定義

 contains
以下で、
 subroutine サブルーチン名[(仮引数1,仮引数2,...)]
  [型, 引数授受特性 :: 仮引数名]
   {処理}
 end subroutine
*引数授受特性
 『intent(in)』 入力引数(上書き禁止)
 『intent(out)』 出力引数(値読み込み禁止)
 『intent(inout)』 入出力引数(読み書き自由)
入力と出力を明確に区別することで、人間にもコンパイラにもわかりやすくなります。
デフォルトはinout。ちなみにFortranの引数は参照(アドレス)渡しです。

学習

関数が「いくつかの引数を受け取り、1つの値を出す」のに対し、
サブルーチンは「受け取った引数に対して処理を行う」もの(出力はいくつでも良い)です。
外部とやりとりする必要のある変数を引数にした、独立プログラム
と考えたほうがいいかもしれません。
理屈の上では、すべての「処理」をサブルーチンに置き換えることが可能です。
極端な話、プログラム本体を丸ごとサブルーチンにしてしまったり、 1行1行をサブルーチンにすることも可能です(>>例)。
実際にサブルーチンを使うべき場面としては、
何度も出てくる処理をまとめる(一般的)
一連の処理をまとめて見やすくする
処理を論理的に分離する(上級)
などがあります。
使いこなすには、センスが必要です。好みの問題とも言えます。

例題

「2つの変数の値を入れ替える処理」をサブルーチンにします。
 real(kind=double) :: x, y
 
 x = zero
 y = one
 call swap(x,y)
 print *, "x =", x, ", y =", y
 call swap(x,y)
 print *, "x =", x, ", y =", y
 
 !--- internal procedures ---
 contains
 
  subroutine swap(a,b)
  implicit none
  real(kind=double), intent(inout) :: a, b
  real(kind=double) :: temp
    temp = a
    a    = b
    b    = temp
  end subroutine swap
 x = 1.0000000000, y = 0.0000000000
 x = 0.0000000000, y = 1.0000000000
この場合、
同じ事を2回書くより簡潔で、修正・デバッグが簡単になった
「3つの代入文」が、明確な役割を持つ「1つの処理」になった
本体と無関係な一時変数『temp』を本体から分離した
のがポイントです。
ちなみに、「代入文3つで値を入れ替える」というのは定石です。覚えておきましょう。
Lesson 27. モジュール

定義:モジュールの作成

本体とは別のファイル"モジュール名.f90"を作り、
 module モジュール名
 [use 他のモジュール]
 implicit none
 [変数の宣言]
 
 contains

 [subroutineやfunction]

 end module
という形式にする。
混乱を避けるため、ファイル名とモジュール名は揃えておいた方が良い。
モジュール本体(containsの上)に具体的な命令を書き込むことはできない
また、
相互参照するモジュールは作れない
3つ以上で循環参照もダメ。上下関係の厳しい世界なのだ。

モジュールのコンパイル

以下の3通りから、好きな方法でやると良い。
1.通常のコンパイル(堅実だがコンパイルに時間が掛かる)
$ ifort モジュール名.f90 本体.f90
モジュールを左に書くこと。複数ある場合は、use「される」モジュールをuse「する」モジュールより左側に書く。

2.分割コンパイル(一手間多いがコンパイルが速い)
はじめに一回だけ
$ ifort -c モジュール名.f90
$ ifort モジュール名.o 本体.f90
本体を書き換える度に(書き換えられていない)モジュールをコンパイルし直す必要がないのが利点。
モジュールを書き換えた場合は、もちろんモジュールからコンパイルし直して下さい。

3.make(本格的に計算するなら)
makefileという一種のスクリプトを書き、コンパイルする。
$ make
モジュールの数が多くなったらこれに頼るべき。(makeの説明

学習:モジュールの役割

モジュールそのものはただの「器」に過ぎません。中に何を入れるかによって、その役割が変わってきます。 例えば以下のような使い方が出来ます。
・共通の定数をまとめて置いておく
大槻先生の"KindNumbers"、私の"bacs"がこれにあたります。
・汎用的な(他のプログラムでも使える)ルーチンを独立させる
上の例の『swap』をモジュールに入れるとこれです。
・汎用的な機能の集合体(パッケージ)
私の"ranpack"などがこれです。
・プログラム本体から分離された関数、サブルーチンを、別ファイルにする
単に本体が長くなるのが嫌なときに。
・共通の変数をまとめて置いておく
いわゆるグローバル変数。私としては非推奨。

ある程度のプログラマになると、モジュールを使ってプログラムをうまく設計することが求められます。
が、皆さんは「サブルーチンの置き場所」くらいにとらえておけば十分です。

学習:副プログラムのパターン

実際に副プログラムを使う際の構成としては、
(1)本体の『contains』以下に、関数やサブルーチンを書き込んで使う(単一ファイル)
(2)新しくモジュールを作り、その『contains』以下に書き込む。本体でモジュールを『use』して使う(複数ファイル)
の2つが考えられます。まずは(1)で作り、必要な場合だけ(2)の形にするのが良いでしょう。
モジュールにすると、変数の扱いに多少注意が必要です。

*注意
古いFORTRANの慣習では、全ての関数、サブルーチンは「外部手続き」として単独で別ファイルにすることになっています。
複数ファイル(ルーチン)をまとめた「サブルーチン群」になっている場合もあります。清水研で使われていた"rironken.for"など。
が、Fortran90の仕様上、この方法は非推奨です(>>例)。 Fortran90(以降)世代の皆さんは、作ったサブルーチンを
必ず『program』か『module』の『contains』に入れるようにしましょう。
他人からもらったサブルーチンも、モジュールに入れてから使うようにした方が安全です。

実践:モジュール化の例

いつか見たプログラムをモジュールを使って書くと以下のようになる。
"hontai.f90"
program hontai
 use bacs
 use module1
 implicit none
 complex(kind=double), allocatable  :: Amat(:,:), Bmat(:,:), Cmat(:,:)
 integer :: m,n

  m = 2
  n = 3
  allocate(Amat(m,n), Bmat(m,n), Cmat(m,m))

  ! --- define matrices ---   注:ここの値は適当に決める。
  Amat(:,:) = cOne    ! cOne  = (1.0d0, 0.0d0) をAmatの全ての要素に代入
  Bmat(:,:) = cZero   ! cZero = (0.0d0, 0.0d0) をBmatの全ての要素に代入 
  Bmat(1,1:n) = img   ! img   = (0.0d0, 1.0d0) を1行目に入れる

  call matpro_ND(m,n,Amat,Bmat,Cmat)
 
  ! --- print C matrix ---   ! ループを使ってうまく表示してみよう。
  print *, "Real part of C matrix"
  print *, dble(Cmat)        ! 実数部分を(倍精度で)取り出す
  print *, "Imagnary part of C matrix"
  print *, aImag(Cmat)       ! 虚数部分を取り出す

 end program
"module1.f90"
program module1
 use bacs
 implicit none
 
 contains
  
  subroutine matpro_ND(m,n,A,B,C)
  implicit none
  integer,              intent(in)  :: m, n
  complex(kind=double), intent(in)  :: A(m,n), B(m,n)
  complex(kind=double), intent(out) :: C(m,m)
  integer :: j,k
   Cmat(:,:) = cZero
   do j=1,m
      do k=1,n
         Cmat(1:m,j) = Cmat(1:m,j) + Amat(1:m,k)*conjg(Bmat(j,k))
      end do
   end do
  end subroutine
  
 end module
今、サブルーチン『matpro_ND』は純粋に数学的な処理なので、本体側でどのような問題を扱っているかに依存しない。
こういう場合はモジュールとして分離するのが望ましい。

謝辞

Fortran実用編はここまでです。
プログラミング言語としてのFortranは、ほぼ学び終えたと考えて良いでしょう。 あとは実際に自分の手でプログラムを組んでみて、技術としてのプログラミングを学ぶだけです。
わからないことが出てきたら、ネットで調べるなり人に聞くなりしましょう。
基礎が出来ていれば、何が分からないか・何を調べればよいか自分でわかりますね。
また、私(著者)に質問すれば、その内容がこの講義ノートにも反映されることになります。遠慮無く質問して下さい。

ちなみに・・・プロの計算屋を目指すなら、より高度な技法を身につける必要があります。
もし希望するのであれば、個人的に修行をつけます。ご相談下さい。