この記事に書いてあること
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_t
はabstract
であることに注意してください。
型(クラス)の基本形を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_t
はabstract
なので、利用するときに実体クラスを作成しなければなりません。
ここでは、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,abstract
やabstract interface
- ファクトリ関数
- classのallocatable変数
といった機能を活用することで、最終的にコードの保守管理が容易になっていくのではないかと思います。
実行結果の例
以上の解説は、github上のflinkedlistのm_linkedlist.f90
の設計思想を解説したものです。
test/test_m_linkedlist.f90
とtest/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でこのようなデータ構造を作るとすれば、
- 可変サイズの配列を作るモジュール(C++の
std::vector<void*>
のような構造)を定義 - リストモジュールとして、上記の可変サイズ配列を扱う構造体を定義
という流れになるのかと思っています。このようなpython式リストの弱点は、要素を追加するとき、偶然に内部の配列サイズを超えるような状況になると、配列をallocateしなおして新しい配列に要素をコピーする処理が必要になるという点です。