モチベーション
ミュータブルな値の管理は参照されている箇所やその値の状態に意識を向けねばならず、テスタビリティの低下や認知負荷の向上を招きやすいです。そのためできることならミュータブルな実装は避けたほうが安全です。
TypeScriptではif/elseもtry/catchも、どちらも文なので評価して値を返すことはできません。そこでif/elseを式として実装し、式として評価することで処理の結果をイミュータブルに扱えるようにしてみます。
実装と使い方
if式を定義する
条件と、条件がtrueの場合に実行したい無名関数を渡すと状態を持つ Evaluator
を返す If
関数を定義します。if式はifでfalseとなっても何がしかの値を、最初に渡した無名関数と同じ型の値を返す必要があり、 Evaluator
を返すことが式として評価されるための制約にもなりえます(elseの指定も矯正されるということです)。
/**
* 条件がtrueのときに実行する関数の型定義(エイリアス)
*/
type BasicExpression<T> = () => T;
export const If = <T>(condition: boolean, block: BasicExpression<T>)
: Evaluator<T> => {
return new Evaluator<T>(condition, block);
};
この実装例ではやたらと () => T
の型が登場するのでエイリアスとしてtypeを作っておきます(なくてもいいけど、あったほうがちょっと読みやすくなるくらい)。
type BasicExpression<T> = () => T;
elseIf
ないしは else
が呼ばれた際、条件と条件がtrueのときに処理する関数のペアを配列に持っておきます。評価順序が大事なのですが、内部的にはキューのように最後尾に積んでおきます。
type ConditionFunctionPair<T> = [boolean, BasicExpression<T>];
private readonly condtionFunctions: ConditionFunctionPair<T>[]
ConditionFunctionPair
は条件式とtrueの場合に実行したい関数のペアとなる型エイリアスです。
If
を式として評価するので If
単体で使うことは想定しません。 else
まで呼ばれて評価される前提で、elseまで呼ばれた際に condtionFunctions
に積んだ条件を一個ずつ評価・実行します。 クラス内部では condtionFunctions
はタプルの配列として扱われています。 If
同様、 elseIf
も else
も関数です。
else(alternative: BasicExpression<T>): T {
this.condtionFunctions.push([true, alternative]);
for (const pair of this.condtionFunctions) {
if (pair[0]) {
return pair[1]();
}
}
throw new Error('All conditions was not matched');
}
else
まで到達したら、一個ずつ条件を評価していくので elseIf
は condtionFunctions に条件と条件がtrueの際に実行する関数のタプルを積んで状態のみを返します。
elseIf(condition: boolean, block: BasicExpression<T>): Evaluator<T> {
this.condtionFunctions.push([condition, block]);
return this;
}
Evaluator
はIf
が呼ばれたあとの状態と評価順序を定義するクラスです。概念的にはビルダパターンのような構造になっています。ここまでをまとめると以下のようになります。
/**
* 条件と式の組み合わせを状態として保持し、評価の順番を定義する
*/
class Evaluator<T> {
constructor(
condition: boolean,
block: BasicExpression<T>,
) {
this.condtionFunctions.push([condition, block]);
}
private readonly condtionFunctions: ConditionFunctionPair<T>[] = [];
elseIf(condition: boolean, block: BasicExpression<T>): Evaluator<T> {
this.condtionFunctions.push([condition, block]);
return this;
}
else(alternative: BasicExpression<T>): T {
this.condtionFunctions.push([true, alternative]);
for (const pair of this.condtionFunctions) {
if (pair[0]) {
return pair[1]();
}
}
throw new Error('All conditions was not matched');
}
}
使い方
if文と同じです。ただし、式として評価できるので結果を変数にとっておくことができます。
const base = "The quick brown fox jumps over the lazy dog.";
const result = If(base.length > 30, () => 'over 30')
.else(() => 'under 30')
elseIf を挟む場合も同様です。上記の例とは別の例です。 elseIf は状態を更新する中間操作なので複数重ねられます。たとえば、なんかしらステータスを返すロジックがあるとして...(ここではモックとして固定値を返すものとします)。
class StatusFindService {
execute(): string {
return '5';
}
}
下記のコードでは2番目に評価される result === '5', () => ({message: 'not found'})
が結果になります。
const result = new StatusFindService().execute();
const res = If(result === '0', () => ({message: 'success'}))
.elseIf(result === '5', () => ({message: 'not found'}))
.else(() => ({message: 'failure'}));
したがって res
は {message: 'not found'}
になります。その他、elseIfはelseが呼ばれるまで評価されないためいくつか重ねることができます。
参考
ソースコードは expr.ts にあります。
また、こちらの記事は大変参考にさせていただきました: https://qiita.com/hatakoya/items/018afbfb1bd45136618a
おまけ
Kotlinの Result
、scala.util.Try を参考にした resultify を作っています。