Fortran における構造化と抽象データ構造(Abstruct Data Type)の実現
プログラムの構造化は、制御の構造化、データの構造化、抽象データ構造化(制御とデータの一体化)に分けられると前の記事(こちら)で述べました。この記事では、Fortran での抽象データ構造の実現法について概観します。
Fortran における抽象データ構造の実現方法
抽象データ構造とは、データ変数とそれを操作するサブルーチン・関数を一体として考えるもので、利用者側にはデータ変数の詳細は明かされず、それに対して許される操作のインターフェースのみが明かされることになります。「情報隠蔽(Encapsulation)」と言われるものです。
情報隠蔽の正否についてにわかには判断はつきませんが、F. P. Brooks の IBM System/360 の開発について記した有名な本『人月の神話』 (F. P. Brooks, The Mythical Man-Month: Essays on Software Engineering, (1975), Addison-Wesley) が参考になります。第7章で Brooks は全員がすべての詳細を知るべきと考え、D. L. Parnas の情報隠蔽の考え方を全否定しています。そして過剰な情報の波にのまれていく様子が描かれています。しかし20年後に出された改訂版の第18章には、後に Parnas と直接話したことですっかり考えを改めたと書かれています。
抽象データ構造の実現法としては、オブジェクト指向プログラミング(OOP)がよく取り上げられますが、それ以外にもさまざまなアプローチがあります。データとプロシージャ(サブルーチン・関数)の結びつきの疎密によって分類することが可能です。
- Fortran 90 で導入された Module を用いて、疎結合の極限を実現できます。データとプロシージャは同じプログラム単位に記述されていますが、呼び出し法などは従来と変わりません。
- Fortran 2003 で導入されたオブジェクト指向 (OO) は、密な結合の極限にあたります。データとプロシージャは静的に結び付けられており、全てのインスタンスでプロシージャは共通になります。継承に応じた動的な変化は多態のような仕組みを用います。
- その中間的なものとして、type-bound procedure を procedure pointer を使って、動的にデータと結び付ける方法があります。この場合はインスタンス毎にプロシージャを変えられます。
Fortran には、このどのやり方も実現することができる文法が用意されています。以下にその実例を挙げます。
実際の例
1. 疎結合モデル: Module による
Fortranでは module
を用いることで、データと手続きの疎結合が可能です。module
にデータ型や関数をまとめることで、異なるプログラム部品間で依存を最小化しつつ、コードの再利用を促進することができます。
module SparseModule
implicit none
type :: SparseData
integer :: id
real :: value
end type SparseData
contains
subroutine print_sparse(data)
type(SparseData), intent(in) :: data
print *, 'ID:', data%id, ' Value:', data%value
end subroutine print_sparse
end module SparseModule
program main
use SparseModule
implicit none
type(SparseData) :: data_instance
data_instance%id = 1
data_instance%value = 3.14
call print_sparse(data_instance)
end program main
このように、モジュールを使うことでデータ構造と手続きを明確に分離し、疎結合を実現できます。
2. 中間結合モデル: type-bound procedure による
Fortran では継承を使わずに、type-bound procedure
の pointer(関数ポインタ)を利用して、柔軟なデータ構造を作成することが可能です。これにより、異なる振る舞いを持つオブジェクトを扱うことができます。
module FunctionPointerModule
implicit none
type :: FunctionPointerData
integer :: id
procedure(print_proc), pointer :: print => null()
end type FunctionPointerData
abstract interface
subroutine print_proc(this)
import :: FunctionPointerData
class(FunctionPointerData), intent(in) :: this
end subroutine print_proc
end interface
contains
subroutine print_implementation(this)
class(FunctionPointerData), intent(in) :: this
print *, 'ID:', this%id
end subroutine print_implementation
end module FunctionPointerModule
program main
use FunctionPointerModule
implicit none
type(FunctionPointerData) :: data_instance
data_instance%id = 2
data_instance%print => print_implementation
if (associated(data_instance%print)) then
call data_instance%print()
endif
end program main
この例では、継承なしに type-bound procedure を使って、異なる振る舞いをデータ型に結びつけることができます。このアプローチにより、柔軟で拡張可能な設計が可能となります。もちろん継承を使うことも可能なので、問題に応じて結合度を変えることができます。
type と type-bound procedure pointer の間に循環参照が生じるので、定義の import が必要になります。
3. 密結合モデル: OOP の CLASS による
Fortran 2003以降では、CLASS
を使ったオブジェクト指向プログラミングが可能になり、密結合なデータモデルを作成することができます。このモデルでは、派生型と多態性を用いることで、オブジェクト間に強い結合性を持たせています。ここでは抽象クラスと、抽象インターフェースも使ってみました。
オブジェクト指向は、名詞的と称され、データ変数をプロシージャより重視するものと言われたりもします。しかし抽象度が上がると、データの詳細は隠され、まず動作(メソッド)のみが列挙的に定義されます。これが抽象クラスにあたります。
module OOPModule
implicit none
type, abstract :: Object
contains
procedure(aprint), deferred :: print
end type Object
abstract interface
subroutine aprint(this)
import
implicit none
class(Object), intent(in) :: this
end subroutine aprint
end interface
type, extends(Object) :: BaseData
integer :: id
contains
procedure :: print => base_print
end type BaseData
type, extends(BaseData) :: DerivedData
real :: value
contains
procedure :: print => derived_print
end type DerivedData
contains
subroutine base_print(this)
class(BaseData), intent(in) :: this
print *, 'Base ID:', this%id
end subroutine base_print
subroutine derived_print(this)
class(DerivedData), intent(in) :: this
print *, 'Derived ID:', this%id, ' Value:', this%value
end subroutine derived_print
end module OOPModule
program main
use OOPModule
implicit none
class(BaseData), allocatable :: obj
type(DerivedData) :: derived_instance
derived_instance%id = 3
derived_instance%value = 1.618
obj = derived_instance
call obj%print()
end program main
ここでは、CLASS
と継承を用いた密結合モデルを示しています。この方式では、多態性を活用し、異なる型のオブジェクトを同じインターフェースで扱うことができます。密結合のため、オブジェクト間の関係が強くなり、コード全体の構造が把握しやすくなるという特徴があります。
まとめと結論
本記事では、Fortran における抽象データ構造の実現法を、データとプロシージャの結びつきの疎密度から三つの場合に分けて紹介しました。
それは、1. 疎結合の極限としての Module 2. 中間結合としての type-bound procedure pointer 3. 密結合の極限としての class による OOP です。
伝統的な Fortran は、疎結合として理解できると考えられます。一時期はオブジェクト指向ではないからということで、だいぶ肩身の狭い思いをさせられました。「オブジェクト指向に非ずば人に非ず」式にオブジェクト指向がもてはやされた時期がありましたが、2010 年代からはオブジェクト指向プログラミングに批判が高まっているように感じます。最近の流行りは中間結合的な方法に類するものに見受けられます。継承を使わず必要に応じてデータ型とプロシージャを結び付ける方法を、他言語は様々な形で実現しています。
なお、N. Wirth は 1990 年の以下の論文において、オブジェクト指向を批判的に論じ、procedure pointer を用いての中間結合的な方法の利点を述べています。
N. Wirth, From Modula to Oberon: The Programming Language Oberon. Information Processing Letters, 14(3), 193-204. (1990).
強調したいのは、抽象データ構造を実現する場合に、これらの結合様式の内のどれかが優れていて他が劣っているというものではないことです。さらに言えば、これらの文法要素を使えば必ず抽象データ構造が実現されるわけではなく、また逆に必ずしも抽象データ構造を構成しなければならないわけでもないことです。あくまで自分の問題の特質に応じて、より適切な方法を選べばよいのだと思います。
付記
倒置法
OOP や中間結合方式で type-bound procedure を用いた呼び出しを行う場合、語順が少し入れ替わりますが、これは自然言語の文法では倒置法にあたる構文に相当するとみなせます。(命令文なので主語の S は無いとして)V + O1 + O2 が O1 + V + O2 と目的語が先頭に飛び出すようなものです。
call sub(this, O2, ...)
を
call this%sub(O2, ...)
と書き換えることになります。Fortran の場合 call という動詞が付きますがw
ChatGPT
この記事も ChatGPT 4o with canvas を用いて書きました。Version up したせいか、先週よりも利口になった気がします。Fortran のコードはほぼ自動生成されたものです。Intel Fortran で実行できるように、わずかに修正を加えました。
Canvas は日本語変換と相性が悪くて色々苦労させられました。また文章の末尾の方がよく消えます。文章を勝手に書き換えるので、時々困りました。バージョン管理も出鱈目です。しかし全体として見ると、驚異的な代物と感じました。