Fortran
Fortran2003

Fortranからみたオブジェクト指向:オブジェクト指向でFortranコードを書く

FortranもFortran2003からオブジェクト指向ができるようになっています。

Fortranのオブジェクト指向はFortranを拡張することでできるようになっていますので、オブジェクト指向とは、ということを理解するのにちょうどよいと思いました。

そこで、

"[Python]クラス継承(super)"

https://qiita.com/Kodaira_/items/42dfe18c81af98bf0db3

の記事にあるPythonコードをFortranに移植することを試みることにします。

なお、これをJuliaに移植したものは

"Pythonの複数クラス継承をJuliaで書いてみる"

https://qiita.com/cometscome_phys/items/fd55fc364d0997b38d68

にあります。


環境


  • Mac OS 10.14.2

  • gcc version 8.3.0 (Homebrew GCC 8.3.0)


オブジェクト指向とは

Fortranからみたオブジェクト指向は、構造体にfunctionやsubroutineがくっついたもの、と考えるとよいかもしれません。


type

Fortranでは、typeを使って、

    type Creature

integer::level
integer::hp
integer::mp
integer::attack
integer::defence
character(len=18)::weapon
character(len=18)::job
end type Creature

のように、自分の独自定義の型を定義することができます。

そして、この型の中身を取り出したい時は、

module mod_creature

implicit none
type Creature
integer::level
integer::hp
integer::mp
integer::attack
integer::defence
character(len=18)::weapon
character(len=18)::job
end type Creature
end module mod_creature

program main
use mod_creature
implicit none
type(creature)::my_creature

my_creature%level = 1
my_creature%hp = 0
my_creature%mp = 0
my_creature%attack = 0
my_creature%defence = 0
my_creature%weapon = "none"
my_creature%job = "neet"

write(*,*) my_creature%level

end program

のように、my_creature%levelでtypeの中で定義したもの(ここではlevel)に値を代入したり表示させたりすることができます。

このようなtypeを作ると、それを引数としたfunctionやsubroutineを定義することができて、

module mod_creature

implicit none
type Creature
integer::level
integer::hp
integer::mp
integer::attack
integer::defence
character(len=18)::weapon
character(len=18)::job
end type Creature

contains

subroutine creature_status(self)
type(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

end module mod_creature

と新しいsubroutine creature_statusを定義すれば、

program main

use mod_creature
implicit none
type(creature)::my_creature

my_creature%level = 1
my_creature%hp = 0
my_creature%mp = 0
my_creature%attack = 0
my_creature%defence = 0
my_creature%weapon = "none"
my_creature%job = "neet"

write(*,*) my_creature%level

call creature_status(my_creature)

end program

とすることで呼び出すことができます。

ここまではFortranで昔からある「構造体」と呼ばれるものです。

変数をひとまとめにすることでわかりやすくなっているかと思います。

subroutineやfunctionにたくさんの引数を入れなくてもまとめたこのtypeを呼んであげればよいので、見た目もスッキリしますし、違う引数を間違えて入れたりというようなバグの発生も抑えられます。


隠蔽の開始

さて、上のtypeも使いやすいですが、さらにバグを発生させにくくするにはどうすればよいでしょうか?

このtypeを使うには、定義されているtypeがどのような変数を持っているかを把握し、適切に値を変更する必要があります。

しかし、間違えて変数を書き換えてしまう可能性もありますので、直接typeの中身をいじらないで済む方法を考えてみましょう。

一番簡単なコードは、

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon
character(len=18)::job= "neet"
end type Creature

contains

subroutine creature_status(self)
type(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine set_level(self,level)
type(creature)::self
integer::level
self%level = level
return
end subroutine set_level

function get_level(self) result(level)
type(creature)::self
integer::level

level = self%level
return
end function get_level

end module mod_creature

levelの値を取得したりセットしたりするsubroutineやfunctionを定義したものでしょう。

ここで、typeにprivate属性をつけましたので、もうmainプログラムから直接値を変更したり参照したりすることはできません。

また、デフォルトの値を入れておきました。

このモジュールを使うには、

program main

use mod_creature
implicit none
type(creature)::my_creature
integer::newlevel

call creature_status(my_creature)

call set_level(my_creature,2)
newlevel = get_level(my_creature)
write(*,*) newlevel

end program

とします。これによって、moduleで定義されたfunctionあるいはsubroutineによってのみ、値を変更したり参照したりが可能となりました。

意図しない代入や値の参照を防ぐことができています。

typeを操作するものは全てmoduleの中に入れなければならないので、デバッグする際にはこのmoduleの中身にだけ注力すればよいので、デバッグがしやすくなっているはずです。これを「隠蔽」と呼ぶかと思います。


オブジェクト指向

ここまでくればFortranのオブジェクト指向はもうすぐです。

moduleの中で定義したset_levelなどは、今考えているtype専用のsubroutineです。ですので、callする時はいつも引数にそのtypeが入っています。

他のtypeなどは入りません。

それならば、「この関数もtypeの定義の中に入れてもいいのでは?」と考えることができます。そこで、

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon
character(len=18)::job= "neet"
contains
procedure:: status => creature_status
procedure:: set_level => creature_set_level
procedure:: get_level => creature_get_level
end type Creature

contains

subroutine creature_status(self)
class(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine creature_set_level(self,level)
class(creature)::self
integer::level
self%level = level
return
end subroutine creature_set_level

function creature_get_level(self) result(level)
class(creature)::self
integer::level

level = self%level
return
end function creature_get_level

end module mod_creature

としました。ここで、typeの定義の中にcontains文でサブルーチンが登録されています。また、どのサブルーチンをどのような名前で使うか、ということがprocedureのところに書かれています。そして、呼んでいるサブルーチンでは、typeだったものがclassになっています。

これでFortranでのオブジェクト指向なmoduleができました。

このmoduleは

program main

use mod_creature
implicit none
type(creature)::my_creature
integer::newlevel

call my_creature%status()

call my_creature%set_level(2)
newlevel = my_creature%get_level()
write(*,*) newlevel

end program

のように使います。先ほどまで呼んでいたsubroutineは、my_creatureに付属しているような形で使われています。そして、最初の引数が消えています。型がclassの引数は省略され、その結果、my_creature専用のsubroutineとなっています。

これで、不用意にコードを変更することが難しくなりました。creature typeのものはすべてtypeの部分を見ればよいことになるので、バグの混入の危険性が減っています。そして、変更したい場合はここだけを見ればよい、ということがすぐにわかります。

このように、コードの保守性を上げることも、オブジェクト指向の重要な目的の一つです。


コンストラクタ

さて、ここまでくればほとんどオブジェクト指向ができたようなものです。あとは、typeを定義した時に初期に行う動作を登録できれば、毎回初期化用関数を呼ぶ必要がなくなりますので、便利になります。そのためには、呼ぶとそのtypeを返す関数と、typeと同じ関数があるとよいです。つまり、

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon="None"
character(len=18)::job= "neet"
contains
procedure:: status => creature_status
procedure:: set_level => creature_set_level
procedure:: get_level => creature_get_level

end type Creature

!コンストラクタの宣言
interface Creature
module procedure init_creature
end interface Creature

contains

! コンストラクタ
type(Creature) function init_creature(level,weapon)
!integer,optional::level
integer,intent(in),optional::level
Character(len=*),intent(in),optional::weapon
if (present(level)) then
call init_creature%set_level(level)

end if
if (present(weapon)) then
init_creature%weapon = weapon
end if

end function init_creature

subroutine creature_status(self)
class(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine creature_set_level(self,level)
class(creature)::self
integer::level
self%level = level
return
end subroutine creature_set_level

function creature_get_level(self) result(level)
class(creature)::self
integer::level

level = self%level
return
end function creature_get_level

end module mod_creature

としました。ここであたらしくinterfaceinit_creatureを定義しています。creatureと呼ぶとinit_creatureが呼び出される形になっています。もし、初期化する関数を変えたければ、新しい関数を定義して、interfaceの部分でそちらを使うように指定すればよく、コード全体の修正の必要はありません。creatureという関数を呼び出せよいというわけです。このような関数を、コンストラクタ、と呼びます。

これを使ったmain部分は、

program main

use mod_creature
implicit none
type(creature)::my_creature
integer::newlevel

my_creature = creature()

call my_creature%status()

call my_creature%set_level(2)
newlevel = my_creature%get_level()
write(*,*) newlevel

end program

となります。なお、init_creatureの引数はoptional属性を持っているので、creatureを呼ぶ時に引数を省略することができます。一つ注意すべき点としては、init_creatureを定義するさいにはきちんとintent(in)を使わないとコンパイルでエラーが出る、ということです。


継承

あとオブジェクト指向で重要なものは、「継承」です。これは、定義されているクラスとほとんど同じだけれども少し違うクラスを作るときに役立ちます。

というわけで、creatureクラスと似ているが違うクラスを定義してみましょう。

    type,extends(Creature)::Warrior

end type Warrior

type,extends(Creature)::Magic
end type Magic

これで、Creatureクラス(Fortranではtype)で定義されたlevelなどの要素を持ったtypeである、WarriorMagicができました。なお、Fortranでは大文字と小文字を区別しませんので、見やすいほうに合わせて適宜大文字を使ってもかまいません。

上記のtypeは追加の要素を持っていませんが、ここで新しい要素を足しておくこともできます。そして、extendsは継承を表しており、これをつけておくとCreaturetypeで定義したsubroutineやfunctionを使うことができます。

例えば、

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon="None"
character(len=18)::job= "neet"
contains
procedure:: status => creature_status
procedure:: set_level => creature_set_level
procedure:: get_level => creature_get_level

end type Creature

type,extends(Creature)::Warrior
end type Warrior

type,extends(Creature)::Magic
end type Magic

!コンストラクタの宣言
interface Creature
module procedure init_creature
end interface Creature

contains

! コンストラクタ
type(Creature) function init_creature(level,weapon)
!integer,optional::level
integer,intent(in),optional::level
Character(len=*),intent(in),optional::weapon
if (present(level)) then
call init_creature%set_level(level)

end if
if (present(weapon)) then
init_creature%weapon = weapon
end if

end function init_creature

subroutine creature_status(self)
class(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine creature_set_level(self,level)
class(creature)::self
integer::level
self%level = level
return
end subroutine creature_set_level

function creature_get_level(self) result(level)
class(creature)::self
integer::level

level = self%level
return
end function creature_get_level

end module mod_creature

と先ほどと同じくCreaturetypeにのみ関数が定義されている状態で、

program main

use mod_creature
implicit none
type(creature)::my_creature
type(Warrior)::my_warrior
integer::newlevel

my_creature = creature()

call my_creature%status()

call my_creature%set_level(2)
newlevel = my_creature%get_level()
write(*,*) newlevel

my_warrior = warrior()

call my_warrior%status()

call my_warrior%set_level(2)
newlevel = my_warrior%get_level()
write(*,*) newlevel

end program

のように、WarriortypeがCreaturetypeで使用できる関数を使うことができています。この機能によって、何度も同じコードを書く手間が省け、複数似たものを書くことによるバグの混入を防げます。

次に、それぞれの初期化を変えてみましょう。

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon="None"
character(len=18)::job= "neet"
contains
procedure:: status => creature_status
procedure:: set_level => creature_set_level
procedure:: get_level => creature_get_level

end type Creature

type,extends(Creature)::Warrior
end type Warrior

type,extends(Creature)::Magic
end type Magic

!コンストラクタの宣言
interface Creature
module procedure init_creature
end interface Creature

interface Warrior
module procedure init_warrior
end interface Warrior

interface Magic
module procedure init_magic
end interface Magic

contains

! コンストラクタ
type(Creature) function init_creature(level,weapon)
!integer,optional::level
integer,intent(in),optional::level
Character(len=*),intent(in),optional::weapon
if (present(level)) then
call init_creature%set_level(level)

end if
if (present(weapon)) then
init_creature%weapon = weapon
end if

end function init_creature

type(Warrior) function init_warrior(level,weapon)
integer,intent(in),optional::level
Character(len=18),intent(in),optional::weapon
if (present(weapon)) then
if (present(level)) then
init_warrior%Creature = creature(level,weapon)
else
init_warrior%Creature = creature(weapon=weapon)
end if
else
if (present(level)) then
init_warrior%Creature = creature(level=level)
else
init_warrior%Creature = creature()
end if
end if

init_warrior%attack = init_warrior%attack+ 3*init_warrior%level
if (present(weapon)) then
else
init_warrior%weapon = "sword"
end if
if (init_warrior%job == "neet") then
init_warrior%job = "Warrior"
else
init_warrior%job = init_warrior%job//"Warrior"
end if

end function init_warrior

type(Magic) function init_magic(level,weapon)
integer,intent(in),optional::level
Character(len=18),intent(in),optional::weapon
if (present(weapon)) then
if (present(level)) then
init_magic%Creature = creature(level,weapon)
else
init_magic%Creature = creature(weapon=weapon)
end if
else
if (present(level)) then
init_magic%Creature = creature(level=level)
else
init_magic%Creature = creature()
end if
end if

init_magic%mp = init_magic%mp+ 4*init_magic%level
if (present(weapon)) then
else
init_magic%weapon = "rod"
end if
if (init_magic%job == "neet") then
init_magic%job = "Magic"
else
init_magic%job = init_magic%job//"Magic"
end if

end function init_magic

subroutine creature_status(self)
class(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine creature_set_level(self,level)
class(creature)::self
integer::level
self%level = level
return
end subroutine creature_set_level

function creature_get_level(self) result(level)
class(creature)::self
integer::level

level = self%level
return
end function creature_get_level

end module mod_creature

のように、新しくinterfaceをつけて、Warriorの初期化はinit_warriorが行うこととしました。

これによって、main文の中身を全く変化させることなく、それぞれの初期化を変更できました。

なお、levelなどの継承した部分にアクセスするにはmy_warrior%levelでできますが、my_warrior%creature%levelでも可能です。これができるために、継承した部分だけを初期化するのにinit_warrior%Creature = creature(level,weapon)が使えます。


多重継承

MagicとWarriorの両方のクラスを継承したMagicWarriorクラスを作ることを考えます。

いまのところ、FortranでPythonの記事にあるような多重継承のやり方はわかりませんでした。しかし、julia言語で同等の機能をすでに実装していますので、同様にやれば同等の機能を持つ手法を実装できます。つまり、creatureをoptional引数として持っていれば、可能です。そのコードは

module mod_creature

implicit none
type Creature
private
integer::level= 1
integer::hp= 0
integer::mp= 0
integer::attack= 0
integer::defence= 0
character(len=18)::weapon="None"
character(len=18)::job= "neet"
contains
procedure:: status => creature_status
procedure:: set_level => creature_set_level
procedure:: get_level => creature_get_level

end type Creature

type,extends(Creature)::Warrior
end type Warrior

type,extends(Creature)::Magic
end type Magic

type,extends(Warrior)::MagicWarrior
end type MagicWarrior

!コンストラクタの宣言
interface Creature
module procedure init_creature
end interface Creature

interface Warrior
module procedure init_warrior
end interface Warrior

interface Magic
module procedure init_magic
end interface Magic

interface MagicWarrior
module procedure init_magicwarrior
end interface MagicWarrior

contains

! コンストラクタ
type(Creature) function init_creature(level,weapon)
!integer,optional::level
integer,intent(in),optional::level
Character(len=*),intent(in),optional::weapon
if (present(level)) then
call init_creature%set_level(level)

end if
if (present(weapon)) then
init_creature%weapon = weapon
end if

end function init_creature

type(Warrior) function init_warrior(level,weapon,my_creature)
integer,intent(in),optional::level
Character(len=18),intent(in),optional::weapon
type(Creature),intent(in),optional::my_creature
if (present(my_creature)) then
init_warrior%Creature = my_creature
else
if (present(weapon)) then
if (present(level)) then
init_warrior%Creature = creature(level,weapon)
else
init_warrior%Creature = creature(weapon=weapon)
end if
else
if (present(level)) then
init_warrior%Creature = creature(level=level)
else
init_warrior%Creature = creature()
end if
end if
end if

init_warrior%attack = init_warrior%attack+ 3*init_warrior%level
if (present(weapon)) then
else
init_warrior%weapon = "sword"
end if
write(*,*) init_warrior%job
if (init_warrior%job == "neet") then
init_warrior%job = "Warrior"
else
init_warrior%job = trim(init_warrior%job)//"Warrior"
end if

end function init_warrior

type(Magic) function init_magic(level,weapon)
integer,intent(in),optional::level
Character(len=18),intent(in),optional::weapon
if (present(weapon)) then
if (present(level)) then
init_magic%Creature = creature(level,weapon)
else
init_magic%Creature = creature(weapon=weapon)
end if
else
if (present(level)) then
init_magic%Creature = creature(level=level)
else
init_magic%Creature = creature()
end if
end if

init_magic%mp = init_magic%mp+ 4*init_magic%level
if (present(weapon)) then
else
init_magic%weapon = "rod"
end if
if (init_magic%job == "neet") then
init_magic%job = "Magic"
else
init_magic%job = init_magic%job//"Magic"
end if

end function init_magic

type(MagicWarrior) function init_magicwarrior(level,weapon)
integer,intent(in),optional::level
Character(len=18),intent(in),optional::weapon
type(magic)::my_magic
my_magic = magic(level,weapon)

init_magicwarrior%warrior = warrior(my_creature=my_magic%creature)
end function init_magicwarrior

subroutine creature_status(self)
class(creature)::self
write(*,*) "Job:",self%job,"| HP:",self%hp,"| MP:",self%mp,"| Atk:",self%attack,&
"| Def:",self%defence,"| Weapon:",self%weapon
return
end subroutine creature_status

subroutine creature_set_level(self,level)
class(creature)::self
integer::level
self%level = level
return
end subroutine creature_set_level

function creature_get_level(self) result(level)
class(creature)::self
integer::level

level = self%level
return
end function creature_get_level

end module mod_creature

となります。ポイントは、MagicWarriortypeはWarriortypeから継承して作られており、Warriortypeは初期化の際にoptional引数としてcreatureを使える、ということです。これによって、Magicから転職したWarriorすなわちMagicWarriorが定義できます。

main文は

program main

use mod_creature
implicit none
type(creature)::my_creature
type(Warrior)::my_warrior
type(magicWarrior)::my_magicwarrior
integer::newlevel

my_creature = creature()

call my_creature%status()

call my_creature%set_level(2)
newlevel = my_creature%get_level()
write(*,*) newlevel

my_warrior = warrior()

call my_warrior%status()

call my_warrior%set_level(2)
newlevel = my_warrior%get_level()
write(*,*) newlevel

my_magicwarrior = magicwarrior()

call my_magicwarrior%status()

call my_magicwarrior%set_level(2)
newlevel = my_magicwarrior%get_level()
write(*,*) newlevel

end program

となります。ちゃんとcreaturetypeで定義された関数がMagicWarriortypeでも使えていることがわかります。


まとめ

オブジェクト指向をFortran 2003の機能で実装してみました。

Fortranから眺めてみると、オブジェクト指向というのは、「構造体typeにfunctionやsubroutineが定義できるようになったもの」であり、その他、継承など便利な機能を追加したもの、と言うことができるでしょう。