🔗

できる限りType-Safe Pipeline — 流れる構造のデザイン

に公開

経緯

前回パイプランのやつをあげてみたものの、各stepsの構造で、anyが気になりすぎたので
もっと厳格にやれないかと色々考察していた。
見た目はだいぶカッコいいんだけど・・いいんだけどぉぉぉ・・・

できる限りType-safeにしようとしたら、どんな感じになるかなぁって。

中身の話

結局たどり着いたのは、外見FP、中身OOPな構造で、Stepをクラス化した。
んで、オブジェクトのチェインで繋いで、次のstepへの橋渡し。
重要かなと思うのは、現在のPipeを見たときに、前側の出力と後ろ側の入力までしか
Pipeは理解することができないという点で、型としては前後がないとダメなんだけど
構造として見えてはいけないから、unknownで見えてないフリしてる。

あと、各メソッドの名前をPipeっぽくしてみたら、意外と素敵な仕上がりに。
最後に、streamで初期値を流し込むと、ぜんぶがぶわっと動き出すの、楽しいよ。

余談

知らんかったことをAIさんから色々教わり、どうにか形になった。
structuredClone なんて知らんかったよ。

地味に知らなかったのは、型の排除 Exclude
ってか、最初に中身を教えられて、実はこれがあってって説明してくれたけど、
一発で回答でてこないことあるんだね(説明不足かな・・・)

タイトルもAIさんが考えてくれたけど、あまりに強気だったので、先頭に
「できるかぎり」をつけました(穴は空いてるだろ多分)

コード

https://github.com/risk/ts-playground/blob/main/src/typesafe-pipeline/pipeline.ts

解説

入力と出力の定義

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L9-L10
Pipe単体の入力と出力の型定義。
入力は、Excludeで null と undefined を入りこまないようにしている。
出力は、次の入力 もしくは Error となっていて、型でエラーかどうかを扱う仕組み。

Pipe

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L19-L104
今回の本題で、Pipeを繋いで最後に流すって感じにしたのはこの部分。
処理のイメージとしては、Pipeを繋いでいき(joint)、最後に流す(stream)と処理が走る。
仕組み的には、前回のPipelineではanyの配列で関数を保持していた部分は
オブジェクトのchainに置き換えることにした。

属性の話

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L21-L25
この部分も説明しだすと長いのでAIにまず概要を書いてもらった

プロパティ / 引数 説明
result Result<O> 現在のステップの実行結果を保持する。
初期状態では Error('not executed') が格納されている。
start Pipe<Init, unknown, Init> | null パイプラインの起点を示す参照。
どのステップから実行を開始するかを特定するために使用される。
entryInput Result<I> 最初のステップが受け取る入力データ。
from() で作成された場合のみ明示的に設定される。
next Pipe<O, unknown, Init> | null 次に実行されるステップへのリンク。
ステップ生成時に自動的に接続される。
parent (constructor 引数) Pipe<any, I, Init> | null 直前のステップ(親ノード)。
null の場合は最初のステップ(エントリ)であることを示す。
step (constructor 引数) (input: Input<I>) => Result<O> ステップ本体の処理関数。
入力を受け取り、変換または処理結果を返す。
recover (constructor 引数) (error: Error) => Result<I> エラー時に入力を修正または再供給するリカバリ関数。
デフォルトでは単にエラーを通過させる。

💡 コンストラクタ補足
コンストラクタ内では parent が存在する場合、自動的に
parent.next = thisthis.start = parent.start が設定され、
ステップチェーンが形成される。

つまるところ、Pipeは前後関係を保持しているんだけど、ポイントになるのは
parent と next。
この2つは、
「Pipeを中心に見た時、parentの出力の型 と nextの入力の型 だけを意識したい」
とういう要望を持っていて、nextは処理の都合上、次のパイプが生成されて初めて実体が出来る
という縛りがある。
なので、あと設定になっているんだけど、このときに
じゃあ、Pipeが持ってるnextの型ってなんやねん問題がでてきてしまう。
なので、unknown と any の出番。ようは、「しらんがな」で突き通す。
ただし、プログラムの中では意識しないことを設計上約束する必要があると。

errorPassthrough

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L17
各レイヤで、エラー復帰が出来る仕組み(recover)を追加しているのだけど、通常はそのまま流したいので
そのまま流してくれるメソッドを作成しておいた。

from

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L37-L44
Pipeの起点になるメソッド、始点Pipe(parentがnull、startにセットされる)を作る。
startはインスタンスが増えても引き継がれていき、どのpipeからでも始点が参照できるので
最後にstreamを読んだときは、始点から走り始めることが出来るようになっている。

joint

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L46-L51
次のPipeをつなぐ処理。流す(stream)時は、繋いだパイプが順番に呼ばれていく。
つなぐパイプは、parentがセットされて前の結果を受け取れるようになり、
次のパイプがjointされれば、nextが埋まる
そんな感じで、パイプがどんどん連なっていくのである。

branch

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L53-L58
枝分かれの定義だが、完全に別れるというよりは、別のPipeを接続出来る処理。
なので、pipeをどっかに定義しておけば、そのpipeを繋ぎこむだけで処理の切り替えができちゃう。

window

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L60-L64
これは、のぞき窓。データの流れを覗くための処理で、流れる値には変化を与えない想定
だが・・・やろうと思えば変えられちゃうケースがあり得るかと悩みどころ。
ここもstructuredCloneすべきだったかもしれない(今気がついた)

データを流す(stream)と実行を司る部分

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L66-L103
まとめてしまうが、Pipe同士の接続を見ながら、データを実際に流す処理を行う部分。
streamを呼び出すと、startのpipeを参照して、データを流し始める。
流れたデータは接続されたpipeを通って、処理を行う

実行部分

https://github.com/risk/ts-playground/blob/166364ed1e6b221741ebe35b73dd4e05984ba3fd/src/typesafe-pipeline/pipeline.ts#L106-L123
以前までとそんなに呼び方は変わってないです、中身に色々防御を入れた感じなので。

Discussion