在操作外部資源時,會造成副作用,例如新增一筆資料到資料庫,就是一個包含副作用的操作。
實務上有個場景還滿常見,我們常常需要操作多個外部資源,只要其中一個環節失敗,我們就要對前面已經造成的副作用的操作做復原。手動做這些操作其實有點繁瑣惱人,所以我們 FP 工具 effect 也提供了解決這類問題的框架。

以課程系統來舉例,這次我們想連通三種不同類型的資料服務,三個服務儲存的資料分別是課程的附檔文件或影片 (attachment)、新增課程的 Log (record),以及課程相關資訊 (meta)。當任意一個操作失敗時,都要對前面的操作做復原,所以我們所建立的服務除了提供新增功能以外,還要提供刪除功能。
export interface MinIo {
createAttachment: Effect.Effect<never, MinIoError, Attachment>
deleteAttachment: (
attachment: Attachment
) => Effect.Effect<never, never, void>
}
const MinIo = { context: Context.Tag<MinIo>() }
export interface Elastic {
createCourseRecord: (
attachment: Attachment
) => Effect.Effect<never, ElasticError, CourseRecord>
deleteCourseRecord: (
record: CourseRecord
) => Effect.Effect<never, never, void>
}
const Elastic = { context: Context.Tag<Elastic>() }
export interface Mongo {
createCourseMeta: (
record: CourseRecord
) => Effect.Effect<never, MongoError, Course>
deleteCourseMeta: (course: Course) => Effect.Effect<never, never, void>
}
const Mongo = { context: Context.Tag<Mongo>() }
接著我們用 Effect.acquireRelease 來定義要執行的動作跟失敗的動作
const tryCreateAttachment = pipe(
MinIo.context,
Effect.flatMap(({ createAttachment, deleteAttachment }) =>
Effect.acquireRelease(
// acquire
createAttachment,
//release
(attachment, exit) =>
Exit.isFailure(exit) ? deleteAttachment(attachment) : Effect.unit
)
)
)
const tryCreateCourseRecord = (attachment: Attachment) =>
pipe(
Elastic.context,
Effect.flatMap(({ createCourseRecord, deleteCourseRecord }) =>
Effect.acquireRelease(createCourseRecord(attachment), (record, exit) =>
Exit.isFailure(exit) ? deleteCourseRecord(record) : Effect.unit
)
)
)
const tryCreateCourseMeta = (record: CourseRecord) =>
pipe(
Mongo.context,
Effect.flatMap(({ createCourseMeta, deleteCourseMeta }) =>
Effect.acquireRelease(createCourseMeta(record), (record, exit) =>
Exit.isFailure(exit) ? deleteCourseMeta(record) : Effect.unit
)
)
)
type CreateAttachment = Effect.Effect<MinIo | Scope, MinIoError, Attachment>
把滑鼠移到 createAttachment 上可以看到它的型別多了一個之前沒看過的 Scope。這表示使用到 acquireRelease 的 effect 都需要依賴 Scope 環境來運行。
那 ... 甚麼是 Scope 呢? 我們直接從 Scope 的用法說起
effect 盒子裡面新增一個 finalizer,來表示一連串 effect 操作結束後要執行的動作。Scope 範圍內的一連串 effect 操作結束後選擇執行 finalizer 。而說穿了,acquireRelease 就是一種特化的 Scope ,它保證只要 aquire 被執行,而且 Scope 內的各種 effect 操作也都執行完畢,release 就會被執行。實作上我們可以用以下方法把各種依賴 Scope 的 effect 組合起來。
export const tryCreateCourse = Effect.scoped(
pipe(
tryCreateAttachment,
Effect.flatMap(tryCreateCourseRecord),
Effect.flatMap(tryCreateCourseMeta)
)
)
最終就會拿到這樣的以下型別
Effect.Effect<MinIo | Elastic | Mongo, MinIoError | ElasticError | MongoError, Course>
表示我們只要集齊三個外部依賴,就可以在出錯也會復原的情況下新增課程。
最後我們再利用昨天的 Layer 技巧把上面的 effect 執行起來。
const layer = Layer.mergeAll(MinIoLayer, ElasticSearchLayer, MongoLayer)
// Layer<never, never, S3 | ElasticSearch | Database>
如何定義 Layer 昨天已經有說,今天就不再多占版面。
然後把 layer 丟給 tryCreateCourse ,最後再做個 runPromise 就能執行起來囉 !
pipe(
tryCreateCourse,
Effect.provide(layer),
Effect.either, // 轉換成 either 型別避免報錯
Effect.runPromise
)