概要
Fortranでは,正攻法でメソッドチェーンを記述することはできません.
ユーザ定義演算子を用いれば,メソッドチェーンを模擬できるので,その方法を紹介します.
環境
- Windows 10
- gfortran 8.1.0
メソッドチェーン
メソッドチェーン(Method Chaining)とは,オブジェクト指向プログラミングの枠組みにおいて,オブジェクトを返す複数のメソッドを,一時変数を用いずに単一の文で実行する記述法です.
Fortranの用語を用いると,型束縛手続き1チェーン(Type-Bound Procedure Chaining)となりますが,長いのでメソッドチェーンと呼びます.
例えば,成分に倍精度実数x,y
とそのsetterset_x, set_y
を持つユーザ定義派生型filed
の値を設定することを考えると,Fortran的な考え方であれば以下のように書くのが一般的だと考えられます.
type(field) :: f
call f%set_x(1d0)
call f%set_y(2d0)
それを,
f = new_field()%set_x(1d0)%set_y(2d0)
のように書こうというのがメソッドチェーンです.
このように書く利点は,中間変数を宣言する手間が省けて,1文で記述できるということでしょう.それ以外にも,optional
な引数を多く持つような初期化ルーチンの実装と呼出しが楽になることも,利点として挙げられます.
差分法(1次元,等間隔格子)において,空間離散化に関する計算条件を設定するサブルーチンを実装する場合,必要な情報は
- 計算領域の長さ$L_x$
- 格子点数$N_x$
- 格子点間隔$\varDelta x$
のうち,二つです.
計算領域長さ$L_x$と格子点数$N_x$がわかれば,格子点間隔$\varDelta x$が計算できますし,計算領域長さ$L_x$と格子点間隔$\varDelta x$がわかれば,格子点数$N_x$は求められます.格子点数$N_x$と格子点間隔$\varDelta x$があれば,計算領域の長さ$L_x$がわかります.
そのため,サブルーチンの引数は全てoptional
になり,正しい組合せだけでなく,引数が足りない場合や情報に不整合がある場合のエラー処理も含めて,実装が非常に煩雑になります.
type(field) :: f
call f%set_space_domain(Lx = 1d0, Nx = 101)
これはまだ引数が3個ですが,例えば時間に関する離散化の情報など,計算が複雑になるにつれて,必要になる情報が増えていきます.このような場合にメソッドチェーンが利用できると,複雑さがある程度緩和できると考えられます.
type(field) :: f
f = new_field()%space_length(1d0)%grid_points(101)%time_length(10d0)%time_interval(1d-4)
Fortranにおけるメソッドチェーン
Fortran規格(2018まで)に定められたオブジェクト指向プログラミングの機能では,メソッドチェーンは実現できません.
前節のnew_field()%set_x(1d0)%set_y(2d0)
を例に説明すると,派生型の構造成分(new_field()
やset_x(1d0)
)は,右端を除いてすべて派生型でなければなりません.
つまり,階層的な派生型の呼出しにおいて,括弧が許されるのは,派生型の配列を参照する場合のみです.
しかし,ユーザ定義演算子で,メソッドチェーンをある程度模擬できることが判りました.
以降,徐々にプログラムを変更しながら,その方法を確認します.
対象とする問題
対象とするのは,倍精度実数x,y
を成分に持つユーザ定義派生型filed
で,x,y
の値を,型束縛されたサブルーチン(戻り値なしのメソッド)によって設定しています.
program main
use, intrinsic :: iso_fortran_env
use :: type_field
implicit none
type(field) :: f
f = new_field()
print *,f ! 7.0914309329389134E-317 3.2697343492277031E-317 ※実行毎に変化
call f%set_x(1d0)
call f%set_y(2d0)
print *,f ! 1.0000000000000000 2.0000000000000000
end program main
type_field.f90
module type_field
use, intrinsic :: iso_fortran_env
implicit none
private
public :: field
public :: new_field
public :: write(formatted)
type :: field
real(real64),private :: x
real(real64),private :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
end type
interface write(formatted)
procedure print_field
end interface
contains
function new_field() result(new_f)
use, intrinsic :: iso_fortran_env
implicit none
type(field) :: new_f
end function new_field
subroutine set_x(this, x)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(inout) :: this
real(real64),intent(in) :: x
this%x = x
end subroutine set_x
subroutine set_y(this, y)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(inout) :: this
real(real64),intent(in) :: y
this%y = y
end subroutine set_y
subroutine print_field(this, Unit, IOType, argList, IOStatus, IOMessage)
class(field), intent(in ) :: this
integer, intent(in ) :: Unit
character(*), intent(in ) :: IOType
integer, intent(in ) :: argList(:)
integer, intent( out) :: IOStatus
character(*), intent(inout) :: IOMessage
print *, this%x, this%y
end subroutine print_field
end module type_field
この処理を,メソッドチェーンによって設定することを考えます.
サブルーチンを関数に置き換え
メソッドチェーンではオブジェクトを返すメソッドを繋げていくので,型束縛手続きをサブルーチンから関数に置き換えます.
function set_x(this, x) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = this
field_updated%x = x
end function set_x
function set_y(this, y) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = this
field_updated%y = y
end function set_y
メインルーチンでの実行部分は,次のように置き換えられます.
f = new_field()
print *,f ! 1.0041627337587499E-317 3.2697343492277031E-317 ※実行毎に変化
f = f%set_x(1d0)
f = f%set_y(2d0)
print *,f ! 1.0000000000000000 2.0000000000000000
ここでは,
f = new_field()%set_x(1d0)%set_y(2d0)
という書き方はできません.
f = f%set_x(1d0)%set_y(2d0)
という書き方もエラーが出ます.
main.f90
program main
use, intrinsic :: iso_fortran_env
use :: type_field
implicit none
type(field) :: f
f = new_field()
! f = new_field()%set_x(1d0)%set_y(2d0)
print *,f
! f = f%set_x(1d0)%set_y(2d0)
f = f%set_x(1d0)
f = f%set_y(2d0)
print *,f
end program main
type_field.f90
module type_field
use, intrinsic :: iso_fortran_env
implicit none
private
public :: field
public :: new_field
public :: write(formatted)
type :: field
real(real64),private :: x
real(real64),private :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
end type
interface write(formatted)
procedure print_field
end interface
contains
function new_field() result(new_f)
use, intrinsic :: iso_fortran_env
implicit none
type(field) :: new_f
end function new_field
function set_x(this, x) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = this
field_updated%x = x
end function set_x
function set_y(this, y) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = this
field_updated%y = y
end function set_y
subroutine print_field(this, Unit, IOType, argList, IOStatus, IOMessage)
class(field), intent(in ) :: this
integer, intent(in ) :: Unit
character(*), intent(in ) :: IOType
integer, intent(in ) :: argList(:)
integer, intent( out) :: IOStatus
character(*), intent(inout) :: IOMessage
print *, this%x, this%y
end subroutine print_field
end module type_field
ユーザ定義2項演算子の導入
Fortranでは,引数を一つ取る関数に単項演算を,引数を二つ取る関数に2項演算を割り当てることができます.既存の演算子を上書きすることもできれば,演算子を新たに設ける事もできます.ただし,新たに設ける演算子は,.
に挟まれた63文字以内の英字で定義する必要があります.記号や数字は使えません.
型束縛手続きに対しては,generic
文を用いて演算子との対応を記述します.
type :: field
real(real64),private :: x
real(real64),private :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
generic :: operator(.setX.) => set_x ! 型束縛手続きset_xを 2項演算子.setX. に割り当て
generic :: operator(.setY.) => set_y ! 型束縛手続きset_yを 2項演算子.setY. に割り当て
end type
このように派生型field
を定義すると,field
型変数f
に対してf .setX. 1d0
と書くと,f%set_x(1d0)
が実行されます2.このとき,左の項は変数である必要はなく,型さえ合っていれば実行できます.
この仕組みを利用することで,
f = new_field()%set_x(1d0)%set_y(2d0)
とは書けなくても,
f = (new_field() .setX. 1d0) .setY. 2d0
と書けるようになります.もちろん正しく実行されます.new_field() .setX. 1d0
を囲む括弧は不要ですが,判りにくくなるので書いておいた方がよいと思います.
f = (new_field() .setX. 1d0) .setY. 2d0
print *,f ! 1.0000000000000000 2.0000000000000000
f = new_field() .setX. 3d0 .setY. 4d0
print *,f ! 3.0000000000000000 4.0000000000000000
main.f90
program main
use, intrinsic :: iso_fortran_env
use :: type_field
implicit none
type(field) :: f
f = (new_field() .setX. 1d0) .setY. 2d0
print *,f
f = new_field() .setX. 3d0 .setY. 4d0
print *,f
end program main
type_field.f90
module type_field
use, intrinsic :: iso_fortran_env
implicit none
private
public :: field
public :: new_field
public :: write(formatted)
type :: field
real(real64),private :: x
real(real64),private :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
generic :: operator(.setX.) => set_x
generic :: operator(.setY.) => set_y
end type
interface write(formatted)
procedure print_field
end interface
contains
function new_field() result(new_f)
use, intrinsic :: iso_fortran_env
implicit none
type(field) :: new_f
end function new_field
function set_x(this, x) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = this
field_updated%x = x
end function set_x
function set_y(this, y) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = this
field_updated%y = y
end function set_y
subroutine print_field(this, Unit, IOType, argList, IOStatus, IOMessage)
class(field), intent(in ) :: this
integer, intent(in ) :: Unit
character(*), intent(in ) :: IOType
integer, intent(in ) :: argList(:)
integer, intent( out) :: IOStatus
character(*), intent(inout) :: IOMessage
print *, this%x, this%y
end subroutine print_field
end module type_field
ユーザ定義単項演算子の導入
ここまではよい感じでメソッドチェーンを模擬できる様子を見てきましたが,単項演算が絡むと問題がこじれます.
field
型変数の成分x,y
の値を0
で初期化するような関数clear()
を定義し,メソッドチェーンの中で呼びたい,つまり下記のような書き方を模擬したいということもあるでしょう.
f = new_field()%clear()%set_x(1d0)%set_y(2d0)
単項演算子も,2項演算子と同様に,generic
文を用いて型束縛手続きと対応付けます.
type :: field
real(real64) :: x
real(real64) :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
procedure,public,pass :: clear => clear_field
generic :: operator(.setX.) => set_x
generic :: operator(.setY.) => set_y
generic :: operator(.clear.) => clear
end type
function clear_field(this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
type(field) :: field_updated
field_updated%x = 0d0
field_updated%y = 0d0
end function clear_field
単項演算子を利用する際は,前置する必要があります.
f = .clear. new_field()
print *,f ! 0.0000000000000000 0.0000000000000000
つまり,
f = new_field()%clear()%set_x(1d0)%set_y(2d0)
を演算子で模擬しようとすると,単項演算子は左に置かなくてはならず,かなり奇妙な形になってしまいます.
f = ((.clear. new_field()) .setX. 1d0) .setY. 2d0
print *,f ! 1.0000000000000000 2.0000000000000000
f = .clear. ((new_field() .setX. 1d0) .setY. 2d0)
print *,f ! 0.0000000000000000 0.0000000000000000
main.f90
program main
use, intrinsic :: iso_fortran_env
use :: type_field
implicit none
type(field) :: f
f = ((.clear. new_field()) .setX. 1d0) .setY. 2d0
print *,f
end program main
type_field.f90
module type_field
use, intrinsic :: iso_fortran_env
implicit none
private
public :: field
public :: new_field
public :: write(formatted)
type :: field
real(real64) :: x
real(real64) :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
procedure,public,pass :: clear => clear_field
generic :: operator(.setX.) => set_x
generic :: operator(.setY.) => set_y
generic :: operator(.clear.) => clear
end type
interface write(formatted)
procedure print_field
end interface
contains
function new_field() result(new_f)
use, intrinsic :: iso_fortran_env
implicit none
type(field) :: new_f
end function new_field
function clear_field(this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
type(field) :: field_updated
field_updated%x = 0d0
field_updated%y = 0d0
end function clear_field
function set_x(this, x) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = this
field_updated%x = x
end function set_x
function set_y(this, y) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = this
field_updated%y = y
end function set_y
subroutine print_field(this, Unit, IOType, argList, IOStatus, IOMessage)
class(field), intent(in ) :: this
integer, intent(in ) :: Unit
character(*), intent(in ) :: IOType
integer, intent(in ) :: argList(:)
integer, intent( out) :: IOStatus
character(*), intent(inout) :: IOMessage
print *, this%x, this%y
end subroutine print_field
end module type_field
2項演算の順序を入れ替え,左に置く形にする
右に書いたり左に書いたりするのがややこしく,かつ単項演算子は必ず左に置かなければなりません.
そこで,2項演算の順序を入れ替えて,全て左に繋げる形式に変更することを考えます.
手続きを呼び出した実体の指定
Fortranでは,型束縛手続きに,その手続きを呼び出した実体を仮引数として渡すことも,渡さないことも選択できます3.また,実体を仮引数として渡す場合,引数を第1引数から変更することもできます.
その指定は,派生型を定義する際に型束縛手続きの属性として指定します.
type :: field
! 略
contains
! 略
procedure,public,pass(this) :: r_set_x
! 略
end type field
contains
function r_set_x(x, this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = set_x(this,x)
end function r_set_x
ユーザ定義派生型の属性pass/nopass
は,手続きを呼び出した実体を仮引数として渡すか否かを指定する属性です.nopass
の場合は渡しません.pass
の場合は渡すようになり,手続きの第1引数が当該の仮引数として扱われます.pass(仮引数名)
とすると,その仮引数名をもつ仮引数が,手続きを呼び出した実体の仮引数となります.
上の例では,procedure,public,pass(this) :: r_set_x
として手続きr_set_x
を定義しており,r_set_x
の実装では,r_set_x(x, this)
として仮引数を定義しています.従って,第2引数が,手続きを呼び出した実体の仮引数となります.
演算子の名前
これまで,演算子の名前は,set_x
であれば.setX.
などと関数名から付けていました.しかし左にリテラルを置くと,1d0 .setX. f
のように奇妙なことになります.そのため,演算子名を「f
の成分x
の値として設定する」という意図で,.asXof.
と変更します.成分y
のsetterについても同様に型束縛手続きと演算子を定義します.
type :: field
! 略
contains
! 略
procedure,public,pass(this) :: r_set_x
procedure,public,pass(this) :: r_set_y
! 略
generic :: operator(.asXof.) => r_set_x
generic :: operator(.asYof.) => r_set_y
end type field
こうすると,左に手続きを繋げて行くことが可能になります.
f = 2d0 .asYof. (1d0 .asXof. (.clear. new_field()))
print *,f ! 1.0000000000000000 2.0000000000000000
main.f90
program main
use, intrinsic :: iso_fortran_env
use :: type_field
implicit none
type(field) :: f
f = 2d0 .asYof. (1d0 .asXof. (.clear. new_field()))
print *,f
end program main
type_field.f90
module type_field
use, intrinsic :: iso_fortran_env
implicit none
private
public :: field
public :: new_field
public :: write(formatted)
type :: field
real(real64) :: x
real(real64) :: y
contains
procedure,public,pass :: set_x
procedure,public,pass :: set_y
procedure,public,pass :: clear => clear_field
procedure,public,pass(this) :: r_set_x
procedure,public,pass(this) :: r_set_y
generic :: operator(.setX.) => set_x
generic :: operator(.setY.) => set_y
generic :: operator(.asXof.) => r_set_x
generic :: operator(.asYof.) => r_set_y
generic :: operator(.clear.) => clear
end type field
contains
function new_field() result(new_f)
use, intrinsic :: iso_fortran_env
implicit none
type(field) :: new_f
end function new_field
function clear_field(this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
type(field) :: field_updated
field_updated%x = 0d0
field_updated%y = 0d0
end function clear_field
function set_x(this,x) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = this
field_updated%x = x
end function set_x
function r_set_x(x, this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: x
type(field) :: field_updated
field_updated = set_x(this,x)
end function r_set_x
function set_y(this,y) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = this
field_updated%y = y
end function set_y
function r_set_y(y, this) result(field_updated)
use, intrinsic :: iso_fortran_env
implicit none
class(field),intent(in) :: this
real(real64),intent(in) :: y
type(field) :: field_updated
field_updated = set_y(this,y)
end function r_set_y
subroutine print_field(this, Unit, IOType, argList, IOStatus, IOMessage)
class(field), intent(in ) :: this
integer, intent(in ) :: Unit
character(*), intent(in ) :: IOType
integer, intent(in ) :: argList(:)
integer, intent( out) :: IOStatus
character(*), intent(inout) :: IOMessage
print *, this%x, this%y
end subroutine print_field
end module type_field
まとめ
Fortranでメソッドチェーンを記述したいという要望に対して,ユーザ定義演算子を用いメソッドチェーンを模擬する方法を紹介しました.
変数の初期化に用いる事を想定し,手続きをユーザ定義演算子として定義し,それを繋げて行く方法です.ある成分に値を代入する処理を2項演算として書くと,それなりによくメソッドチェーンを模擬できましたが,単項演算が絡むと,奇妙な書き方をする必要が生じます.
その対策として,左に繋げて行く方法も紹介しました.
実際に利用する場合は,単項演算になりそうな処理はコンストラクタ(new_field
)で実行するなど,コーディングルールを定める必要がありそうです.