昨天我們介紹了 Option、Either、Result、Exit、Cause 的用法,今天我們來介紹其他常用的 Data Types。我保證這篇是最後一篇 data types 的文章了。這部分我自己也覺得有點無聊。但是為了完整性,就介紹完唄~😌
import { Duration } from "effect"
// 1) 建立時間長度(常用單位 + 無窮)
const ms100 = Duration.millis(100)
const oneSec = Duration.seconds(1)
const fiveMin = Duration.minutes(5)
const forever = Duration.infinity
// 2) 時間運算(加總 / 倍數)
const ninetySec = Duration.sum(Duration.seconds(30), Duration.minutes(1)) // 1m 30s
const backoff = Duration.times(oneSec, 4) // 4 秒
// 3) 比較
const isShorter = Duration.lessThan(Duration.millis(900), oneSec)
// 4) 轉字串(人類可讀)
const label = Duration.format(ninetySec) // "1m 30s"
// 這裡列出常用的程式碼範例,更多時間單位可以去官方文件查看
Duration.decode(100) // same as Duration.millis(100)
Duration.decode("100 millis") // same as Duration.millis(100)
Duration.decode("2 seconds") // same as Duration.seconds(2)
Duration.decode("5 minutes") // same as Duration.minutes(5)
Duration.decode("7 hours") // same as Duration.hours(7)
Duration.decode("3 weeks") // same as Duration.weeks(3)
Duration.decode(Infinity) // same as Duration.infinity
單位 | 英文 | 建構函數 | Decode 字串範例 |
---|---|---|---|
毫秒 | millis (milliseconds) | Duration.millis(n) |
"100 millis" 或直接數字 100 |
秒 | seconds | Duration.seconds(n) |
"2 seconds" |
分鐘 | minutes | Duration.minutes(n) |
"5 minutes" |
小時 | hours | Duration.hours(n) |
"7 hours" |
天 | days | Duration.days(n) |
"3 days" |
週 | weeks | Duration.weeks(n) |
"3 weeks" |
無窮 | infinity | Duration.infinity |
Infinity |
每隔 750ms 輪詢一次,逾時 5 秒就停止。
import { Duration, Effect, Schedule } from "effect"
// 1) 固定間隔輪詢 + 總逾時
const interval = Duration.millis(750)
const totalTimeout = Duration.seconds(5)
// 模擬後端工作:第 5 次才完成
let pollCount = 0
function checkJobStatus() {
return Effect.sync<"pending" | "done">(() => {
pollCount++
return pollCount >= 5 ? "done" : "pending"
})
}
// 宣告式排程 + 全域逾時
const pollOnce = checkJobStatus().pipe(
Effect.tap(
(s) => Effect.sync(() => console.log(`🛰️ 輪詢 #${pollCount}: ${s}`))
),
Effect.flatMap(
(s) => (s === "done" ? Effect.succeed("done") : Effect.fail("pending"))
)
)
const polling = Effect.retry(pollOnce, Schedule.spaced(interval))
const program = Effect.timeout(polling, totalTimeout)
void Effect.runPromise(
Effect.match(program, {
onSuccess: () => console.log("✅ 完成"),
onFailure: () => console.log("⌛ 逾時")
})
)
// 輸出:
// 🛰️ 輪詢 #1: pending
// 🛰️ 輪詢 #2: pending
// 🛰️ 輪詢 #3: pending
// 🛰️ 輪詢 #4: pending
// 🛰️ 輪詢 #5: done
// ✅ 完成
使用者滾動頁面時,紀錄「滾動進度百分比」給後端。但為了避免高頻打 API 造成壓力,限制最多每 300ms 上報一次(throttle)。這種情境通常採首次立即上報,之後需間隔 ≥ 300ms。
import { Duration, Effect } from "effect"
// 範例:滾動進度上傳 — 最多每 300ms 上傳一次
const minInterval = Duration.millis(300)
let lastEmitAt = 0
function reportScrollProgress(progressPercent: number) {
const now = Date.now()
const elapsedMs = now - lastEmitAt
if (Duration.lessThan(Duration.millis(elapsedMs), minInterval)) return
lastEmitAt = now
console.log(`📊 ${new Date(now).toISOString()} | +${elapsedMs}ms | Scroll progress: ${progressPercent}%`)
}
// 模擬高頻事件(Effect 版):每 50ms 產生一次 scroll 進度
const simulation = Effect.gen(function*() {
let progress = 0
while (progress < 100) {
progress = Math.min(100, progress + Math.floor(Math.random() * 7))
reportScrollProgress(progress)
yield* Effect.sleep(Duration.millis(50))
}
console.log("🛑 模擬結束")
})
Effect.runFork(simulation)
// 輸出:
// 📊 2025-10-09T17:23:06.337Z | +1760030586337ms | Scroll progress: 3%
// 📊 2025-10-09T17:23:06.645Z | +308ms | Scroll progress: 15%
// 📊 2025-10-09T17:23:06.956Z | +311ms | Scroll progress: 35%
// 📊 2025-10-09T17:23:07.263Z | +307ms | Scroll progress: 51%
// 📊 2025-10-09T17:23:07.573Z | +310ms | Scroll progress: 74%
// 📊 2025-10-09T17:23:07.880Z | +307ms | Scroll progress: 95%
// 🛑 模擬結束
在真實服務中,SSE(Server-Sent Events)常經過反向代理與雲端邊緣(如 Nginx、Application Load Balancer)。多數代理會在「連線一段時間沒有資料流動」時主動關閉(idle timeout)。若我們沒有定期送出心跳(heartbeat),SSE 連線就會被視為閒置而被切斷。
這個例子用 Duration
建模「閒置逾時」與「心跳維持」,並對比:
ping
。Duration.lessThan
判斷 idleMs < idleTimeout
,不需心算單位、語意更清晰。import { Effect, Duration } from "effect"
const idleTimeout = Duration.seconds(3)
function simulate(label: string, enableHeartbeat: boolean) {
return Effect.gen(function*() {
console.log(`${label} OPEN`)
const bootAt = Date.now()
let lastActivityAt = bootAt
for (let i = 0; i <= 24; i++) {
const elapsedMs = Date.now() - bootAt
if (i === 0 || i === 20) {
lastActivityAt = Date.now()
console.log(`${label} event: data`)
}
if (enableHeartbeat && i % 4 === 0) {
lastActivityAt = Date.now()
console.log(`${label} ping`)
}
const idleMs = Date.now() - lastActivityAt
if (!Duration.lessThan(Duration.millis(idleMs), idleTimeout)) {
console.log(`${label} CLOSED by proxy (idle ${idleMs}ms @ t≈${elapsedMs}ms)`)
return
}
yield* Effect.sleep(Duration.millis(250))
}
console.log(`${label} END`)
})
}
Effect.runFork(simulate("[no-heartbeat]", false))
// 輸出:
// [no-heartbeat] OPEN
// [no-heartbeat] event: data
// [no-heartbeat] CLOSED by proxy (idle 3028ms @ t≈3028ms)
Effect.runFork(simulate("[heartbeat]", true))
// 輸出:
// [heartbeat] OPEN
// [heartbeat] event: data
// [heartbeat] ping
// [heartbeat] ping
// [heartbeat] ping
// [heartbeat] ping
// [heartbeat] ping
// [heartbeat] event: data
// [heartbeat] ping
// [heartbeat] ping
// [heartbeat] END
idleTimeout = Duration.seconds(3)
: 以不可變時間長度表達「閒置逾時」規則。lastActivityAt
: 代表最近一次「有流動」的時間戳(包含資料與心跳)。Duration.lessThan(Duration.millis(idleMs), idleTimeout)
: 可讀性高的比較;若不小於(即 ≥ 逾時),就視為被代理關閉。Effect.sleep(Duration.millis(250))
: 以非阻塞方式推進時間,模擬長連線下的實際流逝。Effect.runFork(Effect.sleep(Duration.infinity))
: 讓程序持續運作,不因主流程結束而提前退出。END
,證明連線被維持。Duration
是非負時間長度的資料型別;常用於 timeout、重試 backoff、排程。sum / times
:合成更長的時間;lessThan
:可讀的比較;format
:顯示用途。decode
:解析 number(毫秒)與人類可讀字串(如 "1m 30s"),適合讀取設定/環境變數。Duration.infinity
。一種「類陣列的不可變序列」,針對「重複接合(反覆 concat/append/flatten)」最佳化,透過結構共享降低每次拼接的複製成本。
不是單純一條連續陣列,而是由多個「分塊(block)」或「節點」組成,可視為分塊向量/淺層樹:
old:
┌───────┬───────┬───────┐
│ Blk#1 │ Blk#2 │ Blk#3 │
└───────┴───────┴───────┘
append [E,F] → new:
┌───────┬───────┬───────┬───────┐
│ Blk#1 │ Blk#2 │ Blk#3 │ Blk#4*│
└───────┴───────┴───────┴───────┘
^^^^^^^^^^^^^^^^^^^^^^ 共享 ^ 新建(只含新元素)
讀取時用 index 計算定位到對應的分塊與偏移;共享的是「節點引用」,不是共享某個索引值。
Blk#1
與 new 的 Blk#1
指向相同節點,沒有重新拷貝。Blk#1
範圍,會建立「新節點」掛接到新版本,而不是就地改舊的。ReadonlyArray
(Chunk.toReadonlyArray
)。Chunk.unsafeFromArray
但務必確保來源不再被修改(繞過拷貝與不可變防護,需謹慎)。在迴圈或串流中需要「多次」把小陣列累積成大集合,例如批次蒐集、日誌累積、分頁聚合、資料管線的中繼緩衝和 LLM chunking 拼接。
import { Chunk, Equal } from "effect"
const c1 = Chunk.make(1, 2, 3)
// 效能注意事項
// Chunk.fromIterable 會複製可疊代物件的元素建立新資料。
// 對大型資料或重複呼叫來說,這個拷貝成本可能影響效能。
const c2 = Chunk.fromIterable([4, 5])
const arr = Chunk.toReadonlyArray(c1) // readonly [1,2,3]
// 2) 接合 / 變換 / 篩選
const c3 = Chunk.appendAll(c1, c2) // [1,2,3,4,5]
const mapped = Chunk.map(c3, (n) => n * 2) // [2,4,6,8,10]
const filtered = Chunk.filter(mapped, (n) => n > 5) // [6,8,10]
// 3) 取捨 / 切片
const dropped = Chunk.drop(c3, 2) // [3,4,5]
const taken = Chunk.take(c3, 3) // [1,2,3]
const sliced = Chunk.take(Chunk.drop(c3, 1), 3) // [2,3,4]
// 3.5) 建立空集合 + 累積(適合重複接合場景)
const empty = Chunk.empty<number>() // []
const built = Chunk.appendAll(
Chunk.append(Chunk.append(empty, 1), 2),
Chunk.fromIterable([3, 4])
) // [1,2,3,4]
// 3.6) unsafeFromArray(避免複製,需小心)
// Chunk.unsafeFromArray 直接由陣列建立 Chunk,不會進行拷貝。
// 透過避免複製可提升效能,但需特別小心,
// 因為它繞過了不可變性(immutability)保證。
const direct = Chunk.unsafeFromArray([6, 7, 8]) // ⚠️ 請勿改動來源陣列
// 3.7) 比較 — 結構相等
// 比較兩個 Chunk 是否相等請使用 Equal.equals。
// 會以結構相等(逐一比對內容)的方式比較。
const isEqual = Equal.equals(c3, Chunk.make(1, 2, 3, 4, 5)) // true
// 4) 疊代(保持不可變)
let sum = 0
for (const n of c3) sum += n // 15
Chunk
提供不可變、結構共享的序列操作;常見 API 與陣列近似但不變更原值。import { Chunk, Effect, Ref, Console, Duration } from "effect"
// 將一段文字依指定大小切成多個片段
function splitTextBySize(text: string, chunkSize: number): ReadonlyArray<string> {
if (chunkSize <= 0) return [text]
const result: Array<string> = []
for (let i = 0; i < text.length; i += chunkSize) {
result.push(text.slice(i, i + chunkSize))
}
return result
}
// 直接以 Chunk 進行縫合(避免中間陣列配置)
function stitchChunk(parts: Chunk.Chunk<string>): string {
return Chunk.reduce(parts, "", (acc, s) => acc + s)
}
// 以 Effect 模擬串流:逐一送出片段,片段間以 delayMs 間隔
function mockTextStreamEffect(
text: string,
chunkSize: number,
delayMs: number,
onChunk: (chunk: string, index: number) => Effect.Effect<void>
) {
const pieces = splitTextBySize(text, Math.max(1, chunkSize))
return Effect.forEach(
pieces,
(piece, idx) =>
Effect.gen(function*() {
yield* onChunk(piece, idx)
if (idx < pieces.length - 1) {
yield* Effect.sleep(Duration.millis(Math.max(0, delayMs)))
}
}),
{ concurrency: 1 }
)
}
// 使用 Chunk 接收串流並即時縫合(純 Effect)
function consumeStreamWithChunkEffect() {
return Effect.gen(function*() {
// 更小的示例文字,方便閱讀與觀察每次拼接
const sampleText = "你好,這是串流測試。讓我們逐塊接收並縫合。"
// 以 Ref<Chunk<string>> 做為累積容器(適合重複 append 場景)
const receivedRef = yield* Ref.make(Chunk.empty<string>())
yield* mockTextStreamEffect(
sampleText,
6, // 每 6 個字元切一塊
100, // 每 100ms 送出一次
(chunk, idx) =>
Effect.gen(function*() {
// 每收到一個片段就接到 Chunk 後面
yield* Ref.update(receivedRef, (acc) => Chunk.append(acc, chunk))
// 只輸出片段與當前統計,避免每步驟字符串重建
const snapshot = yield* Ref.get(receivedRef)
yield* Console.log(`片段#${idx}:`, JSON.stringify(chunk))
yield* Console.log("目前片段數:", Chunk.size(snapshot))
// 每次縫合並顯示目前完整結果
const current = stitchChunk(snapshot)
yield* Console.log("目前拼接:", current)
})
)
const final = yield* Ref.get(receivedRef)
yield* Console.log("串流完成,片段數:", Chunk.size(final))
})
}
// 執行
Effect.runFork(consumeStreamWithChunkEffect())
Equal.equals
)。Hash.hash
),讓集合能快又準地查找。HashSet
能正確去重、HashMap
能用「內容一樣」當 Key。HashMap
/HashSet
,用內容判斷是否相同。import { Equal, Hash, HashMap, HashSet } from "effect"
// 1) 未實作 Equal → 比較是否為同一個物件(同一個記憶體位址)
console.log("1) 未實作 Equal:比較是否為同一個物件(同一記憶體位址)")
const a = { name: "Alice", age: 30 }
const b = { name: "Alice", age: 30 }
console.log("===:", a, "vs", b, "=>", a === b) // false(不是同一個物件 / 記憶體位址不同)
console.log("Equal.equals:", a, "vs", b, "=>", Equal.equals(a, b)) // false(依 === 判斷:不是同一個物件)
// 2) 使用 Equal + Hash 實作 Equal Interface,用以比較 class 物件是否結構相等
class Person implements Equal.Equal {
constructor(
readonly id: number,
readonly name: string,
readonly age: number
) {}
[Equal.symbol](that: Equal.Equal): boolean {
if (that instanceof Person) {
return (
Equal.equals(this.id, that.id) &&
Equal.equals(this.name, that.name) &&
Equal.equals(this.age, that.age)
)
}
return false
}
[Hash.symbol](): number {
// 以 id 產生雜湊值(快速不等判斷)
return Hash.hash(this.id)
}
}
console.log("2) 類別自訂值相等(Equal + Hash)")
const alice = new Person(1, "Alice", 30)
const aliceSame = new Person(1, "Alice", 30)
const bob = new Person(2, "Bob", 40)
console.log("Equal.equals:", alice, "vs", aliceSame, "=>", Equal.equals(alice, aliceSame)) // true
console.log("Equal.equals:", alice, "vs", bob, "=>", Equal.equals(alice, bob)) // false
// 3) HashSet:以值相等去重(需元素實作 Equal)
console.log("3) HashSet:值相等去重(元素需實作 Equal)")
let setWithEqual = HashSet.empty<Person>()
const p1 = new Person(1, "Alice", 30)
const p2 = new Person(1, "Alice", 30)
setWithEqual = HashSet.add(setWithEqual, p1)
setWithEqual = HashSet.add(setWithEqual, p2)
console.log("HashSet size (Equal):", HashSet.size(setWithEqual)) // 1
let setWithPlain = HashSet.empty<{ name: string; age: number }>()
const o1 = { name: "Alice", age: 30 }
const o2 = { name: "Alice", age: 30 }
setWithPlain = HashSet.add(setWithPlain, o1)
setWithPlain = HashSet.add(setWithPlain, o2)
console.log("HashSet size (plain):", HashSet.size(setWithPlain)) // 2
// 4) HashMap:Key 以值相等(需 Key 實作 Equal)
console.log("4) HashMap:Key 值相等(Key 需實作 Equal)")
let map = HashMap.empty<Person, number>()
const key1 = new Person(1, "Alice", 30)
const key2 = new Person(1, "Alice", 30)
map = HashMap.set(map, key1, 1)
map = HashMap.set(map, key2, 2)
console.log("HashMap size:", HashMap.size(map)) // 1(第二次覆蓋第一次)
console.log("HashMap get:", HashMap.get(map, key2))
/** 輸出:
1) 未實作 Equal:比較是否為同一個物件(同一記憶體位址)
===: { name: 'Alice', age: 30 } vs { name: 'Alice', age: 30 } => false
Equal.equals: { name: 'Alice', age: 30 } vs { name: 'Alice', age: 30 } => false
2) 類別自訂值相等(Equal + Hash)
Equal.equals: Person { id: 1, name: 'Alice', age: 30 } vs Person { id: 1, name: 'Alice', age: 30 } => true
Equal.equals: Person { id: 1, name: 'Alice', age: 30 } vs Person { id: 2, name: 'Bob', age: 40 } => false
3) HashSet:值相等去重(元素需實作 Equal)
HashSet size (Equal): 1
HashSet size (plain): 2
4) HashMap:Key 值相等(Key 需實作 Equal)
HashMap size: 1
HashMap get: { _id: 'Option', _tag: 'Some', value: 2 }
*/
a
/ b
,用 ===
或 Equal.equals(a, b)
都是 false
,因為它們不是同一個實例(記憶體位址不同)。Person
實作 [Equal.symbol](that)
:逐欄位比對 id/name/age
,內容一樣才算相等;實作 [Hash.symbol]()
:用 id
產生雜湊值。alice
與 aliceSame
被判定為相等(true
),alice
與 bob
不相等(false
)。Person
時,因為元素有 Equal/Hash
,集合能以「內容」去重,大小為 1
。Equal
),集合以參考相等判斷,兩個不同實例會同時存在,大小為 2
。Person
當 Key,key1
與 key2
內容相同,第二次 set
會覆蓋第一次的值,所以大小仍為 1
,並可 get(key2)
取得 Some(2)
。說明
HashSet/HashMap
以 Equal/Hash
的結構語義判斷,避免 duplicate。Equal.symbol
與 Hash.symbol
以參與集合操作。本篇補齊 Effect 中幾個在實務上常見的 Data Type,這些 Data Type 只要用對,在語意、可讀性與效能上都能得到很好的提升。不過其實我們還差 Data 這個 Data Type 要講,但內容有點多,就留給讀者自己研究吧~這篇就先這樣唄。🙂