iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0

這篇我們要來看 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 就有內建這樣的功能,而且還有很好的整合,我們一起來看看吧

Effect 的 Match

我們先來看個範例吧,如果把上面的 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
);

(playground link)

像這樣我們就可以寫多個條件來判斷在不同的資料下我們要採取哪種動作

注意:使用 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
);

(playground link)

如果你實際註解掉那行程式碼的話,你會發現它在 Match.exhaustive 那行出現的了錯誤,讓你知道你忘記處理 string 的資料了,這是個非常好用的功能,可以避免我們沒有處理到部份的情況

object 的模式比對

前面的都還比較像是一般的 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
);

(playground link)

還有另一種用法是用來判斷像是包含了 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
);

(playground link)

關於 tag

還記得之前在提到 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
);

(playground link)

同樣的,你可以試著把其中一個 Match.tag 註解掉看看,你會發現 TypeScript 同樣的在 exhaustive 那邊提示你有少處理了

以上就是這次介紹的 pattern matching 了,當你有很多種不同類型的資料需要做不同的處理時,這個會比內建的 switch 要來的更有彈性讓你可以寫出更複雜的判斷,同時透過 Match.exhaustive 的檢查避免意外的少處理到資料,這可以幫助我們大大的提升程式的健壯性和可維護性。

下一篇我們來要看 Effect 內建的一些資料型態

Reference


上一篇
26. Effect schema:資料格式驗證
系列文
Effect 魔法:打造堅不可摧的應用程式28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言