iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0

https://ithelp.ithome.com.tw/upload/images/20250928/201682015hBC78dTKH.png

前言

今天要介紹的是 Container (容器),這會是後續 Functor、Monad、Applicative 概念的基礎。

在之前的文章中,我們學習到 Functional Programming 的核心魅力之一是函式組合 (Function Composition),透過 pipecompose,我們可以將一系列微小、單一職責的純函式 (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 而斷裂了。

https://ithelp.ithome.com.tw/upload/images/20250928/201682019yY6e1qQtg.png
圖 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

這就是我們需要新工具的時刻!我們需要一種方法,既能保持函式組合的宣告性,又能優雅地處理 nullundefined 這類潛在的「危險」。
 

為什麼我們需要容器 (Container)?

與其在每個函式中都用 if/else 處理一次 null 的檢查,不如將「值可能不存在」這個脈絡 (context) 本身,進行抽象化。

核心思想是:將資料放入一個特殊的「盒子」裡

這個「盒子」,我們稱之為容器 (Container)。它的職責很單純:包裹一個值,並明確化這個值的「狀態脈絡」。

  • 沒有 Container:值就是值(可能隨時為 undefined)。
  • 有了 Container:值 + 「這個值可能為空」的脈絡 (context)。

一旦值被放進容器,它就與外部世界隔離開來,我們不能再直接對它進行操作,而是透過一套規則來互動。
這個看似簡單的步驟,卻是後續所有強大抽象的基礎。透過將值包裹起來,我們為處理控制流、錯誤處理、非同步操作等複雜概念建立了穩固的基礎。  

什麼是容器 (Container)?

定義與實作

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) 呢? 目前可以先用「ofnew 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 值的容器。)

https://ithelp.ithome.com.tw/upload/images/20250928/201682012KV01CfLCE.png
圖 2 將一個值放入 Container 中(資料來源: 自行繪製)

Container 有幾個特性:

  • Container 是一個只包含一個屬性 $value 的物件
    • $value 用於儲存容器內的值,可存放任意型別資料(例如:原始型別、物件、甚至其他 Container)
  • 它不是為物件導向設計的資料結構,而是資料的載體
  • 一旦資料進了 Container,它就會留在那裡,不建議直接取出資料(例如:不推薦使用 myContainer.$value 來取值)
    • 目的是保護值,並提供一個統一的介面來操作它

容器帶來的新挑戰

現在我們可以將一個潛在的 nullundefined 值安全地包裹進 Container 中,我們的函式組合鏈可以傳遞一個 Container 物件,而不會因為 null 這種「裸值」而直接崩潰。

回想一下文章開頭那個 processUserName 會報錯的例子:

  • 當時 getProperty('name') 回傳了 undefined,直接傳給 toUpperCase,結果爆炸。
  • 如果一開始就是 Container.of(user),傳遞下去的就是 Container(undefined),而不是「危險的裸值」。

這樣一來,Container 就像一個自動檢查員,幫我們擋掉 nullundefined 的風險。

但新的問題也出現了:

🔺 我們原本的純函數,無法直接對容器裡的值進行操作。

以下面這段程式碼來說,add 函式期望接收一個數字(例如 3),但我們卻給了它一個 Container 物件。我們雖然保護了值,但也同時失去了與它互動的能力。

const add = (x) => x + 2;
const containerOfThree = Container.of(3);

add(containerOfThree); // "[object Object]2" 

https://ithelp.ithome.com.tw/upload/images/20250928/20168201tuOGvNfG2e.png
圖 3 add 函式無法接觸盒子內的值(資料來源: flaticon & 部分自行繪製)

我們已經有了一個安全的盒子,但現在浮現出了兩個問題:

  1. 組合的問題:如果我們有一組值,例如一個數字陣列 [1,2,3] 或一串字串 ['Hello', ' ', 'World'],是否存在一種通用且可靠的方法,能將它們組合成一個單一的值?我們知道數字可以用加總、字串可以用串接,但能否將這種「組合」的行為抽象出來,找到一個共通的模式?
  2. 應用的問題:如果我們在容器中有一個單一的值,例如 Container(5) ,而我們想對它應用一個簡單的函式 addOne,該怎麼做?是否存在一種通用的方式,能讓我們將函式「映射 (map)」到容器裡的值,而不必手動拆箱與重包裝?

這兩個挑戰——組合值與映射 Context ——正是 Functional Programming 的核心課題。
在下一篇文章中,我們將首先探索「組合」的問題,並介紹我們可能每天都在使用、卻可能從未意識到的強大結構:Monoid。

小結

以下簡單總結今天的重點:

容器是什麼?

一個簡單的物件,像個盒子,透過 .of() 方法將我們的資料值安全地包裹起來。

為什麼要用容器?

為了將「值」與其「脈絡」(例如:這個值可能為空)明確化。這能保護我們的函式組合流程,不因意外的 nullundefined 而中斷。

容器的挑戰

我們學會了如何將值安全地放入容器,但還沒學會如何操作容器裡的值。這問題的答案會在後續文章揭曉。

Reference


上一篇
[Day 13] 函數的語言:型別簽章(Type Signature)簡介
下一篇
[Day 15] 初探 Monoid:組合的力量
系列文
30 天的 Functional Programming 之旅18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言