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
としました。ここであたらしくinterface
とinit_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である、Warrior
とMagic
ができました。なお、Fortranでは大文字と小文字を区別しませんので、見やすいほうに合わせて適宜大文字を使ってもかまいません。
上記のtypeは追加の要素を持っていませんが、ここで新しい要素を足しておくこともできます。そして、extends
は継承を表しており、これをつけておくとCreature
typeで定義した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
と先ほどと同じくCreature
typeにのみ関数が定義されている状態で、
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
のように、Warrior
typeがCreature
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
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
となります。ポイントは、MagicWarrior
typeはWarrior
typeから継承して作られており、Warrior
typeは初期化の際に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
となります。ちゃんとcreature
typeで定義された関数がMagicWarrior
typeでも使えていることがわかります。
まとめ
オブジェクト指向をFortran 2003の機能で実装してみました。
Fortranから眺めてみると、オブジェクト指向というのは、「構造体typeにfunctionやsubroutineが定義できるようになったもの」であり、その他、継承など便利な機能を追加したもの、と言うことができるでしょう。