LoginSignup
5
1

More than 3 years have passed since last update.

Fortranでメソッドチェーンを模擬するトリック

Posted at

概要

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の値を,型束縛されたサブルーチン(戻り値なしのメソッド)によって設定しています.

main.f90
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
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
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
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
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
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
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
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
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
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)で実行するなど,コーディングルールを定める必要がありそうです.


  1. 手続きとは,サブルーチンと関数を包括した呼称です. 

  2. Fortranは英字の大小を区別しないので,.setx.と書いても問題なく実行できます. 

  3. C++/JavaでいうthisやPythonでいうselfを,設けるか設けないかを設定できるということです. 

5
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1