👻

Debug or Delete? ― 型が異なる構造のChain

に公開

Debug or Delete ?

ハロウィンなので、ハロウィンネタを。

ライブラリに実装したコードが読みにくくて、そっち閉じされると悲しいなって思ったので、できる限り簡素にして、元々はこんな構造から始まりましたというネタです。

ライブラリは r-pipeline なんですが、違う型なのにChainが繋がっていくという不思議な構造を持っています。

「前後で違う型のチェインをどうやって実現する?どんな構造にしているのか?」

という部分の解説です。

ポイントにしているのは any を使えば、たいして難しくないのですが、それじゃあTypeScriptの恩恵を受けられなくなっちゃうので、

「できるだけ型の情報を残しつつ、anyを使わずにChainを処理させるにはどうすればいい?」

という視点の考察記事になります。

コード

全体構造はこちら
https://github.com/risk/ts-playground/blob/main/src/debugOrDelete/debugOrDelete.ts

実行用 Interface

コイツがChain構造の要です。
https://github.com/risk/ts-playground/blob/316df1fb810b2e5079697333218f0fed15f7c7b6/src/debugOrDelete/debugOrDelete.ts#L6-L8

このあとに定義する Nodeクラス は、このInterfaceを継承させます。
どう使うかは、このあとの部分で。

Nodeクラス

このクラスをChainで繋いでいきます。
https://github.com/risk/ts-playground/blob/316df1fb810b2e5079697333218f0fed15f7c7b6/src/debugOrDelete/debugOrDelete.ts#L10-L27
内部構造は 実データのの x と、前ノード prev、次ノード next を持ったクラスになってます。
また、

  • 次をつなぐためのメソッドである addNext
  • 実行する本体である execute

を持たせています。

まずは Node<P, T> ですが、この定義は

  • Pが一つ前の実データの型
  • Tが現在のデータ型

になっています。
なので、実際のデータに当たる x は T型になります。

前のノードを保持する部分は、わかっているのは前データの型だけなので
Node<unknown, P> になります。前の前のノードの型なんか知らん と。

次のノードを保持する部分は、わかっているのは、自分のデータ型だけなので
Node<T, unknown> になっています。未来の話もわからんのです。

Nodeを中心に見た場合、見える範囲は、一つ前の型 と 自分の型だけなので、それを素直に表現します。

AddNext ― 実際にノードをつなごう

実際にノードをつなぐ処理は、データを受け取ったデータの型を推測させて Nodeにセットします。
https://github.com/risk/ts-playground/blob/316df1fb810b2e5079697333218f0fed15f7c7b6/src/debugOrDelete/debugOrDelete.ts#L16-L20

Nodeは <P, T> を取りますので
インスタンスを生成するときに、Pは自分の型(T)、Tに新しいデータの型(NEXT_T)を渡します。
インスタンスを作るときに、自分のインスタンスと新しいデータを渡すことで、つながりを作ります。
さらに、後処理として、this.next(自分の次)に、作成したインスタンスを入れてあげます。
これで前後関係が繋がったことになります。

prev → this → next

つまり、this を中心としてみた時

Node<unknown, P> → Node<P, T> → Node<T, unknown>

となります。

execute ― 実行関数

折角繋いだのだから、実行したいですね。なので実行関数の定義で、interfaceで定義したメソッドを実装しています。
https://github.com/risk/ts-playground/blob/316df1fb810b2e5079697333218f0fed15f7c7b6/src/debugOrDelete/debugOrDelete.ts#L16-L20

execute は

  • 引数に prev と next どちらを返すかの、reverseフラグ
  • 戻り値に、次の実行対象を返します、戻り値の型は executer

というメソッドになっています。
Pipeに付属する型情報が邪魔をして同じ変数に入れられなくなっちゃうので
必要なところだけを取り出した executer interdace 定義する必要があったんですね。

中の処理では、実行したときに型が取れてるかわかるようにログを仕込んでます。

this.prev.x の実体と型、自分の x を表示するようにしており、
ここで、this.prev.x これが正しく見えるということは、Pipelineをつなぐことが出来ると言えます。

実際に動作確認

https://github.com/risk/ts-playground/blob/316df1fb810b2e5079697333218f0fed15f7c7b6/src/debugOrDelete/debugOrDelete.ts#L29-L42

処理的には単純で executer が格納できる変数を用意して、Nodeを構築します。
addNodeは、次のthisを返してくれる仕組みにしているので

const root = new Node(null, 1)
const last = root.addNext('a').addNext(2).addNext(true)

こんな感じに作ることが出来ます、ちょっとおしゃれでしょ?

出来上がったものは、以下の通りなので

  • 先頭から最後尾は Nodeの先頭(root)
  • 最後尾から先頭は Nodeの末尾(last)

それぞれ変数に代入しておきます。

最後に、while で cur/revCur を executeを呼び出す(方向は reverse フラグで)
これで Node が次の実装対象を返してくるので、Chain処理ができます。

出力は以下のようにでてきます。

normal
empty(null) 1
1(number) a
a(string) 2
2(number) true
reverse
2(number) true
a(string) 2
1(number) a
empty(null) 1

まとめ

構造としては、解いてしまえばわかりやすいかと思います。
ポイントとしては、

  • 知らない部分は「正しく知らない」と宣言する部分
  • 知ってる範囲で処理を完結させる

この2つだと思っています。

知らないのだから無理やり「何でもいいよ!」とanyを使ってしまえば、自由すぎる構造になりバグを発生させる確立が大幅に上がります。
また、知らない範囲を処理しようとすることは、「隣の家の枝を切る」ようなものなので、やらないに越したことはありません。

扱うべきデータを正しく定義して、その範疇で処理を行うことで、構造的に壊れていないプログラムを作ることが出来ると思います。
Typescriptは「知らない(uknown)」を定義出来る言語ですので、上手に付き合っていければと思います。

もう一度 Debug or Delete?

仕組みを色々解説してきましたが、紐解いてみれば読めるコードになってきたんじゃないでしょうか。
なので Debug を選んでいただければと・・・よろしくお願いします。

Discussion