docs/fileio_oo_to_fp.html §9 対比表の「状態」の行で簡単に触れた以下の対比を、
図と具体例を使って詳しく解説します。
| 観点 | 現状 (Python OO) | 提案 (Rust FP) |
|---|---|---|
| 状態 | 30 個近いインスタンス変数を mutate | 不変な struct を組み立てて 1 回返すだけ |
ScanDataPy/model/file_io.py の TsmFileIo.__init__ は以下のような構造です:
class TsmFileIo:
def __init__(self, filename_obj, num_fluo_ch=2):
# ━━━ ステップ 1: 30 個近くのフィールドをダミー値で初期化 ━━━
self.filename = filename_obj.name
self.file_path = filename_obj.path
self.full_filename= filename_obj.fullname
self.header = 0 # ← 後で bytes になる
self.full_frames = np.array([0,]) # ← 後で 3D 配列になる
self.dark_frame = np.array([0,]) # ← 後で 2D 配列になる
self.ch_frames = np.array([0,]) # ← 後で 4D 配列になる
self.num_fluo_ch = num_fluo_ch
self.full_frame_interval = 0 # ← 後で ms 値になる
self.ch_frame_interval = 0 # ← 後で ms 値になる
self.data_pixel = np.empty([0, 0]) # ← 後で [X, Y] になる
self.num_full_frames = np.empty([0,]) # ← 後で int になる
self.num_ch_frames = np.empty([0,]) # ← 後で int になる
self.full_3D_size = 0 # ← 後で配列になる
self.ch_3D_size = 0 # ← 後で配列になる
# ... 計 30 個近く
# ━━━ ステップ 2: ファイル読んで上書き (mutate) ━━━
self.read_infor() # self.data_pixel, num_full_frames を上書き
self.read_data() # self.full_frames, dark_frame を上書き
self.elec_data_obj = TbnFileIo(...) # さらに別オブジェクトを生やす
self として残り、
デバッガで触ったり、別スレッドが読みに来たり、別経路でアクセスされたりします。
「ありえない状態」が実際に存在するのがこの設計の致命傷です。
| # | 症状 | 具体的な事故シナリオ |
|---|---|---|
| ① | 中間状態が型上見えてしまう | read_data() 途中で例外。tsm.data_pixel は本物、tsm.full_frames は [0,]。呼び出し側は型からは判らず誤動作 |
| ② | 失敗時にゴミが残る | __init__ 中の raise 後、ローカル変数 tsm は消えるが、デバッグ経路や別オブジェクトの参照が残るパターンで「半完成」オブジェクトが生き残る |
| ③ | 後からの上書きを誰も止められない | 500 行後に tsm.full_frames = modified_array でうっかり上書き。後で plot(tsm.full_frames) したら「生データのつもりが加工後」 |
| ④ | TBN を独立してテストできない | TbnFileIo(filename, full_frame_interval, num_full_frames) は TSM の値を要求。TBN 単独で __init__ できない |
| ⑤ | ダミー値分のメモリが無駄 | __init__ で 30 個確保 → read_data で破棄 → 本物の配列で再確保。確保・解放サイクルが二重 |
| ⑥ | 並行アクセスでデータ競合 | 2 スレッドが同じ tsm を共有して片方が tsm.full_frames = ... していたら、他方の読み取りが壊れる |
pub struct TsmData {
pub header: TsmHeader,
pub full_frames: Array3<i32>, // dark 減算済み (Y, X, N)
pub dark_frame: Array2<i16>, // (Y, X)
}
// 注: channels (4D 配列) は持たない。OO 版 builder.py が各 Ch を対等な
// FramesData として登録していた設計を踏襲し、チャネル分離は domain 層 (builder) で行う。
pub fn open_tsm(path: impl AsRef<Path>) -> Result<TsmData, ScanIoError> {
// ━━━ 副作用 (1 回だけ) ━━━
let bytes = std::fs::read(path.as_ref())?;
// ━━━ 純粋関数の合成 (let で名前を付けるだけ。self.* への代入は一切なし) ━━━
let header = parse_tsm_header(bytes.get(..2880).ok_or(...)?)?;
let raw = raw_i16_from_bytes(&bytes[2880..], &header)?;
let arr = reshape_fortran_3d(raw, header.shape_with_dark())?;
let arr = axis_transform(arr);
let (full, dark) = split_dark(arr);
let full = subtract_dark_frame(full.view(), dark.view());
// チャネル分離 (split_channels) は scandata-io ではなく builder 側で必要時に呼ぶ
// ━━━ ★ 全フィールドが揃ったタイミングで 1 回だけ struct を構築 ━━━
Ok(TsmData { header, full_frames: full, dark_frame: dark })
}
split_channels を呼ぶ。| # | OO で何が起きる | FP でどう解消 |
|---|---|---|
| ① | ダミー値や半完成状態が観察可能 | TsmData は関数戻り値。呼び出し側は完成形しか見られない |
| ② | 部分的に埋まったオブジェクトが残る | ? で早期 return すると TsmData 自体が作られない (まだ Ok(...) していないので) |
| ③ | tsm.full_frames = ... が成功 |
let tsm = open_tsm(...)?; は immutable。mut を付けない限り上書きはコンパイルエラー |
| ④ | TSM 経由でしか TBN を作れない | open_tbn(path) が独立関数。open_tsm_pair(tsm_path) は副作用層で 2 ファイル合成するだけ |
| ⑤ | 30 個確保 → 破棄 → 再確保 | 本物の値を作ってから 1 回だけ struct に埋め込む。ダミー値領域なし |
| ⑥ | mutate なので競合可能 | 不変なので Arc<TsmData> で何スレッドにも安全に配布可能 |
両者の時系列を並べると、観測可能な状態数の違いが鮮明です。
「不変 struct を返すだけ」設計の最大の恩恵は、ファイル無しでオブジェクトをリテラルで組み立てられること。 これは Modifier テストの書きやすさに直結します。
| 観点 | OO (mutate) | FP (不変 struct) |
|---|---|---|
| フィールド数 | 30+ (一部ダミー初期化) | 必要なものだけ (4〜5) |
| 初期化タイミング | 段階的 (read_infor → read_data → TbnFileIo 生成) | 1 回だけ |
| 中間状態の可観測性 | 4 状態すべて観測可能 | 観測不可能 (関数戻り値が完成形) |
| 失敗時の挙動 | 一部フィールドが埋まったゴミが残る | オブジェクト自体が作られない |
| 並行性 | データ競合の危険 | 完全に安全 (不変なので Arc<T> 配布可) |
| テスト容易性 | 実ファイルが必要 | リテラルで作れる |
| メモリ | ダミー値分の無駄 | 最適 |
| 不変性保証 | なし (後から上書き可) | コンパイラが保証 |
作成 2026-05-17 · fileio_oo_to_fp.html §9 対比表「状態」行の詳細解説