Fortran
ModernFortran

Fortranのユーザ定義派生型の出力用サブルーチンの書き方

概要

JavaでいうtoStringやPythonでいう__str__に相当するサブルーチンをFortranで作成する方法についてまとめました.入力用のサブルーチンも作成可能ですが,本記事では画面出力についてのみ言及します.

はじめに

Fortranでは,複数の変数や関数あるいはサブルーチンをまとめてユーザ定義派生型(他言語でいう構造体やクラス)を作る事ができます.派生型変数をprint文やwrite文に渡すと派生型成分の値が画面に出力されますが,その書式を制御したいという要望は必ず持つはずです.print/write文で出力する際に書式を指定し,成分を一つずつ参照するのが最も簡単ですが,privateな成分を参照することはできません.
他言語では,例えばJavaではtoStringメソッドをオーバーライドすることで出力される内容を制御できますし,Pythonでも特殊メソッドである__str__をオーバーライドすることで同様の事ができます.
本記事では,ユーザ定義派生型入出力(user-defined derived-type IO)を利用することで,上記の要望を実現する方法を述べます.

問題設定

二つの実数を成分として持つ派生型tuple2を定義し,その各成分の出力を整形します.派生型の定義,メインルーチンでの初期化と表示のプログラムを次のように作りました.

class_tuple2.f90
module class_Tuple2
    implicit none
    private

    type,public :: tuple2
        real(8),public :: x
        real(8),public :: y
    end type tuple2
end module class_Tuple2
main.f90
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0)

    write(*,*) r
    print '(F7.3,F7.3)', r%x,r%y
end program main

次のような出力が得られます.

   1.00000000000000        2.00000000000000
  1.000  2.000

派生型tuple2は成分として実数x, yを持っています.メインルーチンでは派生型変数としてrを宣言し,コンストラクタによってxに1,yに2を代入しました.次の行ではwrite文にrを渡しています.このようにすると,rの成分全てを表示してくれます.下のprint文では各成分を個別に参照し,それらの書式を設定しています.

これで一応要望は達成できるわけですが,二つの数字を[]で囲んで表示したいと考えると,毎回[]を書く必要がありますし,そもそも派生型の成分がprivateだとメインルーチンから参照することすらできません.

Fortranを含むオブジェクト指向プログラミング言語では,こういう問題に対する解決策を用意しており,先述の通りJavaではtoStringメソッド,Pythonでは__str__メソッドがあります.Fortranではユーザ定義派生型入出力がそれらに相当します.

その他,ユーザ定義派生型用サブルーチンが必要になる状況(2018年5月4日加筆)

上記の状況に加えて,tuple2(に限らず派生型)の成分がpointerallcatable属性を持っていると,write(*,*)を用いて一括で表示することはできません.このような場合にも,やはりユーザ定義派生型入出力のサブルーチンを設ける必要があります.

class_tuple2.f90
    type,public :: tuple2
        real(8),pointer,public :: x        !write(*,*)/print*で一括出力はできない
        real(8),allocatable,public :: y(:) !write(*,*)/print*で一括出力はできない
    end type tuple2

cure_honeyさん,ご指摘ありがとうございました.

ユーザ定義派生型入出力

ユーザ定義派生型入出力は,派生型を定義しているモジュール内に,出力あるいは入力を行うサブルーチンを定義し,print/write文あるいはread文に当該派生型変数が渡された際に,そのサブルーチンを暗黙的に呼び出します.
tuple2を定義しているモジュールに,サブルーチンを追加します.

class_tuple2.f90
module class_Tuple2
    implicit none
    private
    public :: write(formatted)

    type,public :: tuple2
        real(8),public :: x
        real(8),public :: y
    end type tuple2

    interface write(formatted)
        procedure printTuple2
    end interface

    contains

    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit
        character(*), intent(in   ) :: IOType
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage


        !print "(A1, F7.3, A1 , F7.3, A1 )", '[',this%x, ',', this%y, ']' !Fortran的な書式指定
        print "( '[', F7.3, ',', F7.3, ']' )", this%x, this%y             !printf風の書式指定
    end subroutine printTuple2
end module class_Tuple2

画面表示を行うサブルーチンとして,printTuple2を定義し,print "( '[', F7.3, ',', F7.3, ']' )", this%x, this%yの行で書式付きで画面表示を行います1
print/write文にtuple2型変数が渡された時にこのサブルーチンが呼ばれるようにするための設定が,interface write(formatted)で行われています.書式付きのprint/write文が呼ばれたときに手続としてprintTuple2が呼ばれるようになります.Fortranでは手続(サブルーチンおよび関数)のオーバーロードはこのinterfaceを用いて行われますので,ここではprint/write文をオーバーロードしていると考えればわかりやすいと思います.オーバーロードできる入出力文には

  • write(formatted)
  • write(unformatted)
  • read(formatted)
  • read(unformatted)

があります2
最後に,モジュール全体をprivateにしていたので,public :: write(formatted)でオーバーロードされたprint/write文を外部に公開します.
tuple2型変数rを書式無しで表示していますが,コンソール出力は見事に整形されています.

main.f90
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0)

    write(*,*) r
end program main
 [  1.000,  2.000]

引数の意味

さて,これで要望はかなえられたわけですが,サブルーチンの引数の意味は説明していませんでした.派生型変数の成分を画面表示するだけでよければこれで終わりなのですが,実用上は,表示する桁数を変化させたり,他の変数も同時に表示したりする場合があります.サブルーチンの引数は,そのような場合に有効に利用できます.
サブルーチンの引数のうち,意味がすぐにわかる引数を説明していきます.

Unit, IOStatus, IOMessage

    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        implicit none
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit
        character(*), intent(in   ) :: IOType
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage

        print "( '[', F7.3, ',', F7.3, ']' )", this%x, this%y
    end subroutine printTuple2
  • Unitは装置番号を意味しています.write文の装置番号がそのまま渡されます.print文を使う,あるいはwrite文で装置番号を*にすると,負の値(Intel Fortranでは-1)となります.
  • IOStatusは,ファイル等への出力結果に応じて整数値を持ちます.0以外の場合は何らかの対策を取る必要があります3
  • IOMessageは発生したエラーの内容が文字列で返されます3

つまり,print文ではほとんどの引数に出番はありませんが,write文を使うと引数を有効に利用できます.

write(unit=Unit,fmt="( '[', F7.3, ',', F7.3, ']' )",iostat = IOStatus, iomsg=IOMessage) this%x, this%y

write文と引数を見比べてみると,write文では書式がベタ打ちされていて,引数ではIOTypeargListがまだ説明されていません.これらが関係ありそうだと推察できます.

argList

Fortranでは,書式指定を行う場合,整数型ならI4,実数型ならF,文字型ならAを使い,型を表すアルファベットの後ろに数字を置いて表示桁数を指定します.これと同様に,ユーザ定義派生型の書式を指定する場合にはDT()5を使用し,括弧の中に表示桁数を指定します.このとき,桁数の指定には7.3のような小数を使うことはできず,全て整数で指定する必要があります.つまり,

main.f90
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0)

    print '(DT(7,3))',r
end program main

と書く必要があります.そして,括弧内に置かれた数字が一つずつargList(:)の要素となります.サブルーチンprintTuple2の中でargListの内容を表示してみると,書式指定時に渡した数字(7と3)が表示されています.

    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        implicit none
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit
        character(*), intent(in   ) :: IOType
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage

        print*,size(argList),argList(:)
    end subroutine printTuple2
           2           7           3

Fortranでは,実数の書式指定は全体の表示桁数.小数点以下の桁数となっているので,ユーザ定義派生型でもこのルールに従って書式を指定することにします.argListの要素として渡された整数を内部ファイルによって文字列に変換し,書式指定子Fx.xを作り,画面表示する書式fmtを構成します.fmtに自動再割付文字列character(:),allocatableを用いる事で,桁数(文字数)の指定を簡略化できます.

        block
            character(2) :: width_tol, width_dec !全体の表示桁数,小数点以下の表示桁数
            character(6) :: RealSpec             !書式指定子 Fx.x を作る
            character(:),allocatable :: fmt      !画面表示の書式

            write(width_tol,'(I2)') argList(1)
            write(width_dec,'(I2)') argList(2)
            RealSpec = 'F'//width_tol//'.'//width_dec
            fmt = "(' ['"//RealSpec//","//RealSpec//"']')"

            write(unit=Unit, fmt = fmt, iostat = IOStatus, iomsg = IOMessage) this%x,this%y
        end block

このように処理を書いて実行すると,正しく書式指定ができていることが分かります.

 [  1.000  2.000]

書式をDT(16,5)に変更すると,表示桁数が変わることが確認できます.

 [         1.00000         2.00000]

定義される型の用途によって表示したい書式が変わってくるはずのですので,必ずしも表示の桁数を指定する必要はありませんが,与える数字とそれによって制御される書式の関係は,ドキュメントとして必ず明示するようにしましょう.

IOType

この引数は,複数のユーザ定義型を表示する際に,どの型に対する書式指定かを明示するために利用します.IOTypeの挙動をまとめると,次のようになります.

  • 何も書式を指定しなかった場合(print *,rとした場合),IOType"LISTDIRECTED"となります.
  • 書式のみを指定した場合(print '(DT(7,3))',rとした場合),IOType"DT"となります.
  • 型を表すアルファベットと括弧の間に文字列を置いた場合(print '(DT"abc"(7,3))',rとした場合),IOType"DTabc"となります.
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0)

    print *,r
    print '(DT(7,3))',r
    print '(DT"abc"(7,3))',r
end program main
    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        implicit none
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit
        character(*), intent(in   ) :: IOType
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage

        print *,IOType
    end subroutine printTuple2
 LISTDIRECTED
DT
DTabc

これらの挙動をうまく利用すると,ユーザ定義派生型入出力サブルーチン内で書式指定と型の整合性を確認することができます.つまり,

  • IOType"LISTDIRECTED"なら書式無しで表示する.
  • IOTypeの3文字目以降の文字列と型の名前が一致していれば書式付きで表示し,一致しなければ警告を出す.
  • 安全側に倒す意味で,IOTypeDTのみであれば警告を出す.

というようにサブルーチンを設計すると,型に合わない表示を行う可能性を減らすことができます.最終的に,ユーザ定義派生型出力のサブルーチンは次のようになりました.

    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        implicit none
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit
        character(*), intent(in   ) :: IOType
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage

        if(IOType == "LISTDIRECTED" .or. size(argList)<2)then !書式指定なしか,書式指定の数字が不足している場合
            write(unit=Unit, fmt = *, iostat = IOStatus, iomsg = IOMessage) this%x,this%y
            return
        else
            if(IOType(3:) /= "tuple2")then !型の指定が間違っている場合
                print *,"error : type mismatch"
                return
            end if
            block
                character(2) :: width_tol, width_dec !全体の表示桁数,小数点以下の表示桁数
                character(6) :: RealSpec             !書式指定子 Fx.x を作る
                character(:),allocatable :: fmt      !画面表示の書式

                write(width_tol,'(I2)') argList(1)
                write(width_dec,'(I2)') argList(2)
                RealSpec = 'F'//width_tol//'.'//width_dec
                fmt = "(' ['"//RealSpec//","//RealSpec//"']')"

                write(unit=Unit, fmt = fmt, iostat = IOStatus, iomsg = IOMessage) this%x,this%y
            end block
            return
        end if

    end subroutine printTuple2

実行結果を見てみると,書式指定が無い場合には成分の値が整形されずに出力され,書式指定に失敗している場合にエラーが出力されていることが分かります.

main.f90
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0)

    print *,r                   !書式無し
    print '(DT(7,3))',r         !型の名前が不足
    print '(DT"abc"(7,3))',r    !型の名前間違い
    print '(DT"tuple2"(7,3))',r
end program main
    1.00000000000000        2.00000000000000
error : type mismatch
error : type mismatch
 [  1.000  2.000]

まとめ

ユーザ定義派生型入出力をうまく使うことで,派生型の出力を楽に整形できるようになりました.

Fortranでは,派生型が定義されているモジュールと同一モジュール内で定義されているサブルーチンは,派生型のprivateな成分を参照することができます.そのため,tuple2の成分x,yprivateとしても(初期値設定以外は)問題なく動作します.

次に示すサンプルプログラムでは,tuple2の成分x,yprivateに変更しています.それと同時に,コンストラクタtuple2をオーバーロードして,privateな成分にも値を代入できるようにしています.

プログラム全景

メインルーチン

main.f90
program main
    use class_Tuple2
    implicit none

    type(tuple2) :: r
    r = tuple2(1d0,2d0) !built-inコンストラクタではなくオーバーロードしたtuple2が呼ばれる

    print *,r
    print '(DT"tuple2"(7,3))',r
end program main

tupe2を定義したモジュール

class_tuple2.f90
module class_Tuple2
    implicit none
    private
    public :: tuple2
    public :: write (formatted)

    type :: tuple2
        real(8),private :: x
        real(8),private :: y
    end type tuple2

    interface tuple2
        procedure constructTuple2ByValues
    end interface

    interface write (formatted)
        procedure printTuple2
    end interface

    contains

    function constructTuple2ByValues(x,y) result(init)
        implicit none
        real(8),value :: x
        real(8),value :: y

        type(tuple2) :: init
        init%x = x
        init%y = y
    end function constructTuple2ByValues

    subroutine printTuple2(this, Unit, IOType, argList, IOStatus, IOMessage)
        implicit none
        class(tuple2),intent(in   ) :: this
        integer,      intent(in   ) :: Unit   !writeで指定した装置番号,*の場合は-1
        character(*), intent(in   ) :: IOType !*の時はLISTDIRECTED
        integer,      intent(in   ) :: argList(:)
        integer,      intent(  out) :: IOStatus
        character(*), intent(inout) :: IOMessage

        if(IOType == "LISTDIRECTED" .or. size(argList)<2)then !書式指定なしか,書式指定の数字が不足している場合
            write(unit=Unit, fmt = *, iostat = IOStatus, iomsg = IOMessage) this%x,this%y
            return
        else
            if(IOType(3:) /= "tuple2")then !型の指定が間違っている場合
                print *,"error : type mismatch"
                return
            end if
            block
                character(2) :: width_tol, width_dec !全体の表示桁数,小数点以下の表示桁数
                character(6) :: RealSpec             !書式指定子 Fx.x を作る
                character(:),allocatable :: fmt      !画面表示の書式

                write(width_tol,'(I2)') argList(1)
                write(width_dec,'(I2)') argList(2)
                RealSpec = 'F'//width_tol//'.'//width_dec
                fmt = "(' ['"//RealSpec//","//RealSpec//"']')"

                write(unit=Unit, fmt = fmt, iostat = IOStatus, iomsg = IOMessage) this%x,this%y
            end block
            return
        end if

    end subroutine printTuple2

end module class_Tuple2

実行結果

    1.00000000000000        2.00000000000000
 [  1.000  2.000]

  1. Fortran的な書式指定の方が好きなのですが,ここではprintf風の書式指定を使っています. 

  2. print/write文でしか試していませんが,write(formatted)とwrite(unformatted)を同時に定義しても,write(formatted)をオーバーロードしたサブルーチンが呼ばれるようです. 

  3. これに関しては,以前の記事でも少しだけ触れています. 

  4. 変数と区別するために大文字を使っていますが,Fortranはcase-insensitiveです. 

  5. Defined Typeの略だと思います.