背景
switch文が好きじゃなかったのでTypeScriptでswitch式を書いてみる。
switch文のイケてないところ
- 値を返せない
- 一致条件しか使えない
- なんかかっこわるい
値を返せないのは関数型スタイルで書きたいときにつらい。
あとなんかとりあえずかっこわるい。
劇的ビフォーアフター
Before
普通にswitch文でこう書くところを、
const hoge = 1
switch (hoge) {
case 1:
console.log("A");
break;
case 2:
console.log("B");
break;
case 3:
console.log("C");
break;
default:
console.log("default");
break;
}
After
こう書けるようにする。
const hoge = 1
const result = when(hoge)
.on(v => v === 1, () => "A")
.on(v => v === 2, () => "B")
.on(v => v === 3, () => "C")
.otherwise(() => "default")
console.log(result)
もちろん型の恩恵も得られる。型注釈を省略せずに書くとこうなっている。
const result: string = when(hoge)
.on((v: number) => v === 1, (): string => "A")
.on((v: number) => v === 2, (): string => "B")
.on((v: number) => v === 3, (): string => "C")
.otherwise((): string => "default")
ケースごとに異なる型を返してみると、型の恩恵を得られている感がある。
const result: string | number | string[] = when(hoge)
.on((v: number) => v === 1, (): string => "A")
.on((v: number) => v === 2, (): boolean => 100)
.on((v: number) => v === 3, (): string[] => ["aaa", "bbb"])
.otherwise((): string => "default");
マッチする条件は関数で書くので、switch文と違って好きな条件で書ける。
const result = when("abcde")
.on(v => v.length < 3, () => "length less than 3.")
.on(v => v.startsWith("a"), () => "start with 'a'.")
.otherwise(() => "default")
console.log(result) // -> start with 'a'.
otherwise()
を呼ぶ前に値を使うとコンパイルエラーになるので、switch文でいうdefaultを強制されている感じ。
const result = when(hoge)
.on(v => v === 1, () => "A")
.on(v => v === 2, () => "B")
.on(v => v === 3, () => "C")
// .otherwise(() => "default")
result.startsWith('a') // string型ではないのでコンパイルエラー!!!
result.otherwise(() => 'default').startsWith('a') // OK
v => v === 1
みたいにアロー関数を毎回書くのが面倒なら、比較用に関数を作るとシンプルになる。
const eq = <T>(val1: T) => (val2: T) => val1 === val2
const result = when(1)
.on(eq(1), () => "A")
.on(eq(2), () => "B")
.on(eq(3), () => "C")
.otherwise(() => "default")
console.log(result) // -> "A"
ソースコード
type ChainedWhen<T, R> = {
on: <A>(pred: (v: T) => boolean, fn: () => A) => ChainedWhen<T, R | A>;
otherwise: <A>(fn: () => A) => R | A;
};
const match = <T, R>(val: any): ChainedWhen<T, R> => ({
on: <A>(pred: (v: T) => boolean, fn: () => A) => match<T, R | A>(val),
otherwise: <A>(fn: () => A): A | R => val
});
const chain = <T, R>(val: T): ChainedWhen<T, R> => ({
on: <A>(pred: (v: T) => boolean, fn: () => A) =>
pred(val) ? match(fn()) : chain<T, A | R>(val),
otherwise: <A>(fn: () => A) => fn()
});
const when = <T>(val: T) => ({
on: <A>(pred: (v: T) => boolean, fn: () => A) =>
pred(val) ? match<T, A>(fn()) : chain<T, A>(val)
});
ざっくり説明:
- when: マッチしたら
match()
、しなかったらchain()
を返す - chain: マッチしたら
match()
、しなかったらchain()
を返す。otherwise
つきで返すのがwhen()
との違い。 - match: 「マッチしてもしなくても関係なく渡された値を返す」関数を返す。