Chapter 1: Null許容世界の歩き方 — 安全な「演算」としてのn-API
1. 前章の結論:欠けていたのは「安全な演算」だった
Section titled “1. 前章の結論:欠けていたのは「安全な演算」だった”前章で我々は、歴史的にnullが引き起こしてきた問題の真相に迫りました。その結論は、「nullという概念そのものが悪なのではなく、null(空集合)とペアになるべき安全な演算が、多くの言語で定義されていなかったことこそが問題の根源である」というものでした。代数構造は「集合」と「演算」のペアで初めて成立します。nullという「集合」の要素を認めながら、それに対応する「演算」を用意しなかったこと、それが“億万ドルの間違い”の正体でした。
本章では、その欠けていた「安全な演算」を、Timelineライブラリがどのように具体的に実装しているかを解説します。その答えが、n接頭辞を持つ関数群 (n-API) です。
2. 最も基本的な演算: nMap — nullを安全に通過させる
Section titled “2. 最も基本的な演算: nMap — nullを安全に通過させる”n-APIの設計思想を理解するために、まずは最も基本的なmap操作から見ていきましょう。
nの付かないオリジナルのmap関数は、入力タイムラインの値がnullであることを想定していません。そのため、以下のようにnullを許容するタイムラインに適用すると、マッピング関数 (x => x.toUpperCase()) の内部でTypeError: Cannot read properties of nullのような実行時エラーが発生する潜在的なリスクを抱えています。
// エラーが発生する可能性のあるコードconst nameTimeline = Timeline<string | null>("Alice");
const upperCaseName = nameTimeline.map(x => x.toUpperCase()); // 'x'がnullの場合、エラーになる
nameTimeline.define(Now, null); // ここで例外がスローされる可能性があるこの問題を解決するのがnMapです。nMapは、nullの存在を前提として設計された「安全な演算」です。
F#: nMap: ('a -> 'b) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null
Section titled “F#: nMap: ('a -> 'b) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null”Note: In F#, this type constraint (when 'a : null...) is syntactically part of the generic parameter declaration. This notation is for documentation clarity.
TS: .nMap<B>(f): Timeline<B | null>
Section titled “TS: .nMap<B>(f): Timeline<B | null>”nMapが提供する演算ルールは、極めてシンプルです。
「入力タイムラインの値がnullであれば、マッピング関数を実行せず、出力タイムラインの値を即座にnullにする」
このルールにより、NullPointerExceptionのリスクはnMap演算の内部で吸収されます。nullはパイプラインを破壊することなく、安全に「通過」していくのです。
// TSconst nullableNumbers = Timeline<number | null>(5);const doubled = nullableNumbers.nMap(x => x * 2);console.log(doubled.at(Now)); // 10
// 入力がnullになると...nullableNumbers.define(Now, null);// ...関数は実行されず、出力も安全にnullになるconsole.log(doubled.at(Now)); // null3. 動的グラフの安全な航行: nBind
Section titled “3. 動的グラフの安全な航行: nBind”この「nullを安全に通過させる」という設計哲学は、より複雑なbind操作にも適用されます。bindは、値に応じて後続のタイムラインを動的に切り替える強力な機能ですが、入力がnullになる可能性を考慮しなければ、nMapと同様のリスクを抱えます。
nBindは、bind操作にnull安全性を組み込んだ演算です。
F#: nBind: ('a -> Timeline<'b>) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null
Section titled “F#: nBind: ('a -> Timeline<'b>) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null”Note: In F#, this type constraint (when 'a : null...) is syntactically part of the generic parameter declaration. This notation is for documentation clarity.
TS: .nBind<B>(monadf): Timeline<B | null>
Section titled “TS: .nBind<B>(monadf): Timeline<B | null>”入力タイムラインがnullになった場合、nBindは後続のタイムラインを生成するための関数 (monadf) を実行しません。代わりに出力タイムラインを即座にnullにします。
これは、非同期処理のチェーンなどで極めて有用です。例えば、「ユーザーIDを取得し、そのIDを使ってユーザープロファイルを取得する」という処理において、最初の「ユーザーIDの取得」が失敗しnullを返した場合でも、後続のプロファイル取得処理が安全にスキップされます。
// TSconst maybeNumber = Timeline<number | null>(5);
// 入力が5なので、関数が実行され、結果のTimelineが生成されるconst result = maybeNumber.nBind(x => Timeline(x.toString()));console.log(result.at(Now)); // "5"
// 入力がnullになると...maybeNumber.define(Now, null);// ...関数は実行されず、出力も安全にnullになるconsole.log(result.at(Now)); // null4. リソース管理との融合: nUsing
Section titled “4. リソース管理との融合: nUsing”usingは、タイムラインの値と、DOM要素やタイマーといった外部リソースのライフサイクルを同期させる、高度な操作です。この文脈においても、nullの扱いは重要になります。「リソースが存在しない」という状態を、エラーではなく、正当な状態としてエレガントに扱う必要があるからです。
nUsingは、この要求に応えるための演算です。
F#: nUsing: ('a -> Resource<'b>) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null
Section titled “F#: nUsing: ('a -> Resource<'b>) -> Timeline<'a> -> Timeline<'b> when 'a : null and 'b : null”Note: In F#, this type constraint (when 'a : null...) is syntactically part of the generic parameter declaration. This notation is for documentation clarity.
TS: .nUsing<B>(resourceFactory): Timeline<B | null>
Section titled “TS: .nUsing<B>(resourceFactory): Timeline<B | null>”nUsingは、「入力がnullを受け取ったら、リソース確保のロジック (resourceFactory) を実行せず、結果もnullになるタイムラインを返す」という演算を定義します。
これにより、「ユーザーがDOM要素を選択している場合はそのリソースを確保し、何も選択していない(nullの)場合は何もしない」といった、UIプログラミングで頻出するシナリオを、if文による命令的な分岐なしに、単一の宣言的なパイプラインとして記述できます。
// TSconst optionalUserId = Timeline<number | null>(123);
// optionalUserIdが123なので、リソースが確保されるconst resource = optionalUserId.nUsing(id => { // この部分はidがnullでない場合のみ実行される return createResource(`data_for_${id}`, () => console.log(`cleanup for ${id}`));});console.log(resource.at(Now)?.resource); // "data_for_123"
// optionalUserIdがnullになると...optionalUserId.define(Now, null);// ...リソース確保関数は実行されず、結果もnullになる。// (前のリソースのクリーンアップ関数がここで呼ばれる)console.log(resource.at(Now)); // null5. まとめ:n-APIという設計思想
Section titled “5. まとめ:n-APIという設計思想”nMap、nBind、nUsing。これらのAPIを通じて、共通の設計思想が見えてきます。n-API群が提供する「演算」とは、すべて以下のシンプルなルールに基づいています。
「nullという入力を特別扱いし、例外を発生させることなく、パイプラインの構造を維持したままnullという出力を返す」
このルールこそが、前章で我々が「欠けていた」と結論付けた、「null(空集合)とペアになる安全な演算」の具体的な答えです。
この設計により、プログラマはパイプラインの「外側」で、防御的なnullチェックのif文を繰り返す必要がなくなります。その結果、nullの存在を認めつつも、その危険性に煩わされることなく、よりクリーンで宣言的なコードを書くことに集中できるのです。
Canvasデモ (Placeholder)
Section titled “Canvasデモ (Placeholder)”mapとnMapの振る舞いを並べて比較するデモ。
左側(map)では、nullが入力されるとパイプラインが赤い警告表示と共に停止する。右側(nMap)では、nullが入力されるとパイプラインの色が変わり、後続の処理をスキップしてそのままnullが出力される様子を視覚化する。