LoginSignup
3
0

More than 1 year has passed since last update.

TypeScriptでif文を式として実装する

Last updated at Posted at 2022-06-26

モチベーション

ミュータブルな値の管理は参照されている箇所やその値の状態に意識を向けねばならず、テスタビリティの低下や認知負荷の向上を招きやすいです。そのためできることならミュータブルな実装は避けたほうが安全です。

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 同様、 elseIfelse も関数です。

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;
}

EvaluatorIf が呼ばれたあとの状態と評価順序を定義するクラスです。概念的にはビルダパターンのような構造になっています。ここまでをまとめると以下のようになります。

/**
 * 条件と式の組み合わせを状態として保持し、評価の順番を定義する
 */
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 を作っています。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0