
今天要介紹的是 Container (容器),這會是後續 Functor、Monad、Applicative 概念的基礎。
在之前的文章中,我們學習到 Functional Programming 的核心魅力之一是函式組合 (Function Composition),透過 pipe 或 compose,我們可以將一系列微小、單一職責的純函式 (Pure Functions) 串聯起來,形成一個清晰、可讀性極高的資料處理管道 ,然而這個優雅的組合鏈仍有些缺點,以下就來看看它在實際應用中可能會遇到的問題吧~
來看一個簡單的範例,假設我們想從一個使用者物件中,提取其名稱、轉為大寫,並截取前五個字元。
// 輔助函式
const getProperty = (prop) => (obj) => obj[prop];
const toUpperCase = (str) => str.toUpperCase();
const slice = (start, end) => (str) => str.slice(start, end);
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
// 建立處理管道
const processUserName = pipe(
getProperty('name'),
toUpperCase,
slice(0, 5)
);
// 理想情況
const user = { name: 'Buckethead' };
processUserName(user); // 'BUCKE'
這段程式碼看起來非常完美,但現實世界的資料不可能是完美的。如果我們從 API 或資料庫收到的 user 物件剛好沒有 name 屬性呢?
const userWithNoName = { id: 1 };
processUserName(userWithNoName);
// 🔺 TypeError: Cannot read properties of undefined (reading 'toUpperCase')
程式碼會出錯,因為 getProperty('name') 回傳了 undefined,而 toUpperCase 無法在 undefined 上執行。我們pipe 管道,就這樣因為 undefined 而斷裂了。

圖 1 斷裂的 pipe 管道(資料來源: 自行繪製)
為了避免這種情況,我們最直覺的反應就是用 if/else 來預防:
function processImperative(user) {
if (user && user.name) {
const upperName = user.name.toUpperCase();
return upperName.slice(0, 5);
}
return null;
}
這樣寫雖然安全,但我們失去了組合鏈的優雅。核心邏輯被 if 的檢查淹沒,程式碼又變成聚焦在「怎麼做」的實作細節,而非高層次的「做什麼」,簡單來說,這樣寫就不 FP 了XD
這就是我們需要新工具的時刻!我們需要一種方法,既能保持函式組合的宣告性,又能優雅地處理 null 或 undefined 這類潛在的「危險」。
與其在每個函式中都用 if/else 處理一次 null 的檢查,不如將「值可能不存在」這個脈絡 (context) 本身,進行抽象化。
核心思想是:將資料放入一個特殊的「盒子」裡。
這個「盒子」,我們稱之為容器 (Container)。它的職責很單純:包裹一個值,並明確化這個值的「狀態脈絡」。
Container:值就是值(可能隨時為 undefined)。Container:值 + 「這個值可能為空」的脈絡 (context)。一旦值被放進容器,它就與外部世界隔離開來,我們不能再直接對它進行操作,而是透過一套規則來互動。
這個看似簡單的步驟,卻是後續所有強大抽象的基礎。透過將值包裹起來,我們為處理控制流、錯誤處理、非同步操作等複雜概念建立了穩固的基礎。
Container 是一個可以容納任何型別值的物件,我們可以將它視為「一個單純包東西的盒子」。
class Container {
constructor(value) {
// 我們用一個私有屬性來儲存值,$value 可以是任何型別的值
// 在 JS 中,有時會用錢幣符號 ($) 或底線 (_) 開頭來表示這是一個內部屬性,提醒開發者不應直接存取它。
this.$value = value;
}
}
$value 用於儲存容器內的值,它可以是任何型別的資料(數字、字串、物件,甚至是另一個 Container)。這個結構並非為物件導向設計,它更像是一個純粹的資料載體。
Container.of()既然容器內要裝一個值,那我們就需要「把值放進容器」的操作方法,那要如何把值放進去呢?
在 FP 中,通常不會直接使用 new Container(x) 來建立實例(或說是把東西放進去),而是透過一個靜態工廠方法 .of() 來建立容器、將值放入。
class Container {
constructor(value) {
this.$value = value;
}
// 靜態工廠方法
static of(value) {
return new Container(value);
}
}
// 使用方式
const myContainer = Container.of('Hello FP!');
至於為什麼要用 of,而不是用 new Container(x) 呢? 目前可以先用「of 比 new Container(x) 更簡潔、語意更清楚」的優點來思考,of 這個詞本身就帶有「將...放入」的意涵,可以將 of 想成是一種正確放資料進容器的方式。
一旦我們將值放入容器,它就會一直待在裡面。
const numberContainer = Container.of(123);
console.log(numberContainer);
// Container { $value: 123 }
const userContainer = Container.of({ name: 'Monica Lin' });
console.log(userContainer);
// Container { $value: { name: 'Monica Lin' } }
(補充:為了方便理解與閱讀,後續文章會統一使用 Container(x) 這種格式來表示一個包裹著 x 值的容器。)

圖 2 將一個值放入 Container 中(資料來源: 自行繪製)
Container 有幾個特性:
$value 的物件
$value 用於儲存容器內的值,可存放任意型別資料(例如:原始型別、物件、甚至其他 Container)myContainer.$value 來取值)
現在我們可以將一個潛在的 null 或 undefined 值安全地包裹進 Container 中,我們的函式組合鏈可以傳遞一個 Container 物件,而不會因為 null 這種「裸值」而直接崩潰。
回想一下文章開頭那個 processUserName 會報錯的例子:
getProperty('name') 回傳了 undefined,直接傳給 toUpperCase,結果爆炸。Container.of(user),傳遞下去的就是 Container(undefined),而不是「危險的裸值」。這樣一來,Container 就像一個自動檢查員,幫我們擋掉 null 與 undefined 的風險。
但新的問題也出現了:
🔺 我們原本的純函數,無法直接對容器裡的值進行操作。
以下面這段程式碼來說,add 函式期望接收一個數字(例如 3),但我們卻給了它一個 Container 物件。我們雖然保護了值,但也同時失去了與它互動的能力。
const add = (x) => x + 2;
const containerOfThree = Container.of(3);
add(containerOfThree); // "[object Object]2"

圖 3 add 函式無法接觸盒子內的值(資料來源: flaticon & 部分自行繪製)
我們已經有了一個安全的盒子,但現在浮現出了兩個問題:
[1,2,3] 或一串字串 ['Hello', ' ', 'World'],是否存在一種通用且可靠的方法,能將它們組合成一個單一的值?我們知道數字可以用加總、字串可以用串接,但能否將這種「組合」的行為抽象出來,找到一個共通的模式?Container(5) ,而我們想對它應用一個簡單的函式 addOne,該怎麼做?是否存在一種通用的方式,能讓我們將函式「映射 (map)」到容器裡的值,而不必手動拆箱與重包裝?這兩個挑戰——組合值與映射 Context ——正是 Functional Programming 的核心課題。
在下一篇文章中,我們將首先探索「組合」的問題,並介紹我們可能每天都在使用、卻可能從未意識到的強大結構:Monoid。
以下簡單總結今天的重點:
一個簡單的物件,像個盒子,透過 .of() 方法將我們的資料值安全地包裹起來。
為了將「值」與其「脈絡」(例如:這個值可能為空)明確化。這能保護我們的函式組合流程,不因意外的 null 或 undefined 而中斷。
我們學會了如何將值安全地放入容器,但還沒學會如何操作容器裡的值。這問題的答案會在後續文章揭曉。