im を組み合わせた ValueObject + Repository 設計の青写真
· 作成 2026-05-17
ここまで見てきた 3 つの道具は、互いに直交する独立した役割を持ちます:
| 道具 | 表現するもの | 例 |
|---|---|---|
| Newtype | 「値」の意味 | Channel(u8), FrameIndex(usize) |
| Phantom Type | 「データ」の状態 | FramesData<Raw>, <DfOverF> |
im クレート | 「コレクション」の不変性 | im::HashMap<DataTag, ValueObj> |
3 つを組み合わせると、次の 3 つの恩恵が同時に得られます:
im)im の 3 層構造。下の層が上の層の構成要素になる。
3 層を組み合わせた具体的な Rust コード例:
use std::sync::Arc;
use std::marker::PhantomData;
use im::HashMap;
use ndarray::{Array2, Array3};
// ─────────────── 層 1: Newtype ───────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Channel(pub u8);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Filename(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DataTag {
pub filename: Filename,
pub channel: Channel,
}
// ─────────────── 層 2: Phantom Type + ValueObject ───────────────
pub struct Raw;
pub struct DarkSubtracted;
pub struct ChannelSplit;
pub struct DfOverF;
#[derive(Clone)]
pub struct FramesData<S> {
pub data: Arc<Array3<i16>>, // 巨大データは Arc 共有
pub tag: DataTag,
_state: PhantomData<S>,
}
// Raw のときだけ呼べる遷移
impl FramesData<Raw> {
pub fn subtract_dark(self, dark: &Array2<i16>) -> FramesData<DarkSubtracted> {
// 純粋関数として実装
let new_data = /* 引き算 */ self.data;
FramesData { data: new_data, tag: self.tag, _state: PhantomData }
}
}
// DarkSubtracted 以降ならどれでも
pub trait PostDark {}
impl PostDark for DarkSubtracted {}
impl PostDark for ChannelSplit {}
impl<S: PostDark> FramesData<S> {
pub fn df_over_f(self, baseline: f64) -> FramesData<DfOverF> { /* ... */
FramesData { data: self.data, tag: self.tag, _state: PhantomData }
}
}
// 統一型: Repository に入れるためには enum で型消去
#[derive(Clone)]
pub enum ValueObj {
FramesRaw(FramesData<Raw>),
FramesDark(FramesData<DarkSubtracted>),
FramesSplit(FramesData<ChannelSplit>),
FramesDfOverF(FramesData<DfOverF>),
// Image / Trace / Text なども同様に...
}
// ─────────────── 層 3: im::HashMap で Repository ───────────────
#[derive(Clone)]
pub struct Repository {
items: HashMap<DataTag, ValueObj>,
}
impl Repository {
pub fn new() -> Self { Self { items: HashMap::new() } }
pub fn insert(&self, tag: DataTag, v: ValueObj) -> Self {
Self { items: self.items.update(tag, v) }
}
pub fn remove(&self, tag: &DataTag) -> Self {
Self { items: self.items.without(tag) }
}
pub fn get(&self, tag: &DataTag) -> Option<&ValueObj> {
self.items.get(tag)
}
}
ScanDataPy の Modifier チェーンは「fold」で美しく表現できます:
// Modifier は「FramesData<S> → FramesData<S'>」の純粋関数
// 型が変わるので Box<dyn ...> ではなく具体的に並べる
pub fn load_and_process(path: &std::path::Path)
-> Result<FramesData<DfOverF>, DomainError>
{
let tsm = read_tsm(path)?; // 副作用は最外層
let raw = tsm.to_frames_raw(); // FramesData<Raw>
let dark_sub = raw.subtract_dark(&tsm.dark_frame);
let split = dark_sub.split_channels(NumFluoCh(2));
let dfof = split.df_over_f(1000.0);
Ok(dfof)
}
あるいは同種のモディファイア(同じ型同士の変換)が並ぶ場合は fold で:
// TraceData<BlComp> に複数の smoothing を適用
let smoothers: Vec<Smoother> = vec![...];
let smoothed = smoothers.iter()
.try_fold(trace, |acc, m| m.apply(&acc))?;
fold で 1 行に。
| ScanDataPy(現状) | ScanDataNet(FP 強寄り) | 使う道具 |
|---|---|---|
dict[str, str] 型の data_tag |
DataTag { filename: Filename, channel: Channel } |
Newtype |
FramesData.data: np.ndarray (mutable) |
FramesData<S> { data: Arc<Array3<i16>> } |
Phantom Type + Arc |
Modifier が data_tag を update() で破壊変更 |
Modifier が新 FramesData<S'> を返す純粋関数 |
Phantom Type |
Repository._data: list を append/remove |
Repository::insert/remove(&self) -> Self |
im::HashMap |
find_by_keys が set(values()) で比較 (バグあり) |
get(&DataTag) で型一致を保証 |
Newtype + HashMap |
| undo / redo なし | RepoHistory: Vec<Repository> で実装可 |
im の構造共有 |
Visitor を accept(visitor) で実装 |
match value_obj { ... } でパターン |
Rust の enum |
3 つの道具を ScanDataNet に組み込むまでの推奨順序:
im は L2 でドメイン層に組み込む。
現状の PHASE_L1_TSM_PARSER.md の TsmFile 設計を、本ガイドの方針で見直すと:
| 現プラン (L1) | 本ガイドでの推奨 | 備考 |
|---|---|---|
pub fn open(path: impl AsRef<Path>) |
同じで OK | Newtype FilePath を使うとさらに型安全 |
full_frames: Array3<i32> |
同じで OK(L1 では生の ndarray) |
L2 で Arc<Array3> 包む |
fn split_channels(&self, num_fluo_ch: usize) |
fn split_channels(&self, num: NumFluoCh) |
Newtype に置換 |
| Phantom Type の導入 | L1 では導入しない | L1 は bit-perfect 一致が最優先。L2 で TsmFile → FramesData<Raw> 変換層を追加 |
.npy との bit-perfect 一致の検証が複雑化します。
L1 は素朴な TsmFile 構造体で完結させ、L2 で本ガイドの設計に変換するのが安全です。
Arc<ndarray> で共有し、im は ValueObject の容器としてだけ使うimTsmFile で .npy bit-perfect 一致を最優先cargo new fp-play --bin で 100〜200 行のサンプルから始め、
動いてから ScanDataNet/server/crates/scandata-domain/ の本実装に転用するパターン。
im クレートによる永続データ構造get() -> Option<&ValueObj> の戻り値型の解説../../ScanDataNet/FUNCTIONAL_PROGRAMMING_GUIDELINES.md../../ScanDataNet/PHASE_L1_TSM_PARSER.md../oo_vs_fp_state.html