5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FortranAdvent Calendar 2024

Day 13

Fortranでもインターフェース継承してみる

Last updated at Posted at 2024-12-12

この記事に書いてあること

Fortranで type,abstractを活用し、SOLID原則を意識したコードを書いてみます。

※SOLIDとかオブジェクト指向とか書いていますが、この方式のみを勧める意図は全くありません。各自の問題の特性に応じて使い分ければ良いと思います。

インターフェースとは

Fortranでは、interface文があるのでそちらに意識が行きがちですが、fortranのinterface文は「オブジェクト指向」の文脈で「インターフェース」と呼ばれている事柄の一部だと思うようになりました。

突然他言語の例ですが、例えばPythonで「インターフェースを定義して」と言われた場合、クラスとそのメソッドを次のように定義することがあります。

from abc import ABC, abstractmethod

class i_sample_t(ABC):
    @abstractmethod
    def myprocedure(self):
        """メソッドインターフェース
        """

つまり、Fortranで書くと次のような定義全体が、オブジェクト指向の文脈では「インターフェース」と呼ばれているように思います。

! 動的に生成されるクラスの基本となる"かたち"を決める
type,abstract :: i_sample_t
    contains
    procedure(interface_myprocedure),deferred :: myprocedure
end type

! 手続きの"かたち"を決める
abstract interface
    subroutine interface_myprocedure(self, ...ほかに色々な引数)
        import i_sample_t
        class(i_sample_t),intent(in) :: self
    end subroutine
end interface

これを実際に使うには、type,extends(i_sample_t) :: sample_tのように実体となる型とその手続きmyprocedureを型毎に定義しなければなりません。

こういうことをするのは面倒に感じるかもしれませんが、一度定義してしまえば、

  • 型(クラス)を利用する場面
  • 内部処理を変更する場面

などで書き換えが少なくなります。(言い換えると、インターフェースを守るように書かざるを得なくなる)
特に、はじめにインターフェースを決めておき、機能一つにつき型(クラス)を一つ実装するような方針を取ると、複数人でコードを書く場合に手戻りが少なくなります。
このあたりの事情についてはSOLID原則を見るとよいと思います。

インターフェース継承の事例

どのようなデータ型でも格納できるリスト構造を考えてみます。
ひとつは、class(*)という無限多層性を使うことですが、これは今回考えません。この記事では、

  • リスト構造を管理する構造体 type :: list_t
  • リストに格納するデータ要素を表す構造体 type,abstract :: node_t

の2つを考えます。
リストへの値の保持とリンク機構の構築は、上記の2つの型に対して実装します。
実際に利用する場面では、type,extends(node_t) :: mynodeのようにして、好きなデータ型を内包するクラスを作り、それをリストに追加すればいいわけです。

具体例

例えば、リンクリストは次のような定義を書けば作成できます。
ここでは、node_tabstractであることに注意してください。
型(クラス)の基本形をabstractで宣言しているので、その基本形を継承したクラスであれば、何でもlist_tに追加することができるのです。

type,abstract :: node_t
    class(node_t),pointer,private :: nxt => null()
    class(node_t),pointer,private :: bef => null()
end type

type :: list_t
    integer(int64),private :: n_elements = 0
    class(node_t),pointer,private :: head => null()
    class(node_t),pointer,private :: tail => null()
    class(node_t),pointer,private :: current_node => null() ! for iteration
    integer,private :: warning_output_unit = error_unit
  contains
    ! private procedures
    procedure,non_overridable,private :: list_t_get_current_node_ptr, list_t_get_current_node_allocatable
    procedure,non_overridable,private :: list_t_get_node_ptr, list_t_get_node_allocatable
    ! public procedures
    procedure,non_overridable,public :: entries => get_the_numver_of_entries
    procedure,non_overridable,public :: append => list_t_append
    procedure,non_overridable,public :: insert => list_t_insert
    procedure,non_overridable,public :: remove => list_t_remove_idx
    procedure,non_overridable,public :: clear => list_t_clear
    generic,public :: get_nodeclass => list_t_get_node_ptr, list_t_get_node_allocatable
    generic,public :: get_current_nodeclass => list_t_get_current_node_ptr,list_t_get_current_node_allocatable 
end type

利用するときに継承してクラスを作る

さて、前述のnode_tabstractなので、利用するときに実体クラスを作成しなければなりません。
ここでは、type,abstract,extends(node_t)::i_shape_tというインターフェースを追加し、それを継承した実体クラス「円(circle_t)」、「長方形(rectangle_t)」を作ります。

    ! define an abstract type to enable the use of print structures easily
    type,abstract,extends(node_t) :: i_node_with_io_t 
      contains
      procedure(interface_write_formatted),deferred,private :: write_formatted
      generic :: write(formatted) => write_formatted
    end type
    type,abstract,extends(i_node_with_io_t) :: i_shape_t
        contains
        procedure(area_interface),pass,deferred,public :: area
    end type

    ! define interfaces for abstract types
    abstract interface
        subroutine interface_write_formatted(self, unit, iotype, v_list, iostat, iomsg)
          import i_node_with_io_t
          class(i_node_with_io_t), intent(in) :: self
          integer, intent(in) :: unit
          character(*), intent(in) :: iotype
          integer, intent(in) :: v_list(:)
          integer, intent(out) :: iostat
          character(*), intent(inout) :: iomsg
        end subroutine
        function area_interface(self) result(area)
          import i_shape_t, real64
          class(i_shape_t),intent(in) :: self
          real(real64) :: area
        end function
    end interface

    type,extends(i_shape_t) :: circle_node_t
        real(real64) :: r
        contains
        procedure,public :: area => area_circle
        procedure,private :: write_formatted => circle_node_t_write_formatted
    end type
    type,extends(i_shape_t) :: rectangle_node_t
        real(real64) :: width
        real(real64) :: height
        contains
        procedure,public :: area => area_rect
        procedure,private :: write_formatted => rectangle_node_t_write_formatted
    end type

ファクトリ関数

リストに挿入するデータ型を2つ(circle_node_t, rectanble_node_t)定義したので、これを手軽に生成する関数を作ってみましょう。

      function circle_node_factory(r) result(node)
        real(real64),intent(in) :: r
        class(circle_node_t), allocatable :: node 
        allocate(circle_node_t::node)
        node%r = r
      end function
      function rectangle_node_factory(w, h) result(node)
        real(real64),intent(in) :: w, h
        class(rectangle_node_t), allocatable :: node 
        allocate(rectangle_node_t::node)
        node%width = w
        node%height = h
      end function

引数は、それぞれのクラスを一つ生成するのに必要十分なデータとします。
これを使うときは次のように書けます。

class(node_t), allocatable :: mynode
mynode=circle_node_factory(1d0) ! circle_node_t型が生成される
mynode=rectangle_node_factory(1d0,2d0) ! rectangle_node_t型が生成される

どちらの型も、class(node_t)とできる点に注目してください。
ここまで準備したら、好きなだけリストにi_shape_tの実体クラスを追加できます。

class(node_t), allocatable :: node_copied
class(node_t), pointer ::  node_ptr
call list%append(circle_node_factory(real(i, kind=real64))) !末尾に追加
call list%append(rectangle_node_factory(real(i, kind=real64),real(i*3, kind=real64)))
call list%insert(2,circle_node_factory(10d0)) !2個目の位置に挿入
call list%remove(1) !1個目の要素を削除
call list%get_nodeclass(2,node_copied) !2個目の要素をコピーして取り出す
call list%get_nodeclass(2,node_ptr) !2個目の要素をポインタとして取り出す

上記のコードにはselect typeは出てきません。

  • type,abstractabstract interface
  • ファクトリ関数
  • classのallocatable変数

といった機能を活用することで、最終的にコードの保守管理が容易になっていくのではないかと思います。

実行結果の例

以上の解説は、github上のflinkedlistm_linkedlist.f90の設計思想を解説したものです。
test/test_m_linkedlist.f90test/m_linkedlist_node_definition.f90がサンプルコードになっており、テスト実行結果の表示を一部抜粋すると下記の通りになります。

!=== LINKEDLIST TEST ==================
 Show numbers of nodes in the list:                    8
 !---loop head to tail with index ----
    1) i32node_t       : idx  =         1
    2) r64node_t       : x    =     1.000
    3) circle_node_t   : r    =     1.000, area  =     3.140
    4) rectangle_node_t: width=     1.000, height=     3.000, area=     3.000
    5) r32node_t       : idx  =         2
    6) r64node_t       : x    =     2.000
    7) circle_node_t   : r    =     2.000, area  =    12.560
    8) rectangle_node_t: width=     2.000, height=     6.000, area=    12.000
    cputime=  69.000E-6[sec]

この記事では解説しませんでしたが、github上ではint32やreal64といったデータを格納するクラスも定義しています。1)~8)の各ノードはnode_tクラスを継承しているため、要素ごとに異なる型でもリストに追加することができています。

まとめ

以上の解説が厳密に正しいかどうかはさておき、Fortranでもオブジェクト指向(特にSOLID原則)を意識した設計ができるということを書いてみました。計算負荷がとても高い部分と、データ構造の保守管理部分でうまく切り分けて、適材適所で使い分けたいものです。

リンクリストに関する独り言

この記事に書いたリンクリストは、挿入するnode_tクラスをひとつずつ鎖のようにつなげています。この方式は常に必要最小限のメモリしか占有しないというところが利点になります。弱点を挙げると、$i$番目要素にアクセスするには先頭から順にサーチする必要があるということです。
ここで、同じくpythonでリストと呼ばれている代物は、名前はリストですが内部構造が少し違います。よく「pythonのリストはアクセス時間がO(1)である」と言われますが、これは、pythonでは、リストの要素を示す"ポインタの配列"がまずあって、配列の$i$番目要素に格納されているポインタの指示先を参照するためです。Fortran風に書くと、次のような型定義になるでしょうか。

type :: entity_t
   ! なんでも指し示すことができるポインタのイメージ
   integer(int64),pointer :: ptr ! Cでいう void* ptr;を想像しています
end type
type :: list_t
   ! python風のリストデータ構造(イメージ)
   type(entity_t),allocatable :: vector_of_entity(:)
   contains
      procedure :: append
      ! その他もろもろのリストメソッド
end type
type(list_t) :: list
type(entity_t) :: entity
! ... 色々な処理
entity = list%vector_of_entity(i) ! O(1)の速度でリスト要素へアクセス

fortranでこのようなデータ構造を作るとすれば、

  1. 可変サイズの配列を作るモジュール(C++の std::vector<void*>のような構造)を定義
  2. リストモジュールとして、上記の可変サイズ配列を扱う構造体を定義

という流れになるのかと思っています。このようなpython式リストの弱点は、要素を追加するとき、偶然に内部の配列サイズを超えるような状況になると、配列をallocateしなおして新しい配列に要素をコピーする処理が必要になるという点です。

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?