概要
JavaでいうtoString
やPythonでいう__str__
に相当するサブルーチンをFortranで作成する方法についてまとめました.入力用のサブルーチンも作成可能ですが,本記事では画面出力についてのみ言及します.
はじめに
Fortranでは,複数の変数や関数あるいはサブルーチンをまとめてユーザ定義派生型(他言語でいう構造体やクラス)を作る事ができます.派生型変数をprint文やwrite文に渡すと派生型成分の値が画面に出力されますが,その書式を制御したいという要望は必ず持つはずです.print/write文で出力する際に書式を指定し,成分を一つずつ参照するのが最も簡単ですが,privateな成分を参照することはできません.
他言語では,例えばJavaではtoString
メソッドをオーバーライドすることで出力される内容を制御できますし,Pythonでも特殊メソッドである__str__
をオーバーライドすることで同様の事ができます.
本記事では,ユーザ定義派生型入出力(user-defined derived-type IO)を利用することで,上記の要望を実現する方法を述べます.
問題設定
二つの実数を成分として持つ派生型tuple2
を定義し,その各成分の出力を整形します.派生型の定義,メインルーチンでの初期化と表示のプログラムを次のように作りました.
module class_Tuple2
implicit none
private
type,public :: tuple2
real(8),public :: x
real(8),public :: y
end type tuple2
end module class_Tuple2
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
(に限らず派生型)の成分がpointer
やallcatable
属性を持っていると,write(*,*)
を用いて一括で表示することはできません.このような場合にも,やはりユーザ定義派生型入出力のサブルーチンを設ける必要があります.
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
を定義しているモジュールに,サブルーチンを追加します.
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を書式無しで表示していますが,コンソール出力は見事に整形されています.
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文では書式がベタ打ちされていて,引数ではIOType
とargList
がまだ説明されていません.これらが関係ありそうだと推察できます.
argList
Fortranでは,書式指定を行う場合,整数型ならI
4,実数型ならF
,文字型ならA
を使い,型を表すアルファベットの後ろに数字を置いて表示桁数を指定します.これと同様に,ユーザ定義派生型の書式を指定する場合にはDT()
5を使用し,括弧の中に表示桁数を指定します.このとき,桁数の指定には7.3
のような小数を使うことはできず,全て整数で指定する必要があります.つまり,
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文字目以降の文字列と型の名前が一致していれば書式付きで表示し,一致しなければ警告を出す. - 安全側に倒す意味で,
IOType
がDT
のみであれば警告を出す.
というようにサブルーチンを設計すると,型に合わない表示を行う可能性を減らすことができます.最終的に,ユーザ定義派生型出力のサブルーチンは次のようになりました.
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
実行結果を見てみると,書式指定が無い場合には成分の値が整形されずに出力され,書式指定に失敗している場合にエラーが出力されていることが分かります.
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,y
をprivate
としても(初期値設定以外は)問題なく動作します.
次に示すサンプルプログラムでは,tuple2
の成分x,y
をprivate
に変更しています.それと同時に,コンストラクタtuple2
をオーバーロードして,private
な成分にも値を代入できるようにしています.
#プログラム全景
メインルーチン
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を定義したモジュール
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]