LoginSignup
34
27

More than 5 years have passed since last update.

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

Posted at

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が定義できるようになったもの」であり、その他、継承など便利な機能を追加したもの、と言うことができるでしょう。

34
27
0

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
34
27