ユーザー定義派生型の使い方失敗談とその対策

## 執筆者が体験したユーザー定義派生型の使い方失敗5例

@soybean です．
ライブラリ[plantFEM](https://github.com/kazulagi/plantFEM.git)をメンテする上で，様々な失敗を重ねてきましたので，その例と（当面の）対策について書きます．
そのいくつかは，よく知られたアンチパターンに結びついています．

## 【失敗1】派生型のIOを担うメンバ関数/サブルーチンが複雑になりすぎて，関数名が禁句扱いになる．

例えば，3次元オブジェクトを持つ派生型```mesh```を以下のように定義します．

```Fortran
use iso_fortran_env
type mesh
    real(real64), allocatable :: x(:), y(:), z(:)
contains
    procedure, public :: import => importMesh
end type mesh

contains

! ここにimportMeshのプログラムを書く
!...
```
すると，```importMesh```のようなインポーターを書きたくなりますが，3次元オブジェクトの入出力形式は多種多様ですので，必然的に内部で条件分岐を伴う複雑な実装を施すこととなります．

その結果，ナイーブに実装した場合，派生型のIOを担うメンバ関数/サブルーチンが複雑になりすぎて，保守性が低下します．

```Fortran
subroutine importMesh(this, fileName,VTK_reader_option1,VTK_reader_option2,&
VTK_reader_option3,..,PLY_reader_option1,PLY_reader_option2,PLY_reader_option3,... )
    class(mesh) :: this
    character(*),intent(in) :: fileName
    ! 読み込み時，拡張子ごとに異なる追加情報が必要な場合，optional引数が多数必要になる．
    character(*),optional,intent(in) :: VTK_reader_option1,VTK_reader_option2,&
    VTK_reader_option3,..,PLY_reader_option1,PLY_reader_option2,PLY_reader_option3...
    
    ! fileNameから拡張子を判定
    ! 拡張子ごとにif分岐を設ける．
    if( extention(fileName)== 'vtk' ) then
        call importMeshVTK(this, fileName,VTK_reader_option1,VTK_reader_option2,&
        VTK_reader_option3,..)
    elseif( extention(fileName)== 'ply' ) then
        call importMeshPLY(this, fileName,PLY_reader_option1,PLY_reader_option2,&
        PLY_reader_option3,...)
    ...
    end if
    
end subroutine importMesh
```

### 【原因】場当たり的に追加した```optional```引数の多用により，入力クラッジ(input kludge)が発生している．

新しいファイル形式に対応するたびに安直に条件分岐と```optional```引数を増やした結果，内部の仕様を知らないと正しい引数の組み合わせがわからないことになっています．


### 【対策】機能の切り分けを行うとともに，必須かつ共通な引数を```optional```なしで必須の引数として宣言する．

個別の明確な機能をもつ個別の関数/サブルーチンに分割（```importVTKFILE()```など）し，また不正な引数を入れられないように，引数の数を削減してかつ強制することが考えられます．なお，ここで

- optional引数を派生型のメンバ変数にして解決
- ``import```のかわりに```read```などと別の関数を作って対策

などとすると，事態が一層悪化することがあります．


## 【失敗2】1つのメンバ関数/サブルーチンに，無駄に複数の名前を割り当てる．

はじめ，```import```を作った後で，他のライブラリでは```read```を遣っていると気が付き，さらに他のライブラリでは```read_file```を遣っていると気が付き...
全てのユーザーを呼び込もうとして多数の名前をつけてしまう危険性があります．
その結果，```import```と```open```を切り分けたい時に変更が困難となってしまう等の弊害が発生します．

```Fortran
use iso_fortran_env
type mesh
    real(real64), allocatable :: x(:), y(:), z(:)
contains
    procedure, public :: import => importMesh
    procedure, public :: read   => importMesh
    procedure, public :: open        => importMesh
    procedure, public :: import_file => importMesh
    procedure, public :: read_file   => importMesh
    procedure, public :: open_file   => importMesh
    
end type mesh

contains

! ここにimportMeshのプログラムを書く
!...
```

### 【原因】関数名決定時に優先順位を取り違えている．

関数名/サブルーチン名として，統一され，かつ意味が一意に特定できることが肝要であり，表面的な呼称を他ライブラリと合わせることはより優先順位が低いことですので，その点を明らかにして実装をすべきでした．

### 【対策】意味の似た動詞は，目的語とセットにして名称とするなどして，各関数/サブルーチンの役割を明確化する．また，幅広い意味にとれる単語は使用しない．

ライブラリごとに構造は異なるものですし，構造が類似していれば手続き名称が多少異なっても慣熟は早いものです．「手続き名を他ライブラリに合わせれば，新規ユーザーが直感的に使える」という幻想から，構造と機能の異なる手続きを同じものと誤解させるほうがユーザーを困惑させます．

## 【失敗3】 ```intent(in)```で十分なところを，```intent(inout)```にしてしまう．

この失敗は，

- はじめ，```intent(inout)```で読み込むことを想定していたのが，実装を進める内に```intent(in)```で十分であることに気がついたものの，なぜか放置された．
- ```allocated()```を使おうとして，「```intent(in)```属性では```allocated()```を呼べない」などとコンパイルエラーが出て，安直に```intent(inout)```とした．

といった経緯で発生しえます．
本手続きを```pure```にし，```do concurrent```内で呼ぶ際や，他の```intent(in)```で受け渡す手続きで呼ぶ場合に問題が顕在化します．

### 【対策】```intent(inout)```は，```intent(in)```で済むように工夫する．

これに尽きますが，見落とした場合には，後から幾多の依存関係を整理しながら```intent(inout)```を```intent(in)```に変更するお仕事が生まれます．


## 【失敗4】```String```, ```List```, ```Dictionary```等の型を自作し，保守しきれなくなり，特定の関数を呼ぶときの特殊な型としていつまでも残ってしまう．

他の言語の機能を真似て，基本的な型を自作した上で，実装当初は便利に感じてさまざまな関数/
サブルーチンで用いたとします．
しかし，よくあることとして，こうした低レベルな自作型は求める機能が多いため，保守コストを払いきれなくなり，やがて使わなくなります．

その結果として，「あるサブルーチンHogeを呼ぶ際にだけ必要な奇っ怪な型であるList型」のようなものが錬成されてしまい，リファクタリングに多大なコストを払うことになります．

### 【対策】stdlib等の外部ライブラリを積極的に活用する．

stdlib等の優れた外部ライブラリがありますので，それらを利用して車輪の再発明を抑制します．

stdlibについてはこちらの記事が詳しいです．

https://qiita.com/implicit_none/items/6fc196bd96fb5495ed44


一方で，外部ライブラリへの依存度が高すぎるライブラリは，砂上の楼閣と呼ばれるアンチパターンに該当します．塩梅が難しい...


### 【失敗5】コンストラクタは定義したものの，デストラクタを定義し忘れてメモリが解放できなくなる．

初期化用の手続きとして，```init```等のコンストラクタを定義することは基本的です．

しかし，インスタンスを1回呼ぶだけのテストコードを実行している場合には，うっかりデストラクタを定義し忘れることがあります．

すると，ループ処理などを書いた際に，一度立てたインスタンスがいつまでもメモリを専有したり，```init```で再初期化しようとしてバグを生じたりといった失敗が生じます．

### 【対策】デストラクタはコンストラクタとセットで定義しておく．

当たり前ではありますが...



## まとめ

派生型を使う場合にうっかり踏んだ失敗と，その対策をまとめました．
対策として，よりよい方法が気もしますので，詳しい方がおられましたらご指摘頂ければ幸いです．