0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ts-patternにインスパイアされて劣化車輪の再発明をした

Posted at

序文

Qiitaの記事トレンドを眺めていたらts-patternを見つけました。
見ているうちになんか作れそうな気がしたのでコードを書いてみましたが、やはり私程度では太刀打ちできず劣化車輪の再発明となってしまいました。
しかし書いたアルゴリズムはif-elseを式として扱えない他の言語に置き換わったときに使えそうな気がします。
要はせっかく書いたし公開したいってことです。

コード

class MatchExpression<T, U> {
    private target: T;
    private result: U;
    private defaultValue: U;
    private isSet: boolean = false;

    constructor(target: T, defaultValue: U) {
        this.target = target;
        this.result = defaultValue;
        this.defaultValue = defaultValue;
    }

    when(res: U, f: (it: T) => boolean): this {
        if (this.isSet) {
            return this;
        }

        if (f(this.target)) {
            this.result = res;
            this.isSet = true;
        }

        return this;
    }

    run(): U {
        if (!this.isSet) {
            this.result = this.defaultValue;
        }

        return this.result;
    }
}

使い方

const res = (new MatchExpression('aaa', 'YYY'))
    .when('bbb', (it) => it === 'zzz')
    .when('ccc', (it) => it > 'zzz')
    .when('ddd', (it) => it === 'aaa')
    .when('eee', (it) => it === 'www')
    .run();

console.log(res);  // ddd
  • when(条件がtrueだった時の値, 判定するための無名関数) => this(メソッドチェーンするため)
  • run() => 判定結果

解説

これにより

const res = (() => {
  switch (prop) {
    case CASE1:
      return a;
    case CASE2:
      return b;
  }
  return default;
})();

みたいに無名関数を定義してif式っぽくする必要がなくなります。参考

private target: T;
private result: U;
private defaultValue: U;

型は統一したかったため、switch-case文でいう switch(/** ここ */) に当たるものを targetとしてT型、返却値をU型としています。
複数型を返したい場合に応えられるようジェネリクスにも対応させました。

when(res: U, f: (it: T) => boolean): this {
  if (this.isSet) {
    return this;
  }

  if (f(this.target)) {
    this.result = res;
    this.isSet = true;
  }

  return this;
}

nullやundefinedを返したい場合もあると考えたので、 isSet というすでに条件に一致する値が出ているかどうかを持たせています。ここでメソッドチェーンをしたかったために返していた this が活きています。

run(): U {
  if (!this.isSet) {
    this.result = this.defaultValue;
  }

  return this.result;
}

最終的にはU型が代入されてほしいので、これで終わりを示す run() を定義しました。
switch-case文でいうdefaultをrunに与えなかった理由は、this.resultが最後まで空のままな可能性があるためです。
最終的には値が入るので、strictモードで開発するならNon-null assertion operatorをつければいいのですが、気にくわなかったのでconstructorに持たせています。

再度繰り返しますが、劣化車輪の再発明なので素直に ts-pattern 使った方がいいです。私は逆張りなので自分でやってみましたが。
このコードでの今見えている課題はif-elseがネストしている場合です。結局whenの中に条件分岐を書かなければいけないうえ、 $A & B$ と $A & C$ があったとき、if-elseなら if(A) の中に if(B)else if(C) を書けばいいところを、 $A & B$ と $A & C$ それぞれ別に書かなければいけません。
逆にメソッドチェーンにした利点(?)としては、以下のような使い方ができる点です。

let stmt = (new MatchExpression('aaa', 'YYY'))
    .when('bbb', (it) => it === 'zzz')
    .when('ccc', (it) => it > 'zzz')

if (isTrue) {
  stmt = stmt.when('ddd', (it) => it === 'aaa')
    .when('eee', (it) => it === 'www')
}

stmt.run();

書いていて思いましたが、結局ミュータブル定義をやめたくてこのような実装をしたのに結局ミュータブル定義しているので利点ではないですね。
でもPythonでSQLAlchemyをかいているとこんな感じで条件分岐させてSQLを作成することがあるので、もしかしたら別言語で作ったときに役立つかもしれないですね(?)

書けたコードを見せびらかして満足したので終わります。

0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?