副プログラム(サブルーチン)
大きなプログラムは機能的にまとまったいくつかの「部品」に分割すると作りやすくなります. このような部品のことを副プログラム (subprogram) といいます. Fortran の副プログラムには「関数」と「手続き」の二種類があります.
関数
これまでにもいくつか組込み関数を利用してきましたが,関数は自分でも作成することができます.
簡単な例として,2つの整数を受け取り,和の値を返す関数 add を作ってみましょう. 以下のプログラムは関数 add の定義と,add を利用するメインプログラムの2つの部分から成っています.
!------------------------------------------------------------ ! 和を返す関数 !------------------------------------------------------------ integer function add(p, q) ! 関数の型 function 関数名(引数リスト) implicit none ! おまじない integer :: p, q ! 仮引数の型 add = p + q ! 関数名に値をセット return ! 呼び出し元に値を返して終了 end function add ! end function 関数名 !------------------------------------------------------------ ! メインプログラム !------------------------------------------------------------ program work0801 implicit none integer :: nx, ny integer :: add write(*,*) 'Input two integers:' read(*,*) nx, ny write(*,*) add(nx, ny) stop end program work0801
関数の定義
関数 add の構造を詳しくみてみましょう.
■ integer function add(p, q)
関数は function 文で定義します. function 文は次のように書きます.
関数の型 function 関数名(a1, a2, ..., an)
「関数の型」には関数が返すデータ型を指定します.
関数 add は整数を返すので関数の型は integer
となります.
a1
, a2
, ..., an
は関数が受け取る「仮引数」 (dummy argument) と呼ばれます.
仮引数が複数個あるときはカンマ ,
で区切って並べます.
引数を持たない関数を定義することもできます.
関数 add では p, q が仮引数になります.
メインプログラム中では add(nx, ny)
と呼び出されているので,実行時に
p, q は nx, ny の値を持つことになります.
メインプログラム側の引数のことを「実引数」 (actual argument) といいます.
■ integer :: p, q
仮引数の型を宣言します.
■ add = p + q
関数名に値を代入すると戻り値(関数の値)が設定されます. この代入は関数の中で何度行っても構いませんが,最後に代入された値が関数の値となります.
■ return
関数を終了し,関数の呼び出し元(この例ではメインプログラム)に制御を戻します. return 文が end 文の直前にあるときは省略することができます.
■ end function add
「 end function 関数名
」で関数定義の終わりを示します.
メインプログラムの注意点
関数 add をメインプログラムや他の関数から利用する場合は,関数 add が整数型の関数であることをコンパイラに教えてあげなくてはいけません. 宣言方法は変数宣言の場合とまったく同様です.
integer :: add
組込み関数の場合はコンパイラが関数の型を知っているので宣言する必要はありません.
練習問題 (1) — 2数の最大値
work0801 を書き換えて,2つの整数を受け取り,大きい方の値を返す関数 max2 を作れ.
練習問題 (2) — 3数の最大値
work0801 を書き換えて,3つの整数を受け取り,最大値を返す関数 max3 を作成せよ.
階乗
0 以上の整数 n を受け取り,階乗 n! の値を返す関数 fact を考えてみましょう. 整数型では値の範囲が狭いので,関数の型を real(8) 型とします.
!------------------------------------------------------------ ! 階乗 n! !------------------------------------------------------------ real(8) function fact(n) implicit none integer, intent(IN) :: n ! 仮引数 ! local integer :: i ! ローカル変数(関数の中だけで使う作業変数) fact = 1.0d+0 do i = n, 1, -1 fact = fact * i end do return end function fact !------------------------------------------------------------ ! メインプログラム !------------------------------------------------------------ program work0804 implicit none real(8) :: fact integer :: n do n = 0, 20, 2 write(*,'(I5,E20.12)') n, fact(n) end do stop end program work0804
仮引数の有効範囲
ある関数で宣言された変数は,基本的にその関数の中だけで有効です. 他の関数やメインプログラムに同じ名前の変数があっても,まったく別のものとして扱われます.
この例では n という変数がメインプログラムと関数 fact の両方に現れていますが,関数の仮引数は関数の中だけで有効で,メインプログラムの n とは何の関係もありません. この性質があるため,関数の設計はメインプログラムや他の関数とまったく独立に行うことができます.
ただし,Fortran には「仮引数の値を変更すると呼び出し元の実引数の値も変更される」という決まりがあります. 不用意に仮引数の値を変更しないように注意が必要です. (以下の「仮引数の値の変更」を参照してください.)
手続き
値を返さない副プログラムを「手続き」(サブルーチン)といいます. 手続きを使うといくつかの処理をひとまとめにすることができ,プログラムの見通しがよくなります.
例として,呼び出された回数を画面に出力する手続き count を作ってみましょう.
!------------------------------------------------------------ ! 呼び出された回数を表示する !------------------------------------------------------------ subroutine count() ! subroutine 手続き名(引数リスト) implicit none integer, save :: n = 1 ! ローカル変数の宣言 write(*,*) n n = n + 1 return ! 呼び出し元に戻る end subroutine count ! end subroutine 手続き名 !------------------------------------------------------------ ! メインプログラム !------------------------------------------------------------ program work0805 implicit none call count() call count() call count() stop end program work0805
このプログラムを実行すると次のようになります.
% ./a.out 1 2 3
手続きの定義
手続きは subroutine 文で定義します. 手続きの構造は関数とほとんど同じですが,値を返さないので subroutine に型はありません.
subroutine 手続き名(a1, a2, ..., an) implicit none : return end subroutine 手続き名
手続きの呼び出し
手続き副プログラムは call 文によって呼び出します.
call 手続き名(引数リスト)
引数がない場合は,引数リストを省略し,次のように呼び出すこともできます.
call 手続き名() call 手続き名 ! () を省略することも可能
save 属性
副プログラム(関数と手続き)の中で宣言された変数や配列の内容は,値を設定する前は「未定義」です. また,副プログラムの中でいったん値を設定しても,return 文または end 文によって副プログラムを終了した時点で再び未定義となり,次にまたその副プログラムが呼ばれても前回の値は残っていません. (残っていることもありますが,それはたまたまです.)
ただし,型宣言のときに save 属性を付ければ,変数の値を副プログラムの実行後も保持しておくことができます.
サブルーチン count では変数 n に save 属性が付けられており,初期値 1 が設定されています. したがって,count の動作は次のようになります.
- 1回目
write(*,*) n
の実行時の n は 1.次行のn = n + 1
によって値が 2 に更新される.- 2回目
write(*,*) n
の実行時の n は 2.次行のn = n + 1
によって値が 3 に更新される.- 3回目以降
- …
仮引数の値の変更
Fortran には「副プログラムの中で仮引数の値を変更すると,呼び出し元の実引数の値も変更される」という重要な規則があります(これを引数の「参照渡し」といいます). 次のプログラムでは手続き twice の中で仮引数の値が2倍されます. 結果的に呼び出し元の実引数の値も2倍されることを確認して下さい.
!------------------------------------------------------------ ! 仮引数を2倍する !------------------------------------------------------------ subroutine twice(n) implicit none integer, intent(INOUT) :: n n = n * 2 return end subroutine twice !------------------------------------------------------------ ! メインプログラム !------------------------------------------------------------ program work0806 implicit none integer :: n = 10 write(*,*) 'Before: ', n call twice(n) write(*,*) 'After : ', n stop end program work0806
このプログラムを実行すると次のようになります.
% ./a.out Before: 10 After : 20
手続き twice の実行の前後で,メインプログラム中の変数 n の値が 10 から 20 に変化したことに注意してください.
注意
twice の引数に定数を与えるとエラーになります.
次の例では twice の中で 10 = 10 * 2
という無効な代入文が実行されることになります.
call twice(10) ! Bus error
引数の変更は慎重に行いましょう.
引数を通じて値を返す
引数の「参照渡し」を利用すると,手続きと呼び出し元の間でデータのやり取りをすることができます. 関数は呼び出し元にひとつの値しか返せませんが,引数を利用すると,複数の値を同時に返すことができるわけです. 次のプログラムは与えられた配列 x の最大値と最小値を同時に返す手続き副プログラムです.
!------------------------------------------------------------ ! 配列 x(1:n) の最小値 xmin と最大値 xmax を返す !------------------------------------------------------------ subroutine minmax(n, x, xmin, xmax) implicit none integer, intent(IN) :: n ! 配列 x の寸法 real(8), intent(IN) :: x(n) ! 配列 x real(8), intent(OUT) :: xmin, xmax ! x の最小値と最大値 xmin = minval(x(1:n)) xmax = maxval(x(1:n)) return end subroutine minmax !------------------------------------------------------------ ! メインプログラム !------------------------------------------------------------ program work0807 implicit none integer, parameter :: nmax = 5 integer :: i real(8) :: vec(nmax) = (/ (i, i = 1, nmax) /) real(8) :: vmin, vmax write(*,'(5F12.7)') vec(1:nmax) call minmax(nmax, vec, vmin, vmax) write(*,'(2F12.7)') vmin, vmax stop end program work0807
配列の渡し方
配列を副プログラムの仮引数にする場合,配列の寸法も仮引数として副プログラムに渡すことによって,いろいろなサイズの配列に対応することができます. これを「整合配列」といいます.
仮引数の intent 属性
副プログラムの中で仮引数の値を設定(変更)すると,呼び出し元の実引数の値も変更されます. このような動作を「副作用がある」ということがあります. 意図しない値の変更(うっかりミス)を防ぐため,Fortran 90 以降では副プログラム中での仮引数の取り扱いを intent 属性によって指定することができます.
intent 属性には次の3種類があります.
intent(IN) ..... 仮引数が副プログラム中で参照されるだけであり,値は変更されない. intent(OUT) .... 仮引数を参照する前に副プログラム中で必ず値を設定しなければならない. 仮引数によって呼び出し元から情報を受け取るのではなく,副プログラム から呼び出し元に情報を受け渡す場合に用いられる. intent(INOUT) .. 副プログラム中での仮引数の参照・値の設定に特に制限を設けない.
intent 属性の省略
intent 属性は省略することができます. その場合は intent(INOUT) が指定されたものとみなされます.
自動配列
Fortran 90 以降では副プログラムのローカル変数(その副プログラム内でのみ参照可能な変数)として「自動配列」 (automatic array) を使うことができます. 自動配列の寸法は副プログラムに引数として与えられた整数などを使って指定します.
次の例は与えられた配列を交換する手続きです.
subroutine swap(n, a, b) implicit none integer, intent(IN) :: n real(8), intent(INOUT) :: a(1:n), b(1:n) ! local real(8) :: work(1:n) ! 自動配列の宣言 work = a a = b b = work return end subroutine swap
自動配列の記憶領域は,副プログラムの実行開始時に確保され,終了時に解放されます. したがって,自動配列に save 属性をつけることはできません.
割り付け配列 (allocatable array)
自動配列の代わりに「割り付け配列」 (allocatable array) を使うこともできます. 割り付け配列は変数宣言時に配列の階数 (rank) だけを指定しておき,必要な大きさのメモリを実行時に割り付けることができます.
subroutine swap(n, a, b) implicit none integer, intent(IN) :: n real(8), intent(INOUT) :: a(1:n), b(1:n) ! local real(8), allocatable :: work(:) ! 割り付け配列の宣言 allocate(work(1:n)) ! メモリ確保 work = a a = b b = work deallocate(work) ! メモリ解放 return end subroutine swap
自動配列 or 割り付け配列?
割り付け配列には allocate, deallocate が必要になる点を除くと,自動配列と割り付け配列の機能はほぼ同じです. 副プログラムの中ではどちらを使うべきでしょうか?
自動配列はメモリの「スタック」と呼ばれる領域を使用する場合が多いのに対し,割り付け配列はメモリの「ヒープ」領域を使用する場合が多いとされています. このため,割り付け配列よりも自動配列のほうがメモリ割り付けが高速であると期待されます. しかしながら,使用可能なスタックのサイズには制限がある場合が多く,自動配列で使用できる配列サイズが制限されることがあります.
したがって,まず自動配列を試してみて,スタック不足が起こるようなら割り付け配列に切り替えるのがよさそうです.
ソースファイルの分割
機能的に独立した副プログラム(関数やサブルーチン)ごとにソースファイルを分けておくと,プログラムの再利用が簡単になります.
以下の例では,階乗を計算する関数 fact() だけを含むソースファイル fact.f90 と,それを利用する主プログラムのソースファイル main.f90 を別々に作成し,この2つのソースファイルから実行可能ファイル a.out を作っています. fact.f90 や main.f90 といったファイル名は好きに付けることができますが,ファイルに含まれる副プログラムや主プログラムの名前と同じにしておくと分かりやすいでしょう.
fact.f90 の内容
real(8) function fact(n) implicit none integer, intent(IN) :: n integer :: i fact = 1 do i = 2, n fact = fact * i end do return end function fact
main.f90 の内容
program main implicit none real(8) :: fact write(*,*) '20! = ', fact(20) stop end program main
コンパイル方法
複数のソースファイルをコンパイルするにはいくつか方法がありますが,一番簡単なのは必要なソースファイルをすべてコンパイラに渡してしまうことです. こうすればあとは勝手にコンパイラが処理してくれます.
% gfortran fact.f90 main.f90 % ./a.out 20! = 2.4329020081766400E+018
本日の課題
1 から n までの和
整数 n を引数として受け取り,1 から n までの和を返す関数 sum1 を作成せよ. 和の計算には do ループを利用し, n = 1, 10, 100 に対して検算を行うメインプログラムも作成すること.
メッセージの表示
画面に hello と表示する手続き型サブルーチン print_hello
を作成せよ.
チャレンジ問題
ルジャンドル多項式
n 次のルジャンドル多項式 Pn(x) を返す関数 lp(n, x)
を作成せよ.
[-1, 1]
の実数 x に対して Pn(x) は次の漸化式で定義される.
P0(x) = 1 P1(x) = x Pn(x) = [ (2*n-1) * x * Pn-1(x) - (n-1) * Pn-2(x) ] / n (n = 2, 3, ...)
解答例のプログラムを実行すると x, P1(x), P2(x), P3(x), P4(x) の値が表になって出力される. 適当なデータファイルに結果を保存し,Gnuplot などでグラフを描いてみよ. P2(x) のグラフをプロットするには次のようにする.
% ./a.out > lp.dat % gnuplot gnuplot> plot 'lp.dat' using 1:3 w l
関数 lp() だけを含むソースファイル lp.f90 と,lp() を利用してデータを出力する主プログラム lp_main.f90 に分けておくと,lp() を再利用するとき便利である.