Chapter 6: map/bind/using 総合利用ガイド:API選択の完全な指針
このチャプターは、timeline.jsを利用する開発者が、map, bind, usingという3つの主要な変換APIの選択において、一切の迷いや手戻りを発生させないための、具体的かつ厳密な判断基準を提供することを目的とします。
「bindとusingはペアで使うのか?」「mapと何が違うのか?」といった、開発者が直面する具体的な問いに答えるため、表面的な「役割の違い」の説明ではなく、内部の仕組みとTypeScriptの型シグネチャから解き明かし、どのような問題状況に、どのAPIが唯一の解決策となるのかを定義します。
1. 2種類のオブジェクト
Section titled “1. 2種類のオブジェクト”このライブラリでリソース管理を正しく理解するには、まず 我々(Timelineライブラリ)が扱うオブジェクトが2種類ある ことを認識する必要があります。
-
Timelineオブジェクト:timeline.jsライブラリが管理する、リアクティブな値と依存関係を持つJavaScriptオブジェクトです。Timeline(初期値)によって生成されます。 -
外部リソース (External Resource):
timeline.jsの管理外にある、あらゆるオブジェクトです。DOM要素、GLibのタイマー、ネットワーク接続などがこれにあたります。これらは明示的に生成・破棄されない限り、リソースを占有し続けます。
両者は生成方法とライフサイクル管理が根本的に異なります。この違いを理解することが、map/bind/usingの役割を理解する鍵となります。
2. 概念的コードによる本質の理解
Section titled “2. 概念的コードによる本質の理解”実際のtimeline.jsライブラリはDependencyCoreによるリソース管理機能を持つため複雑ですが、リアクティブな値の更新メカニズムという根幹においては、以下のミニマルなコードと等価です。
概念的な実装(型付き)
Section titled “概念的な実装(型付き)”// map: Timeline<A> から Timeline<B> への変換// 引数関数の型: (value: A) => Bconst map = <A, B>( f: (value: A) => B, timelineA: Timeline<A>): Timeline<B> => { const timelineB = Timeline(f(timelineA.at(Now))); const newFn = (valueA: A) => { timelineB.define(Now, f(valueA)); }; // 簡略化した依存関係の登録 timelineA._fns.push(newFn); return timelineB;};// bind: Timeline<A> から Timeline<B> への変換// 引数関数の型: (value: A) => Timeline<B>const bind = <A, B>( monadicFn: (value: A) => Timeline<B>, timelineA: Timeline<A>): Timeline<B> => { const initialInnerTimeline = monadicFn(timelineA.at(Now)); const timelineB = Timeline(initialInnerTimeline.at(Now)); const newFn = (valueA: A) => { // 実際には古いinnerTimelineのリソースはここで破棄される const newInnerTimeline = monadicFn(valueA); timelineB.define(Now, newInnerTimeline.at(Now)); }; timelineA._fns.push(newFn); return timelineB;};// using: Timeline<A> から Timeline<B | null> への変換// 引数関数の型: (value: A) => Resource<B> | nullconst using = <A, B>( resourceFactory: (value: A) => Resource<B> | null, timelineA: Timeline<A>): Timeline<B | null> => { const initialResource = resourceFactory(timelineA.at(Now)); const timelineB = Timeline(initialResource ? initialResource.resource : null); const newFn = (valueA: A) => { // 実際には古いresourceのcleanup()がここで実行される const newResourceData = resourceFactory(valueA); const newResource = newResourceData ? newResourceData.resource : null; timelineB.define(Now, newResource); // 実際には新しいcleanup()がここで登録される }; timelineA._fns.push(newFn); return timelineB;};このコードから、以下の極めて重要な共通構造がわかります。
timelineBの再利用: 3つのAPIはどれも、最初に呼び出された時に結果となるtimelineBを一度だけ生成します。以降、ソースであるtimelineAが更新されるたびに、そのtimelineBオブジェクトは再利用され、内部の値だけが更新されます。
Note: なぜこのような仕様になっているのか?(設計思想の復習)
答え:ブロックユニバースモデルに基づいているからです。
概念的には、
Timelineはすべてブロックユニバースに存在する、その名の通り時間軸のタイムラインであり、変化することはありません。プログラミングの文脈でいうところのImmutable(不変) です。しかし、
Nowが概念的に時間軸に沿って我々の視点とともに動くMutableなカーソルであるのと全く同じ理由で、Timelineの中身はミニブロックユニバースそのものであり我々の視点と「常に同期している」Mutableな値であるのが自然な実装となります。
map/bind/usingを通じて定義されたImmutableなTimelineBの中で発生し、TimelineAの中身のMutableな更新によりReactiveに更新され続けるvalue/innerTimeline/resourceはそれぞれすべて、上記の哲学的設計理念からでMutableである必要があります。
innerTimelineやresourceがmutableに「破壊」されなければならない根本的な理由は、概念的なMutableなカーソルであるNowが未来方向へ動くのと同期して依存グラフが時間発展とともに変化しており、そのそれぞれの時間座標において異なるからです。この概念的にMutableな
Nowと依存グラフの同期(書き換え)作業は、一括してDependencyCoreという水面下の依存グラフマネージャーが、命令的プログラミングのパラダイムで司ります。これがillusionという概念です。
3. 自動破棄のメカニズム — bindとusingの共通基盤
Section titled “3. 自動破棄のメカニズム — bindとusingの共通基盤”3.1 Timelineオブジェクトの自動破棄:2段階のプロセス
Section titled “3.1 Timelineオブジェクトの自動破棄:2段階のプロセス”bindは、引数に取る関数(monadicFn)が呼び出されるたびに、新しいinnerTimelineオブジェクトを生成します。では、そのオブジェクト自体のリソース管理はどうなっているのでしょうか。
この自動破棄は、bindとusingに共通する、以下の2段階のプロセスで行われます。
ステップ1: リアクティブ接続の直接的な破棄(DependencyCoreの役割)
Section titled “ステップ1: リアクティブ接続の直接的な破棄(DependencyCoreの役割)”bind/usingは、ソースTimelineが次に更新された際、DependencyCoreのdisposeIllusion機能を呼び出します。これにより、古いinnerTimelineから出力Timelineへの リアクティブな接続(依存関係のレコード)が、DependencyCoreの管理台帳から直接的に削除されます。
ステップ2:Timelineオブジェクトの間接的な破棄(ガベージコレクタの役割)
Section titled “ステップ2:Timelineオブジェクトの間接的な破棄(ガベージコレクタの役割)”ステップ1で リアクティブな接続 が断ち切られることで、古いinnerTimelineオブジェクトは、他にどこからも参照されていない「孤立した」状態になります。
DependencyCoreによるこの「孤立化」こそが、JavaScriptエンジンのガベージコレクタ(GC)がそのオブジェクトを不要と判断し、メモリを自動的に解放するための前提条件となります。
したがって、DependencyCoreは、リアクティブな接続 を断ち切ることで、間接的にTimelineオブジェクト自体の破棄(GCによるメモリ解放)を可能にしているのです。
この DependencyCoreによる孤立化 → GCによるメモリ解放 という2段階のプロセスは、bindとusingで完全に共通のメカニズムです。
3.2 usingの追加機能:外部リソースの直接的な破棄
Section titled “3.2 usingの追加機能:外部リソースの直接的な破棄”usingは、上記の共通メカニズムに加えて、cleanup関数を通じて外部リソースを直接的に破棄するという、極めて重要な追加機能を持っています。
usingに渡す関数は{ resource, cleanup }という構造のオブジェクトを返します。usingは、disposeIllusionが実行されるまさにその瞬間に、DependencyCoreのonDisposeコールバック機能を利用して、ユーザーが提供した cleanup関数(例: () => dom.remove())を追加で実行させます。
これにより、Timelineオブジェクトの間接的な破棄と同時に、外部リソースの直接的な破棄が、完全に同期して行われます。
4. 依存グラフの振る舞いから理解するAPI選択
Section titled “4. 依存グラフの振る舞いから理解するAPI選択”どのAPIを選択すべきかは、ライブラリの根底にある「依存グラフ」が、あなたのロジックによって時間と共にどう振る舞うべきか、で決まります。
3つのAPIはすべて、Timeline<A>から新しいTimeline<B>への依存関係を構築しますが、その関係性の性質が異なります。
-
依存グラフの振る舞い: 静的(Static)
mapが構築する依存グラフは、Timeline同士の固定された関係です。一度確立された接続は、時間と共に変化しません。
-
引数関数の型:
(value: A) => Bmapは、あるTimeline<A>の値を、常に同じ変換ロジックで別の値Bに変換し続ける関数を要求します。
-
返り値の型:
Timeline<B>mapは、変換後の値Bを内部に持つ、新しいTimeline<B>を返します。
-
自動破棄の対象: なし
-
依存グラフの振る舞い: 動的(Dynamic) - Timeline間
bindの核心は、ソースTimelineの値に応じて、接続先のTimelineそのものを動的に切り替える能力にあります。これは、依存グラフの配線が時間発展することを意味します。
-
引数関数の型:
(value: A) => Timeline<B>bindは、入力値Aを元に、次に接続すべき新しいTimeline<B>を返却する関数を要求します。
-
返り値の型:
Timeline<B>bindは、最初に単一のTimeline<B>を生成して返します。 ソースTimelineが更新されるたびに、引数関数が返す新しいinnerTimelineが生成され、その中身の値だけが、最初に生成されたこのTimeline<B>にコピー(反映)されます。
-
自動破棄の対象:
-
接続 (直接)
-
Timeline(間接)
-
-
依存グラフの振る舞い: 動的(Dynamic) - Timelineと外部リソースのライフサイクルを同期
usingもbindと同様に動的な依存グラフを構築しますが、その目的はTimelineの状態変化と、外部リソースの生成・破棄(ライフサイクル)を完全に同期させる 点に特化しています。
-
引数関数の型:
(value: A) => Resource<B> | nullusingは、入力値Aを元に、生成された外部リソースBと、それを破棄するためのcleanup関数をペアにしたResource<B>オブジェクトを返却する関数を要求します。
-
返り値の型:
Timeline<B | null>usingは、生成されたresourceを内部値として持つ、新しいTimeline<B | null>を返します。
-
自動破棄の対象:
-
接続 (直接)
-
Timeline(間接) -
外部リソース (直接)
-
5. 実践シナリオ集
Section titled “5. 実践シナリオ集”シナリオ1: mapが最適なケース
Section titled “シナリオ1: mapが最適なケース”ユーザーのスコア(Timeline<number>)を、画面表示用のラベル(Timeline<string>)に変換します。
const scoreTimeline: Timeline<number> = Timeline(100);// f: (score: number) => stringconst labelTimeline: Timeline<string> = scoreTimeline.map(score => `Score: ${score}`);シナリオ2: bindが最適なケース
Section titled “シナリオ2: bindが最適なケース”ユーザーが選択したデータソース("posts"または"users")に応じて、表示する内容をAPIから取得するTimelineを切り替えます。
const sourceChoiceTimeline: Timeline<string> = Timeline("posts");
// monadf: (choice: string) => Timeline<Post[] | User[]>const dataTimeline: Timeline<Post[] | User[]> = sourceChoiceTimeline.bind(choice => { if (choice === "posts") { return fetchPostsApi(); // -> returns Timeline<Post[]> } else { return fetchUsersApi(); // -> returns Timeline<User[]> }});シナリオ3: usingが最適なケース
Section titled “シナリオ3: usingが最適なケース”extension.jsのリファレンスコードが完璧な実例です。Timeline<data>の値に基づいてDOM要素を生成し、データが更新されたら古いDOMを破棄して新しいものを再生成します。
// resourceFactory: (items: Item[]) => Resource<Icon[]>dynamicDataTimeline.using(items => { const icons = items.map(item => new St.Icon(...)); container.add_child(...); // returns { resource: Icon[], cleanup: () => void } return createResource(icons, () => { icons.forEach(icon => icon.destroy()); });});シナリオ4: bindとusingの組み合わせ
Section titled “シナリオ4: bindとusingの組み合わせ”「コンポーネントが表示されている間だけ、DOM要素を管理する」という、最も実践的なパターンです。
// monadf: (isVisible: boolean) => Timeline<DOMElement | null>isVisibleTimeline.bind(isVisible => { // ★外側のbind: コンポーネント全体の「存在」を管理 if (!isVisible) { return Timeline(null); // 非表示なら、何も接続しない }
// isVisibleがtrueの場合のみ、以下のリアクティブな処理が実行される // resourceFactory: (data: any) => Resource<DOMElement> return someDataTimeline.using(data => { // ★内側のusing: 表示中の「DOM要素」を管理 const dom = createDomElement(data); return createResource(dom, () => dom.remove()); });});-
外側の
bindは、コンポーネント全体のライフサイクルを管理し、不要になった際に内側のusingへのリアクティブな接続を断ち切ります。 -
内側の
usingは、その接続が断ち切られた瞬間に、自身のcleanup関数を実行し、DOM要素を安全に破棄します。
map, bind, usingは、どちらを使うべきか迷うような競合するAPIではありません。引数に渡す関数の型シグネチャによって、選択すべきAPIは常に一つに定まります。
そして、bindとusingは階層的に組み合わせて使うことで、完全な自動リソース管理を実現する、補完的な関係にあるのです。この構造を理解することが、堅牢でリークのないリアクティブなアプリケーションを構築するための鍵となります。