這篇我們要來看 Effect 裡怎麼做模式比對,模式比對是我覺得一個非常方便的功能,如果有看過我之前寫的文章,你可能會知道我是個 Rust 的愛好者, Rust 語言裡有個很方便的功能是 match
let n = 10;
match n {
..0 => println!("less then zero"),
0..=10 => println!("between 0 ~ 10"),
11.. => println!("larger than 10"),
}
它就像是 switch
,但比 switch
更加的強大,為了有這樣方便的功能,在 TypeScript 中也有去實作模式比對的套件,例如 ts-pattern ,不過我們現在在用的 Effect 就有內建這樣的功能,而且還有很好的整合,我們一起來看看吧
我們先來看個範例吧,如果把上面的 Rust 的範例用 Effect 的 Match 改寫就會變成
import { Match, Number, pipe } from "effect";
pipe(
Match.value(10),
// 這邊使用 Number 的 function 來幫助我們判斷大小
Match.when(Number.lessThan(0), (n) => console.log("less than 0")),
Match.when(Number.between({ maximum: 10, minimum: 0 }), (n) =>
console.log("between 0~10")
),
Match.when(Number.greaterThan(10), (n) => console.log("larger then 10")),
// 若沒有一個 match 符合的話,會拋出錯誤
Match.orElseAbsurd
);
像這樣我們就可以寫多個條件來判斷在不同的資料下我們要採取哪種動作
注意:使用
Match
的時候,一定要開啟 TypeScript 的strict
模式才能發揮最大的效果
你可能會想,這樣跟我自己寫一些 if
else
有什麼差別,我們再來看一個範例,假設你今天有個輸入的資料,它可能是字串或是數字,但你需要把它統一的轉換成數字時,我們可以用 Match
這樣寫
type StringOrInt = string | number;
const input: StringOrInt = "42";
const res = pipe(
Match.value(input),
// 額外限制我們的回傳值只能是 number
Match.withReturnType<number>(),
Match.when(Match.number, (x) => x),
// 你可以試看看把這行註解掉
Match.when(Match.string, (x) => Number.parseInt(x, 10)),
Match.exhaustive
);
如果你實際註解掉那行程式碼的話,你會發現它在 Match.exhaustive
那行出現的了錯誤,讓你知道你忘記處理 string 的資料了,這是個非常好用的功能,可以避免我們沒有處理到部份的情況
前面的都還比較像是一般的 switch
,模式比對還有一個特點是可以針對你的 object 內的資料做判斷,例如我們自訂一個類似於 Either 的資料類型
type Either<L, R> =
| { left: L; right?: never }
| { left?: never; right: R };
const data: Either<Error, number> = { right: 42 };
接著我們就可以用 Match 來判斷這個 object 裡面是不是有某個符合條件的值
pipe(
Match.value(data),
Match.when({ left: Match.any }, ({ left }) => console.log("error", left)),
Match.when({ right: Match.number }, ({ right }) =>
console.log("result", right)
),
Match.exhaustive
);
還有另一種用法是用來判斷像是包含了 type
的 object 是哪一種,例如像 OpenAI assistant API 的事件就有:
// 這邊是簡化的版本
type Events =
| { type: "thread.run.created"; run_id: string }
| { type: "thread.run.completed" };
const event: Events = {
type: "thread.run.completed",
};
而我們就可以用像這樣的方式在不同的事件發生時做不同的動作
pipe(
Match.value(event as Events),
Match.when({ type: "thread.run.completed" }, () => console.log("completed")),
Match.when({ type: "thread.run.created" }, ({ run_id }) =>
console.log("created", run_id)
),
Match.exhaustive
);
話說你也可以把上面的 Match 變成一種處理的 function ,只要把 Match.value
換成 Match.type
就行了
const processEvent = pipe(
Match.type<Events>(),
Match.when({ type: "thread.run.completed" }, () => console.log("completed")),
Match.when({ type: "thread.run.created" }, ({ run_id }) =>
console.log("created", run_id)
),
Match.exhaustive
);
還記得之前在提到 service 跟自訂 error 時有出現 _tag
這個 property 嗎?那時還說 Effect 裡很常用這個 property ,主要是用來區分不同的 object ,而這個 _tag
當然在這邊也派的上用場,可以幫助我們區分不同的 object
例如我們現在定義兩個 TaggedError
import { Data } from 'effect'
class ValidationError extends Data.TaggedError("ValidationError")<{
message: string
data: unknown
}> {}
class DataError extends Data.TaggedError("DataError")<{ message: string }> {}
type Errors = DataError | ValidationError
接著我們可以使用 Match.tag
來判斷是哪種 error
const handleError = pipe(
Match.type<Errors>(),
Match.tag("ValidationError", (error) =>
console.log("ValidationError", error)
),
Match.tag("DataError", (error) => console.log("DataError", error)),
Match.exhaustive
);
同樣的,你可以試著把其中一個 Match.tag
註解掉看看,你會發現 TypeScript 同樣的在 exhaustive 那邊提示你有少處理了
以上就是這次介紹的 pattern matching 了,當你有很多種不同類型的資料需要做不同的處理時,這個會比內建的 switch 要來的更有彈性讓你可以寫出更複雜的判斷,同時透過 Match.exhaustive
的檢查避免意外的少處理到資料,這可以幫助我們大大的提升程式的健壯性和可維護性。
下一篇我們來要看 Effect 內建的一些資料型態