IT pass HikiWiki - [itbase2020]Fortran 実習 サブルーチンと関数 Diff

  • Added parts are displayed like this.
  • Deleted parts are displayed like this.

{{toc}}

#プログラムが長くなると・複雑になると, プログラム全体を
#ひとつのまとまりとして扱うのが難しくなります.
#例えば, たくさんの処理で構成されるプログラムに間違いがあった時,
#その間違った箇所を特定するのも難しくなるでしょう.
#あるいは, エディタ (例えば emacs) でプログラムのファイルを開いたとき,
#長いプログラムの中の間違った箇所に移動するのにも時間がかかるかもしれません.
#また, 一つのプログラムの中で, 同じような処理を何度も繰り返すことが
#良くありますが, そんなときに同じような処理をする命令文を何度も書くのは
#とても苦痛であり, さらにそんなことでも間違ってしまう原因になります.
#
#このような時のために, プログラミング言語では, プログラムの単位を分けて
#(比較的) 小さなプログラムの集まりによって一つのプログラムを作る
#方法が用意されています.

ひとつの program 文にすべての処理を書くと扱いにくくなります.
そこで, Fortran では, プログラムを分けて作るために下の二つの仕組みが
用意されています.
* サブルーチン
* 関数
これらは, Fortran ではまとめて副プログラムと呼ばれています.
(program 文を含むプログラム単位を主プログラムと呼びます.)

ここでは, そんなサブルーチンと関数について実習してみましょう.


= サブルーチン

まず, 下のようなプログラムを summation3.f90 というファイル名で作成して実行してみましょう.

  program summation3

    implicit none

    integer :: i
    integer :: num

    ! 1 から 10 までの和の計算
    num = 0                     ! num の初期化
    do i = 1, 10
      num = num + i
    end do
    write( 6, * ) num

    ! 1 から 20 までの和の計算
    num = 0                     ! num の初期化
    do i = 1, 20
      num = num + i
    end do
    write( 6, * ) num

    ! 1 から 30 までの和の計算
    num = 0                     ! num の初期化
    do i = 1, 30
      num = num + i
    end do
    write( 6, * ) num

  end program summation3

このプログラムは, 3 つの和
* 1 から 10 までの和
* 1 から 20 までの和
* 1 から 30 までの和
を計算します.
このプログラムでは, それぞれの和を計算する命令文はほとんど同じで,
唯一の違いは和を取る数の最大値です.
その共通部分を抜き出して, 1 から n までの和として一般化してみると
下のように書くことができるでしょう.

    num = 0
    do i = 1, n
      num = num + i
    end do

そこで, この一般化した書き方を使って, サブルーチンを
使って書いてみましょう.


例えば, 上のプログラムは下のように書き直すことができます.
下のようなプログラムを summationsub.f90 というファイル名で作成して実行してみましょう.

  program summationsub

    implicit none

    integer :: num

    ! 1 から 10 までの和の計算
    call calcsum(10,num)
    write( 6, * ) num

    ! 1 から 20 までの和の計算
    call calcsum(20,num)
    write( 6, * ) num

    ! 1 から 30 までの和の計算
    call calcsum(30,num)
    write( 6, * ) num

  end program summationsub

  subroutine calcsum(n,num)  ! サブルーチンの定義

    implicit none

    integer, intent(in)  :: n
    integer, intent(out) :: num

    integer :: i

    num = 0                  ! num の初期化
    do i = 1, n
      num = num + i
    end do

  end subroutine calcsum

上の例では, 1 から n までの和の計算がサブルーチンとしてまとめられています.
そして, 主プログラムでは, そのサブルーチンを 3 回呼び出すことで三つの和を
求めています.


== サブルーチンの構造

サブルーチンは,

  subroutine <サブルーチン名> ( 引数1, 引数2, ... )

で始まり,

  end subroutine <サブルーチン名>

で終わる単位です.

そして, そのサブルーチンは, 主プログラム (や副プログラムから)

  call <サブルーチン名> ( 引数1, 引数2, ... )

として呼び出します.
引数とは, 副プログラムに渡す変数です.
サブルーチンは, 引数を使って呼び出し元から変数を受け取り, それを使って
処理し, 引数を使って処理結果を呼び出し元に戻します.
つまり, 引数の中には処理のための入力と出力の両方を含むことができます.
(引数が一つもないサブルーチンを作ることもできます.)

サブルーチンの構造をさらに詳しく見てみましょう.
サブルーチンは下のようになっています.

  subroutine <サブルーチン名> ( 引数1, 引数2, ... )

    implicit none

    [引数 1 の宣言文]
    [引数 2 の宣言文]
    ...


    [サブルーチン内で使う変数の宣言文]


    [命令文]


  end subroutine <サブルーチン名>

サブルーチンの引数にある変数は, その型や大きさ (配列の場合) などを
サブルーチンの中で宣言しなければいけません.
そして, サブルーチンを呼び出すときに渡す変数・数値の型は,
サブルーチンで宣言されている型と同じでなければなりません.


また, 引数以外でサブルーチンの中で使う変数も, サブルーチンの中で
宣言しなければいけません.
さらに, 上のような形でサブルーチンの中で宣言された変数は,
主プログラムの中では使えません. 逆に, 主プログラムの中で宣言された
変数はサブルーチンの中では使えません.


== intent 属性

上の例では, 引数の宣言文に intent(XX) で属性を付けています.
intent(XX) は, その引数が入力用なのか出力用なのか, その両方なのかを
指定します.

# RT
  delimiter = %

属性 % 意味 % 使用例
in % 入力用 % intent(in)
out % 出力用 % intent(out)
inout % 入出力用 %intent(inout)

つまり, 入力用の引数 (上の例では n) の値をサブルーチンの中で変更しようと
すると, コンパイル時にエラーが出ます.
また, 出力用の引数 (上の例では num) に値を代入せずに参照すると, コンパイル時にエラーが出ることもあります (gfortran では出ないようですが).

この intent の属性は必須ではありません (書かなくても動作します).
しかし, この属性は付けておくことを推奨します.
なぜならば, 入力用の引数でありながら, 間違ってサブルーチン内で値を
変更するプログラムを書いた場合に, コンパイラがエラーを出して教えて
くれるからです.


== 変数のスコープ

変数には「スコープ」という概念があって, 変数が有効となっている範囲を
意味します. 上の例のプログラムでは, サブルーチン calcsum の中で
宣言されている変数 i のスコープは, calcsum の中だけです.
したがって, 主プログラムでその変数 i を使うことはできません.
また, もし主プログラムの中で同じ名前の変数 i を定義したとしても,
その変数のスコープは主プログラムの中だけです.
そのとき, サブルーチンの中の変数と主プログラムの中の変数とは
独立で, 保持している値には何の関係もありません.

なお, 上の例では, calcsum の第二引数にある変数 num

  subroutine calcsum(n,「num」)  ! サブルーチンの定義

は, 主プログラムで calcsum を呼び出すときに与えられている引数の
変数 num と同じ名前です.

    call calcsum(10,「num」)

これは同じ名前である必要はありません.
例えば, calcsum を下のように書き換えても動作は変わりません.

  subroutine calcsum(n,val)  ! サブルーチンの定義

    implicit none

    integer, intent(in)  :: n
    integer, intent(out) :: val

    integer :: i

    val = 0                  ! val の初期化
    do i = 1, n
      val = val + i
    end do

  end subroutine calcsum



なお, 上の例では, 主プログラムである summationsub からサブルーチンを呼び
出していますが, サブルーチンからサブルーチンを呼び出すこともできます.

* 注意:

  サブルーチンはサブルーチンを呼び出すことができますが, このままではサブルーチンが自分自身を呼び出すことはできません.
  (つまり, calcsum の中で calcsum を call することはできません.)
  自分が自分を呼び出すことを「再帰的呼び出し」と呼びます.
  Fortran 90 では再帰的呼び出しが可能です. しかし, そのためにはサブルーチンの
  宣言にキーワードを付ける必要があります. これについてはここでは詳しく説明しません.
  興味があれば調べてみると良いでしょう.


== 配列を引数にする

引数には配列を与えることもできます.
下のようなプログラムを summationsubarr.f90 というファイル名で作成して実行してみましょう.

  program summationsubarr

    implicit none

    integer, parameter :: nn = 3
    real(8) :: vector1(nn), vector2(nn)
    real(8) :: val1, val2
    integer :: i

    do i = 1, nn
      vector1(i) = 2 * i
      vector2(i) = -3 * (nn-i)
    end do

    call calcsumarr(nn, vector1, vector2, val1, val2)

    write( 6, * ) val1
    write( 6, * ) val2

  end program summationsubarr

  subroutine calcsumarr(nn, vector1, vector2, val1, val2)

    implicit none

    integer, intent(in) :: nn              ! 引数の配列の大きさ
    real(8), intent(in) :: vector1(nn), vector2(nn)
    real(8), intent(out) :: val1, val2

    integer :: i

    val1 = 0
    do i = 1, nn
      val1 = val1 + vector1(i)**2
    end do
    val1 = sqrt( val1 )

    val2 = sqrt(  vector2(1)*vector2(1) &
                + vector2(2)*vector2(2) &
                + vector2(3)*vector2(3) )

  end subroutine calcsumarr

上の例のプログラムでは, 要素数 nn(=3) の 1 次元配列 vector1, vector2 をベクトルとみなし, それらをサブルーチンに渡して, サブルーチンの中でそれらのベクトルの大きさを計算しています.

このプログラムのように, 配列をサブルーチンに渡す時には, 呼び出す側とサブルーチンが同じ型で同じ大きさの配列を宣言しなければなりません.

同じようにして 2, 3, ... 次元配列もサブルーチンに渡すことができます.


=== 練習問題

上の例に挙げた summationsubarr.f90 を基にして, vector1 と vector2 をベクトルと見立てて, それらの内積と外積を計算し, 主プログラムに値を戻すプログラムを作りなさい.

ヒント
* 外積の計算結果はベクトル (3 成分を持つ) ですから, 結果のベクトルを納める配列を用意しましょう.
  * サブルーチンの中で外積を計算した結果を納めるための配列を用意するのと同時に, 呼び出し側でも同じ型で同じ大きさの配列を用意しなければなりません.
* 外積の各成分を計算しましょう.
  * do 文を使って計算することもできますが, よくわからなければ, 各成分ごとに計算してみると良いでしょう.
* ((<プログラム例|URL:http://itpass.scitec.kobe-u.ac.jp/~itbase/exp/fy2020/fortran_program/summationsubarr_mod.f90>))


= 関数

上で説明したように, サブルーチンは,

  call <サブルーチン名> ( 引数1, 引数2, ... )

の形式で呼び出すため, もしサブルーチンの中の処理結果を呼び出し元に
戻したいときには, 引数のどれかを出力用として用いることになります.
しかし, 何か値を計算するだけならば, 数学で記述するように,

  y = func( x )

といった形で, 出力を = で代入する書き方の方が分かりやすいものです.

このように書くための仕組みが「関数」です.
既に説明したように, Fortran には予め用意された「組み込み関数」が多数
用意されていますが, ここで説明するのはそれらとは異なり, ユーザが
定義する関数です.

下のようなプログラムを summationfunc.f90 というファイル名で作成して実行してみましょう.

  program summationfunc

    implicit none

    integer :: num
    integer :: calcsumfunc                ! 主プログラムの中で使う関数の宣言

    num = calcsumfunc(10)                 ! 1 から 10 までの和の計算
    write( 6, * ) num

    num = calcsumfunc(20)                 ! 1 から 20 までの和の計算
    write( 6, * ) num

    num = calcsumfunc(30)                 ! 1 から 30 までの和の計算
    write( 6, * ) num

  end program summationfunc

  function calcsumfunc(n) result(num)      ! 関数の(内容の)定義

    implicit none

    integer, intent(in) :: n

    integer :: num
    integer :: i

    num = 0
    do i = 1, n
      num = num + i
    end do

  end function calcsumfunc

このプログラムは, 上で説明した summationsub.f90 を関数を使って書き直したものです.
calcsumfunc は, 1 から n (引数) までの和を計算する関数になっています.


== 関数の構造

関数は,

  function <関数名> ( 引数1, 引数2, ... ) result( 戻り値 )

で始まり,

  end function <関数名>

で終わる単位です.

そして, その関数は, 主プログラム (や副プログラムから)

  変数 = <関数名> ( 引数1, 引数2, ... )

として呼び出します.
関数は, 引数を使って呼び出し元から変数を受け取り, それを使って
処理し, 戻り値を呼び出し元に戻します.
(引数が一つもない関数も作れます.)

関数の構造をさらに詳しく見てみましょう.
関数は下のようになっています.

  function <サブルーチン名> ( 引数1, 引数2, ... ) result( 戻り値 )

    implicit none

    [引数 1 の宣言文]
    [引数 2 の宣言文]
    ...

    [戻り値の宣言文]


    [関数内で使う変数の宣言文]


    [命令文]
    戻り値 = ...

  end function <関数名>


基本的な構造はサブルーチンと良く似ています.
ただし, 最後に戻り値として宣言された変数に, 呼び出し元に戻す
値を代入する必要があります.
また, 呼び出し元では, その関数を変数のように宣言しておく必要が
あることに注意しましょう.
上の summationfunc.f90 の例でいうところの,

    integer :: calcsumfunc                ! 主プログラムの中で使う関数の定義

の部分がそれに該当します.


= 複数のファイルを使ったプログラム

これまでに示した例のプログラムでは,
主プログラムの内容をサブルーチンや関数として分割しましたが, 作成した
サブルーチンや関数は主プログラムと同じファイルの中に書かれていることを
想定していました.
つまり, プログラムはすべて一つのファイルだけで完結していたことになります.
しかし, プログラムが長くなってくると,
長いファイルの中の目的とする編集点を探すのも手間になり,
一つのファイルにすべてを書いていることが効率を下げることも良くあるでしょう.

より使いやすい方法としては, サブルーチンや関数を主プログラム
とは別のファイルに書くことができます.
下のようにして試してみましょう.

上で作った summationsub.f90 を書き直して, 主プログラムとサブルーチンを
別のファイルに書いてみましょう.

つまり, 下の部分を summationsub.f90 から削除し, calcsum.f90 という新しい
ファイルに書き込みましょう.

  subroutine calcsum(n,num)  ! サブルーチンの定義

    implicit none

    integer, intent(in)  :: n
    integer, intent(out) :: num

    integer :: i

    num = 0                  ! num の初期化
    do i = 1, n
      num = num + i
    end do

  end subroutine calcsum

このとき, summationsub.f90 と calcsum.f90 の二つのファイルで
一つのプログラムを構成していることになります.

では, この二つのファイルで構成されるプログラムをコンパイルしてみましょう.
二つのファイルがないとプログラムとして完成しませんので, コンパイルには
下のように両方のファイルを指定しなければいけません.

  $ gfortran -o summationsub summationsub.f90 calcsum.f90

実行する手順は変わりません.

  $ ./summationsub


このように, プログラムを複数のファイルに分けることで, 例えば,
calcsum.f90 は別の主プログラムと組み合わせて使うこともできるようになります.
また, ここで説明した例では二つのファイルに分けましたが, もっと多くの
ファイルに分けてプログラムを作ることもできます.
その時には, コンパイル時にすべてのファイルを指定しなければいけませんので
注意しましょう.


== 補足: make

なお, 複雑で大規模なプログラムでは多数のファイルに分割して作ることが
良くあります.
場合によっては, ファイルごとに分担して多数の人が作ることになるかも
しれません.
そんなときには, コンパイルするためだけに非常に多くのファイルを指定しなければ
いけなくなります.
コンパイルするたびに長いコマンドを打つのはとても大変です.
そのようなときのための仕組みも用意されています.
それは, make という仕組みです.

なお, make は Fortran のためだけの仕組みではありません.
他のプログラミング言語や, プログラミング言語以外でも, 予め決まっている
手順に沿った処理のための便利な仕組み/ソフトウェアです.

make では, 使用するプログラムのファイルや
コンパイルの方法を Makefile (あるいは makefile) という名前のファイルに予め
書いておくことで,

  $ make

と打つだけでその方法に従って順次コンパイルしてくれるとても便利な方法です.
ここでは詳しいことは説明しませんが, 興味があれば調べてみると良いでしょう.